Browse Source

Show payment details on admin, portal, and PDF invoices

main
def 6 days ago
parent
commit
4bd7fd5267
  1. 134
      backend/app.py
  2. 33
      templates/invoices/view.html
  3. 37
      templates/portal_invoice_detail.html

134
backend/app.py

@ -115,6 +115,75 @@ def fmt_money(value, currency_code="CAD"):
return f"{amount:.2f}" return f"{amount:.2f}"
return f"{amount:.8f}" return f"{amount:.8f}"
def payment_method_label(method, currency=None):
method_key = str(method or "").strip().lower()
currency_key = str(currency or "").strip().upper()
if method_key == "square":
return "Square"
if method_key == "etransfer":
return "e-Transfer"
if method_key == "cash":
return "Cash"
if method_key == "other":
if currency_key in {"ETH", "ETHO", "ETI", "USDC", "EGAZ", "ALT", "CAD"}:
return currency_key
return "Other"
if method_key == "crypto_etho":
return "ETHO"
if method_key == "crypto_egaz":
return "EGAZ"
if method_key == "crypto_alt":
return "ALT"
if currency_key in {"ETH", "ETHO", "ETI", "USDC", "EGAZ", "ALT"}:
return currency_key
return method or "Unknown"
def get_invoice_payments(invoice_id):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT
id,
payment_method,
payment_currency,
payment_amount,
cad_value_at_payment,
reference,
sender_name,
txid,
wallet_address,
payment_status,
confirmations,
confirmation_required,
received_at,
created_at,
notes
FROM payments
WHERE invoice_id = %s
ORDER BY COALESCE(received_at, created_at) ASC, id ASC
""", (invoice_id,))
rows = cursor.fetchall()
conn.close()
out = []
for row in rows:
item = dict(row)
item["payment_method_label"] = payment_method_label(
item.get("payment_method"),
item.get("payment_currency"),
)
item["payment_amount_display"] = fmt_money(
item.get("payment_amount"),
item.get("payment_currency") or "CAD",
)
item["cad_value_display"] = fmt_money(item.get("cad_value_at_payment"), "CAD")
item["received_at_local"] = fmt_local(item.get("received_at") or item.get("created_at"))
out.append(item)
return out
def normalize_oracle_datetime(value): def normalize_oracle_datetime(value):
if not value: if not value:
return None return None
@ -3354,6 +3423,52 @@ def invoice_pdf(invoice_id):
pdf.drawString(left, y, line_text) pdf.drawString(left, y, line_text)
y -= 13 y -= 13
if invoice_payments:
y -= 8
if y < 170:
pdf.showPage()
y = height - 50
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(left, y, "Payments Applied")
y -= 16
for p in invoice_payments:
if y < 110:
pdf.showPage()
y = height - 50
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(
left,
y,
f"{p.get('payment_method_label', 'Unknown')} | {p.get('payment_amount_display', '')} {p.get('payment_currency', '')} | {str(p.get('payment_status') or '').upper()}"
)
y -= 13
details_parts = []
if p.get("received_at_local"):
details_parts.append(f"At: {p.get('received_at_local')}")
if p.get("txid"):
details_parts.append(f"TXID: {p.get('txid')}")
elif p.get("reference"):
details_parts.append(f"Ref: {p.get('reference')}")
if p.get("wallet_address"):
details_parts.append(f"Wallet: {p.get('wallet_address')}")
details = " | ".join(details_parts)
if details:
pdf.setFont("Helvetica", 9)
for chunk_start in range(0, len(details), 108):
if y < 95:
pdf.showPage()
y = height - 50
pdf.setFont("Helvetica", 9)
pdf.drawString(left + 10, y, details[chunk_start:chunk_start+108])
y -= 11
y -= 6
if settings.get("invoice_footer"): if settings.get("invoice_footer"):
y -= 8 y -= 8
pdf.setFont("Helvetica-Bold", 11) pdf.setFont("Helvetica-Bold", 11)
@ -3379,7 +3494,6 @@ def invoice_pdf(invoice_id):
@app.route("/invoices/view/<int:invoice_id>") @app.route("/invoices/view/<int:invoice_id>")
def view_invoice(invoice_id): def view_invoice(invoice_id):
ensure_invoice_quote_columns()
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(dictionary=True)
@ -3404,17 +3518,15 @@ def view_invoice(invoice_id):
conn.close() conn.close()
return "Invoice not found", 404 return "Invoice not found", 404
invoice["oracle_quote"] = None
invoice["quote_expires_at_local"] = fmt_local(invoice.get("quote_expires_at"))
if invoice.get("oracle_snapshot"):
try:
invoice["oracle_quote"] = json.loads(invoice["oracle_snapshot"])
except Exception:
invoice["oracle_quote"] = None
conn.close() conn.close()
settings = get_app_settings() settings = get_app_settings()
return render_template("invoices/view.html", invoice=invoice, settings=settings) invoice_payments = get_invoice_payments(invoice_id)
return render_template(
"invoices/view.html",
invoice=invoice,
settings=settings,
invoice_payments=invoice_payments
)
@app.route("/invoices/edit/<int:invoice_id>", methods=["GET", "POST"]) @app.route("/invoices/edit/<int:invoice_id>", methods=["GET", "POST"])
@ -4703,6 +4815,7 @@ def portal_invoice_detail(invoice_id):
crypto_error = "Transaction was not confirmed in time. Please refresh your quote and try again." crypto_error = "Transaction was not confirmed in time. Please refresh your quote and try again."
pdf_url = f"/invoices/pdf/{invoice_id}" pdf_url = f"/invoices/pdf/{invoice_id}"
invoice_payments = get_invoice_payments(invoice_id)
conn.close() conn.close()
@ -4719,6 +4832,7 @@ def portal_invoice_detail(invoice_id):
pending_crypto_payment=pending_crypto_payment, pending_crypto_payment=pending_crypto_payment,
crypto_quote_window_expires_iso=crypto_quote_window_expires_iso, crypto_quote_window_expires_iso=crypto_quote_window_expires_iso,
crypto_quote_window_expires_local=crypto_quote_window_expires_local, crypto_quote_window_expires_local=crypto_quote_window_expires_local,
invoice_payments=invoice_payments,
) )

33
templates/invoices/view.html

@ -216,6 +216,39 @@ body {
</div> </div>
{% endif %} {% endif %}
{% if invoice_payments %}
<div class="notes-box">
<strong>Payments Applied</strong><br><br>
<table class="summary-table">
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>Wallet: {{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if settings.payment_terms %} {% if settings.payment_terms %}
<div class="notes-box"> <div class="notes-box">
<strong>Payment Terms</strong><br><br> <strong>Payment Terms</strong><br><br>

37
templates/portal_invoice_detail.html

@ -341,6 +341,43 @@
</div> </div>
{% endif %} {% endif %}
{% if invoice_payments %}
<div class="detail-card" style="margin-top:1.25rem;">
<h3>Payments Applied</h3>
<table class="portal-table">
<thead>
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
</thead>
<tbody>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>{{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pdf_url %} {% if pdf_url %}
<div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div> <div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div>
{% endif %} {% endif %}

Loading…
Cancel
Save