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),
|
||||
|
||||
Reference in New Issue
Block a user