Browse Source

Add v0.1.0 payment recording and invoice status updates

main
def 2 weeks ago
parent
commit
15b9e37b0d
  1. 2
      VERSION
  2. 384
      backend/app.py
  3. 1
      templates/dashboard.html
  4. 43
      templates/payments/list.html
  5. 102
      templates/payments/new.html

2
VERSION

@ -1 +1 @@
0.0.9
0.1.0

384
backend/app.py

@ -27,10 +27,9 @@ def index():
outstanding_invoices = cursor.fetchone()["outstanding_invoices"]
cursor.execute("""
SELECT COALESCE(SUM(payment_amount), 0) AS revenue_received
SELECT COALESCE(SUM(cad_value_at_payment), 0) AS revenue_received
FROM payments
WHERE payment_status = 'confirmed'
AND payment_currency = 'CAD'
""")
revenue_received = cursor.fetchone()["revenue_received"]
@ -65,6 +64,38 @@ def clients():
conn.close()
return render_template("clients/list.html", clients=clients)
@app.route("/clients/new", methods=["GET", "POST"])
def new_client():
if request.method == "POST":
company_name = request.form["company_name"]
contact_name = request.form["contact_name"]
email = request.form["email"]
phone = request.form["phone"]
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT MAX(id) AS last_id FROM clients")
result = cursor.fetchone()
last_number = result["last_id"] if result["last_id"] else 0
client_code = generate_client_code(company_name, last_number)
insert_cursor = conn.cursor()
insert_cursor.execute(
"""
INSERT INTO clients
(client_code, company_name, contact_name, email, phone)
VALUES (%s, %s, %s, %s, %s)
""",
(client_code, company_name, contact_name, email, phone)
)
conn.commit()
conn.close()
return redirect("/clients")
return render_template("clients/new.html")
@app.route("/services")
def services():
conn = get_db_connection()
@ -79,6 +110,68 @@ def services():
conn.close()
return render_template("services/list.html", services=services)
@app.route("/services/new", methods=["GET", "POST"])
def new_service():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
client_id = request.form["client_id"]
service_name = request.form["service_name"]
service_type = request.form["service_type"]
billing_cycle = request.form["billing_cycle"]
currency_code = request.form["currency_code"]
recurring_amount = request.form["recurring_amount"]
status = request.form["status"]
start_date = request.form["start_date"] or None
description = request.form["description"]
cursor.execute("SELECT MAX(id) AS last_id FROM services")
result = cursor.fetchone()
last_number = result["last_id"] if result["last_id"] else 0
service_code = generate_service_code(service_name, last_number)
insert_cursor = conn.cursor()
insert_cursor.execute(
"""
INSERT INTO services
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
)
conn.commit()
conn.close()
return redirect("/services")
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
conn.close()
return render_template("services/new.html", clients=clients)
@app.route("/invoices")
def invoices():
conn = get_db_connection()
@ -131,18 +224,10 @@ def new_invoice():
errors.append("Total amount must be a valid number.")
if errors:
cursor.execute("""
SELECT id, client_code, company_name
FROM clients
ORDER BY company_name
""")
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
""")
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name")
services = cursor.fetchall()
conn.close()
@ -166,12 +251,11 @@ def new_invoice():
cursor.execute("SELECT MAX(id) AS last_id FROM invoices")
result = cursor.fetchone()
number = (result["last_id"] or 0) + 1
invoice_number = f"INV-{number:04d}"
insert = conn.cursor()
insert.execute("""
insert_cursor = conn.cursor()
insert_cursor.execute("""
INSERT INTO invoices
(
client_id,
@ -202,18 +286,10 @@ def new_invoice():
return redirect("/invoices")
cursor.execute("""
SELECT id, client_code, company_name
FROM clients
ORDER BY company_name
""")
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
""")
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name")
services = cursor.fetchall()
conn.close()
@ -226,99 +302,209 @@ def new_invoice():
form_data={},
)
@app.route("/clients/new", methods=["GET", "POST"])
def new_client():
@app.route("/payments")
def payments():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT
p.*,
i.invoice_number,
c.client_code,
c.company_name
FROM payments p
JOIN invoices i ON p.invoice_id = i.id
JOIN clients c ON p.client_id = c.id
ORDER BY p.id DESC
""")
payments = cursor.fetchall()
conn.close()
return render_template("payments/list.html", payments=payments)
@app.route("/payments/new", methods=["GET", "POST"])
def new_payment():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if request.method == "POST":
company_name = request.form["company_name"]
contact_name = request.form["contact_name"]
email = request.form["email"]
phone = request.form["phone"]
invoice_id = request.form.get("invoice_id", "").strip()
payment_method = request.form.get("payment_method", "").strip()
payment_currency = request.form.get("payment_currency", "").strip()
payment_amount = request.form.get("payment_amount", "").strip()
cad_value_at_payment = request.form.get("cad_value_at_payment", "").strip()
reference = request.form.get("reference", "").strip()
sender_name = request.form.get("sender_name", "").strip()
txid = request.form.get("txid", "").strip()
wallet_address = request.form.get("wallet_address", "").strip()
notes = request.form.get("notes", "").strip()
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT MAX(id) AS last_id FROM clients")
result = cursor.fetchone()
last_number = result["last_id"] if result["last_id"] else 0
errors = []
client_code = generate_client_code(company_name, last_number)
if not invoice_id:
errors.append("Invoice is required.")
if not payment_method:
errors.append("Payment method is required.")
if not payment_currency:
errors.append("Payment currency is required.")
if not payment_amount:
errors.append("Payment amount is required.")
if not cad_value_at_payment:
errors.append("CAD value at payment is required.")
cursor = conn.cursor()
cursor.execute(
"""
INSERT INTO clients
(client_code, company_name, contact_name, email, phone)
VALUES (%s, %s, %s, %s, %s)
""",
(client_code, company_name, contact_name, email, phone)
)
conn.commit()
conn.close()
amount_value = None
cad_value = None
return redirect("/clients")
if not errors:
try:
amount_value = float(payment_amount)
if amount_value <= 0:
errors.append("Payment amount must be greater than zero.")
except ValueError:
errors.append("Payment amount must be a valid number.")
return render_template("clients/new.html")
try:
cad_value = float(cad_value_at_payment)
if cad_value < 0:
errors.append("CAD value at payment cannot be negative.")
except ValueError:
errors.append("CAD value at payment must be a valid number.")
@app.route("/services/new", methods=["GET", "POST"])
def new_service():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
if errors:
cursor.execute("""
SELECT
i.id,
i.invoice_number,
c.client_code,
c.company_name,
i.total_amount,
i.amount_paid,
i.currency_code
FROM invoices i
JOIN clients c ON i.client_id = c.id
ORDER BY i.id DESC
""")
invoices = cursor.fetchall()
conn.close()
if request.method == "POST":
client_id = request.form["client_id"]
service_name = request.form["service_name"]
service_type = request.form["service_type"]
billing_cycle = request.form["billing_cycle"]
currency_code = request.form["currency_code"]
recurring_amount = request.form["recurring_amount"]
status = request.form["status"]
start_date = request.form["start_date"] or None
description = request.form["description"]
form_data = {
"invoice_id": invoice_id,
"payment_method": payment_method,
"payment_currency": payment_currency,
"payment_amount": payment_amount,
"cad_value_at_payment": cad_value_at_payment,
"reference": reference,
"sender_name": sender_name,
"txid": txid,
"wallet_address": wallet_address,
"notes": notes,
}
cursor.execute("SELECT MAX(id) AS last_id FROM services")
result = cursor.fetchone()
last_number = result["last_id"] if result["last_id"] else 0
service_code = generate_service_code(service_name, last_number)
return render_template(
"payments/new.html",
invoices=invoices,
errors=errors,
form_data=form_data,
)
cursor.execute("SELECT client_id, total_amount, amount_paid FROM invoices WHERE id = %s", (invoice_id,))
invoice = cursor.fetchone()
if not invoice:
conn.close()
return "Invoice not found", 404
client_id = invoice["client_id"]
new_amount_paid = float(invoice["amount_paid"]) + amount_value
if new_amount_paid >= float(invoice["total_amount"]):
new_status = "paid"
paid_at_sql = "NOW()"
elif new_amount_paid > 0:
new_status = "partial"
paid_at_sql = "NULL"
else:
new_status = "pending"
paid_at_sql = "NULL"
insert_cursor = conn.cursor()
insert_cursor.execute(
"""
INSERT INTO services
insert_cursor.execute("""
INSERT INTO payments
(
invoice_id,
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
client_id,
service_code,
service_name,
service_type,
billing_cycle,
status,
currency_code,
recurring_amount,
start_date,
description
payment_method,
payment_currency,
payment_amount,
cad_value_at_payment,
reference,
sender_name,
txid,
wallet_address,
payment_status,
received_at,
notes
)
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'confirmed', NOW(), %s)
""", (
invoice_id,
client_id,
payment_method,
payment_currency,
payment_amount,
cad_value_at_payment,
reference or None,
sender_name or None,
txid or None,
wallet_address or None,
notes or None
))
update_cursor = conn.cursor()
if new_status == "paid":
update_cursor.execute("""
UPDATE invoices
SET amount_paid = %s,
status = %s,
paid_at = NOW()
WHERE id = %s
""", (new_amount_paid, new_status, invoice_id))
else:
update_cursor.execute("""
UPDATE invoices
SET amount_paid = %s,
status = %s
WHERE id = %s
""", (new_amount_paid, new_status, invoice_id))
conn.commit()
conn.close()
return redirect("/services")
return redirect("/payments")
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC")
clients = cursor.fetchall()
cursor.execute("""
SELECT
i.id,
i.invoice_number,
c.client_code,
c.company_name,
i.total_amount,
i.amount_paid,
i.currency_code
FROM invoices i
JOIN clients c ON i.client_id = c.id
ORDER BY i.id DESC
""")
invoices = cursor.fetchall()
conn.close()
return render_template("services/new.html", clients=clients)
return render_template(
"payments/new.html",
invoices=invoices,
errors=[],
form_data={},
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5050)

1
templates/dashboard.html

@ -10,6 +10,7 @@
<p><a href="/clients">Clients</a></p>
<p><a href="/services">Services</a></p>
<p><a href="/invoices">Invoices</a></p>
<p><a href="/payments">Payments</a></p>
<p><a href="/dbtest">DB Test</a></p>
<table border="1" cellpadding="10">

43
templates/payments/list.html

@ -0,0 +1,43 @@
<!doctype html>
<html>
<head>
<title>Payments</title>
</head>
<body>
<h1>Payments</h1>
<p><a href="/">Home</a></p>
<p><a href="/payments/new">Record Payment</a></p>
<table border="1" cellpadding="6">
<tr>
<th>ID</th>
<th>Invoice</th>
<th>Client</th>
<th>Method</th>
<th>Currency</th>
<th>Amount</th>
<th>CAD Value</th>
<th>Reference</th>
<th>Received</th>
</tr>
{% for p in payments %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.invoice_number }}</td>
<td>{{ p.client_code }} - {{ p.company_name }}</td>
<td>{{ p.payment_method }}</td>
<td>{{ p.payment_currency }}</td>
<td>{{ p.payment_amount }}</td>
<td>{{ p.cad_value_at_payment }}</td>
<td>{{ p.reference }}</td>
<td>{{ p.received_at }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

102
templates/payments/new.html

@ -0,0 +1,102 @@
<!doctype html>
<html>
<head>
<title>New Payment</title>
</head>
<body>
<h1>Record Payment</h1>
{% 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>
Invoice *<br>
<select name="invoice_id" required>
<option value="">Select invoice</option>
{% for i in invoices %}
<option value="{{ i.id }}" {% if form_data.get('invoice_id') == (i.id|string) %}selected{% endif %}>
{{ i.invoice_number }} - {{ i.client_code }} - {{ i.company_name }} - Due {{ i.total_amount - i.amount_paid }} {{ i.currency_code }}
</option>
{% endfor %}
</select>
</p>
<p>
Payment Method *<br>
<select name="payment_method" required>
<option value="">Select method</option>
<option value="square" {% if form_data.get('payment_method') == 'square' %}selected{% endif %}>square</option>
<option value="etransfer" {% if form_data.get('payment_method') == 'etransfer' %}selected{% endif %}>etransfer</option>
<option value="crypto_etho" {% if form_data.get('payment_method') == 'crypto_etho' %}selected{% endif %}>crypto_etho</option>
<option value="crypto_egaz" {% if form_data.get('payment_method') == 'crypto_egaz' %}selected{% endif %}>crypto_egaz</option>
<option value="crypto_alt" {% if form_data.get('payment_method') == 'crypto_alt' %}selected{% endif %}>crypto_alt</option>
<option value="cash" {% if form_data.get('payment_method') == 'cash' %}selected{% endif %}>cash</option>
<option value="other" {% if form_data.get('payment_method') == 'other' %}selected{% endif %}>other</option>
</select>
</p>
<p>
Payment Currency *<br>
<select name="payment_currency" required>
<option value="">Select currency</option>
<option value="CAD" {% if form_data.get('payment_currency') == 'CAD' %}selected{% endif %}>CAD</option>
<option value="ETHO" {% if form_data.get('payment_currency') == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if form_data.get('payment_currency') == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if form_data.get('payment_currency') == 'ALT' %}selected{% endif %}>ALT</option>
</select>
</p>
<p>
Payment Amount *<br>
<input type="number" step="0.00000001" min="0.00000001" name="payment_amount" value="{{ form_data.get('payment_amount', '') }}" required>
</p>
<p>
CAD Value At Payment *<br>
<input type="number" step="0.00000001" min="0" name="cad_value_at_payment" value="{{ form_data.get('cad_value_at_payment', '') }}" required>
</p>
<p>
Reference<br>
<input name="reference" value="{{ form_data.get('reference', '') }}">
</p>
<p>
Sender Name<br>
<input name="sender_name" value="{{ form_data.get('sender_name', '') }}">
</p>
<p>
TXID<br>
<input name="txid" value="{{ form_data.get('txid', '') }}">
</p>
<p>
Wallet Address<br>
<input name="wallet_address" value="{{ form_data.get('wallet_address', '') }}">
</p>
<p>
Notes<br>
<textarea name="notes">{{ form_data.get('notes', '') }}</textarea>
</p>
<p>
<button type="submit">Record Payment</button>
</p>
</form>
</body>
</html>
Loading…
Cancel
Save