fix: separa Cuentas por Cobrar (ventas a crédito) y Cuentas por Pagar (órdenes de compra)
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user