|
|
|
|
@ -144,6 +144,8 @@ def payment_method_label(method, currency=None):
|
|
|
|
|
return "e-Transfer" |
|
|
|
|
if method_key == "cash": |
|
|
|
|
return "Cash" |
|
|
|
|
if method_key == "credit": |
|
|
|
|
return "Credit" |
|
|
|
|
if method_key == "other": |
|
|
|
|
if currency_key in {"ETH", "ETHO", "ETI", "USDC", "EGAZ", "ALT", "CAD"}: |
|
|
|
|
return currency_key |
|
|
|
|
@ -5554,6 +5556,8 @@ def portal_dashboard():
|
|
|
|
|
return "e-Transfer" |
|
|
|
|
if method_key == "cash": |
|
|
|
|
return "Cash" |
|
|
|
|
if method_key == "credit": |
|
|
|
|
return "Credit" |
|
|
|
|
if method_key == "crypto_etho": |
|
|
|
|
return "ETHO" |
|
|
|
|
if method_key == "crypto_egaz": |
|
|
|
|
@ -6152,6 +6156,11 @@ def portal_invoice_detail(invoice_id):
|
|
|
|
|
|
|
|
|
|
pdf_url = f"/invoices/pdf/{invoice_id}" |
|
|
|
|
invoice_payments = get_invoice_payments(invoice_id) |
|
|
|
|
client_credit_balance_dec = get_client_credit_balance(client["id"]) |
|
|
|
|
client_credit_balance = _fmt_money(client_credit_balance_dec) |
|
|
|
|
|
|
|
|
|
credit_applied = (request.args.get("credit_applied") or "").strip() |
|
|
|
|
credit_error = (request.args.get("credit_error") or "").strip() |
|
|
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
@ -6163,6 +6172,10 @@ def portal_invoice_detail(invoice_id):
|
|
|
|
|
pdf_url=pdf_url, |
|
|
|
|
pay_mode=pay_mode, |
|
|
|
|
crypto_error=crypto_error, |
|
|
|
|
credit_applied=credit_applied, |
|
|
|
|
credit_error=credit_error, |
|
|
|
|
client_credit_balance=client_credit_balance, |
|
|
|
|
client_credit_balance_dec=client_credit_balance_dec, |
|
|
|
|
crypto_options=crypto_options, |
|
|
|
|
selected_crypto_option=selected_crypto_option, |
|
|
|
|
pending_crypto_payment=pending_crypto_payment, |
|
|
|
|
@ -6173,6 +6186,117 @@ def portal_invoice_detail(invoice_id):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/portal/invoice/<int:invoice_id>/apply-credit", methods=["POST"]) |
|
|
|
|
def portal_apply_credit(invoice_id): |
|
|
|
|
client = _portal_current_client() |
|
|
|
|
if not client: |
|
|
|
|
return redirect("/portal") |
|
|
|
|
if portal_terms_required(client): |
|
|
|
|
return redirect("/portal/terms") |
|
|
|
|
|
|
|
|
|
from decimal import Decimal, ROUND_HALF_UP |
|
|
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
SELECT id, client_id, invoice_number, status, total_amount, amount_paid, currency_code |
|
|
|
|
FROM invoices |
|
|
|
|
WHERE id = %s AND client_id = %s |
|
|
|
|
LIMIT 1 |
|
|
|
|
""", (invoice_id, client["id"])) |
|
|
|
|
invoice = cursor.fetchone() |
|
|
|
|
|
|
|
|
|
if not invoice: |
|
|
|
|
conn.close() |
|
|
|
|
return redirect("/portal/dashboard") |
|
|
|
|
|
|
|
|
|
if (invoice.get("status") or "").lower() in {"paid", "cancelled"}: |
|
|
|
|
conn.close() |
|
|
|
|
return redirect(f"/portal/invoice/{invoice_id}?credit_error=closed") |
|
|
|
|
|
|
|
|
|
if str(invoice.get("currency_code") or "CAD").upper() != "CAD": |
|
|
|
|
conn.close() |
|
|
|
|
return redirect(f"/portal/invoice/{invoice_id}?credit_error=currency") |
|
|
|
|
|
|
|
|
|
outstanding = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid")) |
|
|
|
|
if outstanding <= 0: |
|
|
|
|
conn.close() |
|
|
|
|
return redirect(f"/portal/invoice/{invoice_id}?credit_error=no-balance") |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
SELECT COALESCE(SUM(amount), 0) AS balance |
|
|
|
|
FROM credit_ledger |
|
|
|
|
WHERE client_id = %s |
|
|
|
|
AND currency_code = 'CAD' |
|
|
|
|
""", (client["id"],)) |
|
|
|
|
credit_row = cursor.fetchone() or {} |
|
|
|
|
credit_balance = to_decimal(credit_row.get("balance")) |
|
|
|
|
|
|
|
|
|
if credit_balance <= 0: |
|
|
|
|
conn.close() |
|
|
|
|
return redirect(f"/portal/invoice/{invoice_id}?credit_error=no-credit") |
|
|
|
|
|
|
|
|
|
apply_amount = credit_balance if credit_balance < outstanding else outstanding |
|
|
|
|
apply_amount = Decimal(str(apply_amount)).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
INSERT INTO credit_ledger |
|
|
|
|
( |
|
|
|
|
client_id, |
|
|
|
|
entry_type, |
|
|
|
|
amount, |
|
|
|
|
currency_code, |
|
|
|
|
reference_type, |
|
|
|
|
reference_id, |
|
|
|
|
notes |
|
|
|
|
) |
|
|
|
|
VALUES (%s, 'invoice_deduction', %s, 'CAD', 'invoice', %s, %s) |
|
|
|
|
""", ( |
|
|
|
|
client["id"], |
|
|
|
|
str(-apply_amount), |
|
|
|
|
invoice_id, |
|
|
|
|
f"Applied client credit to invoice {invoice.get('invoice_number') or invoice_id}", |
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
INSERT INTO payments |
|
|
|
|
( |
|
|
|
|
invoice_id, |
|
|
|
|
client_id, |
|
|
|
|
payment_method, |
|
|
|
|
payment_currency, |
|
|
|
|
payment_amount, |
|
|
|
|
cad_value_at_payment, |
|
|
|
|
expected_amount_cad, |
|
|
|
|
received_amount_cad, |
|
|
|
|
reference, |
|
|
|
|
payment_status, |
|
|
|
|
review_status, |
|
|
|
|
received_at, |
|
|
|
|
notes |
|
|
|
|
) |
|
|
|
|
VALUES (%s, %s, 'credit', 'CAD', %s, %s, %s, %s, %s, 'confirmed', 'approved', UTC_TIMESTAMP(), %s) |
|
|
|
|
""", ( |
|
|
|
|
invoice_id, |
|
|
|
|
client["id"], |
|
|
|
|
str(apply_amount), |
|
|
|
|
str(apply_amount), |
|
|
|
|
str(apply_amount), |
|
|
|
|
str(apply_amount), |
|
|
|
|
f"Applied client credit to {invoice.get('invoice_number') or invoice_id}", |
|
|
|
|
"portal_credit_apply", |
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
conn.commit() |
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
recalc_invoice_totals(invoice_id) |
|
|
|
|
|
|
|
|
|
return redirect(f"/portal/invoice/{invoice_id}?credit_applied={apply_amount}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/portal/terms", methods=["GET", "POST"]) |
|
|
|
|
def portal_terms(): |
|
|
|
|
client = _portal_current_client() |
|
|
|
|
|