diff --git a/pos/blueprints/accounting_bp.py b/pos/blueprints/accounting_bp.py index dc98425..7533925 100644 --- a/pos/blueprints/accounting_bp.py +++ b/pos/blueprints/accounting_bp.py @@ -565,67 +565,115 @@ def balance_sheet(): @accounting_bp.route('/aging', methods=['GET']) @require_auth('accounting.view') def aging_report(): - """Antiguedad de saldos (accounts receivable aging). + """Antiguedad de saldos. - Returns individual credit sales with outstanding balance, ready to be - collected, viewed or cancelled. + Returns individual credit sales with outstanding balance when type=receivable + (default), or purchase orders payable to suppliers when type=payable. """ + report_type = request.args.get('type', 'receivable') conn = get_tenant_conn(g.tenant_id) cur = conn.cursor() - cur.execute(""" - SELECT s.id, s.total, s.created_at, s.status, - c.id, c.name, c.rfc, - COALESCE((SELECT SUM(amount) FROM sale_payments sp WHERE sp.sale_id = s.id), 0) as payments_total - FROM sales s - JOIN customers c ON c.id = s.customer_id - WHERE s.sale_type = 'credit' - AND s.status = 'completed' - ORDER BY s.created_at DESC - """) - rows = [] - for r in cur.fetchall(): - sale_id = r[0] - total = float(r[1]) if r[1] else 0 - created_at = r[2] - status = r[3] - customer_name = r[5] - payments_total = float(r[7]) if r[7] else 0 - paid = payments_total - balance = round(total - paid, 2) + if report_type == 'payable': + # Accounts payable: purchase orders to suppliers that are not paid/cancelled + cur.execute(""" + SELECT po.id, po.supplier_invoice, po.total, po.created_at, po.expected_date, + po.status, s.id, s.name + FROM purchase_orders po + JOIN suppliers s ON s.id = po.supplier_id + WHERE po.status NOT IN ('paid', 'cancelled') + ORDER BY po.created_at DESC + """) + for r in cur.fetchall(): + po_id = r[0] + invoice = r[1] or f'OC-{po_id}' + total = float(r[2]) if r[2] else 0 + created_at = r[3] + expected_date = r[4] + status = r[5] + vendor_name = r[7] - # Only receivables with pending balance - if balance <= 0: - continue + paid = 0 # TODO: sum supplier payments when that table is added + balance = round(total - paid, 2) + if balance <= 0: + continue - # Default due date = 30 days after issuance - due_date = created_at + timedelta(days=30) if created_at else None - days_overdue = (datetime.now(created_at.tzinfo) - due_date).days if due_date else 0 + days_overdue = (datetime.now(created_at.tzinfo) - expected_date).days if expected_date and created_at else 0 + if days_overdue > 0: + po_status = 'overdue' + label = 'Vencida' + elif paid > 0: + po_status = 'partial' + label = 'Parcial' + else: + po_status = 'pending' + label = 'Pendiente' - if days_overdue > 0: - sale_status = 'overdue' - label = 'Vencida' - elif paid > 0: - sale_status = 'partial' - label = 'Parcial' - else: - sale_status = 'pending' - label = 'Vigente' + rows.append({ + 'po_id': po_id, + 'invoice': invoice, + 'vendor_name': vendor_name, + 'issue_date': created_at.isoformat() if created_at else None, + 'due_date': expected_date.isoformat() if expected_date else None, + 'total': total, + 'paid': paid, + 'balance': balance, + 'days_overdue': days_overdue, + 'status': po_status, + 'status_label': label, + }) + else: + # Accounts receivable: credit sales to customers + cur.execute(""" + SELECT s.id, s.total, s.created_at, s.status, + c.id, c.name, c.rfc, + COALESCE((SELECT SUM(amount) FROM sale_payments sp WHERE sp.sale_id = s.id), 0) as payments_total + FROM sales s + JOIN customers c ON c.id = s.customer_id + WHERE s.sale_type = 'credit' + AND s.status = 'completed' + ORDER BY s.created_at DESC + """) + for r in cur.fetchall(): + sale_id = r[0] + total = float(r[1]) if r[1] else 0 + created_at = r[2] + status = r[3] + customer_name = r[5] + payments_total = float(r[7]) if r[7] else 0 + paid = payments_total + balance = round(total - paid, 2) - rows.append({ - 'sale_id': sale_id, - 'invoice': f'VTA-{sale_id}', - 'customer_name': customer_name, - 'issue_date': created_at.isoformat() if created_at else None, - 'due_date': due_date.isoformat() if due_date else None, - 'total': total, - 'paid': paid, - 'balance': balance, - 'days_overdue': days_overdue, - 'status': sale_status, - 'status_label': label, - }) + if balance <= 0: + continue + + due_date = created_at + timedelta(days=30) if created_at else None + days_overdue = (datetime.now(created_at.tzinfo) - due_date).days if due_date else 0 + + if days_overdue > 0: + sale_status = 'overdue' + label = 'Vencida' + elif paid > 0: + sale_status = 'partial' + label = 'Parcial' + else: + sale_status = 'pending' + label = 'Vigente' + + rows.append({ + 'sale_id': sale_id, + 'invoice': f'VTA-{sale_id}', + 'customer_name': customer_name, + 'issue_date': created_at.isoformat() if created_at else None, + 'due_date': due_date.isoformat() if due_date else None, + 'total': total, + 'paid': paid, + 'balance': balance, + 'days_overdue': days_overdue, + 'status': sale_status, + 'status_label': label, + }) totals = { 'count': len(rows), diff --git a/pos/static/js/accounting.js b/pos/static/js/accounting.js index a1935c4..329fb2c 100644 --- a/pos/static/js/accounting.js +++ b/pos/static/js/accounting.js @@ -231,12 +231,12 @@ const Accounting = (() => { } tbody.innerHTML = rows.map(r => { - const status = r.days_overdue > 0 ? 'overdue' : r.paid > 0 && r.balance > 0 ? 'partial' : r.balance <= 0 ? 'ok' : 'pending'; - const label = status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Vigente'; + const status = r.status || (r.days_overdue > 0 ? 'overdue' : r.paid > 0 && r.balance > 0 ? 'partial' : r.balance <= 0 ? 'ok' : 'pending'); + const label = r.status_label || (status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Pendiente'); return ` ${r.invoice || r.folio || '-'} - ${r.name || r.vendor_name || '-'} - ${r.receipt_date ? new Date(r.receipt_date).toLocaleDateString('es-MX') : '-'} + ${r.vendor_name || r.name || '-'} + ${r.issue_date ? new Date(r.issue_date).toLocaleDateString('es-MX') : '-'} ${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'} $${fmt(r.total)} $${fmt(r.paid || 0)} diff --git a/pos/static/pwa/sw.js b/pos/static/pwa/sw.js index 412acbe..853caae 100644 --- a/pos/static/pwa/sw.js +++ b/pos/static/pwa/sw.js @@ -6,7 +6,7 @@ // The fetch handler normalizes static asset URLs (strips ?v= query strings) // so templates can use cache-busting query params freely. -const CACHE_NAME = 'nexus-pos-v24'; +const CACHE_NAME = 'nexus-pos-v25'; const APP_SHELL = [ '/pos/static/css/tokens.css', diff --git a/pos/templates/accounting.html b/pos/templates/accounting.html index 3ec94eb..e3717ad 100644 --- a/pos/templates/accounting.html +++ b/pos/templates/accounting.html @@ -499,7 +499,7 @@ - +