fix: reporte de Clientes usa /aging-summary agrupado por cliente
This commit is contained in:
@@ -687,6 +687,73 @@ def aging_report():
|
||||
return jsonify({'data': rows, 'totals': totals})
|
||||
|
||||
|
||||
@accounting_bp.route('/aging-summary', methods=['GET'])
|
||||
@require_auth('accounting.view')
|
||||
def aging_summary():
|
||||
"""Customer-level accounts receivable summary (used by reports)."""
|
||||
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
|
||||
WHERE s.sale_type = 'credit'
|
||||
AND s.status = 'completed'
|
||||
AND c.credit_balance > 0
|
||||
ORDER BY c.name, s.created_at
|
||||
""")
|
||||
|
||||
customers = {}
|
||||
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,
|
||||
}
|
||||
|
||||
amount = float(r[6]) if r[6] else 0
|
||||
days = r[8] or 0
|
||||
|
||||
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
|
||||
else:
|
||||
customers[cust_id]['d90_plus'] += amount
|
||||
|
||||
customers[cust_id]['total'] += amount
|
||||
|
||||
result = list(customers.values())
|
||||
for c in result:
|
||||
for key in ('corriente', 'd1_30', 'd31_60', 'd61_90', 'd90_plus', 'total'):
|
||||
c[key] = round(c[key], 2)
|
||||
|
||||
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(result),
|
||||
}
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'data': result, 'totals': totals})
|
||||
|
||||
|
||||
# ─── Fiscal Periods ────────────────────────────────
|
||||
|
||||
@accounting_bp.route('/periods', methods=['GET'])
|
||||
|
||||
@@ -504,7 +504,7 @@ const Reports = (() => {
|
||||
agingEl.innerHTML = spinner();
|
||||
|
||||
try {
|
||||
var data = await apiFetch('/pos/api/accounting/aging');
|
||||
var data = await apiFetch('/pos/api/accounting/aging-summary');
|
||||
var clients = data.data || [];
|
||||
var totals = data.totals || {};
|
||||
|
||||
|
||||
@@ -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-v25';
|
||||
const CACHE_NAME = 'nexus-pos-v26';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/static/css/tokens.css',
|
||||
|
||||
@@ -371,7 +371,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/reports.js?v=3" defer></script>
|
||||
<script src="/pos/static/js/reports.js?v=4" 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