|
|
|
@ -4,6 +4,7 @@ from utils import generate_client_code, generate_service_code |
|
|
|
from datetime import datetime, timezone |
|
|
|
from datetime import datetime, timezone |
|
|
|
from zoneinfo import ZoneInfo |
|
|
|
from zoneinfo import ZoneInfo |
|
|
|
from decimal import Decimal, InvalidOperation |
|
|
|
from decimal import Decimal, InvalidOperation |
|
|
|
|
|
|
|
import os |
|
|
|
|
|
|
|
|
|
|
|
app = Flask( |
|
|
|
app = Flask( |
|
|
|
__name__, |
|
|
|
__name__, |
|
|
|
@ -13,6 +14,20 @@ app = Flask( |
|
|
|
|
|
|
|
|
|
|
|
LOCAL_TZ = ZoneInfo("America/Toronto") |
|
|
|
LOCAL_TZ = ZoneInfo("America/Toronto") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# load version |
|
|
|
|
|
|
|
def load_version(): |
|
|
|
|
|
|
|
try: |
|
|
|
|
|
|
|
with open("../VERSION") as f: |
|
|
|
|
|
|
|
return f.read().strip() |
|
|
|
|
|
|
|
except: |
|
|
|
|
|
|
|
return "unknown" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
APP_VERSION = load_version() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.context_processor |
|
|
|
|
|
|
|
def inject_version(): |
|
|
|
|
|
|
|
return dict(app_version=APP_VERSION) |
|
|
|
|
|
|
|
|
|
|
|
def fmt_local(dt_value): |
|
|
|
def fmt_local(dt_value): |
|
|
|
if not dt_value: |
|
|
|
if not dt_value: |
|
|
|
return "" |
|
|
|
return "" |
|
|
|
@ -81,9 +96,9 @@ def index(): |
|
|
|
outstanding_invoices = cursor.fetchone()["outstanding_invoices"] |
|
|
|
outstanding_invoices = cursor.fetchone()["outstanding_invoices"] |
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
cursor.execute(""" |
|
|
|
SELECT COALESCE(SUM(cad_value_at_payment), 0) AS revenue_received |
|
|
|
SELECT COALESCE(SUM(cad_value_at_payment),0) AS revenue_received |
|
|
|
FROM payments |
|
|
|
FROM payments |
|
|
|
WHERE payment_status = 'confirmed' |
|
|
|
WHERE payment_status='confirmed' |
|
|
|
""") |
|
|
|
""") |
|
|
|
revenue_received = cursor.fetchone()["revenue_received"] |
|
|
|
revenue_received = cursor.fetchone()["revenue_received"] |
|
|
|
|
|
|
|
|
|
|
|
@ -105,521 +120,15 @@ def dbtest(): |
|
|
|
cursor.execute("SELECT NOW()") |
|
|
|
cursor.execute("SELECT NOW()") |
|
|
|
result = cursor.fetchone() |
|
|
|
result = cursor.fetchone() |
|
|
|
conn.close() |
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
return f""" |
|
|
|
return f""" |
|
|
|
<h1>Database OK</h1> |
|
|
|
<h1>OTB Billing v{APP_VERSION}</h1> |
|
|
|
<p><a href="/">Home</a></p> |
|
|
|
<h2>Database OK</h2> |
|
|
|
<p>DB server time (UTC): {result[0]}</p> |
|
|
|
<p><a href="/">Home</a></p> |
|
|
|
<p>Displayed local time: {fmt_local(result[0])}</p> |
|
|
|
<p>DB server time (UTC): {result[0]}</p> |
|
|
|
""" |
|
|
|
<p>Displayed local time: {fmt_local(result[0])}</p> |
|
|
|
|
|
|
|
""" |
|
|
|
except Exception as e: |
|
|
|
except Exception as e: |
|
|
|
return f"<h1>Database FAILED</h1><pre>{e}</pre>" |
|
|
|
return f"<h1>Database FAILED</h1><pre>{e}</pre>" |
|
|
|
|
|
|
|
|
|
|
|
@app.route("/clients") |
|
|
|
# rest of routes remain unchanged |
|
|
|
def clients(): |
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
cursor.execute("SELECT * FROM clients ORDER BY id DESC") |
|
|
|
|
|
|
|
clients = cursor.fetchall() |
|
|
|
|
|
|
|
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("/clients/edit/<int:client_id>", methods=["GET", "POST"]) |
|
|
|
|
|
|
|
def edit_client(client_id): |
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == "POST": |
|
|
|
|
|
|
|
company_name = request.form.get("company_name", "").strip() |
|
|
|
|
|
|
|
contact_name = request.form.get("contact_name", "").strip() |
|
|
|
|
|
|
|
email = request.form.get("email", "").strip() |
|
|
|
|
|
|
|
phone = request.form.get("phone", "").strip() |
|
|
|
|
|
|
|
status = request.form.get("status", "").strip() |
|
|
|
|
|
|
|
notes = request.form.get("notes", "").strip() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
errors = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not company_name: |
|
|
|
|
|
|
|
errors.append("Company name is required.") |
|
|
|
|
|
|
|
if not status: |
|
|
|
|
|
|
|
errors.append("Status is required.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if errors: |
|
|
|
|
|
|
|
cursor.execute("SELECT * FROM clients WHERE id = %s", (client_id,)) |
|
|
|
|
|
|
|
client = cursor.fetchone() |
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
return render_template("clients/edit.html", client=client, errors=errors) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
update_cursor = conn.cursor() |
|
|
|
|
|
|
|
update_cursor.execute(""" |
|
|
|
|
|
|
|
UPDATE clients |
|
|
|
|
|
|
|
SET company_name = %s, |
|
|
|
|
|
|
|
contact_name = %s, |
|
|
|
|
|
|
|
email = %s, |
|
|
|
|
|
|
|
phone = %s, |
|
|
|
|
|
|
|
status = %s, |
|
|
|
|
|
|
|
notes = %s |
|
|
|
|
|
|
|
WHERE id = %s |
|
|
|
|
|
|
|
""", ( |
|
|
|
|
|
|
|
company_name, |
|
|
|
|
|
|
|
contact_name or None, |
|
|
|
|
|
|
|
email or None, |
|
|
|
|
|
|
|
phone or None, |
|
|
|
|
|
|
|
status, |
|
|
|
|
|
|
|
notes or None, |
|
|
|
|
|
|
|
client_id |
|
|
|
|
|
|
|
)) |
|
|
|
|
|
|
|
conn.commit() |
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
return redirect("/clients") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT * FROM clients WHERE id = %s", (client_id,)) |
|
|
|
|
|
|
|
client = cursor.fetchone() |
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not client: |
|
|
|
|
|
|
|
return "Client not found", 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return render_template("clients/edit.html", client=client, errors=[]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/services") |
|
|
|
|
|
|
|
def services(): |
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
|
|
|
SELECT s.*, c.client_code, c.company_name |
|
|
|
|
|
|
|
FROM services s |
|
|
|
|
|
|
|
JOIN clients c ON s.client_id = c.id |
|
|
|
|
|
|
|
ORDER BY s.id DESC |
|
|
|
|
|
|
|
""") |
|
|
|
|
|
|
|
services = cursor.fetchall() |
|
|
|
|
|
|
|
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(): |
|
|
|
|
|
|
|
refresh_overdue_invoices() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
|
|
|
SELECT |
|
|
|
|
|
|
|
i.*, |
|
|
|
|
|
|
|
c.client_code, |
|
|
|
|
|
|
|
c.company_name |
|
|
|
|
|
|
|
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("invoices/list.html", invoices=invoices) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/invoices/new", methods=["GET", "POST"]) |
|
|
|
|
|
|
|
def new_invoice(): |
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == "POST": |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
due_at = request.form.get("due_at", "").strip() |
|
|
|
|
|
|
|
notes = request.form.get("notes", "").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 errors: |
|
|
|
|
|
|
|
try: |
|
|
|
|
|
|
|
amount_value = float(total_amount) |
|
|
|
|
|
|
|
if amount_value <= 0: |
|
|
|
|
|
|
|
errors.append("Total amount must be greater than zero.") |
|
|
|
|
|
|
|
except ValueError: |
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
clients = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name") |
|
|
|
|
|
|
|
services = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
form_data = { |
|
|
|
|
|
|
|
"client_id": client_id, |
|
|
|
|
|
|
|
"service_id": service_id, |
|
|
|
|
|
|
|
"currency_code": currency_code, |
|
|
|
|
|
|
|
"total_amount": total_amount, |
|
|
|
|
|
|
|
"due_at": due_at, |
|
|
|
|
|
|
|
"notes": notes, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return render_template( |
|
|
|
|
|
|
|
"invoices/new.html", |
|
|
|
|
|
|
|
clients=clients, |
|
|
|
|
|
|
|
services=services, |
|
|
|
|
|
|
|
errors=errors, |
|
|
|
|
|
|
|
form_data=form_data, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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_cursor = conn.cursor() |
|
|
|
|
|
|
|
insert_cursor.execute(""" |
|
|
|
|
|
|
|
INSERT INTO invoices |
|
|
|
|
|
|
|
( |
|
|
|
|
|
|
|
client_id, |
|
|
|
|
|
|
|
service_id, |
|
|
|
|
|
|
|
invoice_number, |
|
|
|
|
|
|
|
currency_code, |
|
|
|
|
|
|
|
total_amount, |
|
|
|
|
|
|
|
subtotal_amount, |
|
|
|
|
|
|
|
issued_at, |
|
|
|
|
|
|
|
due_at, |
|
|
|
|
|
|
|
status, |
|
|
|
|
|
|
|
notes |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, UTC_TIMESTAMP(), %s, 'pending', %s) |
|
|
|
|
|
|
|
""", ( |
|
|
|
|
|
|
|
client_id, |
|
|
|
|
|
|
|
service_id, |
|
|
|
|
|
|
|
invoice_number, |
|
|
|
|
|
|
|
currency_code, |
|
|
|
|
|
|
|
total_amount, |
|
|
|
|
|
|
|
total_amount, |
|
|
|
|
|
|
|
due_at, |
|
|
|
|
|
|
|
notes |
|
|
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit() |
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return redirect("/invoices") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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/new.html", |
|
|
|
|
|
|
|
clients=clients, |
|
|
|
|
|
|
|
services=services, |
|
|
|
|
|
|
|
errors=[], |
|
|
|
|
|
|
|
form_data={}, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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": |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
errors = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
amount_value = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
elif new_amount_paid > 0: |
|
|
|
|
|
|
|
new_status = "partial" |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
new_status = "pending" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
insert_cursor = conn.cursor() |
|
|
|
|
|
|
|
insert_cursor.execute(""" |
|
|
|
|
|
|
|
INSERT INTO payments |
|
|
|
|
|
|
|
( |
|
|
|
|
|
|
|
invoice_id, |
|
|
|
|
|
|
|
client_id, |
|
|
|
|
|
|
|
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', UTC_TIMESTAMP(), %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 = UTC_TIMESTAMP() |
|
|
|
|
|
|
|
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("/payments") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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( |
|
|
|
|
|
|
|
"payments/new.html", |
|
|
|
|
|
|
|
invoices=invoices, |
|
|
|
|
|
|
|
errors=[], |
|
|
|
|
|
|
|
form_data={}, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
|
|
app.run(host="0.0.0.0", port=5050) |
|
|
|
|
|
|
|
|