Browse Source

Add v0.1.6 invoice edit page with payment lock behavior

main
def 2 weeks ago
parent
commit
be1b1a790f
  1. 2
      VERSION
  2. 136
      backend/app.py
  3. 113
      templates/invoices/edit.html
  4. 7
      templates/invoices/list.html

2
VERSION

@ -1 +1 @@
0.1.5
0.1.6

136
backend/app.py

@ -415,7 +415,8 @@ def invoices():
SELECT
i.*,
c.client_code,
c.company_name
c.company_name,
COALESCE((SELECT COUNT(*) FROM payments p WHERE p.invoice_id = i.id), 0) AS payment_count
FROM invoices i
JOIN clients c ON i.client_id = c.id
ORDER BY i.id DESC
@ -537,6 +538,139 @@ def new_invoice():
form_data={},
)
@app.route("/invoices/edit/<int:invoice_id>", methods=["GET", "POST"])
def edit_invoice(invoice_id):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT i.*,
COALESCE((SELECT COUNT(*) FROM payments p WHERE p.invoice_id = i.id), 0) AS payment_count
FROM invoices i
WHERE i.id = %s
""", (invoice_id,))
invoice = cursor.fetchone()
if not invoice:
conn.close()
return "Invoice not found", 404
locked = invoice["payment_count"] > 0 or float(invoice["amount_paid"]) > 0
if request.method == "POST":
due_at = request.form.get("due_at", "").strip()
notes = request.form.get("notes", "").strip()
if locked:
status = request.form.get("status", "").strip()
if not status:
conn.close()
return render_template("invoices/edit.html", invoice=invoice, clients=[], services=[], errors=["Status is required."], locked=locked)
update_cursor = conn.cursor()
update_cursor.execute("""
UPDATE invoices
SET due_at = %s,
status = %s,
notes = %s
WHERE id = %s
""", (
due_at or None,
status,
notes or None,
invoice_id
))
conn.commit()
conn.close()
return redirect("/invoices")
client_id = request.form.get("client_id", "").strip()
service_id = request.form.get("service_id", "").strip()
currency_code = request.form.get("currency_code", "").strip()
total_amount = request.form.get("total_amount", "").strip()
status = request.form.get("status", "").strip()
errors = []
if not client_id:
errors.append("Client is required.")
if not service_id:
errors.append("Service is required.")
if not currency_code:
errors.append("Currency is required.")
if not total_amount:
errors.append("Total amount is required.")
if not due_at:
errors.append("Due date is required.")
if not status:
errors.append("Status is required.")
if not errors:
try:
amount_value = float(total_amount)
if amount_value < 0:
errors.append("Total amount cannot be negative.")
except ValueError:
errors.append("Total amount must be a valid number.")
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name")
clients = cursor.fetchall()
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name")
services = cursor.fetchall()
if errors:
invoice["client_id"] = int(client_id) if client_id else invoice["client_id"]
invoice["service_id"] = int(service_id) if service_id else invoice["service_id"]
invoice["currency_code"] = currency_code or invoice["currency_code"]
invoice["total_amount"] = total_amount or invoice["total_amount"]
invoice["due_at"] = due_at or invoice["due_at"]
invoice["status"] = status or invoice["status"]
invoice["notes"] = notes
conn.close()
return render_template("invoices/edit.html", invoice=invoice, clients=clients, services=services, errors=errors, locked=locked)
update_cursor = conn.cursor()
update_cursor.execute("""
UPDATE invoices
SET client_id = %s,
service_id = %s,
currency_code = %s,
total_amount = %s,
subtotal_amount = %s,
due_at = %s,
status = %s,
notes = %s
WHERE id = %s
""", (
client_id,
service_id,
currency_code,
total_amount,
total_amount,
due_at,
status,
notes or None,
invoice_id
))
conn.commit()
conn.close()
return redirect("/invoices")
clients = []
services = []
if not locked:
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name")
clients = cursor.fetchall()
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name")
services = cursor.fetchall()
conn.close()
return render_template("invoices/edit.html", invoice=invoice, clients=clients, services=services, errors=[], locked=locked)
@app.route("/payments")
def payments():
conn = get_db_connection()

113
templates/invoices/edit.html

@ -0,0 +1,113 @@
<!doctype html>
<html>
<head>
<title>Edit Invoice</title>
</head>
<body>
<h1>Edit Invoice</h1>
<p><a href="/">Home</a></p>
<p><a href="/invoices">Back to Invoices</a></p>
{% 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 %}
{% if locked %}
<div style="border:1px solid #aa6600; padding:10px; margin-bottom:15px; background:#fff4dd;">
<strong>This invoice is locked for core edits because payments exist.</strong><br>
Core accounting fields cannot be changed after payment activity begins.
</div>
{% endif %}
<form method="post">
<p>
Invoice Number<br>
<input value="{{ invoice.invoice_number }}" readonly>
</p>
{% if not locked %}
<p>
Client *<br>
<select name="client_id" required>
{% for c in clients %}
<option value="{{ c.id }}" {% if invoice.client_id == c.id %}selected{% endif %}>
{{ c.client_code }} - {{ c.company_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Service *<br>
<select name="service_id" required>
{% for s in services %}
<option value="{{ s.id }}" {% if invoice.service_id == s.id %}selected{% endif %}>
{{ s.service_code }} - {{ s.service_name }}
</option>
{% endfor %}
</select>
</p>
<p>
Currency *<br>
<select name="currency_code" required>
<option value="CAD" {% if invoice.currency_code == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if invoice.currency_code == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if invoice.currency_code == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if invoice.currency_code == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Total Amount *<br>
<input type="number" step="0.00000001" min="0" name="total_amount" value="{{ invoice.total_amount }}" required>
</p>
{% else %}
<p>Client<br><input value="{{ invoice.client_id }}" readonly></p>
<p>Service<br><input value="{{ invoice.service_id }}" readonly></p>
<p>Currency<br><input value="{{ invoice.currency_code }}" readonly></p>
<p>Total Amount<br><input value="{{ invoice.total_amount|money(invoice.currency_code) }}" readonly></p>
{% endif %}
<p>
Due Date *<br>
<input type="date" name="due_at" value="{{ invoice.due_at.strftime('%Y-%m-%d') if invoice.due_at else '' }}" required>
</p>
<p>
Status *<br>
<select name="status" required>
<option value="draft" {% if invoice.status == 'draft' %}selected{% endif %}>draft</option>
<option value="pending" {% if invoice.status == 'pending' %}selected{% endif %}>pending</option>
<option value="partial" {% if invoice.status == 'partial' %}selected{% endif %}>partial</option>
<option value="paid" {% if invoice.status == 'paid' %}selected{% endif %}>paid</option>
<option value="overdue" {% if invoice.status == 'overdue' %}selected{% endif %}>overdue</option>
<option value="cancelled" {% if invoice.status == 'cancelled' %}selected{% endif %}>cancelled</option>
</select>
</p>
<p>
Notes<br>
<textarea name="notes" rows="5" cols="60">{{ invoice.notes or '' }}</textarea>
</p>
<p>
<button type="submit">Save Invoice</button>
</p>
</form>
{% include "footer.html" %}
</body>
</html>

7
templates/invoices/list.html

@ -23,6 +23,7 @@
<th>Status</th>
<th>Issued</th>
<th>Due</th>
<th>Actions</th>
</tr>
{% for i in invoices %}
@ -37,6 +38,12 @@
<td>{{ i.status }}</td>
<td>{{ i.issued_at|localtime }}</td>
<td>{{ i.due_at|localtime }}</td>
<td>
<a href="/invoices/edit/{{ i.id }}">Edit</a>
{% if i.payment_count > 0 %}
(Locked)
{% endif %}
</td>
</tr>
{% endfor %}

Loading…
Cancel
Save