@fullqueso/mcp-bc-gastos 1.26.0 → 1.28.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.
Files changed (61) hide show
  1. package/.env.example +1 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +3 -1
  4. package/config/bank-gl-map.json +12 -10
  5. package/config/company-config.js +8 -1
  6. package/lib/bc-client.js +32 -0
  7. package/package.json +1 -1
  8. package/tools/account-transactions.js +1 -1
  9. package/tools/anomaly-detection.js +1 -1
  10. package/tools/auditoria/bank-ledger-entries.js +1 -1
  11. package/tools/auditoria/bank-reconciliation-report.js +1 -1
  12. package/tools/auditoria/find-potential-matches.js +1 -1
  13. package/tools/auditoria/gl-account-entries.js +1 -1
  14. package/tools/auditoria/list-bank-accounts.js +1 -1
  15. package/tools/auditoria/pm-receipts.js +1 -1
  16. package/tools/auditoria/reconcile-pos-sales.js +1 -1
  17. package/tools/auditoria/reconciliation-status.js +1 -1
  18. package/tools/auditoria/suggest-journal-entries.js +1 -1
  19. package/tools/auditoria/unmatched-ledger-entries.js +1 -1
  20. package/tools/auditoria/unmatched-statement-lines.js +1 -1
  21. package/tools/cierre-mensual/generate-closing-journal.js +1 -1
  22. package/tools/cierre-mensual/get-match-results.js +1 -1
  23. package/tools/cierre-mensual/get-questionnaire.js +1 -1
  24. package/tools/cierre-mensual/reconcile-with-bc.js +1 -1
  25. package/tools/cierre-mensual/start-month-closing.js +1 -1
  26. package/tools/cierre-mensual/submit-answers.js +1 -1
  27. package/tools/cobranzas/collection-status.js +1 -1
  28. package/tools/cobranzas/customer-balances.js +1 -1
  29. package/tools/cobranzas/customer-ledger.js +1 -1
  30. package/tools/cobranzas/customer-list.js +1 -1
  31. package/tools/cobranzas/open-payables.js +1 -1
  32. package/tools/cobranzas/open-receivables.js +1 -1
  33. package/tools/cobranzas/vendor-ledger.js +1 -1
  34. package/tools/efficiency-ratios.js +1 -1
  35. package/tools/expense-analysis.js +1 -1
  36. package/tools/expense-details.js +1 -1
  37. package/tools/financials/cash-flow-html.js +41 -13
  38. package/tools/financials/cash-flow.js +24 -112
  39. package/tools/financials/fx-cash.js +227 -0
  40. package/tools/financials/index.js +3 -3
  41. package/tools/get-exchange-rate.js +1 -1
  42. package/tools/inventario/inventory-by-location.js +1 -1
  43. package/tools/inventario/inventory-change.js +1 -1
  44. package/tools/inventario/inventory-levels.js +1 -1
  45. package/tools/inventario/item-card.js +1 -1
  46. package/tools/inventario/item-cost-trend.js +1 -1
  47. package/tools/inventario/item-ledger-entries.js +1 -1
  48. package/tools/inventario/item-value-entries.js +1 -1
  49. package/tools/list-vendors.js +1 -1
  50. package/tools/multi-payment/draft-payables.js +1 -1
  51. package/tools/multi-payment/draft-receivables.js +1 -1
  52. package/tools/multi-payment/draft-summary.js +1 -1
  53. package/tools/payroll/employees.js +1 -1
  54. package/tools/payroll/payroll-documents.js +1 -1
  55. package/tools/payroll/payroll-lines.js +1 -1
  56. package/tools/reports/manager-report.js +1 -1
  57. package/tools/trends.js +1 -1
  58. package/tools/vendor-transactions.js +1 -1
  59. package/tools/ventas/item-sales-detail.js +1 -1
  60. package/tools/ventas/product-performance.js +1 -1
  61. package/tools/ventas/sales-analysis.js +1 -1
@@ -39,6 +39,7 @@ import { round2 } from '../../utils/currency-converter.js';
39
39
  import { logger } from '../../utils/logger.js';
40
40
  import { fetchStoreFinancials, periodLabel, previousMonthRange } from './statements.js';
41
41
  import { buildCashFlowHTML } from './cash-flow-html.js';
42
+ import { computeCashPosition, consolidateCashPositions } from './fx-cash.js';
42
43
 
43
44
  const TAX_RATE = 0.34; // ISLR Venezuela estándar (estimado)
44
45
 
@@ -65,7 +66,7 @@ export const cashFlowTool = {
65
66
  properties: {
66
67
  stores: {
67
68
  type: 'array',
68
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
69
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
69
70
  description: 'Tiendas a analizar. Default: ["all"] (FQ01 + FQ28 + FQ88 + consolidado).',
70
71
  default: ['all'],
71
72
  },
@@ -216,7 +217,7 @@ async function buildStoreCashFlow(bcClient, store, start, end, opts = {}) {
216
217
  fetchStoreFinancials(bcClient, store, start, end),
217
218
  fetchWorkingCapitalAndCapex(bcClient, store.companyId, start, end),
218
219
  opts.includeCashPosition && opts.rates
219
- ? fetchCashPosition(bcClient, store.companyId, start, end, opts.rates)
220
+ ? computeCashPosition(bcClient, store, start, end, opts.rates).catch((e) => { logger.warn(`[cash-flow] cash position failed: ${e.message}`); return null; })
220
221
  : Promise.resolve(null),
221
222
  ]);
222
223
 
@@ -244,80 +245,8 @@ async function buildStoreCashFlow(bcClient, store, start, end, opts = {}) {
244
245
  );
245
246
  }
246
247
 
247
- // ─────────────────────────────────────────────────────────────────────────────
248
- // Posición de caja + efecto cambiario (devaluación VES) — READ-ONLY, NO afecta el FCF.
249
- // Regla VES (confirmada por FP, estructura compartida entre tiendas):
250
- // bancos VES = 18200–18299 ; caja VEB = 18110/18130/18160. Resto de 18xxx = USD.
251
- // El saldo VES vive en additionalCurrency (VES); el USD libro (debit/credit) quedó "stale"
252
- // a tasas históricas. fx_adjustment = USD_real (VES/tasa_fin) − USD_libro (< 0 si VES se devaluó).
253
- // ─────────────────────────────────────────────────────────────────────────────
254
-
255
- const VES_CASH_ACCOUNTS = new Set(['18110', '18130', '18160']);
256
- function isVesCashAccount(accountNumber) {
257
- const n = parseInt(accountNumber, 10);
258
- return (n >= 18200 && n <= 18299) || VES_CASH_ACCOUNTS.has(String(accountNumber));
259
- }
260
-
261
- async function fetchCashPosition(bcClient, companyId, start, end, rates) {
262
- const entries = await bcClient.getCashGLBalances(companyId, end).catch((e) => {
263
- logger.warn(`[cash-flow] cash balances fetch failed: ${e.message}`);
264
- return null;
265
- });
266
- if (entries == null) return null;
267
-
268
- const rateEnd = (bcClient.getExchangeRateForDate(rates, end) || {}).rate || null;
269
- const rateStart = (bcClient.getExchangeRateForDate(rates, start) || {}).rate || null;
270
- if (!rateEnd) return null;
271
-
272
- const warnings = [];
273
- if (entries._truncated) warnings.push('G/L de caja/bancos truncado — saldos pueden estar incompletos.');
274
-
275
- // Acumular por cuenta: USD libro (debit−credit) y VES (additionalCurrency debit−credit)
276
- // (no se usa e.description como nombre: es la nota de la transacción, no el nombre de la cuenta)
277
- const acc = {};
278
- for (const e of entries) {
279
- const a = e.accountNumber;
280
- if (!acc[a]) acc[a] = { usd: 0, ves: 0 };
281
- acc[a].usd += (e.debitAmount || 0) - (e.creditAmount || 0);
282
- acc[a].ves += (e.additionalCurrencyDebitAmount || 0) - (e.additionalCurrencyCreditAmount || 0);
283
- }
284
-
285
- const vesAccounts = [];
286
- let vesBook = 0, vesReal = 0, usdTotal = 0;
287
- for (const [a, v] of Object.entries(acc)) {
288
- const usdBook = round2(v.usd);
289
- if (Math.abs(usdBook) < 0.005 && Math.abs(v.ves) < 0.5) continue; // saldo cero
290
- if (isVesCashAccount(a)) {
291
- const usdReal = round2(v.ves / rateEnd);
292
- vesBook += usdBook;
293
- vesReal += usdReal;
294
- const n = parseInt(a, 10);
295
- vesAccounts.push({
296
- account: a, name: n >= 18200 && n <= 18299 ? 'Banco VES' : 'Caja VES',
297
- usd_book: usdBook, ves_balance: round2(v.ves), usd_real: usdReal,
298
- fx_adjustment: round2(usdReal - usdBook),
299
- });
300
- } else {
301
- usdTotal += usdBook; // cuentas USD: tenencia física en USD → sin ajuste
302
- }
303
- }
304
- vesAccounts.sort((x, y) => x.fx_adjustment - y.fx_adjustment); // mayor pérdida primero
305
-
306
- vesBook = round2(vesBook);
307
- vesReal = round2(vesReal);
308
- usdTotal = round2(usdTotal);
309
-
310
- return {
311
- rate_end: round2(rateEnd),
312
- rate_start: rateStart ? round2(rateStart) : null,
313
- month_devaluation_pct: rateStart && rateStart > 0 ? round2((rateEnd / rateStart - 1) * 100) : null,
314
- ves: { book_usd: vesBook, real_usd: vesReal, fx_adjustment: round2(vesReal - vesBook), accounts: vesAccounts },
315
- usd: { total_usd: usdTotal },
316
- total_cash_book_usd: round2(vesBook + usdTotal),
317
- total_cash_real_usd: round2(vesReal + usdTotal),
318
- warnings,
319
- };
320
- }
248
+ // La posición de caja + efecto cambiario (revaluación del saldo VES contra el saldo REAL del
249
+ // banco, aislando el backlog de conciliación) vive en ./fx-cash.js (módulo compartido).
321
250
 
322
251
  // Buckets del balance general (Chart of Accounts real de BC), por rango de cuenta.
323
252
  // REGLA UNIVERSAL de impacto en caja: delta = −Σ(debit − credit) sobre el rango.
@@ -497,18 +426,28 @@ function buildBridge({ ebit, tax, nopat, delta_ar, delta_taxes, delta_prepaid, d
497
426
  function buildInsights({ ebit, delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor, da, capex, fcf, fcfPct, status, cash_position, warnings }) {
498
427
  const insights = [];
499
428
 
500
- // Efecto cambiario sobre caja VES el saldo en libros sobrestima la caja real porque
501
- // los bolívares no se han revaluado a la tasa de cierre (devaluación no contabilizada).
502
- if (cash_position && cash_position.ves.book_usd > 0) {
503
- const adj = cash_position.ves.fx_adjustment;
504
- const pct = round2((adj / cash_position.ves.book_usd) * 100);
505
- if (adj < 0 && Math.abs(adj) >= 1) {
429
+ // Posición de caja: separar la BRECHA DE CONCILIACIÓN (backlog en VES, a postear) del
430
+ // EFECTO CAMBIARIO sobre la caja real (asiento de diferencial cambiario). El FX se mide
431
+ // sobre el saldo real del banco (statement), no sobre el GL inflado por el backlog.
432
+ if (cash_position && cash_position.totals) {
433
+ const t = cash_position.totals;
434
+ if (Math.abs(t.recon_gap_ves) >= 1000) {
435
+ const gapUsd = cash_position.rate_end ? t.recon_gap_ves / cash_position.rate_end : 0;
436
+ insights.push(
437
+ `Brecha de conciliación: el GL excede al saldo real del banco en ${fmtAbs(t.recon_gap_ves)} VES ` +
438
+ `(~$${fmtAbs(gapUsd)}) — transacciones por postear; no es efecto cambiario.`
439
+ );
440
+ }
441
+ if (t.fx_real < 0 && Math.abs(t.fx_real) >= 1) {
506
442
  const dev = cash_position.month_devaluation_pct != null ? ` (devaluación del mes ${cash_position.month_devaluation_pct}%)` : '';
507
443
  insights.push(
508
- `Caja VES sobrestimada $${fmtAbs(adj)} (${Math.abs(pct)}%) por devaluación no contabilizada ` +
509
- `valor real $${fmtAbs(cash_position.total_cash_real_usd)} vs $${fmtAbs(cash_position.total_cash_book_usd)} en libros${dev}.`
444
+ `Efecto cambiario sobre caja VES real: −$${fmtAbs(t.fx_real)}${dev} postear asiento de ` +
445
+ `diferencial cambiario. Caja VES real $${fmtAbs(t.ves_real_usd)} (vs $${fmtAbs(cash_position.gl_basis.ves_book_usd)} en libros GL).`
510
446
  );
511
447
  }
448
+ if (cash_position.pending_no_statement && cash_position.pending_no_statement.length) {
449
+ insights.push(`${cash_position.pending_no_statement.length} cuenta(s) VES sin statement — revaluación provisional, conciliar antes de postear.`);
450
+ }
512
451
  }
513
452
 
514
453
  // Mayor driver negativo entre los usos de capital de trabajo (7 líneas)
@@ -597,39 +536,12 @@ function consolidateCashFlow(storeObjs) {
597
536
  delta_labor: sum((o) => o.working_capital.delta_labor || 0),
598
537
  capex: sum((o) => o.capex.amount),
599
538
  capex_accounts,
600
- cash_position: consolidateCashPosition(storeObjs),
539
+ cash_position: consolidateCashPositions(storeObjs.map((o) => (o.cash_position ? { ...o.cash_position, _store: o.store_code } : null))),
601
540
  warnings: allWarnings,
602
541
  }
603
542
  );
604
543
  }
605
544
 
606
- // Consolida la posición de caja (FX) sumando escalares y concatenando cuentas VES.
607
- function consolidateCashPosition(storeObjs) {
608
- const positions = storeObjs.map((o) => o.cash_position).filter(Boolean);
609
- if (!positions.length) return null;
610
- const sumP = (fn) => round2(positions.reduce((s, p) => s + fn(p), 0));
611
- const accounts = [];
612
- for (const o of storeObjs) {
613
- if (!o.cash_position) continue;
614
- for (const a of o.cash_position.ves.accounts) accounts.push({ ...a, store: o.store_code });
615
- }
616
- accounts.sort((x, y) => x.fx_adjustment - y.fx_adjustment);
617
- const vesBook = sumP((p) => p.ves.book_usd);
618
- const vesReal = sumP((p) => p.ves.real_usd);
619
- const usdTotal = sumP((p) => p.usd.total_usd);
620
- const ref = positions[0]; // tasa VES compartida entre tiendas
621
- return {
622
- rate_end: ref.rate_end,
623
- rate_start: ref.rate_start,
624
- month_devaluation_pct: ref.month_devaluation_pct,
625
- ves: { book_usd: vesBook, real_usd: vesReal, fx_adjustment: round2(vesReal - vesBook), accounts },
626
- usd: { total_usd: usdTotal },
627
- total_cash_book_usd: round2(vesBook + usdTotal),
628
- total_cash_real_usd: round2(vesReal + usdTotal),
629
- warnings: [],
630
- };
631
- }
632
-
633
545
  // ─────────────────────────────────────────────────────────────────────────────
634
546
  // Comparación MoM (compacta)
635
547
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,227 @@
1
+ // tools/financials/fx-cash.js
2
+ //
3
+ // Módulo compartido: posición de caja + efecto cambiario sobre la caja VES, revaluando
4
+ // contra el SALDO REAL del banco (StatementEndingBalance), no el saldo GL (que puede estar
5
+ // inflado por backlog de conciliación). Usado por get_cash_flow (vista) y, a futuro, por el
6
+ // paso de cierre-mensual (asiento). READ-ONLY. NO afecta el FCF.
7
+ //
8
+ // MATEMÁTICA (por cuenta VES, robusta al backlog):
9
+ // gl_usd = Σ(debit−credit) USD libro
10
+ // gl_ves = Σ(additionalCurrency d−c) VES libro (puede estar inflado)
11
+ // implied_rate = gl_ves / gl_usd tasa promedio histórica de posteo
12
+ // stmt_ves = StatementEndingBalance VES real (del banco)
13
+ // recon_gap = gl_ves − stmt_ves backlog pendiente de postear (NO es FX)
14
+ // real_usd = stmt_ves / rate_end valor USD real de la caja
15
+ // fx_real = stmt_ves × (1/rate_end − 1/implied_rate) ← pérdida cambiaria sobre lo REAL
16
+ //
17
+ // 3 buckets de cuentas VES:
18
+ // 1) banco real con statement → fx_real, ENTRA al total y al asiento.
19
+ // 2) banco real sin statement → provisional sobre GL, flag, NO entra al total.
20
+ // 3) ajuste/tránsito (lista en config) → solo informativo, NO entra al total.
21
+
22
+ import { readFileSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { round2 } from '../../utils/currency-converter.js';
25
+ import { logger } from '../../utils/logger.js';
26
+
27
+ // ── Config bank-gl-map (cargado una vez) ───────────────────────────────────────
28
+ let _bankMap = null;
29
+ function bankMap() {
30
+ if (_bankMap) return _bankMap;
31
+ const path = fileURLToPath(new URL('../../config/bank-gl-map.json', import.meta.url));
32
+ _bankMap = JSON.parse(readFileSync(path, 'utf-8'));
33
+ return _bankMap;
34
+ }
35
+
36
+ // Cuentas VES de ajuste/tránsito (bucket 3) — informativas, fuera del total/asiento.
37
+ function adjustmentAccounts() {
38
+ return new Set(bankMap().adjustment_accounts_ves || []);
39
+ }
40
+
41
+ // Mapa GL→banco para una tienda: { '18250': { bankCode: 'FQ88-MN0005', name: 'Bancrecer 2558 Bs.' } }
42
+ // Solo cuentas VES (el statement está en VES).
43
+ function glToBankForStore(storeCode) {
44
+ const store = (bankMap().stores || {})[storeCode];
45
+ const out = {};
46
+ if (!store) return out;
47
+ for (const [suffix, info] of Object.entries(store.accounts || {})) {
48
+ if (!info.gl || info.currency !== 'VES') continue;
49
+ out[String(info.gl)] = { bankCode: `${storeCode}-${suffix}`, name: info.name || '' };
50
+ }
51
+ return out;
52
+ }
53
+
54
+ // Regla VES (confirmada por FP): bancos VES = 18200–18299 ; caja VEB = 18110/18130/18160.
55
+ const VES_CASH_ACCOUNTS = new Set(['18110', '18130', '18160']);
56
+ export function isVesCashAccount(accountNumber) {
57
+ const n = parseInt(accountNumber, 10);
58
+ return (n >= 18200 && n <= 18299) || VES_CASH_ACCOUNTS.has(String(accountNumber));
59
+ }
60
+
61
+ const RATE_MIN = 350, RATE_MAX = 750; // banda sana para la tasa implícita (flag si está fuera)
62
+
63
+ // ── Cálculo principal ──────────────────────────────────────────────────────────
64
+ // store = { code, name, companyId, companyName }
65
+ export async function computeCashPosition(bcClient, store, start, end, rates) {
66
+ const [entries, statements] = await Promise.all([
67
+ bcClient.getCashGLBalances(store.companyId, end).catch((e) => { logger.warn(`[fx-cash] GL balances failed: ${e.message}`); return null; }),
68
+ bcClient.getBankStatementBalances(store.code, end).catch((e) => { logger.warn(`[fx-cash] statement balances failed: ${e.message}`); return null; }),
69
+ ]);
70
+ if (entries == null) return null;
71
+
72
+ const rateEnd = (bcClient.getExchangeRateForDate(rates, end) || {}).rate || null;
73
+ const rateStart = (bcClient.getExchangeRateForDate(rates, start) || {}).rate || null;
74
+ if (!rateEnd) return null;
75
+
76
+ const warnings = [];
77
+ if (entries._truncated) warnings.push('G/L de caja/bancos truncado — saldos pueden estar incompletos.');
78
+ if (statements == null) warnings.push('Reconciliaciones bancarias no disponibles — sin saldo real; revaluación provisional sobre GL.');
79
+
80
+ const glToBank = glToBankForStore(store.code);
81
+ const adjustment = adjustmentAccounts();
82
+ const stmts = statements || {};
83
+
84
+ // Acumular saldos GL por cuenta (USD libro + VES additionalCurrency)
85
+ const acc = {};
86
+ for (const e of entries) {
87
+ const a = e.accountNumber;
88
+ if (!acc[a]) acc[a] = { usd: 0, ves: 0 };
89
+ acc[a].usd += (e.debitAmount || 0) - (e.creditAmount || 0);
90
+ acc[a].ves += (e.additionalCurrencyDebitAmount || 0) - (e.additionalCurrencyCreditAmount || 0);
91
+ }
92
+
93
+ const bank_accounts = []; // bucket 1
94
+ const pending_no_statement = []; // bucket 2
95
+ const informational = []; // bucket 3
96
+ let usdTotal = 0;
97
+ let glVesBookSum = 0, glVesAtRateSum = 0; // referencia estilo v1.26.0 (todas las VES sobre GL)
98
+
99
+ for (const [a, v] of Object.entries(acc)) {
100
+ const glUsd = round2(v.usd);
101
+ const glVes = round2(v.ves);
102
+ if (Math.abs(glUsd) < 0.005 && Math.abs(glVes) < 0.5) continue; // saldo cero
103
+
104
+ if (!isVesCashAccount(a)) { usdTotal += glUsd; continue; } // cuentas USD: sin ajuste
105
+
106
+ // Referencia GL (v1.26.0): revaluar el saldo GL completo
107
+ glVesBookSum += glUsd;
108
+ glVesAtRateSum += glVes / rateEnd;
109
+
110
+ const impliedRate = glVes !== 0 && glUsd > 0 ? round2(glVes / glUsd) : null;
111
+ const flags = [];
112
+ if (impliedRate != null && (impliedRate < RATE_MIN || impliedRate > RATE_MAX)) flags.push('tasa_implicita_atipica');
113
+ const info = glToBank[a];
114
+ const stmt = info ? stmts[info.bankCode] : null;
115
+
116
+ // fx sobre un saldo VES dado, a la tasa implícita de la cuenta
117
+ const fxOn = (vesBalance) => (impliedRate ? round2(vesBalance * (1 / rateEnd - 1 / impliedRate)) : 0);
118
+
119
+ if (adjustment.has(String(a))) {
120
+ // Bucket 3 — ajuste/tránsito: informativo
121
+ informational.push({ account: a, name: info ? info.name : 'Ajuste/Tránsito VES', gl_usd: glUsd, gl_ves: glVes, implied_rate: impliedRate, fx_theoretical: fxOn(glVes), flags: [...flags, 'ajuste_transito'] });
122
+ } else if (info && stmt) {
123
+ // Bucket 1 — banco real con statement: revaluar saldo REAL
124
+ const stmtVes = round2(stmt.ending || 0);
125
+ const realUsd = round2(stmtVes / rateEnd);
126
+ const fxReal = fxOn(stmtVes);
127
+ if (stmt.stale) flags.push('statement_desactualizado');
128
+ bank_accounts.push({
129
+ account: a, bank_code: info.bankCode, name: info.name,
130
+ gl_usd: glUsd, gl_ves: glVes, implied_rate: impliedRate,
131
+ stmt_ves: stmtVes, statement_date: stmt.statementDate || null,
132
+ recon_gap_ves: round2(glVes - stmtVes), real_usd: realUsd, fx_real: fxReal, flags,
133
+ });
134
+ } else {
135
+ // Bucket 2 — banco/caja sin statement: provisional sobre GL
136
+ pending_no_statement.push({ account: a, name: info ? info.name : 'VES sin statement', gl_usd: glUsd, gl_ves: glVes, implied_rate: impliedRate, fx_provisional: fxOn(glVes), flags: [...flags, 'sin_statement'] });
137
+ }
138
+ }
139
+
140
+ bank_accounts.sort((x, y) => x.fx_real - y.fx_real);
141
+ pending_no_statement.sort((x, y) => x.fx_provisional - y.fx_provisional);
142
+ informational.sort((x, y) => x.fx_theoretical - y.fx_theoretical);
143
+
144
+ const sum = (arr, fn) => round2(arr.reduce((s, o) => s + fn(o), 0));
145
+ const totals = {
146
+ ves_real_usd: sum(bank_accounts, (o) => o.real_usd),
147
+ fx_real: sum(bank_accounts, (o) => o.fx_real),
148
+ recon_gap_ves: sum(bank_accounts, (o) => o.recon_gap_ves),
149
+ bank_book_usd: sum(bank_accounts, (o) => o.gl_usd),
150
+ pending_book_usd: sum(pending_no_statement, (o) => o.gl_usd),
151
+ informational_book_usd: sum(informational, (o) => o.gl_usd),
152
+ usd_accounts_usd: round2(usdTotal),
153
+ };
154
+ totals.total_cash_real_usd = round2(totals.ves_real_usd + usdTotal);
155
+
156
+ const fx_journal = buildFxJournal(bank_accounts);
157
+
158
+ return {
159
+ rate_end: round2(rateEnd),
160
+ rate_start: rateStart ? round2(rateStart) : null,
161
+ month_devaluation_pct: rateStart && rateStart > 0 ? round2((rateEnd / rateStart - 1) * 100) : null,
162
+ bank_accounts,
163
+ pending_no_statement,
164
+ informational,
165
+ usd: { total_usd: round2(usdTotal) },
166
+ totals,
167
+ gl_basis: { ves_book_usd: round2(glVesBookSum), ves_at_rate_usd: round2(glVesAtRateSum), fx_on_gl: round2(glVesAtRateSum - glVesBookSum) },
168
+ fx_journal,
169
+ warnings,
170
+ };
171
+ }
172
+
173
+ // ── Asiento sugerido de diferencial cambiario (READ-ONLY texto) — solo bucket 1 ──
174
+ // DR Pérdida Diferencial Cambiario / CR cada banco VES (en USD) por su fx_real.
175
+ export function buildFxJournal(bankAccounts) {
176
+ const PNL_ACCOUNT = { account: 'Diferencial Cambiario', note: 'Pérdida/Ganancia por diferencial cambiario (P&L)' };
177
+ const rows = [];
178
+ let bankDebit = 0, bankCredit = 0;
179
+ for (const b of bankAccounts) {
180
+ if (Math.abs(b.fx_real) < 0.005) continue;
181
+ // pérdida (fx_real<0) → CR banco ; ganancia (fx_real>0) → DR banco
182
+ const debit = b.fx_real > 0 ? round2(b.fx_real) : 0;
183
+ const credit = b.fx_real < 0 ? round2(-b.fx_real) : 0;
184
+ bankDebit += debit; bankCredit += credit;
185
+ rows.push({ account: b.account, name: b.name, account_type: 'Banco (G/L)', debit_usd: debit, credit_usd: credit });
186
+ }
187
+ const net = round2(bankCredit - bankDebit); // >0 = pérdida neta
188
+ // Contrapartida P&L
189
+ rows.push({
190
+ account: PNL_ACCOUNT.account, name: PNL_ACCOUNT.note, account_type: 'G/L (P&L)',
191
+ debit_usd: net > 0 ? net : 0, credit_usd: net < 0 ? round2(-net) : 0,
192
+ });
193
+ const debitTotal = round2(rows.reduce((s, r) => s + r.debit_usd, 0));
194
+ const creditTotal = round2(rows.reduce((s, r) => s + r.credit_usd, 0));
195
+ return { rows, debit_total: debitTotal, credit_total: creditTotal, balanced: Math.abs(debitTotal - creditTotal) < 0.01, net_fx: net };
196
+ }
197
+
198
+ // Consolida posiciones de varias tiendas (suma bucket 1/2/3 y rehace totales/asiento).
199
+ export function consolidateCashPositions(positions) {
200
+ const ps = positions.filter(Boolean);
201
+ if (!ps.length) return null;
202
+ const ref = ps[0];
203
+ const tag = (arr, code) => arr.map((o) => ({ ...o, store: code }));
204
+ const bank_accounts = ps.flatMap((p) => tag(p.bank_accounts, p._store)).sort((a, b) => a.fx_real - b.fx_real);
205
+ const pending_no_statement = ps.flatMap((p) => tag(p.pending_no_statement, p._store));
206
+ const informational = ps.flatMap((p) => tag(p.informational, p._store));
207
+ const sum = (fn) => round2(ps.reduce((s, p) => s + fn(p), 0));
208
+ const usdTotal = sum((p) => p.usd.total_usd);
209
+ const totals = {
210
+ ves_real_usd: sum((p) => p.totals.ves_real_usd),
211
+ fx_real: sum((p) => p.totals.fx_real),
212
+ recon_gap_ves: sum((p) => p.totals.recon_gap_ves),
213
+ bank_book_usd: sum((p) => p.totals.bank_book_usd),
214
+ pending_book_usd: sum((p) => p.totals.pending_book_usd),
215
+ informational_book_usd: sum((p) => p.totals.informational_book_usd),
216
+ usd_accounts_usd: round2(usdTotal),
217
+ };
218
+ totals.total_cash_real_usd = round2(totals.ves_real_usd + usdTotal);
219
+ return {
220
+ rate_end: ref.rate_end, rate_start: ref.rate_start, month_devaluation_pct: ref.month_devaluation_pct,
221
+ bank_accounts, pending_no_statement, informational,
222
+ usd: { total_usd: round2(usdTotal) }, totals,
223
+ gl_basis: { ves_book_usd: sum((p) => p.gl_basis.ves_book_usd), ves_at_rate_usd: sum((p) => p.gl_basis.ves_at_rate_usd), fx_on_gl: sum((p) => p.gl_basis.fx_on_gl) },
224
+ fx_journal: buildFxJournal(bank_accounts),
225
+ warnings: [...new Set(ps.flatMap((p) => p.warnings))],
226
+ };
227
+ }
@@ -12,7 +12,7 @@ export { cashFlowTool, handleCashFlow } from './cash-flow.js';
12
12
  export const financialStatementsTool = {
13
13
  name: 'get_financial_statements',
14
14
  description:
15
- 'Estado financiero completo (P&L / Profit & Loss) para una o más tiendas Full Queso (FQ01, FQ28, FQ88) en un período. ' +
15
+ 'Estado financiero completo (P&L / Profit & Loss) para una o más tiendas Full Queso (FQ01, FQ28, FQ88, FQFR) en un período. ' +
16
16
  'Ejecuta las queries de Business Central en paralelo (ingresos 40000-49999, COGS 50000-59999, gastos 60000-99999) y ' +
17
17
  'devuelve JSON estructurado: ingresos por cuenta, COGS, margen bruto, gastos por categoría con benchmarks, margen ' +
18
18
  'operativo, ratios clave, score 0-100 e insights accionables. Multi-tienda calcula también el consolidado. ' +
@@ -24,8 +24,8 @@ export const financialStatementsTool = {
24
24
  properties: {
25
25
  stores: {
26
26
  type: 'array',
27
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
28
- description: 'Tiendas a analizar. Default: ["all"] (FQ01 + FQ28 + FQ88 + consolidado).',
27
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
28
+ description: 'Tiendas a analizar. Default: ["all"] (FQ01 + FQ28 + FQ88 + FQFR + consolidado).',
29
29
  default: ['all'],
30
30
  },
31
31
  period: {
@@ -10,7 +10,7 @@ export const exchangeRateTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  date: {
@@ -11,7 +11,7 @@ export const inventoryByLocationTool = {
11
11
  properties: {
12
12
  store: {
13
13
  type: 'string',
14
- enum: ['FQ01', 'FQ28', 'FQ88'],
14
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
15
15
  description: 'Tienda a consultar.',
16
16
  },
17
17
  location_code: {
@@ -11,7 +11,7 @@ export const inventoryChangeTool = {
11
11
  properties: {
12
12
  store: {
13
13
  type: 'string',
14
- enum: ['FQ01', 'FQ28', 'FQ88'],
14
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
15
15
  description: 'Tienda a consultar.',
16
16
  },
17
17
  period: {
@@ -11,7 +11,7 @@ export const inventoryLevelsTool = {
11
11
  properties: {
12
12
  store: {
13
13
  type: 'string',
14
- enum: ['FQ01', 'FQ28', 'FQ88'],
14
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
15
15
  description: 'Tienda a consultar.',
16
16
  },
17
17
  item_category: {
@@ -11,7 +11,7 @@ export const itemCardTool = {
11
11
  properties: {
12
12
  store: {
13
13
  type: 'string',
14
- enum: ['FQ01', 'FQ28', 'FQ88'],
14
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
15
15
  description: 'Tienda a consultar.',
16
16
  },
17
17
  item_number: {
@@ -10,7 +10,7 @@ export const itemCostTrendTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  item_number: {
@@ -10,7 +10,7 @@ export const itemLedgerEntriesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  item_number: {
@@ -10,7 +10,7 @@ export const itemValueEntriesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  item_number: {
@@ -12,7 +12,7 @@ export const listVendorsTool = {
12
12
  properties: {
13
13
  store: {
14
14
  type: 'string',
15
- enum: ['FQ01', 'FQ28', 'FQ88'],
15
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
16
16
  description: 'Tienda a consultar.',
17
17
  },
18
18
  start_date: {
@@ -12,7 +12,7 @@ export const draftPayablesTool = {
12
12
  properties: {
13
13
  store: {
14
14
  type: 'string',
15
- enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
15
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'],
16
16
  description: 'Tienda(s) a consultar.',
17
17
  },
18
18
  status_filter: {
@@ -12,7 +12,7 @@ export const draftReceivablesTool = {
12
12
  properties: {
13
13
  store: {
14
14
  type: 'string',
15
- enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
15
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'],
16
16
  description: 'Tienda(s) a consultar.',
17
17
  },
18
18
  status_filter: {
@@ -10,7 +10,7 @@ export const draftSummaryTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'],
14
14
  description: 'Tienda(s) a consultar.',
15
15
  },
16
16
  },
@@ -10,7 +10,7 @@ export const employeesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'],
14
14
  description: 'Tienda(s) a consultar.',
15
15
  },
16
16
  status: {
@@ -10,7 +10,7 @@ export const payrollDocumentsTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88', 'all'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'],
14
14
  description: 'Tienda(s) a consultar.',
15
15
  },
16
16
  period_code: {
@@ -10,7 +10,7 @@ export const payrollLinesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  document_no: {
@@ -36,7 +36,7 @@ export const managerReportTool = {
36
36
  properties: {
37
37
  store: {
38
38
  type: 'string',
39
- enum: ['FQ01', 'FQ28', 'FQ88'],
39
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
40
40
  description: 'Tienda para la que se genera el reporte.',
41
41
  },
42
42
  date: {
package/tools/trends.js CHANGED
@@ -15,7 +15,7 @@ export const trendsTool = {
15
15
  },
16
16
  store: {
17
17
  type: 'string',
18
- enum: ['FQ01', 'FQ28', 'FQ88'],
18
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
19
19
  description: 'Tienda a analizar. Tendencias se calculan por tienda individual.',
20
20
  default: 'FQ01',
21
21
  },
@@ -11,7 +11,7 @@ export const vendorTransactionsTool = {
11
11
  properties: {
12
12
  store: {
13
13
  type: 'string',
14
- enum: ['FQ01', 'FQ28', 'FQ88'],
14
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
15
15
  description: 'Tienda a consultar.',
16
16
  },
17
17
  vendor_search: {
@@ -27,7 +27,7 @@ export const itemSalesDetailTool = {
27
27
  },
28
28
  stores: {
29
29
  type: 'array',
30
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
30
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
31
31
  description: 'Tiendas. Use ["all"] para todas.',
32
32
  default: ['all'],
33
33
  },
@@ -30,7 +30,7 @@ export const productPerformanceTool = {
30
30
  },
31
31
  stores: {
32
32
  type: 'array',
33
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
33
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
34
34
  description: 'Tiendas a analizar',
35
35
  default: ['all'],
36
36
  },
@@ -30,7 +30,7 @@ export const salesAnalysisTool = {
30
30
  },
31
31
  stores: {
32
32
  type: 'array',
33
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
33
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
34
34
  description: 'Tiendas a analizar. Use ["all"] para todas.',
35
35
  default: ['all'],
36
36
  },