Browse Source

Add v0.2.0 client credit ledger

main
def 2 weeks ago
parent
commit
6826e12085
  1. 2
      VERSION
  2. 139
      backend/app.py
  3. 5
      templates/clients/list.html
  4. 69
      templates/credits/add.html
  5. 48
      templates/credits/list.html

2
VERSION

@ -1 +1 @@
0.1.7
0.2.0

139
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/<int:client_id>")
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/<int:client_id>", 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()

5
templates/clients/list.html

@ -32,7 +32,10 @@
<td>{{ c.email }}</td>
<td>{{ c.phone }}</td>
<td>{{ c.status }}</td>
<td><a href="/clients/edit/{{ c.id }}">Edit</a></td>
<td>
<a href="/clients/edit/{{ c.id }}">Edit</a> |
<a href="/credits/{{ c.id }}">Ledger</a>
</td>
</tr>
{% endfor %}

69
templates/credits/add.html

@ -0,0 +1,69 @@
<!doctype html>
<html>
<head>
<title>Add Credit</title>
</head>
<body>
<h1>Add Credit</h1>
<p><a href="/">Home</a></p>
<p><a href="/credits/{{ client.id }}">Back to Credit Ledger</a></p>
<h3>{{ client.client_code }} - {{ client.company_name }}</h3>
{% if errors %}
<div style="border:1px solid red; padding:10px; margin-bottom:15px;">
<strong>Please fix the following:</strong>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post">
<p>
Entry Type *<br>
<select name="entry_type" required>
<option value="manual_credit">manual_credit</option>
<option value="payment_credit">payment_credit</option>
<option value="invoice_deduction">invoice_deduction</option>
<option value="refund">refund</option>
<option value="adjustment">adjustment</option>
</select>
</p>
<p>
Amount *<br>
<input type="number" step="0.00000001" name="amount" required>
</p>
<p>
Currency Code *<br>
<select name="currency_code" required>
<option value="CAD">CAD</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="ETHO">ETHO</option>
<option value="EGAZ">EGAZ</option>
<option value="ALT">ALT</option>
</select>
</p>
<p>
Notes<br>
<textarea name="notes" rows="5" cols="60"></textarea>
</p>
<p>
<button type="submit">Add Credit Entry</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>

48
templates/credits/list.html

@ -0,0 +1,48 @@
<!doctype html>
<html>
<head>
<title>Client Credit Ledger</title>
</head>
<body>
<h1>Client Credit Ledger</h1>
<p><a href="/">Home</a></p>
<p><a href="/clients">Back to Clients</a></p>
<h3>{{ client.client_code }} - {{ client.company_name }}</h3>
<p><strong>Current Balance:</strong> {{ balance|money('CAD') }}</p>
<p><a href="/credits/add/{{ client.id }}">Add Credit</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Type</th>
<th>Amount</th>
<th>Currency</th>
<th>Reference Type</th>
<th>Reference ID</th>
<th>Notes</th>
<th>Created</th>
</tr>
{% for e in entries %}
<tr>
<td>{{ e.id }}</td>
<td>{{ e.entry_type }}</td>
<td>{{ e.amount|money(e.currency_code) }}</td>
<td>{{ e.currency_code }}</td>
<td>{{ e.reference_type }}</td>
<td>{{ e.reference_id }}</td>
<td>{{ e.notes }}</td>
<td>{{ e.created_at|localtime }}</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}
</body>
</html>
Loading…
Cancel
Save