From 6826e12085efde960da81c3e383076985e53b7ff Mon Sep 17 00:00:00 2001 From: def Date: Sun, 8 Mar 2026 20:17:24 +0000 Subject: [PATCH] Add v0.2.0 client credit ledger --- VERSION | 2 +- backend/app.py | 139 +++++++++++++++++++++++++++++++++--- templates/clients/list.html | 5 +- templates/credits/add.html | 69 ++++++++++++++++++ templates/credits/list.html | 48 +++++++++++++ 5 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 templates/credits/add.html create mode 100644 templates/credits/list.html diff --git a/VERSION b/VERSION index 1180819..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.7 +0.2.0 diff --git a/backend/app.py b/backend/app.py index 21ea6fb..81c200a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -70,7 +70,7 @@ def recalc_invoice_totals(invoice_id): cursor = conn.cursor(dictionary=True) cursor.execute(""" - SELECT total_amount, due_at + SELECT id, total_amount, due_at FROM invoices WHERE id = %s """, (invoice_id,)) @@ -86,35 +86,52 @@ def recalc_invoice_totals(invoice_id): WHERE invoice_id = %s AND payment_status = 'confirmed' """, (invoice_id,)) - total_paid = float(cursor.fetchone()["total_paid"]) + row = cursor.fetchone() - total_amount = float(invoice["total_amount"]) + total_paid = to_decimal(row["total_paid"]) + total_amount = to_decimal(invoice["total_amount"]) if total_paid >= total_amount and total_amount > 0: new_status = "paid" - paid_at_clause = ", paid_at = UTC_TIMESTAMP()" + paid_at_value = "UTC_TIMESTAMP()" elif total_paid > 0: new_status = "partial" - paid_at_clause = ", paid_at = NULL" + paid_at_value = "NULL" else: if invoice["due_at"] and invoice["due_at"] < datetime.utcnow(): new_status = "overdue" else: new_status = "pending" - paid_at_clause = ", paid_at = NULL" + paid_at_value = "NULL" update_cursor = conn.cursor() update_cursor.execute(f""" UPDATE invoices SET amount_paid = %s, - status = %s - {paid_at_clause} + status = %s, + paid_at = {paid_at_value} WHERE id = %s - """, (total_paid, new_status, invoice_id)) + """, ( + str(total_paid), + new_status, + invoice_id + )) conn.commit() conn.close() +def get_client_credit_balance(client_id): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT COALESCE(SUM(amount), 0) AS balance + FROM credit_ledger + WHERE client_id = %s + """, (client_id,)) + row = cursor.fetchone() + conn.close() + return to_decimal(row["balance"]) + @app.template_filter("localtime") def localtime_filter(value): return fmt_local(value) @@ -277,6 +294,110 @@ def edit_client(client_id): return render_template("clients/edit.html", client=client, errors=[]) +@app.route("/credits/") +def client_credits(client_id): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + cursor.execute(""" + SELECT id, client_code, company_name + FROM clients + WHERE id = %s + """, (client_id,)) + client = cursor.fetchone() + + if not client: + conn.close() + return "Client not found", 404 + + cursor.execute(""" + SELECT * + FROM credit_ledger + WHERE client_id = %s + ORDER BY id DESC + """, (client_id,)) + entries = cursor.fetchall() + + conn.close() + + balance = get_client_credit_balance(client_id) + + return render_template( + "credits/list.html", + client=client, + entries=entries, + balance=balance, + ) + +@app.route("/credits/add/", methods=["GET", "POST"]) +def add_credit(client_id): + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + cursor.execute(""" + SELECT id, client_code, company_name + FROM clients + WHERE id = %s + """, (client_id,)) + client = cursor.fetchone() + + if not client: + conn.close() + return "Client not found", 404 + + if request.method == "POST": + entry_type = request.form.get("entry_type", "").strip() + amount = request.form.get("amount", "").strip() + currency_code = request.form.get("currency_code", "").strip() + notes = request.form.get("notes", "").strip() + + errors = [] + + if not entry_type: + errors.append("Entry type is required.") + if not amount: + errors.append("Amount is required.") + if not currency_code: + errors.append("Currency code is required.") + + if not errors: + try: + amount_value = Decimal(str(amount)) + if amount_value == 0: + errors.append("Amount cannot be zero.") + except Exception: + errors.append("Amount must be a valid number.") + + if errors: + conn.close() + return render_template("credits/add.html", client=client, errors=errors) + + insert_cursor = conn.cursor() + insert_cursor.execute(""" + INSERT INTO credit_ledger + ( + client_id, + entry_type, + amount, + currency_code, + notes + ) + VALUES (%s, %s, %s, %s, %s) + """, ( + client_id, + entry_type, + amount, + currency_code, + notes or None + )) + conn.commit() + conn.close() + + return redirect(f"/credits/{client_id}") + + conn.close() + return render_template("credits/add.html", client=client, errors=[]) + @app.route("/services") def services(): conn = get_db_connection() diff --git a/templates/clients/list.html b/templates/clients/list.html index ef81b19..6a09262 100644 --- a/templates/clients/list.html +++ b/templates/clients/list.html @@ -32,7 +32,10 @@ {{ c.email }} {{ c.phone }} {{ c.status }} -Edit + + Edit | + Ledger + {% endfor %} diff --git a/templates/credits/add.html b/templates/credits/add.html new file mode 100644 index 0000000..7774c78 --- /dev/null +++ b/templates/credits/add.html @@ -0,0 +1,69 @@ + + + +Add Credit + + + +

Add Credit

+ +

Home

+

Back to Credit Ledger

+ +

{{ client.client_code }} - {{ client.company_name }}

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +
+ +

+Entry Type *
+ +

+ +

+Amount *
+ +

+ +

+Currency Code *
+ +

+ +

+Notes
+ +

+ +

+ +

+ +
+ +{% include "footer.html" %} + + diff --git a/templates/credits/list.html b/templates/credits/list.html new file mode 100644 index 0000000..4cdd0e3 --- /dev/null +++ b/templates/credits/list.html @@ -0,0 +1,48 @@ + + + +Client Credit Ledger + + + +

Client Credit Ledger

+ +

Home

+

Back to Clients

+ +

{{ client.client_code }} - {{ client.company_name }}

+ +

Current Balance: {{ balance|money('CAD') }}

+ +

Add Credit

+ + + + + + + + + + + + + +{% for e in entries %} + + + + + + + + + + +{% endfor %} + +
IDTypeAmountCurrencyReference TypeReference IDNotesCreated
{{ e.id }}{{ e.entry_type }}{{ e.amount|money(e.currency_code) }}{{ e.currency_code }}{{ e.reference_type }}{{ e.reference_id }}{{ e.notes }}{{ e.created_at|localtime }}
+ +{% include "footer.html" %} + +