@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.
|
|
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&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), '
|
|
215
|
+
${cell('CapEx', signed(-s.capex.amount), 'PP&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('Δ
|
|
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 ? '↑' : '↓'}</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
|
|
5
|
-
// le agrega impuesto estimado, capital de trabajo (
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// (
|
|
15
|
-
//
|
|
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, Δ
|
|
42
|
-
'
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
308
|
-
const
|
|
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: {
|
|
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,
|
|
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('Δ
|
|
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,
|
|
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
|
|
359
|
-
insights.push(
|
|
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
|
-
//
|
|
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
|
-
|
|
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">(−) 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)
|