"""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}"}))