@fullqueso/mcp-bc-gastos 1.23.1 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [1.24.0] — 2026-06-05
4
+
5
+ ### Changed
6
+ - **`get_cash_flow` — working capital 100% desde net-change G/L (7 líneas) + D&A/EBITDA.** Se unificó todo el capital de trabajo sobre el Chart of Accounts real de BC, con la regla universal de impacto en caja `delta = −Σ(debit − credit)` (vale para activos: sube = uso, y pasivos: sube = fuente). Antes ΔAR/ΔAP venían del customer/vendor ledger y ΔInventario del item ledger (fuentes mezcladas); ahora las 7 líneas salen del balance general, misma base USD que el EBIT.
7
+ - **7 líneas de WC** (orden del bridge): ΔAR `15100–15199 + 15900–15998` · Δ Impuestos (activo) `15200–15299` · Δ Prepagados `16000–16999` · Δ Inventario `14000–14999` · ΔAP `22000–22999` · **Δ Impuestos por pagar `23000–23999`** (nueva) · **Δ Pasivos laborales `24000–25999`** (nueva). `net_wc_change` = suma de las 7.
8
+ - **CapEx** afinado a PP&E `11110–11190 + 12110–12240` (excluye `12900` depreciación). Implementado con **2 umbrella queries** (activos `11110–16999`, pasivos `22000–25999`) bucketizadas en código por rango de cuenta — `12900` se trae pero no matchea bucket → se descarta; `17xxx`/`18xxx` quedan fuera de rango. Reemplaza 6 consultas (3 G/L + 3 subledger) por 2.
9
+ - **D&A / EBITDA**: `get_financial_statements` ahora calcula D&A = `Σ(debit−credit)` en `58000–58999` (COGS) + `80000–89999` (OpEx) — filtrando las entries que ya trae (sin query nueva) — y expone `depreciation_amortization` + `ebitda` (`{amount, pct}`). EBIT no cambia; EBITDA = EBIT + D&A. `get_cash_flow` los hereda. Insight automático cuando D&A = 0 ("verificar si se registra depreciación en BC, cuentas 58100-58300 / 81000-82000"). Hoy D&A ≈ 0 → EBIT ≈ EBITDA.
10
+ - `fcf_bridge` pasa a **13 filas** (7 líneas WC). HTML: tarjeta de Capital de Trabajo con las 7 líneas; KPIs EBITDA + D&A; CapEx etiquetado `PP&E 11110-12240`; fila D&A + EBITDA en el P&L de `get_financial_statements`; fila EBITDA en el comparativo multi-tienda.
11
+ - **Verificado contra BC real (FQ88, mayo 2026):** CapEx ANTES `$8,760.92` (cuentas 151xx/152xx) → `$0.00` (sin movimientos PP&E). Cross-check G/L vs subledger casi idéntico (ΔInventario 8,643.73 → 8,644.31; ΔAP 10,559.77 → 10,594.77). Nuevas líneas: Δ Imp. por pagar `+$6,052.21`, Δ Pasivos laborales `+$1,294.00`. Bridge cuadra: NOPAT `$12,340.71` + ΔWC `$17,824.37` − CapEx `$0` = FCF `$30,165.08`. EBIT/EBITDA `$18,698.05` (D&A 0).
12
+
13
+ ### Pendiente (Fase 2)
14
+ - **Efecto cambiario sobre caja VES** — línea reconciliadora bajo el FCF (`FCF + Efecto cambiario = Δ Caja real USD`) para reflejar la erosión en USD del saldo en bolívares por la devaluación diaria (~0.75–1%). Se estimará desde el saldo VES de las cuentas 18xxx-VES × tasas. No incluido en esta versión.
15
+
16
+ ---
17
+
3
18
  ## [1.23.1] — 2026-06-05
4
19
 
5
20
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullqueso/mcp-bc-gastos",
3
- "version": "1.23.1",
3
+ "version": "1.24.0",
4
4
  "description": "MCP server for Business Central operational expense analysis, bank reconciliation, POS reconciliation, accounts receivable/payable, multi-payment draft visibility, payroll, inventory cost analysis, and manager reports - Full Queso franchise stores",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -289,10 +289,21 @@ export function calcScore(ratios) {
289
289
  // generateInsights — array ordenado por severidad (critical → warning → success)
290
290
  // ─────────────────────────────────────────────────────────────────────────────
291
291
 
292
- export function generateInsights(categories, ratios) {
292
+ export function generateInsights(categories, ratios, da = null) {
293
293
  const insights = [];
294
294
  const sevOrder = { critical: 0, warning: 1, success: 2 };
295
295
 
296
+ // D&A en cero — la depreciación periódica no se está contabilizando en BC
297
+ // (por eso EBIT ≈ EBITDA). Avisar para que se verifique el registro.
298
+ if (da === 0) {
299
+ insights.push({
300
+ type: 'warning',
301
+ category: 'depreciation',
302
+ message: 'D&A en cero — verificar si se registra depreciación en BC (cuentas 58100-58300 y 81000-82000).',
303
+ action: 'Confirmar con contabilidad si falta contabilizar la depreciación periódica del período.',
304
+ });
305
+ }
306
+
296
307
  // Margen operativo
297
308
  const op = ratios.operating_margin_pct;
298
309
  if (op.status === 'critical') {
@@ -208,9 +208,11 @@ function renderKpis(s) {
208
208
  const fcfCls = statusValClass(s.fcf.status);
209
209
  return `<div class="kpi-grid cf-kpis">
210
210
  ${cell('EBIT', signed(s.ebit.amount), `${s.ebit.pct}%`)}
211
+ ${cell('D&amp;A', signed(s.da ? s.da.amount : 0), 'dep. 58xxx+8xxxx')}
212
+ ${cell('EBITDA', signed(s.ebitda ? s.ebitda.amount : s.ebit.amount), `${s.ebitda ? s.ebitda.pct : s.ebit.pct}%`)}
211
213
  ${cell('NOPAT', signed(s.nopat.amount), `tax ${signed(-s.tax_estimated.amount)}`)}
212
214
  ${cell('CFO', signed(s.cfo.amount), `${s.cfo.pct}%`)}
213
- ${cell('CapEx', signed(-s.capex.amount), 'PP&amp;E 11110-12899')}
215
+ ${cell('CapEx', signed(-s.capex.amount), 'PP&amp;E 11110-12240')}
214
216
  ${cell('FCF', signed(s.fcf.amount), statusWord(s.fcf.status), fcfCls)}
215
217
  ${cell('FCF Margin', `${s.fcf.pct}%`, '% ingresos', fcfCls)}
216
218
  </div>`;
@@ -236,10 +238,12 @@ function renderWorkingCapital(s) {
236
238
  <div class="card-bd">
237
239
  <div class="wc">
238
240
  ${row('Δ Cobranzas (AR)', wc.delta_ar)}
239
- ${row('Δ Proveedores (AP)', wc.delta_ap)}
240
- ${row('Δ Inventario', wc.delta_inventory)}
241
- ${row('Δ Impuestos corrientes', wc.delta_taxes || 0)}
241
+ ${row('Δ Impuestos (activo)', wc.delta_taxes || 0)}
242
242
  ${row('Δ Prepagados', wc.delta_prepaid || 0)}
243
+ ${row('Δ Inventario', wc.delta_inventory)}
244
+ ${row('Δ Proveedores (AP)', wc.delta_ap)}
245
+ ${row('Δ Impuestos por pagar', wc.delta_taxes_payable || 0)}
246
+ ${row('Δ Pasivos laborales', wc.delta_labor || 0)}
243
247
  <div class="wc-row total">
244
248
  <span class="wc-label">Cambio neto WC</span>
245
249
  <span class="wc-arrow ${wc.net_wc_change >= 0 ? 'up' : 'down'}">${wc.net_wc_change >= 0 ? '&#x2191;' : '&#x2193;'}</span>
@@ -279,6 +283,7 @@ function renderStoresComparison(result) {
279
283
 
280
284
  const body = [
281
285
  row('EBIT', (c) => signed(c.ebit.amount)),
286
+ row('EBITDA', (c) => signed(c.ebitda ? c.ebitda.amount : c.ebit.amount)),
282
287
  row('NOPAT', (c) => signed(c.nopat.amount)),
283
288
  row('CFO', (c) => `<span class="${c.cfo.amount >= 0 ? 'val-green' : 'val-red'}">${signed(c.cfo.amount)}</span>`),
284
289
  row('CapEx', (c) => signed(-c.capex.amount)),
@@ -1,24 +1,31 @@
1
1
  // tools/financials/cash-flow.js
2
2
  //
3
3
  // Tool get_cash_flow — Free Cash Flow (FCF) por método indirecto para tiendas FQ.
4
- // Reutiliza el bloque P&L/EBIT de get_financial_statements (fetchStoreFinancials) y
5
- // le agrega impuesto estimado, capital de trabajo (ΔAR/ΔAP/ΔInventario), CapEx,
6
- // CFO, FCF, bridge tipo waterfall e insights. READ-ONLY.
4
+ // Reutiliza el bloque P&L/EBIT/D&A/EBITDA de get_financial_statements
5
+ // (fetchStoreFinancials) y le agrega impuesto estimado, capital de trabajo (7 líneas),
6
+ // CapEx, CFO, FCF, bridge tipo waterfall e insights. READ-ONLY.
7
7
  //
8
- // CONVENCIÓN DE SIGNOS (impacto en caja; + = fuente de caja, = uso de caja):
9
- // delta_ar = −Σ amountLocalCurrency(customerLedgerEntries) → + = cobró más de lo facturado (AR 15110–15199)
10
- // delta_ap = −Σ amountLocalCurrency(vendorLedgerEntries) + = debe más (fuente)
11
- // delta_inventory = −Σ costAmountActual(itemLedgerEntries) → + = inventario bajó (fuente) (14000–14999)
12
- // delta_taxes = −Σ(debit−credit) G/L 1521015280 → + = impuestos corrientes bajaron (fuente)
13
- // delta_prepaid = −Σ(debit−credit) G/L 16110–16900 → + = prepagados bajaron (fuente)
14
- // net_wc_change = delta_ar + delta_ap + delta_inventory + delta_taxes + delta_prepaid
15
- // nopat = ebit − tax ; cfo = nopat + net_wc_change ; fcf = cfo − capex
16
- // (Resuelve la inconsistencia del spec entre los comentarios del schema y la fórmula:
17
- // se adopta la convención de los comentarios — los deltas ya con signo de caja.)
8
+ // WORKING CAPITAL las 7 líneas salen de net-change G/L (balance general), misma
9
+ // base USD que el EBIT. REGLA UNIVERSAL de impacto en caja (+ = fuente, − = uso):
10
+ // delta = −Σ(debit − credit) sobre el rango de cuenta. Vale para activos (sube =
11
+ // uso) y pasivos (sube = fuente). Rangos (Chart of Accounts real de BC):
12
+ // delta_ar = 15100–15199 + 1590015998 (cuentas por cobrar + otras CxC)
13
+ // delta_taxes = 15200–15299 (impuestos corrientes activo)
14
+ // delta_prepaid = 16000–16999 (anticipos y prepagados)
15
+ // delta_inventory = 14000–14999 (inventarios)
16
+ // delta_ap = 22000–22999 (cuentas por pagar)
17
+ // delta_taxes_payable = 23000–23999 (impuestos por pagar)
18
+ // delta_labor = 24000–25999 (pasivos laborales)
19
+ // net_wc_change = suma de las 7 ; nopat = ebit − tax ; cfo = nopat + net_wc_change ;
20
+ // fcf = cfo − capex.
18
21
  //
19
- // CAPEX = net change (debit−credit) en PP&E 11110–12899 (intangibles 1111011190 +
20
- // tangibles 12110–12240). EXCLUYE 12900 (depreciación acumulada), 17000–17999
21
- // (inversiones corto plazo) y 18000–18999 (caja y bancos = posición de caja final).
22
+ // CAPEX = (debit credit) en PP&E 11110–11190 (intangibles) + 1211012240
23
+ // (tangibles). EXCLUYE 12900 (depreciación acumulada), 17000–17999 (inversiones
24
+ // corto plazo) y 18000–18999 (caja y bancos = posición de caja final).
25
+ //
26
+ // D&A / EBITDA: D&A = Σ(debit−credit) en 58000–58999 (COGS) + 80000–89999 (OpEx),
27
+ // calculada por get_financial_statements. EBITDA = EBIT + D&A (KPI; no es paso del
28
+ // bridge). Hoy D&A suele ser 0 (no se contabiliza depreciación) → EBIT ≈ EBITDA.
22
29
  //
23
30
  // Montos en USD (LCY = Additional Reporting Currency), igual que get_financial_statements.
24
31
 
@@ -44,8 +51,9 @@ export const cashFlowTool = {
44
51
  description:
45
52
  'Free Cash Flow (FCF) por método indirecto para una o más tiendas Full Queso (FQ01, FQ28, FQ88). ' +
46
53
  'Parte del EBIT (igual que get_financial_statements), resta impuesto estimado (ISLR 34%), ajusta por capital de trabajo ' +
47
- '(Δ cuentas por cobrar, Δ cuentas por pagar, Δ inventario, Δ impuestos corrientes, Δ prepagados) para obtener el flujo de ' +
48
- 'caja operativo (CFO), y resta CapEx (PP&E / activos fijos 11110-12899) para el FCF. Incluye NOPAT, margen FCF, status ' +
54
+ '(7 líneas desde net-change G/L: Δ cuentas por cobrar, Δ impuestos corrientes, Δ prepagados, Δ inventario, Δ cuentas por pagar, ' +
55
+ 'Δ impuestos por pagar, Δ pasivos laborales) para obtener el flujo de caja operativo (CFO), y resta CapEx (PP&E 11110-12240) ' +
56
+ 'para el FCF. Reporta también D&A y EBITDA (EBIT + D&A). Incluye NOPAT, margen FCF, status ' +
49
57
  '(healthy/positive_low/negative_watch/' +
50
58
  'negative_critical), bridge tipo waterfall e insights automáticos. Multi-tienda calcula consolidado. Con render_html=true ' +
51
59
  'genera un dashboard HTML (lo guarda en ~/Downloads y retorna la ruta). Úsalo cuando el usuario pida flujo de caja, ' +
@@ -209,11 +217,15 @@ async function buildStoreCashFlow(bcClient, store, start, end) {
209
217
  cogs: financials.cogs.total,
210
218
  gross_margin_amount: financials.gross_margin.amount,
211
219
  ebit: financials.operating_income.amount,
220
+ da: financials.depreciation_amortization.amount,
221
+ ebitda: financials.ebitda.amount,
212
222
  delta_ar: wc.delta_ar,
213
- delta_ap: wc.delta_ap,
214
- delta_inventory: wc.delta_inventory,
215
223
  delta_taxes: wc.delta_taxes,
216
224
  delta_prepaid: wc.delta_prepaid,
225
+ delta_inventory: wc.delta_inventory,
226
+ delta_ap: wc.delta_ap,
227
+ delta_taxes_payable: wc.delta_taxes_payable,
228
+ delta_labor: wc.delta_labor,
217
229
  capex: wc.capex,
218
230
  capex_accounts: wc.capex_accounts,
219
231
  warnings: wc.warnings,
@@ -221,91 +233,90 @@ async function buildStoreCashFlow(bcClient, store, start, end) {
221
233
  );
222
234
  }
223
235
 
224
- // Consultas BC para capital de trabajo (AR/AP/Inventario) + CapEx todas en paralelo
236
+ // Buckets del balance general (Chart of Accounts real de BC), por rango de cuenta.
237
+ // REGLA UNIVERSAL de impacto en caja: delta = −Σ(debit − credit) sobre el rango.
238
+ // - Activo que sube (debit) → −Σ < 0 = uso de caja.
239
+ // - Pasivo que sube (credit) → −Σ > 0 = fuente de caja.
240
+ // CapEx es la excepción: capex = +Σ(debit − credit) sobre PP&E (un débito = inversión).
241
+ // Montos en USD (LCY = Additional Reporting Currency), consistentes con el EBIT.
242
+ const ASSET_BUCKETS = [
243
+ { key: 'capex', ranges: [[11110, 11190], [12110, 12240]] }, // PP&E (excluye 12900 deprec.)
244
+ { key: 'delta_inventory', ranges: [[14000, 14999]] }, // Inventarios
245
+ { key: 'delta_ar', ranges: [[15100, 15199], [15900, 15998]] }, // Cuentas por cobrar + otras CxC
246
+ { key: 'delta_taxes', ranges: [[15200, 15299]] }, // Impuestos corrientes (activo)
247
+ { key: 'delta_prepaid', ranges: [[16000, 16999]] }, // Anticipos y prepagados
248
+ ];
249
+ const LIAB_BUCKETS = [
250
+ { key: 'delta_ap', ranges: [[22000, 22999]] }, // Cuentas por pagar
251
+ { key: 'delta_taxes_payable', ranges: [[23000, 23999]] }, // Impuestos por pagar (pasivo)
252
+ { key: 'delta_labor', ranges: [[24000, 25999]] }, // Pasivos laborales
253
+ ];
254
+
255
+ function bucketOf(accountNumber, buckets) {
256
+ const n = parseInt(accountNumber, 10);
257
+ for (const b of buckets) for (const [lo, hi] of b.ranges) if (n >= lo && n <= hi) return b.key;
258
+ return null; // 12900 (deprec.), gaps, etc. → fuera de todo bucket = descartado
259
+ }
260
+
261
+ // Capital de trabajo + CapEx desde 2 umbrella queries G/L (activos + pasivos),
262
+ // bucketizadas en código por rango de cuenta. Reemplaza el enfoque mixto
263
+ // subledger/G/L: ahora TODO el WC sale del balance general (net-change G/L),
264
+ // misma base USD que el EBIT. AR/Inventario/AP ya no usan customer/vendor/item
265
+ // ledger — todo es net change de las cuentas de balance.
225
266
  async function fetchWorkingCapitalAndCapex(bcClient, companyId, start, end) {
226
- const sd = bcClient._sanitizeOData(start);
227
- const ed = bcClient._sanitizeOData(end);
228
- const periodFilter = `postingDate ge ${sd} and postingDate le ${ed}`;
229
-
230
- const arUrl = bcClient.buildBetaApiUrl(companyId, 'customerLedgerEntries', {
231
- $filter: periodFilter,
232
- $select: 'postingDate,documentType,amountLocalCurrency',
233
- });
234
- const apUrl = bcClient.buildBetaApiUrl(companyId, 'vendorLedgerEntries', {
235
- $filter: periodFilter,
236
- $select: 'postingDate,documentType,amountLocalCurrency',
237
- });
238
- const ileUrl = bcClient.buildApiUrl(companyId, 'itemLedgerEntries', {
239
- $filter: periodFilter,
240
- $select: 'postingDate,entryType,quantity,costAmountActual',
241
- });
242
-
243
- // Rangos G/L del Chart of Accounts real de BC (activos 10000–19999):
244
- // CapEx → PP&E 11110–12899 (excluye 12900 depreciación acumulada)
245
- // Impuestos corrientes → 15210–15280 (IVA crédito, retenciones, anticipos ISLR)
246
- // Prepagados → 16110–16900 (anticipos a proveedores, alquileres/otros prepagados)
247
- // AR (15110–15199) e Inventario (14000–14999) ya se calculan vía subledger
248
- // (customer/item ledger) — NO consultarlos aquí para no duplicar.
249
- // Excluidos del FCF: 17000–17999 (inversiones corto plazo) y 18000–18999 (caja/bancos).
250
- const [arEntries, apEntries, ileEntries, capexEntries, taxEntries, prepaidEntries] = await Promise.all([
251
- bcClient.apiCallAllPages(arUrl).catch((e) => { logger.warn(`[cash-flow] AR fetch failed: ${e.message}`); return null; }),
252
- bcClient.apiCallAllPages(apUrl).catch((e) => { logger.warn(`[cash-flow] AP fetch failed: ${e.message}`); return null; }),
253
- bcClient.apiCallAllPages(ileUrl).catch((e) => { logger.warn(`[cash-flow] ILE fetch failed: ${e.message}`); return null; }),
254
- bcClient.getGLEntries(companyId, start, end, '11110', '12899').catch((e) => { logger.warn(`[cash-flow] CapEx fetch failed: ${e.message}`); return null; }),
255
- bcClient.getGLEntries(companyId, start, end, '15210', '15280').catch((e) => { logger.warn(`[cash-flow] Impuestos fetch failed: ${e.message}`); return null; }),
256
- bcClient.getGLEntries(companyId, start, end, '16110', '16900').catch((e) => { logger.warn(`[cash-flow] Prepagados fetch failed: ${e.message}`); return null; }),
267
+ // Umbrella de activos 11110–16999: cubre CapEx (11xxx–12xxx), inventario (14xxx),
268
+ // AR (151xx/159xx), impuestos activo (152xx) y prepagados (16xxx). 12900 cae dentro
269
+ // pero no matchea bucket se descarta. 17xxx (inversiones) y 18xxx (caja) quedan fuera.
270
+ // Umbrella de pasivos 22000–25999: AP (22xxx), impuestos por pagar (23xxx), laborales (24-25xxx).
271
+ const [assetEntries, liabEntries] = await Promise.all([
272
+ bcClient.getGLEntries(companyId, start, end, '11110', '16999').catch((e) => { logger.warn(`[cash-flow] activos G/L fetch failed: ${e.message}`); return null; }),
273
+ bcClient.getGLEntries(companyId, start, end, '22000', '25999').catch((e) => { logger.warn(`[cash-flow] pasivos G/L fetch failed: ${e.message}`); return null; }),
257
274
  ]);
258
275
 
259
276
  const warnings = [];
277
+ const raw = { capex: 0, delta_inventory: 0, delta_ar: 0, delta_taxes: 0, delta_prepaid: 0, delta_ap: 0, delta_taxes_payable: 0, delta_labor: 0 };
278
+ const capex_accounts = {};
260
279
 
261
- // ΔAR — impacto en caja = −Σ amountLocalCurrency (invoices +, payments −)
262
- let delta_ar = 0;
263
- if (arEntries == null) warnings.push('Customer ledger no disponible — ΔAR asumido en 0.');
264
- else delta_ar = round2(-arEntries.reduce((s, e) => s + (e.amountLocalCurrency || 0), 0));
265
-
266
- // ΔAP — impacto en caja = −Σ amountLocalCurrency (invoices −, payments +)
267
- let delta_ap = 0;
268
- if (apEntries == null) warnings.push('Vendor ledger no disponible — ΔAP asumido en 0.');
269
- else delta_ap = round2(-apEntries.reduce((s, e) => s + (e.amountLocalCurrency || 0), 0));
270
-
271
- // ΔInventario impacto en caja = −Σ costAmountActual (compras +, ventas )
272
- // costAmountActual ya es el costo total de la entrada (no multiplicar por quantity).
273
- let delta_inventory = 0;
274
- if (ileEntries == null) warnings.push('Item ledger no disponible — Δinventario asumido en 0.');
275
- else {
276
- delta_inventory = round2(-ileEntries.reduce((s, e) => s + (e.costAmountActual || 0), 0));
277
- if (ileEntries._truncated) warnings.push('Item ledger truncado (muchas entradas) — Δinventario puede estar incompleto.');
280
+ if (assetEntries == null) {
281
+ warnings.push('G/L de activos (11110–16999) no disponible — CapEx/ΔAR/ΔImpuestos/ΔPrepagados/ΔInventario asumidos en 0.');
282
+ } else {
283
+ for (const e of assetEntries) {
284
+ const key = bucketOf(e.accountNumber, ASSET_BUCKETS);
285
+ if (!key) continue;
286
+ const net = (e.debitAmount || 0) - (e.creditAmount || 0);
287
+ raw[key] += net;
288
+ if (key === 'capex') capex_accounts[e.accountNumber] = round2((capex_accounts[e.accountNumber] || 0) + net);
289
+ }
290
+ if (assetEntries._truncated) warnings.push('G/L de activos truncado (muchas entradas) WC de activos puede estar incompleto.');
278
291
  }
279
292
 
280
- // ΔImpuestos corrientes — impacto en caja = −Σ(debit−credit) G/L 15210–15280
281
- // (activo corriente: aumenta = uso de caja, baja = fuente de caja)
282
- let delta_taxes = 0;
283
- if (taxEntries == null) warnings.push('G/L de impuestos corrientes (15210–15280) no disponible — ΔImpuestos asumido en 0.');
284
- else delta_taxes = round2(-taxEntries.reduce((s, e) => s + ((e.debitAmount || 0) - (e.creditAmount || 0)), 0));
285
-
286
- // ΔPrepagados — impacto en caja = −Σ(debit−credit) G/L 16110–16900
287
- let delta_prepaid = 0;
288
- if (prepaidEntries == null) warnings.push('G/L de prepagados (16110–16900) no disponible — ΔPrepagados asumido en 0.');
289
- else delta_prepaid = round2(-prepaidEntries.reduce((s, e) => s + ((e.debitAmount || 0) - (e.creditAmount || 0)), 0));
290
-
291
- // CapEx — net change (debit−credit) en PP&E 11110–12899 (excluye 12900 depreciación,
292
- // 17xxx inversiones corto plazo y 18xxx bancos)
293
- let capex = 0;
294
- const capex_accounts = {};
295
- if (capexEntries == null) {
296
- warnings.push('G/L de PP&E (activos fijos 11110–12899) no disponible — CapEx asumido en 0.');
293
+ if (liabEntries == null) {
294
+ warnings.push('G/L de pasivos (22000–25999) no disponible ΔAP/ΔImpuestos x pagar/ΔPasivos laborales asumidos en 0.');
297
295
  } else {
298
- for (const e of capexEntries) {
299
- const net = (e.debitAmount || 0) - (e.creditAmount || 0);
300
- capex += net;
301
- capex_accounts[e.accountNumber] = round2((capex_accounts[e.accountNumber] || 0) + net);
296
+ for (const e of liabEntries) {
297
+ const key = bucketOf(e.accountNumber, LIAB_BUCKETS);
298
+ if (!key) continue;
299
+ raw[key] += (e.debitAmount || 0) - (e.creditAmount || 0);
302
300
  }
303
- capex = round2(capex);
304
- // limpiar cuentas con neto 0
305
- for (const k of Object.keys(capex_accounts)) if (capex_accounts[k] === 0) delete capex_accounts[k];
301
+ if (liabEntries._truncated) warnings.push('G/L de pasivos truncado (muchas entradas) — WC de pasivos puede estar incompleto.');
306
302
  }
307
303
 
308
- return { delta_ar, delta_ap, delta_inventory, delta_taxes, delta_prepaid, capex, capex_accounts, warnings };
304
+ // Aplicar signos: CapEx = +Σ(debit−credit); cada delta de WC = −Σ(debit−credit).
305
+ const capex = round2(raw.capex);
306
+ for (const k of Object.keys(capex_accounts)) if (capex_accounts[k] === 0) delete capex_accounts[k];
307
+
308
+ return {
309
+ delta_ar: round2(-raw.delta_ar),
310
+ delta_taxes: round2(-raw.delta_taxes),
311
+ delta_prepaid: round2(-raw.delta_prepaid),
312
+ delta_inventory: round2(-raw.delta_inventory),
313
+ delta_ap: round2(-raw.delta_ap),
314
+ delta_taxes_payable: round2(-raw.delta_taxes_payable),
315
+ delta_labor: round2(-raw.delta_labor),
316
+ capex,
317
+ capex_accounts,
318
+ warnings,
319
+ };
309
320
  }
310
321
 
311
322
  // ─────────────────────────────────────────────────────────────────────────────
@@ -321,12 +332,22 @@ function assembleCashFlow(meta, c) {
321
332
  const tax = ebit > 0 ? round2(ebit * TAX_RATE) : 0;
322
333
  const nopat = round2(ebit - tax);
323
334
 
335
+ // D&A / EBITDA — EBIT no cambia (D&A ya está en COGS/OpEx); EBITDA la re-suma.
336
+ const da = round2(c.da || 0);
337
+ const ebitda = round2(c.ebitda != null ? c.ebitda : ebit + da);
338
+ const ebitdaPct = income > 0 ? round2((ebitda / income) * 100) : 0;
339
+
340
+ // Working capital — las 7 líneas desde net-change G/L (regla −Σ(debit−credit)).
324
341
  const delta_ar = round2(c.delta_ar);
325
- const delta_ap = round2(c.delta_ap);
326
- const delta_inventory = round2(c.delta_inventory);
327
342
  const delta_taxes = round2(c.delta_taxes || 0);
328
343
  const delta_prepaid = round2(c.delta_prepaid || 0);
329
- const net_wc_change = round2(delta_ar + delta_ap + delta_inventory + delta_taxes + delta_prepaid);
344
+ const delta_inventory = round2(c.delta_inventory);
345
+ const delta_ap = round2(c.delta_ap);
346
+ const delta_taxes_payable = round2(c.delta_taxes_payable || 0);
347
+ const delta_labor = round2(c.delta_labor || 0);
348
+ const net_wc_change = round2(
349
+ delta_ar + delta_taxes + delta_prepaid + delta_inventory + delta_ap + delta_taxes_payable + delta_labor
350
+ );
330
351
 
331
352
  const cfo = round2(nopat + net_wc_change);
332
353
  const cfoPct = income > 0 ? round2((cfo / income) * 100) : 0;
@@ -336,8 +357,9 @@ function assembleCashFlow(meta, c) {
336
357
  const fcfPct = income > 0 ? round2((fcf / income) * 100) : 0;
337
358
 
338
359
  const status = fcfStatus(fcf, fcfPct, ebit);
339
- const bridge = buildBridge({ ebit, tax, nopat, delta_ar, delta_ap, delta_inventory, delta_taxes, delta_prepaid, cfo, capex, fcf });
340
- const insights = buildInsights({ ebit, delta_ar, delta_ap, delta_inventory, delta_taxes, delta_prepaid, capex, fcf, fcfPct, status, warnings: c.warnings || [] });
360
+ const wc = { delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor };
361
+ const bridge = buildBridge({ ebit, tax, nopat, ...wc, cfo, capex, fcf });
362
+ const insights = buildInsights({ ebit, ...wc, da, capex, fcf, fcfPct, status, warnings: c.warnings || [] });
341
363
 
342
364
  return {
343
365
  store_code: meta.store_code,
@@ -346,9 +368,11 @@ function assembleCashFlow(meta, c) {
346
368
  cogs: { total: round2(c.cogs) },
347
369
  gross_margin: { amount: round2(c.gross_margin_amount), pct: gmPct },
348
370
  ebit: { amount: ebit, pct: ebitPct },
371
+ da: { amount: da },
372
+ ebitda: { amount: ebitda, pct: ebitdaPct },
349
373
  tax_estimated: { amount: tax, rate: TAX_RATE, note: 'Estimado ISLR 34%' },
350
374
  nopat: { amount: nopat },
351
- working_capital: { delta_ar, delta_ap, delta_inventory, delta_taxes, delta_prepaid, net_wc_change },
375
+ working_capital: { ...wc, net_wc_change },
352
376
  cfo: { amount: cfo, pct: cfoPct },
353
377
  capex: { amount: capex, accounts: c.capex_accounts || {} },
354
378
  fcf: { amount: fcf, pct: fcfPct, status },
@@ -363,33 +387,37 @@ function fcfStatus(fcf, fcfPct, ebit) {
363
387
  return fcf > ebit * -0.2 ? 'negative_watch' : 'negative_critical';
364
388
  }
365
389
 
366
- function buildBridge({ ebit, tax, nopat, delta_ar, delta_ap, delta_inventory, delta_taxes, delta_prepaid, cfo, capex, fcf }) {
390
+ function buildBridge({ ebit, tax, nopat, delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor, cfo, capex, fcf }) {
367
391
  const flow = (label, amount) => ({ label, amount: round2(amount), type: amount >= 0 ? 'source' : 'use' });
368
392
  return [
369
393
  { label: 'EBIT', amount: ebit, type: 'source' },
370
394
  { label: 'Impuesto est.', amount: round2(-tax), type: 'use' },
371
395
  { label: 'NOPAT', amount: nopat, type: 'subtotal' },
372
396
  flow('Δ Cob. (AR)', delta_ar),
373
- flow('Δ Prov. (AP)', delta_ap),
374
- flow('Δ Inventario', delta_inventory),
375
- flow('Δ Impuestos', delta_taxes),
397
+ flow('Δ Impuestos (activo)', delta_taxes),
376
398
  flow('Δ Prepagados', delta_prepaid),
399
+ flow('Δ Inventario', delta_inventory),
400
+ flow('Δ Prov. (AP)', delta_ap),
401
+ flow('Δ Imp. x pagar', delta_taxes_payable),
402
+ flow('Δ Pasivos laborales', delta_labor),
377
403
  { label: 'CFO', amount: cfo, type: 'subtotal' },
378
404
  { label: 'CapEx', amount: round2(-capex), type: 'use' },
379
405
  { label: 'FCF', amount: fcf, type: 'total' },
380
406
  ];
381
407
  }
382
408
 
383
- function buildInsights({ ebit, delta_ar, delta_ap, delta_inventory, delta_taxes, delta_prepaid, capex, fcf, fcfPct, status, warnings }) {
409
+ function buildInsights({ ebit, delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor, da, capex, fcf, fcfPct, status, warnings }) {
384
410
  const insights = [];
385
411
 
386
- // Mayor driver negativo entre los usos de capital de trabajo
412
+ // Mayor driver negativo entre los usos de capital de trabajo (7 líneas)
387
413
  const uses = [
388
414
  { key: 'delta_inventory', label: 'Inventario', val: delta_inventory },
389
415
  { key: 'delta_ar', label: 'Cobranza (AR)', val: delta_ar },
390
416
  { key: 'delta_ap', label: 'Pagos a proveedores (AP)', val: delta_ap },
391
417
  { key: 'delta_taxes', label: 'Impuestos corrientes', val: delta_taxes },
392
418
  { key: 'delta_prepaid', label: 'Prepagados', val: delta_prepaid },
419
+ { key: 'delta_taxes_payable', label: 'Impuestos por pagar', val: delta_taxes_payable },
420
+ { key: 'delta_labor', label: 'Pasivos laborales', val: delta_labor },
393
421
  ].filter((u) => u.val < 0).sort((a, b) => a.val - b.val);
394
422
  if (uses.length) {
395
423
  insights.push(`${uses[0].label} es el mayor uso de caja del período ($${fmtAbs(uses[0].val)}).`);
@@ -400,9 +428,14 @@ function buildInsights({ ebit, delta_ar, delta_ap, delta_inventory, delta_taxes,
400
428
  insights.push(`Cobranza lenta presiona caja: ΔAR −$${fmtAbs(delta_ar)} (>20% del EBIT).`);
401
429
  }
402
430
 
403
- // CapEx solo si hay net change real en PP&E (11110–12899)
431
+ // D&A en cero depreciación posiblemente no contabilizada en BC
432
+ if (da === 0) {
433
+ insights.push('D&A en cero — verificar si se registra depreciación en BC (cuentas 58100-58300 y 81000-82000).');
434
+ }
435
+
436
+ // CapEx — solo si hay net change real en PP&E (11110–12240)
404
437
  if (capex > 0) {
405
- insights.push(`CapEx $${fmtAbs(capex)} en PP&E (activos fijos 11110–12899) detectado.`);
438
+ insights.push(`CapEx $${fmtAbs(capex)} en PP&E (activos fijos 11110–12240) detectado.`);
406
439
  } else if (capex < 0) {
407
440
  insights.push(`Desinversión neta en PP&E $${fmtAbs(capex)} (fuente de caja).`);
408
441
  }
@@ -450,11 +483,15 @@ function consolidateCashFlow(storeObjs) {
450
483
  cogs: sum((o) => o.cogs.total),
451
484
  gross_margin_amount: sum((o) => o.gross_margin.amount),
452
485
  ebit: sum((o) => o.ebit.amount),
486
+ da: sum((o) => (o.da ? o.da.amount : 0)),
487
+ ebitda: sum((o) => (o.ebitda ? o.ebitda.amount : 0)),
453
488
  delta_ar: sum((o) => o.working_capital.delta_ar),
454
- delta_ap: sum((o) => o.working_capital.delta_ap),
455
- delta_inventory: sum((o) => o.working_capital.delta_inventory),
456
489
  delta_taxes: sum((o) => o.working_capital.delta_taxes || 0),
457
490
  delta_prepaid: sum((o) => o.working_capital.delta_prepaid || 0),
491
+ delta_inventory: sum((o) => o.working_capital.delta_inventory),
492
+ delta_ap: sum((o) => o.working_capital.delta_ap),
493
+ delta_taxes_payable: sum((o) => o.working_capital.delta_taxes_payable || 0),
494
+ delta_labor: sum((o) => o.working_capital.delta_labor || 0),
458
495
  capex: sum((o) => o.capex.amount),
459
496
  capex_accounts,
460
497
  warnings: allWarnings,
@@ -222,7 +222,9 @@ function renderPnlCard(s) {
222
222
  ${expenseRows}
223
223
  <div class="pnl-row total"><span class="pnl-label">(&minus;) Total Gastos Operativos</span><span class="pnl-amt">$${fmt(s.expenses.total)}</span></div>
224
224
 
225
- <div class="pnl-row operating"><span class="pnl-label">= Utilidad Operativa <span class="pnl-pct ${valClass(s.ratios.operating_margin_pct.status)}">${s.operating_income.pct}%</span></span><span class="pnl-amt ${valClass(s.ratios.operating_margin_pct.status)}">$${fmt(s.operating_income.amount)}</span></div>
225
+ <div class="pnl-row operating"><span class="pnl-label">= Utilidad Operativa (EBIT) <span class="pnl-pct ${valClass(s.ratios.operating_margin_pct.status)}">${s.operating_income.pct}%</span></span><span class="pnl-amt ${valClass(s.ratios.operating_margin_pct.status)}">$${fmt(s.operating_income.amount)}</span></div>
226
+ ${s.ebitda ? `<div class="pnl-row sub"><span class="pnl-label">(+) Depreciación y Amortización</span><span class="pnl-amt">$${fmt(s.depreciation_amortization.amount)}</span></div>
227
+ <div class="pnl-row gross"><span class="pnl-label">= EBITDA <span class="pnl-pct">${s.ebitda.pct}%</span></span><span class="pnl-amt">$${fmt(s.ebitda.amount)}</span></div>` : ''}
226
228
  </div>
227
229
  </div>
228
230
  </div>`;
@@ -39,9 +39,16 @@ function buildStoreFinancials(storeCode, storeName, incomeEntries, cogsEntries,
39
39
  const opAmount = round2(gmAmount - expenses.total);
40
40
  const opPct = income.total > 0 ? round2((opAmount / income.total) * 100) : 0;
41
41
 
42
+ // D&A — depreciación/amortización ya contenida en COGS (58000-58999) y OpEx
43
+ // (80000-89999). Se calcula filtrando las entries ya traídas (sin query nueva).
44
+ // EBIT NO cambia (D&A ya está restada); EBITDA = EBIT + D&A la re-suma.
45
+ const da = computeDepreciation(cogsEntries, expenseEntries);
46
+ const ebitdaAmount = round2(opAmount + da);
47
+ const ebitdaPct = income.total > 0 ? round2((ebitdaAmount / income.total) * 100) : 0;
48
+
42
49
  const ratios = calcRatios(income, cogs, expenses);
43
50
  const score = calcScore(ratios);
44
- const insights = generateInsights(expenses.categories, ratios);
51
+ const insights = generateInsights(expenses.categories, ratios, da);
45
52
 
46
53
  return {
47
54
  store_code: storeCode,
@@ -51,12 +58,24 @@ function buildStoreFinancials(storeCode, storeName, incomeEntries, cogsEntries,
51
58
  gross_margin: { amount: gmAmount, pct: gmPct },
52
59
  expenses,
53
60
  operating_income: { amount: opAmount, pct: opPct },
61
+ depreciation_amortization: { amount: da },
62
+ ebitda: { amount: ebitdaAmount, pct: ebitdaPct },
54
63
  ratios,
55
64
  insights,
56
65
  score,
57
66
  };
58
67
  }
59
68
 
69
+ // D&A = Σ(debit − credit) sobre 58000-58999 (en COGS) + 80000-89999 (en OpEx).
70
+ // Mismas entries del P&L — no requiere consulta adicional a BC.
71
+ function computeDepreciation(cogsEntries, expenseEntries) {
72
+ const inRange = (acct, lo, hi) => { const n = parseInt(acct, 10); return n >= lo && n <= hi; };
73
+ let da = 0;
74
+ for (const e of cogsEntries) if (inRange(e.accountNumber, 58000, 58999)) da += (e.debitAmount || 0) - (e.creditAmount || 0);
75
+ for (const e of expenseEntries) if (inRange(e.accountNumber, 80000, 89999)) da += (e.debitAmount || 0) - (e.creditAmount || 0);
76
+ return round2(da);
77
+ }
78
+
60
79
  // ─────────────────────────────────────────────────────────────────────────────
61
80
  // fetchStoreFinancials — 3 queries BC en paralelo para una tienda
62
81
  // Devuelve { financials, raw } (raw se usa para consolidar sin re-consultar BC)