@fullqueso/mcp-bc-gastos 1.23.0 → 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,34 @@
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
+
18
+ ## [1.23.1] — 2026-06-05
19
+
20
+ ### Fixed
21
+ - **`get_cash_flow` — CapEx y Working Capital alineados al Chart of Accounts real de BC.** El CapEx sumaba el rango `15000–17999`, que en el CoA real son **activos corrientes** (cuentas por cobrar 151xx, impuestos corrientes 152xx, prepagados 16xxx, inversiones corto plazo 17xxx), **no** activos fijos. Esto inflaba CapEx y distorsionaba el FCF.
22
+ - **CapEx ahora = net change en PP&E `11110–12899`** (intangibles 11110–11190 + tangibles 12110–12240), **excluye `12900`** (depreciación acumulada), `17000–17999` (inversiones corto plazo) y `18000–18999` (caja/bancos).
23
+ - **Nueva línea WC `Δ Impuestos corrientes`** (`15210–15280`: IVA crédito fiscal, retenciones IVA/ISLR, anticipos/excedente ISLR) = `−Σ(debit−credit)`.
24
+ - **Nueva línea WC `Δ Prepagados`** (`16110–16900`: anticipos a proveedores, alquileres/otros prepagados) = `−Σ(debit−credit)`.
25
+ - `net_wc_change = ΔAR + ΔAP + ΔInv + ΔImpuestos + ΔPrepagados`. ΔAR (subledger, equivale a 151xx) y ΔInventario (`itemLedgerEntries`, equivale a 14xxx) sin cambios — no se duplican vía G/L.
26
+ - `fcf_bridge` agrega filas `Δ Impuestos` y `Δ Prepagados` (11 ítems). Insight de "CapEx detectado" solo aparece si hay net change real en `11110–12899`; el insight de mayor uso de caja ahora reporta cualquier driver dominante (no solo inventario).
27
+ - HTML: tarjeta de Capital de Trabajo muestra las dos líneas nuevas; KPI CapEx etiquetado `PP&E 11110-12899`.
28
+ - **Verificado contra BC real (FQ88, mayo 2026):** CapEx ANTES `$8,760.92` (cuentas 151xx/152xx) → DESPUÉS `$0` (sin movimientos PP&E). Bridge cuadra: NOPAT `$12,340.71` + ΔWC `$10,794.99` − CapEx `$0` = FCF `$23,135.70`. Cuentas 15110/15135/15210 ya no aparecen en CapEx.
29
+
30
+ ---
31
+
3
32
  ## [1.23.0] — 2026-05-31
4
33
 
5
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullqueso/mcp-bc-gastos",
3
- "version": "1.23.0",
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), 'activos fijos')}
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,8 +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)}
241
+ ${row('Δ Impuestos (activo)', wc.delta_taxes || 0)}
242
+ ${row('Δ Prepagados', wc.delta_prepaid || 0)}
240
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)}
241
247
  <div class="wc-row total">
242
248
  <span class="wc-label">Cambio neto WC</span>
243
249
  <span class="wc-arrow ${wc.net_wc_change >= 0 ? 'up' : 'down'}">${wc.net_wc_change >= 0 ? '&#x2191;' : '&#x2193;'}</span>
@@ -277,6 +283,7 @@ function renderStoresComparison(result) {
277
283
 
278
284
  const body = [
279
285
  row('EBIT', (c) => signed(c.ebit.amount)),
286
+ row('EBITDA', (c) => signed(c.ebitda ? c.ebitda.amount : c.ebit.amount)),
280
287
  row('NOPAT', (c) => signed(c.nopat.amount)),
281
288
  row('CFO', (c) => `<span class="${c.cfo.amount >= 0 ? 'val-green' : 'val-red'}">${signed(c.cfo.amount)}</span>`),
282
289
  row('CapEx', (c) => signed(-c.capex.amount)),
@@ -1,18 +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
10
- // delta_ap = −Σ amountLocalCurrency(vendorLedgerEntries) + = debe más (fuente)
11
- // delta_inventory = −Σ costAmountActual(itemLedgerEntries) + = inventario bajó (fuente)
12
- // net_wc_change = delta_ar + delta_ap + delta_inventory
13
- // nopat = ebit tax ; cfo = nopat + net_wc_change ; fcf = cfo − capex
14
- // (Resuelve la inconsistencia del spec entre los comentarios del schema y la fórmula:
15
- // se adopta la convención de los comentarios — los 3 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 + 15900–15998 (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.
21
+ //
22
+ // CAPEX = +Σ(debit − credit) en PP&E 11110–11190 (intangibles) + 12110–12240
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.
16
29
  //
17
30
  // Montos en USD (LCY = Additional Reporting Currency), igual que get_financial_statements.
18
31
 
@@ -38,8 +51,10 @@ export const cashFlowTool = {
38
51
  description:
39
52
  'Free Cash Flow (FCF) por método indirecto para una o más tiendas Full Queso (FQ01, FQ28, FQ88). ' +
40
53
  'Parte del EBIT (igual que get_financial_statements), resta impuesto estimado (ISLR 34%), ajusta por capital de trabajo ' +
41
- '(Δ cuentas por cobrar, Δ cuentas por pagar, Δ inventario) para obtener el flujo de caja operativo (CFO), y resta CapEx ' +
42
- '(activos fijos 15000-17999) para el FCF. Incluye NOPAT, margen FCF, status (healthy/positive_low/negative_watch/' +
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 ' +
57
+ '(healthy/positive_low/negative_watch/' +
43
58
  'negative_critical), bridge tipo waterfall e insights automáticos. Multi-tienda calcula consolidado. Con render_html=true ' +
44
59
  'genera un dashboard HTML (lo guarda en ~/Downloads y retorna la ruta). Úsalo cuando el usuario pida flujo de caja, ' +
45
60
  'cash flow, FCF, liquidez, "cuánta caja genera", o capital de trabajo. READ-ONLY.',
@@ -202,9 +217,15 @@ async function buildStoreCashFlow(bcClient, store, start, end) {
202
217
  cogs: financials.cogs.total,
203
218
  gross_margin_amount: financials.gross_margin.amount,
204
219
  ebit: financials.operating_income.amount,
220
+ da: financials.depreciation_amortization.amount,
221
+ ebitda: financials.ebitda.amount,
205
222
  delta_ar: wc.delta_ar,
206
- delta_ap: wc.delta_ap,
223
+ delta_taxes: wc.delta_taxes,
224
+ delta_prepaid: wc.delta_prepaid,
207
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,
208
229
  capex: wc.capex,
209
230
  capex_accounts: wc.capex_accounts,
210
231
  warnings: wc.warnings,
@@ -212,70 +233,90 @@ async function buildStoreCashFlow(bcClient, store, start, end) {
212
233
  );
213
234
  }
214
235
 
215
- // 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.
216
266
  async function fetchWorkingCapitalAndCapex(bcClient, companyId, start, end) {
217
- const sd = bcClient._sanitizeOData(start);
218
- const ed = bcClient._sanitizeOData(end);
219
- const periodFilter = `postingDate ge ${sd} and postingDate le ${ed}`;
220
-
221
- const arUrl = bcClient.buildBetaApiUrl(companyId, 'customerLedgerEntries', {
222
- $filter: periodFilter,
223
- $select: 'postingDate,documentType,amountLocalCurrency',
224
- });
225
- const apUrl = bcClient.buildBetaApiUrl(companyId, 'vendorLedgerEntries', {
226
- $filter: periodFilter,
227
- $select: 'postingDate,documentType,amountLocalCurrency',
228
- });
229
- const ileUrl = bcClient.buildApiUrl(companyId, 'itemLedgerEntries', {
230
- $filter: periodFilter,
231
- $select: 'postingDate,entryType,quantity,costAmountActual',
232
- });
233
-
234
- const [arEntries, apEntries, ileEntries, capexEntries] = await Promise.all([
235
- bcClient.apiCallAllPages(arUrl).catch((e) => { logger.warn(`[cash-flow] AR fetch failed: ${e.message}`); return null; }),
236
- bcClient.apiCallAllPages(apUrl).catch((e) => { logger.warn(`[cash-flow] AP fetch failed: ${e.message}`); return null; }),
237
- bcClient.apiCallAllPages(ileUrl).catch((e) => { logger.warn(`[cash-flow] ILE fetch failed: ${e.message}`); return null; }),
238
- bcClient.getGLEntries(companyId, start, end, '15000', '17999').catch((e) => { logger.warn(`[cash-flow] CapEx 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; }),
239
274
  ]);
240
275
 
241
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 = {};
242
279
 
243
- // ΔAR — impacto en caja = −Σ amountLocalCurrency (invoices +, payments −)
244
- let delta_ar = 0;
245
- if (arEntries == null) warnings.push('Customer ledger no disponible — ΔAR asumido en 0.');
246
- else delta_ar = round2(-arEntries.reduce((s, e) => s + (e.amountLocalCurrency || 0), 0));
247
-
248
- // ΔAP — impacto en caja = −Σ amountLocalCurrency (invoices −, payments +)
249
- let delta_ap = 0;
250
- if (apEntries == null) warnings.push('Vendor ledger no disponible — ΔAP asumido en 0.');
251
- else delta_ap = round2(-apEntries.reduce((s, e) => s + (e.amountLocalCurrency || 0), 0));
252
-
253
- // ΔInventario impacto en caja = −Σ costAmountActual (compras +, ventas )
254
- // costAmountActual ya es el costo total de la entrada (no multiplicar por quantity).
255
- let delta_inventory = 0;
256
- if (ileEntries == null) warnings.push('Item ledger no disponible — Δinventario asumido en 0.');
257
- else {
258
- delta_inventory = round2(-ileEntries.reduce((s, e) => s + (e.costAmountActual || 0), 0));
259
- 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.');
260
291
  }
261
292
 
262
- // CapEx — débitos netos en activos fijos 15000-17999 (excluye 18xxx bancos)
263
- let capex = 0;
264
- const capex_accounts = {};
265
- if (capexEntries == null) {
266
- warnings.push('G/L de activos fijos 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.');
267
295
  } else {
268
- for (const e of capexEntries) {
269
- const net = (e.debitAmount || 0) - (e.creditAmount || 0);
270
- capex += net;
271
- 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);
272
300
  }
273
- capex = round2(capex);
274
- // limpiar cuentas con neto 0
275
- 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.');
276
302
  }
277
303
 
278
- return { delta_ar, delta_ap, delta_inventory, 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
+ };
279
320
  }
280
321
 
281
322
  // ─────────────────────────────────────────────────────────────────────────────
@@ -291,10 +332,22 @@ function assembleCashFlow(meta, c) {
291
332
  const tax = ebit > 0 ? round2(ebit * TAX_RATE) : 0;
292
333
  const nopat = round2(ebit - tax);
293
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)).
294
341
  const delta_ar = round2(c.delta_ar);
295
- const delta_ap = round2(c.delta_ap);
342
+ const delta_taxes = round2(c.delta_taxes || 0);
343
+ const delta_prepaid = round2(c.delta_prepaid || 0);
296
344
  const delta_inventory = round2(c.delta_inventory);
297
- const net_wc_change = round2(delta_ar + delta_ap + 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
+ );
298
351
 
299
352
  const cfo = round2(nopat + net_wc_change);
300
353
  const cfoPct = income > 0 ? round2((cfo / income) * 100) : 0;
@@ -304,8 +357,9 @@ function assembleCashFlow(meta, c) {
304
357
  const fcfPct = income > 0 ? round2((fcf / income) * 100) : 0;
305
358
 
306
359
  const status = fcfStatus(fcf, fcfPct, ebit);
307
- const bridge = buildBridge({ ebit, tax, nopat, delta_ar, delta_ap, delta_inventory, cfo, capex, fcf });
308
- const insights = buildInsights({ ebit, delta_ar, delta_ap, delta_inventory, 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 || [] });
309
363
 
310
364
  return {
311
365
  store_code: meta.store_code,
@@ -314,9 +368,11 @@ function assembleCashFlow(meta, c) {
314
368
  cogs: { total: round2(c.cogs) },
315
369
  gross_margin: { amount: round2(c.gross_margin_amount), pct: gmPct },
316
370
  ebit: { amount: ebit, pct: ebitPct },
371
+ da: { amount: da },
372
+ ebitda: { amount: ebitda, pct: ebitdaPct },
317
373
  tax_estimated: { amount: tax, rate: TAX_RATE, note: 'Estimado ISLR 34%' },
318
374
  nopat: { amount: nopat },
319
- working_capital: { delta_ar, delta_ap, delta_inventory, net_wc_change },
375
+ working_capital: { ...wc, net_wc_change },
320
376
  cfo: { amount: cfo, pct: cfoPct },
321
377
  capex: { amount: capex, accounts: c.capex_accounts || {} },
322
378
  fcf: { amount: fcf, pct: fcfPct, status },
@@ -331,32 +387,40 @@ function fcfStatus(fcf, fcfPct, ebit) {
331
387
  return fcf > ebit * -0.2 ? 'negative_watch' : 'negative_critical';
332
388
  }
333
389
 
334
- function buildBridge({ ebit, tax, nopat, delta_ar, delta_ap, delta_inventory, 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 }) {
335
391
  const flow = (label, amount) => ({ label, amount: round2(amount), type: amount >= 0 ? 'source' : 'use' });
336
392
  return [
337
393
  { label: 'EBIT', amount: ebit, type: 'source' },
338
394
  { label: 'Impuesto est.', amount: round2(-tax), type: 'use' },
339
395
  { label: 'NOPAT', amount: nopat, type: 'subtotal' },
340
396
  flow('Δ Cob. (AR)', delta_ar),
341
- flow('Δ Prov. (AP)', delta_ap),
397
+ flow('Δ Impuestos (activo)', delta_taxes),
398
+ flow('Δ Prepagados', delta_prepaid),
342
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),
343
403
  { label: 'CFO', amount: cfo, type: 'subtotal' },
344
404
  { label: 'CapEx', amount: round2(-capex), type: 'use' },
345
405
  { label: 'FCF', amount: fcf, type: 'total' },
346
406
  ];
347
407
  }
348
408
 
349
- function buildInsights({ ebit, delta_ar, delta_ap, delta_inventory, 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 }) {
350
410
  const insights = [];
351
411
 
352
- // Mayor driver negativo entre los usos de capital de trabajo
412
+ // Mayor driver negativo entre los usos de capital de trabajo (7 líneas)
353
413
  const uses = [
354
414
  { key: 'delta_inventory', label: 'Inventario', val: delta_inventory },
355
415
  { key: 'delta_ar', label: 'Cobranza (AR)', val: delta_ar },
356
416
  { key: 'delta_ap', label: 'Pagos a proveedores (AP)', val: delta_ap },
417
+ { key: 'delta_taxes', label: 'Impuestos corrientes', val: delta_taxes },
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 },
357
421
  ].filter((u) => u.val < 0).sort((a, b) => a.val - b.val);
358
- if (uses.length && uses[0].key === 'delta_inventory') {
359
- insights.push(`Inventario es el mayor uso de caja del período ($${fmtAbs(delta_inventory)}).`);
422
+ if (uses.length) {
423
+ insights.push(`${uses[0].label} es el mayor uso de caja del período ($${fmtAbs(uses[0].val)}).`);
360
424
  }
361
425
 
362
426
  // AR consume >20% del EBIT (cobranza lenta)
@@ -364,9 +428,16 @@ function buildInsights({ ebit, delta_ar, delta_ap, delta_inventory, capex, fcf,
364
428
  insights.push(`Cobranza lenta presiona caja: ΔAR −$${fmtAbs(delta_ar)} (>20% del EBIT).`);
365
429
  }
366
430
 
367
- // CapEx detectado
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)
368
437
  if (capex > 0) {
369
- insights.push(`CapEx $${fmtAbs(capex)} en activos fijos detectado.`);
438
+ insights.push(`CapEx $${fmtAbs(capex)} en PP&E (activos fijos 11110–12240) detectado.`);
439
+ } else if (capex < 0) {
440
+ insights.push(`Desinversión neta en PP&E $${fmtAbs(capex)} (fuente de caja).`);
370
441
  }
371
442
 
372
443
  // Margen FCF bajo
@@ -412,9 +483,15 @@ function consolidateCashFlow(storeObjs) {
412
483
  cogs: sum((o) => o.cogs.total),
413
484
  gross_margin_amount: sum((o) => o.gross_margin.amount),
414
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)),
415
488
  delta_ar: sum((o) => o.working_capital.delta_ar),
416
- delta_ap: sum((o) => o.working_capital.delta_ap),
489
+ delta_taxes: sum((o) => o.working_capital.delta_taxes || 0),
490
+ delta_prepaid: sum((o) => o.working_capital.delta_prepaid || 0),
417
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),
418
495
  capex: sum((o) => o.capex.amount),
419
496
  capex_accounts,
420
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)