fix: reporte de Clientes usa /aging-summary agrupado por cliente
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-26 00:36:50 +00:00
parent 66d1268bfb
commit 59a4893e84
4 changed files with 70 additions and 3 deletions

View File

@@ -687,6 +687,73 @@ def aging_report():
return jsonify({'data': rows, 'totals': totals}) 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 ──────────────────────────────── # ─── Fiscal Periods ────────────────────────────────
@accounting_bp.route('/periods', methods=['GET']) @accounting_bp.route('/periods', methods=['GET'])

View File

@@ -504,7 +504,7 @@ const Reports = (() => {
agingEl.innerHTML = spinner(); agingEl.innerHTML = spinner();
try { try {
var data = await apiFetch('/pos/api/accounting/aging'); var data = await apiFetch('/pos/api/accounting/aging-summary');
var clients = data.data || []; var clients = data.data || [];
var totals = data.totals || {}; var totals = data.totals || {};

View File

@@ -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-v25'; const CACHE_NAME = 'nexus-pos-v26';
const APP_SHELL = [ const APP_SHELL = [
'/pos/static/css/tokens.css', '/pos/static/css/tokens.css',

View File

@@ -371,7 +371,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/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 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>