feat: detalle de cuentas por cobrar con opción de cancelar ticket
This commit is contained in:
@@ -7,7 +7,7 @@ NUMERIC(14,2) in the database.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from middleware import require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
@@ -567,75 +567,76 @@ def balance_sheet():
|
||||
def aging_report():
|
||||
"""Antiguedad de saldos (accounts receivable aging).
|
||||
|
||||
Groups outstanding credit sales by age:
|
||||
- Corriente (not yet due)
|
||||
- 1-30 dias
|
||||
- 31-60 dias
|
||||
- 61-90 dias
|
||||
- 90+ dias
|
||||
Returns individual credit sales with outstanding balance, ready to be
|
||||
collected, viewed or cancelled.
|
||||
"""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.rfc, c.credit_limit, c.credit_balance,
|
||||
s.id as sale_id, s.total, s.created_at,
|
||||
EXTRACT(DAY FROM NOW() - s.created_at)::int as days_outstanding
|
||||
FROM customers c
|
||||
JOIN sales s ON s.customer_id = c.id
|
||||
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'
|
||||
AND c.credit_balance > 0
|
||||
ORDER BY c.name, s.created_at
|
||||
ORDER BY s.created_at DESC
|
||||
""")
|
||||
|
||||
customers = {}
|
||||
rows = []
|
||||
for r in cur.fetchall():
|
||||
cust_id = r[0]
|
||||
if cust_id not in customers:
|
||||
customers[cust_id] = {
|
||||
'id': r[0], 'name': r[1], 'rfc': r[2],
|
||||
'credit_limit': float(r[3]) if r[3] else 0,
|
||||
'credit_balance': float(r[4]) if r[4] else 0,
|
||||
'corriente': 0, 'd1_30': 0, 'd31_60': 0, 'd61_90': 0, 'd90_plus': 0,
|
||||
'total': 0,
|
||||
}
|
||||
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)
|
||||
|
||||
amount = float(r[6]) if r[6] else 0
|
||||
days = r[8] or 0
|
||||
# Only receivables with pending balance
|
||||
if balance <= 0:
|
||||
continue
|
||||
|
||||
if days <= 0:
|
||||
customers[cust_id]['corriente'] += amount
|
||||
elif days <= 30:
|
||||
customers[cust_id]['d1_30'] += amount
|
||||
elif days <= 60:
|
||||
customers[cust_id]['d31_60'] += amount
|
||||
elif days <= 90:
|
||||
customers[cust_id]['d61_90'] += amount
|
||||
# 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
|
||||
|
||||
if days_overdue > 0:
|
||||
sale_status = 'overdue'
|
||||
label = 'Vencida'
|
||||
elif paid > 0:
|
||||
sale_status = 'partial'
|
||||
label = 'Parcial'
|
||||
else:
|
||||
customers[cust_id]['d90_plus'] += amount
|
||||
sale_status = 'pending'
|
||||
label = 'Vigente'
|
||||
|
||||
customers[cust_id]['total'] += amount
|
||||
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,
|
||||
})
|
||||
|
||||
result = list(customers.values())
|
||||
# Round all amounts
|
||||
for c in result:
|
||||
for key in ('corriente', 'd1_30', 'd31_60', 'd61_90', 'd90_plus', 'total'):
|
||||
c[key] = round(c[key], 2)
|
||||
|
||||
# Totals row
|
||||
totals = {
|
||||
'corriente': round(sum(c['corriente'] for c in result), 2),
|
||||
'd1_30': round(sum(c['d1_30'] for c in result), 2),
|
||||
'd31_60': round(sum(c['d31_60'] for c in result), 2),
|
||||
'd61_90': round(sum(c['d61_90'] for c in result), 2),
|
||||
'd90_plus': round(sum(c['d90_plus'] for c in result), 2),
|
||||
'total': round(sum(c['total'] for c in result), 2),
|
||||
'count': len(rows),
|
||||
'total': round(sum(r['total'] for r in rows), 2),
|
||||
'paid': round(sum(r['paid'] for r in rows), 2),
|
||||
'balance': round(sum(r['balance'] for r in rows), 2),
|
||||
}
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'data': result, 'totals': totals})
|
||||
return jsonify({'data': rows, 'totals': totals})
|
||||
|
||||
|
||||
# ─── Fiscal Periods ────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user