@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.
|
|
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), 'PP&E 11110-
|
|
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,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('Δ
|
|
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 ? '↑' : '↓'}</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
|
|
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
|
-
//
|
|
16
|
-
// (
|
|
17
|
-
//
|
|
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.
|
|
18
21
|
//
|
|
19
|
-
// CAPEX =
|
|
20
|
-
// tangibles
|
|
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.
|
|
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
|
-
'(
|
|
48
|
-
'caja operativo (CFO), y resta CapEx (PP&E
|
|
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
|
-
//
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
340
|
-
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 || [] });
|
|
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: {
|
|
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,
|
|
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('Δ
|
|
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,
|
|
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
|
-
//
|
|
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–
|
|
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">(−) 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)
|