fix: separa Cuentas por Cobrar (ventas a crédito) y Cuentas por Pagar (órdenes de compra)
This commit is contained in:
@@ -565,14 +565,66 @@ def balance_sheet():
|
|||||||
@accounting_bp.route('/aging', methods=['GET'])
|
@accounting_bp.route('/aging', methods=['GET'])
|
||||||
@require_auth('accounting.view')
|
@require_auth('accounting.view')
|
||||||
def aging_report():
|
def aging_report():
|
||||||
"""Antiguedad de saldos (accounts receivable aging).
|
"""Antiguedad de saldos.
|
||||||
|
|
||||||
Returns individual credit sales with outstanding balance, ready to be
|
Returns individual credit sales with outstanding balance when type=receivable
|
||||||
collected, viewed or cancelled.
|
(default), or purchase orders payable to suppliers when type=payable.
|
||||||
"""
|
"""
|
||||||
|
report_type = request.args.get('type', 'receivable')
|
||||||
conn = get_tenant_conn(g.tenant_id)
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
cur = conn.cursor()
|
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("""
|
cur.execute("""
|
||||||
SELECT s.id, s.total, s.created_at, s.status,
|
SELECT s.id, s.total, s.created_at, s.status,
|
||||||
c.id, c.name, c.rfc,
|
c.id, c.name, c.rfc,
|
||||||
@@ -583,8 +635,6 @@ def aging_report():
|
|||||||
AND s.status = 'completed'
|
AND s.status = 'completed'
|
||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
""")
|
""")
|
||||||
|
|
||||||
rows = []
|
|
||||||
for r in cur.fetchall():
|
for r in cur.fetchall():
|
||||||
sale_id = r[0]
|
sale_id = r[0]
|
||||||
total = float(r[1]) if r[1] else 0
|
total = float(r[1]) if r[1] else 0
|
||||||
@@ -595,11 +645,9 @@ def aging_report():
|
|||||||
paid = payments_total
|
paid = payments_total
|
||||||
balance = round(total - paid, 2)
|
balance = round(total - paid, 2)
|
||||||
|
|
||||||
# Only receivables with pending balance
|
|
||||||
if balance <= 0:
|
if balance <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Default due date = 30 days after issuance
|
|
||||||
due_date = created_at + timedelta(days=30) if created_at else None
|
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) - due_date).days if due_date else 0
|
||||||
|
|
||||||
|
|||||||
@@ -231,12 +231,12 @@ const Accounting = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = rows.map(r => {
|
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 status = r.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 label = r.status_label || (status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Pendiente');
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
|
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
|
||||||
<td class="td--primary">${r.name || r.vendor_name || '-'}</td>
|
<td class="td--primary">${r.vendor_name || r.name || '-'}</td>
|
||||||
<td>${r.receipt_date ? new Date(r.receipt_date).toLocaleDateString('es-MX') : '-'}</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>${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.total)}</td>
|
||||||
<td class="td--amount">$${fmt(r.paid || 0)}</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)
|
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||||
// so templates can use cache-busting query params freely.
|
// so templates can use cache-busting query params freely.
|
||||||
|
|
||||||
const CACHE_NAME = 'nexus-pos-v24';
|
const CACHE_NAME = 'nexus-pos-v25';
|
||||||
|
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
'/pos/static/css/tokens.css',
|
'/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/splash-loader.js?v=1" defer></script>
|
||||||
<script src="/pos/static/js/pos-utils.js?v=2" 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/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 src="/pos/static/js/sync-engine.js" defer></script>
|
||||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</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>
|
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user