Files
handoff-pro/scripts/invoice_mgmt.py

82 lines
4.3 KiB
Python

"""Invoice management: create, track, sync to JobTread + QBO."""
import json, sys, os
from datetime import datetime, timedelta
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from lib.db import get_db, new_id, now, row_to_dict, rows_to_list
def create(estimate_id, due_date=None):
conn = get_db()
est = row_to_dict(conn.execute("SELECT * FROM estimates WHERE id=?", (estimate_id,)).fetchone())
if not est: conn.close(); return {"error": "estimate not found"}
# Generate invoice number
count = conn.execute("SELECT COUNT(*) as c FROM invoices").fetchone()["c"]
inv_num = f"KC-{count + 1001}"
if not due_date:
due_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
inv_id = new_id()
conn.execute("""INSERT INTO invoices (id, estimate_id, job_id, invoice_number, amount_due, due_date, status, created_at)
VALUES (?,?,?,?,?,?,?,?)""",
(inv_id, estimate_id, est["job_id"], inv_num, est["total_cost"], due_date, "draft", now()))
conn.commit(); conn.close()
return {"id": inv_id, "invoice_number": inv_num, "amount_due": est["total_cost"], "due_date": due_date, "status": "draft"}
def list_invoices(job_id=None):
conn = get_db()
if job_id:
rows = conn.execute("SELECT * FROM invoices WHERE job_id=? ORDER BY created_at DESC", (job_id,)).fetchall()
else:
rows = conn.execute("SELECT * FROM invoices ORDER BY created_at DESC").fetchall()
conn.close()
return {"invoices": rows_to_list(rows)}
def get(invoice_id):
conn = get_db()
inv = row_to_dict(conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone())
if not inv: conn.close(); return {"error": "invoice not found"}
est = row_to_dict(conn.execute("SELECT * FROM estimates WHERE id=?", (inv["estimate_id"],)).fetchone())
if est:
inv["line_items"] = rows_to_list(conn.execute("SELECT * FROM line_items WHERE estimate_id=?", (inv["estimate_id"],)).fetchall())
job = row_to_dict(conn.execute("SELECT * FROM jobs WHERE id=?", (inv["job_id"],)).fetchone())
inv["job"] = job
conn.close()
return inv
def mark_paid(invoice_id, amount_paid=None):
conn = get_db()
inv = row_to_dict(conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone())
if not inv: conn.close(); return {"error": "invoice not found"}
paid = amount_paid if amount_paid else inv["amount_due"]
conn.execute("UPDATE invoices SET amount_paid=?, status='paid' WHERE id=?", (paid, invoice_id))
conn.commit(); conn.close()
return {"ok": True, "id": invoice_id, "amount_paid": paid, "status": "paid"}
def sync_to_qbo(invoice_id):
"""Sync invoice to QuickBooks Online."""
conn = get_db()
inv = row_to_dict(conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone())
if not inv: conn.close(); return {"error": "invoice not found"}
items = rows_to_list(conn.execute("SELECT * FROM line_items WHERE estimate_id=?", (inv["estimate_id"],)).fetchall())
conn.close()
try:
from lib.qbo import create_invoice as qbo_create
qbo_lines = [{"description": i["description"], "amount": i["total"], "qty": i["quantity"]} for i in items]
# Note: customer_id mapping needed — for now use placeholder
result = qbo_create("1", qbo_lines, inv["due_date"])
qbo_id = result.get("Invoice", {}).get("Id")
conn = get_db()
conn.execute("UPDATE invoices SET qbo_invoice_id=?, synced_to_qbo_at=? WHERE id=?", (qbo_id, now(), invoice_id))
conn.commit(); conn.close()
return {"ok": True, "qbo_invoice_id": qbo_id}
except Exception as e:
return {"error": str(e), "msg": "QBO sync failed — check token status"}
if __name__ == "__main__":
from lib.db import init_db; init_db()
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
if cmd == "create": print(json.dumps(create(sys.argv[2], sys.argv[3] if len(sys.argv)>3 else None)))
elif cmd == "list": print(json.dumps(list_invoices(sys.argv[2] if len(sys.argv)>2 else None), indent=2))
elif cmd == "get": print(json.dumps(get(sys.argv[2]), indent=2))
elif cmd == "mark-paid": print(json.dumps(mark_paid(sys.argv[2], float(sys.argv[3]) if len(sys.argv)>3 else None)))
elif cmd == "sync-qbo": print(json.dumps(sync_to_qbo(sys.argv[2])))
else: print(json.dumps({"error": f"unknown: {cmd}"}))