fix: separa Cuentas por Cobrar (ventas a crédito) y Cuentas por Pagar (órdenes de compra)
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-26 00:32:46 +00:00
parent 9a55e15142
commit 66d1268bfb
4 changed files with 105 additions and 57 deletions

View File

@@ -565,14 +565,66 @@ 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()
rows = []
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]
paid = 0 # TODO: sum supplier payments when that table is added
balance = round(total - paid, 2)
if balance <= 0:
continue
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'
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,
@@ -583,8 +635,6 @@ def aging_report():
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
@@ -595,11 +645,9 @@ def aging_report():
paid = payments_total
balance = round(total - paid, 2)
# Only receivables with pending balance
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

View File

@@ -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 `<tr>
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
<td class="td--primary">${r.name || r.vendor_name || '-'}</td>
<td>${r.receipt_date ? new Date(r.receipt_date).toLocaleDateString('es-MX') : '-'}</td>
<td class="td--primary">${r.vendor_name || r.name || '-'}</td>
<td>${r.issue_date ? new Date(r.issue_date).toLocaleDateString('es-MX') : '-'}</td>
<td>${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'}</td>
<td class="td--amount">$${fmt(r.total)}</td>
<td class="td--amount">$${fmt(r.paid || 0)}</td>

View File

@@ -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',

View File

@@ -499,7 +499,7 @@
<script src="/pos/static/js/splash-loader.js?v=1" defer></script>
<script src="/pos/static/js/pos-utils.js?v=2" defer></script>
<script src="/pos/static/js/sidebar.js" defer></script>
<script src="/pos/static/js/accounting.js?v=2" defer></script>
<script src="/pos/static/js/accounting.js?v=3" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/pwa-install.js" defer></script>