@fullqueso/mcp-bc-gastos 1.17.0 → 1.23.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 +96 -1
- package/config/bank-gl-map.json +93 -0
- package/config/bank-keywords.js +35 -0
- package/lib/bc-client.js +73 -13
- package/package.json +3 -2
- package/scripts/diagnose-closing.js +144 -0
- package/scripts/generate-month-closing-xlsx.py +325 -0
- package/scripts/test-closing-flow.js +71 -0
- package/scripts/test-closing-fq01-dec25.js +78 -0
- package/server.js +77 -0
- package/tools/auditoria/bank-ledger-entries.js +3 -5
- package/tools/auditoria/find-potential-matches.js +1 -1
- package/tools/auditoria/gl-account-entries.js +1 -1
- package/tools/auditoria/list-bank-accounts.js +23 -12
- package/tools/auditoria/pm-receipts.js +4 -4
- package/tools/auditoria/reconcile-pos-sales.js +46 -21
- package/tools/auditoria/unmatched-ledger-entries.js +3 -3
- package/tools/auditoria/unmatched-statement-lines.js +2 -2
- package/tools/cierre-mensual/fetch-ledger.js +69 -0
- package/tools/cierre-mensual/generate-closing-journal.js +111 -0
- package/tools/cierre-mensual/get-match-results.js +151 -0
- package/tools/cierre-mensual/get-questionnaire.js +283 -0
- package/tools/cierre-mensual/index.js +6 -0
- package/tools/cierre-mensual/journal-builder.js +219 -0
- package/tools/cierre-mensual/matchers/index.js +161 -0
- package/tools/cierre-mensual/matchers/match-cross-bank.js +168 -0
- package/tools/cierre-mensual/matchers/match-draft-payments.js +178 -0
- package/tools/cierre-mensual/matchers/match-feedback-loop.js +24 -0
- package/tools/cierre-mensual/matchers/match-keywords.js +65 -0
- package/tools/cierre-mensual/matchers/match-open-arap.js +66 -0
- package/tools/cierre-mensual/matchers/match-pos-terminal.js +53 -0
- package/tools/cierre-mensual/reconcile-with-bc.js +116 -0
- package/tools/cierre-mensual/start-month-closing.js +234 -0
- package/tools/cierre-mensual/state-store.js +211 -0
- package/tools/cierre-mensual/submit-answers.js +106 -0
- package/tools/cobranzas/customer-ledger.js +4 -4
- package/tools/cobranzas/customer-list.js +63 -0
- package/tools/cobranzas/vendor-ledger.js +4 -4
- package/tools/financials/aggregator.js +360 -0
- package/tools/financials/cash-flow-html.js +459 -0
- package/tools/financials/cash-flow.js +471 -0
- package/tools/financials/html-template.js +674 -0
- package/tools/financials/index.js +79 -0
- package/tools/financials/statements.js +296 -0
- package/tools/inventario/index.js +3 -0
- package/tools/inventario/inventory-by-location.js +212 -0
- package/tools/inventario/inventory-change.js +386 -0
- package/tools/inventario/inventory-levels.js +214 -0
- package/tools/inventario/item-card.js +1 -50
- package/tools/inventario/item-ledger-entries.js +4 -4
- package/tools/inventario/item-value-entries.js +2 -2
- package/tools/inventario/shared/cost-calculator.js +64 -0
- package/tools/ventas/index.js +1 -0
- package/tools/ventas/item-sales-detail.js +366 -0
- package/utils/rate-limiter.js +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,105 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [1.23.0] — 2026-05-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Dominio `financials/`: tool `get_cash_flow`** — Free Cash Flow (FCF) por método indirecto para una o más tiendas FQ.
|
|
7
|
+
- Parte del EBIT (reutiliza `fetchStoreFinancials` de `get_financial_statements`), resta impuesto estimado (ISLR 34%, solo si EBIT>0) → NOPAT, ajusta por capital de trabajo (ΔAR, ΔAP, Δinventario) → CFO, resta CapEx (activos fijos 15000-17999) → FCF.
|
|
8
|
+
- Fuentes BC: AR/AP vía Finance Beta (`customerLedgerEntries`/`vendorLedgerEntries`, USD = `amountLocalCurrency`), inventario vía `itemLedgerEntries` (`costAmountActual`), CapEx vía `getGLEntries`. Todas en paralelo (`Promise.all`) junto al P&L.
|
|
9
|
+
- Convención de signos: cada delta = impacto en caja (+ = fuente, − = uso); `net_wc_change = ΔAR+ΔAP+Δinv`; `cfo = nopat + net_wc_change`; `fcf = cfo − capex`. (Resuelve la inconsistencia del spec entre comentarios y fórmula a favor de la convención de impacto-en-caja, autoconsistente.)
|
|
10
|
+
- `fcf_status` (healthy/positive_low/negative_watch/negative_critical), `fcf_bridge` (waterfall) e `insights` automáticos. Consolidado multi-tienda + comparación MoM (con `no_previous_data` para tiendas sin mes anterior).
|
|
11
|
+
- `render_html=true`: dashboard HTML reutilizando el design system de financials + **waterfall CSS** del FCF bridge (sin librerías externas). Guarda en `~/Downloads/FQ_CashFlow_[STORE|Consolidado]_YYYYMM.html`.
|
|
12
|
+
- **`tools/financials/cash-flow.js`** y **`tools/financials/cash-flow-html.js`**. Exportados helpers reutilizables (`CSS`, `fmt`, `esc`, `clamp`, `scoreColorName`, `periodLabel`, `previousMonthRange`) desde `html-template.js`/`statements.js`.
|
|
13
|
+
|
|
14
|
+
### Notas
|
|
15
|
+
- Si un endpoint BC (AR/AP/inventario/activos fijos) devuelve vacío o falla, ese delta se asume 0 y se agrega un warning a `insights` (no rompe el cálculo).
|
|
16
|
+
- Si una tienda falla, las demás continúan; el error queda en el campo de esa tienda.
|
|
17
|
+
- Tool count: 53 → 54.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [1.22.0] — 2026-05-31
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Dominio `financials/`: tool `get_financial_statements`** — Estado financiero completo (P&L) para una o más tiendas FQ en un período.
|
|
25
|
+
- P&L completo: ingresos por cuenta, COGS, margen bruto, gastos por categoría (con benchmarks), margen operativo.
|
|
26
|
+
- Queries BC en paralelo `Promise.all` (3 tiendas × 3 queries = 9 simultáneas) + exchange rate compartido.
|
|
27
|
+
- Consolidado multi-tienda re-agregando todas las GL entries crudas (consistencia exacta con la suma de tiendas).
|
|
28
|
+
- Template HTML estático (design system FQ: tokens Dark/Light, header rojo, ring de score, toggle, TL;DR, grid KPIs, tabla P&L, barras de gastos, comparativa multi-tienda, `@media print`). `render_html=true` guarda en `~/Downloads` y retorna la ruta.
|
|
29
|
+
- Comparación mes-a-mes con `compare_previous=true`. Si el período anterior no tiene datos (la tienda aún no operaba en BC, p.ej. FQ01 antes de Dic 2025), se marca `no_previous_data: true` en vez de comparar contra cero (evita score/varianzas fabricados).
|
|
30
|
+
- Score 0-100 ponderado por status de 7 ratios vs benchmarks del sector.
|
|
31
|
+
- **`tools/financials/aggregator.js`** — `EXPENSE_CATEGORIES`, `getStatus`, `aggregateIncome`, `aggregateCOGS`, `aggregateExpenses`, `calcRatios`, `calcScore`, `generateInsights`. Reutiliza `config/expense-accounts.js` y `config/benchmarks.js`.
|
|
32
|
+
|
|
33
|
+
### Notas
|
|
34
|
+
- Reutiliza el `bcClient.getGLEntries()` existente (convención `debitAmount`/`creditAmount`, no el `amount` de Finance Beta). Paginación automática vía `apiCallAllPages`.
|
|
35
|
+
- Bump desde 1.21.0 → 1.22.0 (el spec mencionaba "1.2.0" cuando la versión base era anterior; 1.2.0 sería un downgrade, por eso MINOR sobre la versión real publicada).
|
|
36
|
+
- Tool count: 52 → 53.
|
|
37
|
+
|
|
38
|
+
### Validado (FQ01 Diciembre 2025)
|
|
39
|
+
- `income.total` = $157,693 · `cogs.total` = $100,158 · `gross_margin.pct` = 36.49% · `expenses.otros.amount` = $19,233 (mermas $16,933) · `operating_income.pct` = 10.6% · `score` ≈ 42.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## [1.20.0] - 2026-05-04
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **`get_item_sales_detail` tool (ventas)** — Detalle de ventas por SKU (1–50 ítems) con granularidad day/week/month/total y desglose por tienda. Pensado para análisis de promos, canibalización, lanzamientos y SKUs estacionales que caen fuera del top-20 de `get_sales_analysis`. Fuente: `itemLedgerEntries` con `entryType eq 'Sale'` (mismos signos que el screenshot del Item Application Worksheet — quantity y costAmountActual negativos en BC, invertidos a positivos en la salida). Soporta `include_zero_days` para detección de stockouts y series temporales limpias. Multi-company en paralelo (FQ01/FQ28/FQ88).
|
|
47
|
+
- **`bcClient.getItemLedgerSaleEntries(companyId, { items, startDate, endDate })`** — Helper en `lib/bc-client.js` para fetch paginado de ILE Sale entries por lote de ítems.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Tool count: 45 → 46
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## [1.19.0] - 2026-04-13
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **`get_customer_list` tool (cobranzas)** — Lists customers with number, name, and RIF (taxRegistrationNumber) per store. Supports optional filter by customer number.
|
|
58
|
+
- **`config/bank-gl-map.json`** — Reference config documenting the full bank account suffix → GL account mapping per store (FQ01, FQ28, FQ88), including GL structure for Caja y Bancos (181xx–189xx).
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- **POS reconciliation: per-store bank GL mapping** — `BANK_GL_MAP` in `reconcile-pos-sales.js` now keyed by store (FQ01, FQ28) instead of flat suffix lookup. Adds cash register accounts (MN0030, ME0030) and full FQ28 bank set. `getBankGLAccount()` now accepts store parameter for correct GL resolution.
|
|
62
|
+
- Tool count: 44 → 45
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## [1.18.2] - 2026-04-13
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
- **Inventory tools: exclude FQ-\* sale/combo categories** — `get_inventory_levels`, `get_inventory_change`, and `get_inventory_by_location` now filter out items with `itemCategoryCode` starting with `FQ-`, `FQ_`, or equal to `FQVENTA`. These are sales/combo products, not real inventory, and their BC `unitCost` can be contaminated (zero-inventory edge cases), producing inflated inventory valuations. Real inventory items (TERMINADO, INSUMO, etc.) keep BC's FIFO-based `unitCost` which is accurate.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- **Extracted `getCalculatedCosts` to shared module** — Moved from `item-card.js` to `tools/inventario/shared/cost-calculator.js` along with `isInventoryCategory()`. Avoids duplication across inventory tools.
|
|
73
|
+
- **Updated `calculated_unit_cost_method.md`** — Documented category filtering logic and clarified that calculated cost is for FQ-* items only; real inventory uses BC FIFO cost.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## [1.18.1] - 2026-04-13
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
- `get_inventory_by_location` — Added `as_of_date` parameter for historical inventory reconstruction by location. Filters OData V4 item ledger entries with `Posting_Date <= as_of_date`. Also added `isInventoryCategory` filter to exclude FQ-* sale/combo items (aligns with `get_inventory_levels` behavior).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## [1.18.0] - 2026-03-19
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
**Inventario domain — 2 new tools (inventory levels & change tracking):**
|
|
89
|
+
- `get_inventory_levels` — Inventory snapshot grouped by itemCategoryCode and inventoryPostingGroupCode (congelados/imported/local). Supports historical reconstruction via `as_of_date` (e.g., 1st of month post-EOM adjustment). Returns classification matrix with qty, cost value, and top items per group.
|
|
90
|
+
- `get_inventory_change` — WoW and MoM inventory change analysis. Reconstructs opening/closing inventory per period walking backwards from current balance. Movement breakdown by type (purchases, sales, adjustments, assembly). Trend summary per category and classification.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- Tool count: 42 → 44
|
|
94
|
+
- Inventario domain: 4 → 6 tools
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
3
98
|
## [1.17.0] - 2026-03-18
|
|
4
99
|
|
|
5
100
|
### Highlights
|
|
6
101
|
- **Ventas domain consolidated** from `mcp-bc-analisis-ventas` — 3 new sales analysis tools
|
|
7
|
-
- Full Queso now has one unified BC MCP with
|
|
102
|
+
- Full Queso now has one unified BC MCP with **42 tools** across 8 domains
|
|
8
103
|
|
|
9
104
|
### Added
|
|
10
105
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"last_updated": "2026-05-17",
|
|
4
|
+
"description": "Mapeo banco_code (BC Bank Account suffix) → cuenta GL (Chart of Accounts). Usado por POS reconciliation y journal entry suggestions. Cada tienda tiene su propio CoA con bancos distintos.",
|
|
5
|
+
"stores": {
|
|
6
|
+
"FQ01": {
|
|
7
|
+
"store_name": "FQ01 - Chacao Sambil",
|
|
8
|
+
"accounts": {
|
|
9
|
+
"MN0001": { "gl": "18210", "name": "Banesco 4421 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
10
|
+
"MN0002": { "gl": "18220", "name": "BDV 1925 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
11
|
+
"MN0003": { "gl": "18230", "name": "Bancamiga 7015 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
12
|
+
"MN0004": { "gl": "18240", "name": "BDV 5550 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
13
|
+
"MN0005": { "gl": "18250", "name": "Bancamiga 4523 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
14
|
+
"MN0006": { "gl": "18260", "name": "Bancrecer 2450 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
15
|
+
"MN0007": { "gl": "18270", "name": "Bancrecer 2467 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
16
|
+
"MN0008": { "gl": "18280", "name": "BDV 5187 Bs. (Pago Móvil)", "currency": "VES", "category": "bancos_nacionales" },
|
|
17
|
+
"MN0012": { "gl": "18290", "name": "UBII", "currency": "VES", "category": "bancos_nacionales" },
|
|
18
|
+
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
19
|
+
"ME0030": { "gl": "18140", "name": "Caja tienda ventas $", "currency": "USD", "category": "caja" },
|
|
20
|
+
"ME0006": { "gl": null, "name": "Zelle USD (Wells Fargo)", "currency": "USD", "category": "bancos_extranjeros", "_nota": "GL pendiente de confirmar" }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"FQ28": {
|
|
24
|
+
"store_name": "FQ28 - Marqués El Unicentro",
|
|
25
|
+
"accounts": {
|
|
26
|
+
"MN0001": { "gl": "18210", "name": "BDV 8139 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
27
|
+
"MN0002": { "gl": "18220", "name": "Bancrecer 5474 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
28
|
+
"MN0028": { "gl": "18290", "name": "UBII 6249 Bs", "currency": "VES", "category": "bancos_nacionales", "_nota": "Punto 2 + Punto 3." },
|
|
29
|
+
"MN0029": { "gl": "18291", "name": "UBII 6442 Bs", "currency": "VES", "category": "bancos_nacionales", "_nota": "Punto 4 + Punto 7." },
|
|
30
|
+
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
31
|
+
"MN0031": { "gl": "18160", "name": "Caja transitoria tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
32
|
+
"MN0032": { "gl": "18292", "name": "UBII 4 y 7 (duplicada)", "currency": "VES", "category": "bancos_nacionales", "_nota": "Cuenta duplicada creada por error — NO usar. Punto 4 y Punto 7 van a MN0029. Confirmado 2026-05-17." },
|
|
33
|
+
"MN0033": { "gl": "18295", "name": "UBII 7 bloqueado (duplicada)", "currency": "VES", "category": "bancos_nacionales", "_nota": "Cuenta duplicada creada por error — NO usar. Punto 7 va a MN0029. Confirmado 2026-05-17." },
|
|
34
|
+
"MN0098": { "gl": "18298", "name": "Operaciones en Tránsito", "currency": "VES", "category": "bancos_nacionales" },
|
|
35
|
+
"ME0002": { "gl": "18310", "name": "Bancrecer $", "currency": "USD", "category": "bancos_nacionales_divisas" },
|
|
36
|
+
"ME0005": { "gl": "18410", "name": "Zelle Bofa $ *", "currency": "USD", "category": "bancos_extranjeros" },
|
|
37
|
+
"ME0030": { "gl": "18140", "name": "Caja tienda ventas $", "currency": "USD", "category": "caja" },
|
|
38
|
+
"ME0031": { "gl": "18170", "name": "Caja transitoria tienda ventas $", "currency": "USD", "category": "caja" },
|
|
39
|
+
"ME0035": { "gl": "18150", "name": "Banco Bóveda $", "currency": "USD", "category": "caja" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"FQ88": {
|
|
43
|
+
"store_name": "FQ88 - La Candelaria",
|
|
44
|
+
"_nota": "GL accounts pendientes de confirmar con CoA de FQ88",
|
|
45
|
+
"accounts": {
|
|
46
|
+
"MN0001": { "gl": null, "name": "Bancamiga 0240 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
47
|
+
"MN0002": { "gl": null, "name": "Bancamiga 9379 Bs. *", "currency": "VES", "category": "bancos_nacionales" },
|
|
48
|
+
"MN0003": { "gl": null, "name": "BDV 9127 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
49
|
+
"MN0004": { "gl": null, "name": "BDV 7191 Bs. *", "currency": "VES", "category": "bancos_nacionales" },
|
|
50
|
+
"MN0005": { "gl": null, "name": "Bancrecer 2558 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
51
|
+
"MN0007": { "gl": null, "name": "BDV Pago Movil 5145", "currency": "VES", "category": "bancos_nacionales" },
|
|
52
|
+
"MN0028": { "gl": null, "name": "Ubii Bank", "currency": "VES", "category": "bancos_nacionales" },
|
|
53
|
+
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
54
|
+
"ME0030": { "gl": "18140", "name": "Caja tienda ventas $", "currency": "USD", "category": "caja" },
|
|
55
|
+
"ME0005": { "gl": null, "name": "Zelle USD", "currency": "USD", "category": "bancos_extranjeros" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"gl_structure": {
|
|
60
|
+
"18100": "Caja (header)",
|
|
61
|
+
"18110": "Caja chica VEB",
|
|
62
|
+
"18120": "Caja chica $",
|
|
63
|
+
"18130": "Caja tienda ventas VEB",
|
|
64
|
+
"18140": "Caja tienda ventas $",
|
|
65
|
+
"18150": "Bóveda $",
|
|
66
|
+
"18160": "Caja tránsito tienda ventas VEB",
|
|
67
|
+
"18170": "Caja tránsito tienda ventas $",
|
|
68
|
+
"18199": "Total Caja",
|
|
69
|
+
"18200": "Bancos Nacionales Bolívares (header)",
|
|
70
|
+
"18210": "Banco principal Bs.",
|
|
71
|
+
"18220": "Banco secundario Bs.",
|
|
72
|
+
"18230": "Banco terciario Bs.",
|
|
73
|
+
"18240": "Banco cuarto Bs.",
|
|
74
|
+
"18250": "Banco quinto Bs.",
|
|
75
|
+
"18260": "Banco sexto Bs.",
|
|
76
|
+
"18270": "Banco séptimo Bs.",
|
|
77
|
+
"18280": "Banco octavo Bs.",
|
|
78
|
+
"18290": "UBII principal",
|
|
79
|
+
"18291": "UBII secundario",
|
|
80
|
+
"18292": "UBII terciario",
|
|
81
|
+
"18295": "UBII cuarto",
|
|
82
|
+
"18298": "Operaciones en Tránsito",
|
|
83
|
+
"18299": "Total Bancos Nacionales",
|
|
84
|
+
"18300": "Bancos Nacionales Divisas (header)",
|
|
85
|
+
"18310": "Banco divisas principal",
|
|
86
|
+
"18399": "Total Bancos Nacionales Divisas",
|
|
87
|
+
"18400": "Bancos Extranjeros Divisas (header)",
|
|
88
|
+
"18410": "Banco extranjero principal",
|
|
89
|
+
"18499": "Total Bancos Extranjeros Divisas",
|
|
90
|
+
"18998": "Total Bancos",
|
|
91
|
+
"18999": "Total Caja y Bancos"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/config/bank-keywords.js
CHANGED
|
@@ -30,6 +30,41 @@ export const BANK_KEYWORDS = [
|
|
|
30
30
|
// Transfers
|
|
31
31
|
{ keywords: ['TRANSF RECIBIDA', 'TRANSFERENCIA RECIBIDA'], account: '11000', name: 'Transferencia Recibida' },
|
|
32
32
|
{ keywords: ['TRANSF ENVIADA', 'TRANSFERENCIA ENVIADA'], account: '11000', name: 'Transferencia Enviada' },
|
|
33
|
+
{ keywords: ['PAGO RECIBIDO OTROS BANCOS', 'ABONO RECIBIDO OTR BCOS'], account: '11000', name: 'Pago Recibido Otros Bancos' },
|
|
34
|
+
{ keywords: ['PAGOMOVIL', 'PAGO MOVIL', 'PAGOMOVILBDV'], account: '11000', name: 'Pago Móvil' },
|
|
35
|
+
{ keywords: ['TRANS.CTAS', 'TRASPASO OTRAS CTAS', 'TRF.MB', 'TRF CR INM', 'TRF DR INM'], account: '11000', name: 'Traspaso entre cuentas' },
|
|
36
|
+
{ keywords: ['TRANSF EN LINEA OTR BCOS', 'TRANSF EN LINEA'], account: '11000', name: 'Transferencia en línea' },
|
|
37
|
+
{ keywords: ['DOMICILIACION DE PAGOS', 'DOMICILIACION'], account: '11000', name: 'Domiciliación' },
|
|
38
|
+
|
|
39
|
+
// UBII / merchant clearing (INM.OB family) — see project memory project_ubii_clearing_account.md
|
|
40
|
+
{ keywords: ['CREDITO INM.OB', 'INM.OB'], account: '18290', name: 'UBII Clearing (revisar emparejamiento cross-bank)' },
|
|
41
|
+
|
|
42
|
+
// Loans (líneas de crédito)
|
|
43
|
+
{ keywords: ['DESEMBOLSO DE CREDITO'], account: '23100', name: 'Desembolso Crédito Bancario' },
|
|
44
|
+
{ keywords: ['PAGO CREDITO', 'PAGO DE CREDITO', 'PAGO PRESTAMO'], account: '23100', name: 'Pago Crédito Bancario' },
|
|
45
|
+
{ keywords: ['OP.INTERV', 'OPERACION INTERV', 'INTERV. CAMBIARIA', 'INTERVENCION CAMBIARIA'], account: '11500', name: 'Intervención cambiaria (USD compra/venta)' },
|
|
46
|
+
|
|
47
|
+
// Tax retentions
|
|
48
|
+
{ keywords: ['N/D ISLR', 'RETENCION ISLR'], account: '21300', name: 'Retención ISLR' },
|
|
49
|
+
{ keywords: ['SENIAT', 'IMPUESTO', 'IVA RETEN'], account: '21100', name: 'Retención IVA / SENIAT' },
|
|
50
|
+
|
|
51
|
+
// Statement fee
|
|
52
|
+
{ keywords: ['EMISION DE ESTADO DE CUENTA', 'ESTADO DE CUENTA'], account: '67100', name: 'Comisión emisión estado de cuenta' },
|
|
53
|
+
|
|
54
|
+
// IVSS
|
|
55
|
+
{ keywords: ['PAGO IVSS', 'IVSS'], account: '71300', name: 'Pago IVSS' },
|
|
56
|
+
|
|
57
|
+
// Checks
|
|
58
|
+
{ keywords: ['CHEQUE PAGADO', 'CHQ', 'CHEQUE'], account: '11000', name: 'Cheque' },
|
|
59
|
+
|
|
60
|
+
// Outbound bank payments (PAGO A OTROS BANCOS / PAGO A TERCEROS)
|
|
61
|
+
{ keywords: ['PAGO A OTROS BANCOS', 'PAGO A TERCEROS', 'PAGOS A OTROS BANCOS'], account: '11000', name: 'Pago a otros bancos' },
|
|
62
|
+
|
|
63
|
+
// INM.MB family (Internet/Mobile Banking credit movements, distinct from INM.OB)
|
|
64
|
+
{ keywords: ['CREDITO INM.MB', 'INM.MB'], account: '11000', name: 'Movimiento Internet/Mobile Banking' },
|
|
65
|
+
|
|
66
|
+
// Loan opening fees
|
|
67
|
+
{ keywords: ['GASTOS APERTURA CREDITOS', 'APERTURA CREDITO'], account: '67100', name: 'Gastos apertura crédito' },
|
|
33
68
|
];
|
|
34
69
|
|
|
35
70
|
// Match a bank description against keywords
|
package/lib/bc-client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { logger } from '../utils/logger.js';
|
|
|
2
2
|
import { getExpenseCategory } from '../config/expense-accounts.js';
|
|
3
3
|
import { INCOME_GROUP_LABELS } from '../config/income-accounts.js';
|
|
4
4
|
import { resolveStores } from '../config/company-config.js';
|
|
5
|
+
import { bcLimiter } from '../utils/rate-limiter.js';
|
|
5
6
|
|
|
6
7
|
export class BCClient {
|
|
7
8
|
constructor() {
|
|
@@ -19,6 +20,28 @@ export class BCClient {
|
|
|
19
20
|
this.EXCHANGE_RATE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a string value before embedding it in an OData $filter expression.
|
|
25
|
+
*
|
|
26
|
+
* OData string literals are delimited by single quotes. An attacker who
|
|
27
|
+
* controls a filter parameter (e.g. accountNo, vendorNo, postingDate) could
|
|
28
|
+
* inject arbitrary filter logic by inserting a single quote. Doubling every
|
|
29
|
+
* single quote is the standard OData escape sequence (identical to SQL).
|
|
30
|
+
*
|
|
31
|
+
* Usage inside a $filter template:
|
|
32
|
+
* `accountNo eq '${this._sanitizeOData(args.accountNo)}'`
|
|
33
|
+
*
|
|
34
|
+
* DOES NOT protect numeric / date literals — those should be validated with
|
|
35
|
+
* Number.isFinite() or a date regex before use.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} value
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
_sanitizeOData(value) {
|
|
41
|
+
if (value == null) return '';
|
|
42
|
+
return String(value).replace(/'/g, "''");
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
_cacheKey(prefix, ...parts) {
|
|
23
46
|
return `${prefix}:${parts.join(':')}`;
|
|
24
47
|
}
|
|
@@ -93,6 +116,7 @@ export class BCClient {
|
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
async apiCall(url) {
|
|
119
|
+
bcLimiter.recordCall(); // throws if rate limit exceeded
|
|
96
120
|
const token = await this.getToken();
|
|
97
121
|
logger.debug(`API call: ${url}`);
|
|
98
122
|
|
|
@@ -130,13 +154,20 @@ export class BCClient {
|
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
156
|
|
|
133
|
-
async apiCallAllPages(url) {
|
|
157
|
+
async apiCallAllPages(url, maxPages = 20) {
|
|
134
158
|
const token = await this.getToken();
|
|
135
159
|
let allResults = [];
|
|
136
160
|
let nextUrl = url;
|
|
161
|
+
let pageCount = 0;
|
|
137
162
|
|
|
138
163
|
while (nextUrl) {
|
|
139
|
-
|
|
164
|
+
if (pageCount >= maxPages) {
|
|
165
|
+
logger.warn(`apiCallAllPages: maxPages (${maxPages}) reached — results may be truncated. Set maxPages higher if needed.`);
|
|
166
|
+
allResults._truncated = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
bcLimiter.recordCall();
|
|
170
|
+
logger.debug(`API call (paged ${pageCount + 1}/${maxPages}): ${nextUrl}`);
|
|
140
171
|
const response = await this.fetchWithRetry(nextUrl, {
|
|
141
172
|
headers: { Authorization: `Bearer ${token}` },
|
|
142
173
|
});
|
|
@@ -149,6 +180,7 @@ export class BCClient {
|
|
|
149
180
|
const data = await response.json();
|
|
150
181
|
allResults = allResults.concat(data.value || []);
|
|
151
182
|
nextUrl = data['@odata.nextLink'] || null;
|
|
183
|
+
pageCount++;
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
return allResults;
|
|
@@ -240,13 +272,20 @@ export class BCClient {
|
|
|
240
272
|
return data.value || data;
|
|
241
273
|
}
|
|
242
274
|
|
|
243
|
-
async odataCallAllPages(url) {
|
|
275
|
+
async odataCallAllPages(url, maxPages = 20) {
|
|
244
276
|
const token = await this.getToken();
|
|
245
277
|
let allResults = [];
|
|
246
278
|
let nextUrl = url;
|
|
279
|
+
let pageCount = 0;
|
|
247
280
|
|
|
248
281
|
while (nextUrl) {
|
|
249
|
-
|
|
282
|
+
if (pageCount >= maxPages) {
|
|
283
|
+
logger.warn(`odataCallAllPages: maxPages (${maxPages}) reached — results may be truncated.`);
|
|
284
|
+
allResults._truncated = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
bcLimiter.recordCall();
|
|
288
|
+
logger.debug(`OData call (paged ${pageCount + 1}/${maxPages}): ${nextUrl}`);
|
|
250
289
|
const response = await this.fetchWithRetry(nextUrl, {
|
|
251
290
|
headers: { Authorization: `Bearer ${token}` },
|
|
252
291
|
});
|
|
@@ -259,6 +298,7 @@ export class BCClient {
|
|
|
259
298
|
const data = await response.json();
|
|
260
299
|
allResults = allResults.concat(data.value || []);
|
|
261
300
|
nextUrl = data['@odata.nextLink'] || null;
|
|
301
|
+
pageCount++;
|
|
262
302
|
}
|
|
263
303
|
|
|
264
304
|
return allResults;
|
|
@@ -266,8 +306,10 @@ export class BCClient {
|
|
|
266
306
|
|
|
267
307
|
// Fetch General Ledger Entries for a specific account range
|
|
268
308
|
async getGLEntries(companyId, startDate, endDate, accountMin, accountMax) {
|
|
309
|
+
const sd = this._sanitizeOData(startDate);
|
|
310
|
+
const ed = this._sanitizeOData(endDate);
|
|
269
311
|
const url = this.buildApiUrl(companyId, 'generalLedgerEntries', {
|
|
270
|
-
$filter: `postingDate ge ${
|
|
312
|
+
$filter: `postingDate ge ${sd} and postingDate le ${ed} and accountNumber ge '${accountMin}' and accountNumber le '${accountMax}'`,
|
|
271
313
|
$select: 'entryNumber,postingDate,documentNumber,accountNumber,description,debitAmount,creditAmount',
|
|
272
314
|
$orderby: 'postingDate desc',
|
|
273
315
|
});
|
|
@@ -509,13 +551,13 @@ export class BCClient {
|
|
|
509
551
|
async getDetailedGLEntries(companyId, filters = {}) {
|
|
510
552
|
const conditions = [];
|
|
511
553
|
|
|
512
|
-
if (filters.startDate) conditions.push(`postingDate ge ${filters.startDate}`);
|
|
513
|
-
if (filters.endDate)
|
|
554
|
+
if (filters.startDate) conditions.push(`postingDate ge ${this._sanitizeOData(filters.startDate)}`);
|
|
555
|
+
if (filters.endDate) conditions.push(`postingDate le ${this._sanitizeOData(filters.endDate)}`);
|
|
514
556
|
if (filters.accountNumber) {
|
|
515
|
-
conditions.push(`accountNumber eq '${filters.accountNumber}'`);
|
|
557
|
+
conditions.push(`accountNumber eq '${this._sanitizeOData(filters.accountNumber)}'`);
|
|
516
558
|
} else if (filters.accountMin && filters.accountMax) {
|
|
517
|
-
conditions.push(`accountNumber ge '${filters.accountMin}'`);
|
|
518
|
-
conditions.push(`accountNumber le '${filters.accountMax}'`);
|
|
559
|
+
conditions.push(`accountNumber ge '${this._sanitizeOData(filters.accountMin)}'`);
|
|
560
|
+
conditions.push(`accountNumber le '${this._sanitizeOData(filters.accountMax)}'`);
|
|
519
561
|
}
|
|
520
562
|
|
|
521
563
|
const params = {
|
|
@@ -533,8 +575,10 @@ export class BCClient {
|
|
|
533
575
|
// ── Sales Analysis Methods ──────────────────────────────────────
|
|
534
576
|
|
|
535
577
|
async getSalesInvoicesExpanded(companyId, startDate, endDate) {
|
|
578
|
+
const sd = this._sanitizeOData(startDate);
|
|
579
|
+
const ed = this._sanitizeOData(endDate);
|
|
536
580
|
const url = this.buildApiUrl(companyId, 'salesInvoices', {
|
|
537
|
-
$filter: `postingDate ge ${
|
|
581
|
+
$filter: `postingDate ge ${sd} and postingDate le ${ed}`,
|
|
538
582
|
$orderby: 'postingDate desc',
|
|
539
583
|
$expand: 'salesInvoiceLines',
|
|
540
584
|
});
|
|
@@ -567,7 +611,23 @@ export class BCClient {
|
|
|
567
611
|
|
|
568
612
|
async getItemsCatalog(companyId) {
|
|
569
613
|
const url = this.buildApiUrl(companyId, 'items', {
|
|
570
|
-
$select: 'id,number,displayName,type,unitPrice,unitCost,itemCategoryCode
|
|
614
|
+
$select: 'id,number,displayName,type,unitPrice,unitCost,itemCategoryCode',
|
|
615
|
+
});
|
|
616
|
+
return this.apiCallAllPages(url);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Fetch Sale-type Item Ledger Entries for one or more items in a date range.
|
|
620
|
+
// Returns the raw per-entry rows from BC (signs unchanged: quantity & costAmountActual negative).
|
|
621
|
+
async getItemLedgerSaleEntries(companyId, { items, startDate, endDate }) {
|
|
622
|
+
if (!items || items.length === 0) return [];
|
|
623
|
+
const itemFilter = items.length === 1
|
|
624
|
+
? `itemNumber eq '${items[0]}'`
|
|
625
|
+
: `(${items.map((i) => `itemNumber eq '${i}'`).join(' or ')})`;
|
|
626
|
+
|
|
627
|
+
const url = this.buildApiUrl(companyId, 'itemLedgerEntries', {
|
|
628
|
+
$filter: `${itemFilter} and entryType eq 'Sale' and postingDate ge ${startDate} and postingDate le ${endDate}`,
|
|
629
|
+
$select: 'entryNumber,itemNumber,postingDate,entryType,documentNumber,description,quantity,salesAmountActual,costAmountActual',
|
|
630
|
+
$orderby: 'itemNumber,postingDate',
|
|
571
631
|
});
|
|
572
632
|
return this.apiCallAllPages(url);
|
|
573
633
|
}
|
|
@@ -619,7 +679,7 @@ export class BCClient {
|
|
|
619
679
|
const item = itemsMap[itemNo];
|
|
620
680
|
if (item) {
|
|
621
681
|
line.itemCategoryCode = item.itemCategoryCode;
|
|
622
|
-
line.unitCostUSD = item.
|
|
682
|
+
line.unitCostUSD = item.unitCost || 0;
|
|
623
683
|
line.displayName = item.displayName || line.description;
|
|
624
684
|
}
|
|
625
685
|
if (!line.displayName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullqueso/mcp-bc-gastos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.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": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"start": "node server.js",
|
|
24
24
|
"dev": "nodemon server.js",
|
|
25
25
|
"test-connection": "node test-connection.js",
|
|
26
|
-
"test-tools": "node test-tools.js"
|
|
26
|
+
"test-tools": "node test-tools.js",
|
|
27
|
+
"test-item-sales-detail": "node tests/test-item-sales-detail.js"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|
|
29
30
|
"mcp",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Read-only diagnostic for any (store, month) bank closing snapshot.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/diagnose-closing.js --store FQ01 --month 2025-12
|
|
6
|
+
// node scripts/diagnose-closing.js -s FQ28 -m 2026-01 --bank FQ28-MN0001
|
|
7
|
+
//
|
|
8
|
+
// Hits BC OData/Beta API read-only. Writes a JSON snapshot to /tmp and
|
|
9
|
+
// prints a summary to stderr.
|
|
10
|
+
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { dirname, join } from 'path';
|
|
13
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
14
|
+
import dotenv from 'dotenv';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const envPath = join(__dirname, '..', '.env');
|
|
18
|
+
if (existsSync(envPath)) dotenv.config({ path: envPath });
|
|
19
|
+
|
|
20
|
+
const { BCClient } = await import('../lib/bc-client.js');
|
|
21
|
+
const { handleReconciliationStatus } = await import('../tools/auditoria/reconciliation-status.js');
|
|
22
|
+
const { handleBankReconciliationReport } = await import('../tools/auditoria/bank-reconciliation-report.js');
|
|
23
|
+
const { handleOpenReceivables } = await import('../tools/cobranzas/open-receivables.js');
|
|
24
|
+
const { handleOpenPayables } = await import('../tools/cobranzas/open-payables.js');
|
|
25
|
+
const { resolveStores } = await import('../config/company-config.js');
|
|
26
|
+
|
|
27
|
+
// ── CLI args ───────────────────────────────────────────────
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (let i = 2; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (a === '--store' || a === '-s') { out.store = next; i++; }
|
|
34
|
+
else if (a === '--month' || a === '-m') { out.month = next; i++; }
|
|
35
|
+
else if (a === '--bank' || a === '-b') { out.bank = next; i++; }
|
|
36
|
+
else if (a === '--help' || a === '-h') { out.help = true; }
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const args = parseArgs(process.argv);
|
|
41
|
+
if (args.help || !args.store || !args.month) {
|
|
42
|
+
console.error('Usage: node scripts/diagnose-closing.js --store FQ01 --month 2025-12 [--bank FQ01-MN0001]');
|
|
43
|
+
process.exit(args.help ? 0 : 2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const STORE = args.store;
|
|
47
|
+
const MONTH = args.month;
|
|
48
|
+
const MONTH_END = lastDayOfMonth(MONTH);
|
|
49
|
+
const BANK_FILTER = args.bank || null;
|
|
50
|
+
|
|
51
|
+
const bc = new BCClient();
|
|
52
|
+
const out = { store: STORE, month: MONTH, bank_filter: BANK_FILTER, generated_at: new Date().toISOString() };
|
|
53
|
+
|
|
54
|
+
console.error(`\n=== Diagnostic ${STORE} ${MONTH} ${BANK_FILTER ? '/' + BANK_FILTER : ''} ===\n`);
|
|
55
|
+
|
|
56
|
+
// 1. Reconciliation status
|
|
57
|
+
try {
|
|
58
|
+
console.error('[1/4] reconciliation_status…');
|
|
59
|
+
out.reconciliation_status = await handleReconciliationStatus(bc, { store: STORE });
|
|
60
|
+
const recs = out.reconciliation_status.reconciliations || [];
|
|
61
|
+
const monthRecs = recs.filter((r) => (r.statement_date || '').startsWith(MONTH));
|
|
62
|
+
console.error(` → ${recs.length} open reconciliations total; ${monthRecs.length} for ${MONTH}`);
|
|
63
|
+
for (const r of monthRecs) {
|
|
64
|
+
if (BANK_FILTER && r.bank_account_no !== BANK_FILTER) continue;
|
|
65
|
+
console.error(` ${r.bank_account_no} #${r.statement_no} date=${r.statement_date} matched=${r.matched_lines}/${r.total_lines} ending=${r.statement_ending_balance}`);
|
|
66
|
+
}
|
|
67
|
+
out.month_reconciliations = monthRecs;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(' ✗', err.message);
|
|
70
|
+
out.reconciliation_status_error = err.message;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Bank reconciliation report (per bank or filter)
|
|
74
|
+
try {
|
|
75
|
+
console.error('\n[2/4] bank_reconciliation_report…');
|
|
76
|
+
const targets = BANK_FILTER
|
|
77
|
+
? [BANK_FILTER]
|
|
78
|
+
: (out.month_reconciliations || []).map((r) => r.bank_account_no);
|
|
79
|
+
out.bank_reports = {};
|
|
80
|
+
for (const bank of targets) {
|
|
81
|
+
const r = await handleBankReconciliationReport(bc, {
|
|
82
|
+
store: STORE, bank_account: bank, month: MONTH, include_suggestions: true,
|
|
83
|
+
});
|
|
84
|
+
out.bank_reports[bank] = {
|
|
85
|
+
statement_no: r.statement_no,
|
|
86
|
+
statement_date: r.statement_date,
|
|
87
|
+
balance_last_statement: r.balance_last_statement,
|
|
88
|
+
statement_ending_balance: r.statement_ending_balance,
|
|
89
|
+
unmatched_debits_count: r.unmatched_debits?.count,
|
|
90
|
+
unmatched_debits_total: r.unmatched_debits?.total_amount,
|
|
91
|
+
unmatched_credits_count: r.unmatched_credits?.count,
|
|
92
|
+
unmatched_credits_total: r.unmatched_credits?.total_amount,
|
|
93
|
+
match_rate_pct: r.unmatched_summary?.match_rate_pct,
|
|
94
|
+
suggestions_summary: r.journal_suggestions?.summary,
|
|
95
|
+
};
|
|
96
|
+
console.error(` ${bank}: end=${r.statement_ending_balance} unmD=${r.unmatched_debits?.count} unmC=${r.unmatched_credits?.count} match=${r.unmatched_summary?.match_rate_pct}%`);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(' ✗', err.message);
|
|
100
|
+
out.bank_reports_error = err.message;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. Open receivables / payables as of month_end
|
|
104
|
+
try {
|
|
105
|
+
console.error('\n[3/4] open_receivables + open_payables…');
|
|
106
|
+
const [ar, ap] = await Promise.all([
|
|
107
|
+
handleOpenReceivables(bc, { store: STORE, as_of_date: MONTH_END }),
|
|
108
|
+
handleOpenPayables(bc, { store: STORE, as_of_date: MONTH_END }),
|
|
109
|
+
]);
|
|
110
|
+
out.ar_summary = { customers: ar.receivables_by_customer?.length, aging: ar.aging_summary };
|
|
111
|
+
out.ap_summary = { vendors: ap.payables_by_vendor?.length, aging: ap.aging_summary };
|
|
112
|
+
console.error(` AR: ${ar.receivables_by_customer?.length} customers, total ${ar.aging_summary?.total} USD`);
|
|
113
|
+
console.error(` AP: ${ap.payables_by_vendor?.length} vendors, total ${ap.aging_summary?.total} USD`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(' ✗', err.message);
|
|
116
|
+
out.arap_error = err.message;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 4. Multi-Payment drafts (Gestion BC) — Transferred status
|
|
120
|
+
try {
|
|
121
|
+
console.error('\n[4/4] multi-payment drafts (Transferred)…');
|
|
122
|
+
const { handleDraftPayables, handleDraftReceivables } = await import('../tools/multi-payment/index.js');
|
|
123
|
+
const [dp, dr] = await Promise.all([
|
|
124
|
+
handleDraftPayables(bc, { store: STORE }),
|
|
125
|
+
handleDraftReceivables(bc, { store: STORE }),
|
|
126
|
+
]);
|
|
127
|
+
// Count Transferred drafts
|
|
128
|
+
const tp = (dp?.drafts_by_status?.Transferred || dp?.results || []).length;
|
|
129
|
+
const tr = (dr?.drafts_by_status?.Transferred || dr?.results || []).length;
|
|
130
|
+
out.mp_drafts = { transferred_payables: tp, transferred_receivables: tr };
|
|
131
|
+
console.error(` Transferred drafts → payables: ${tp}, receivables: ${tr}`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(' ✗', err.message);
|
|
134
|
+
out.mp_error = err.message;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const outPath = `/tmp/diagnose-${STORE}-${MONTH}.json`;
|
|
138
|
+
writeFileSync(outPath, JSON.stringify(out, null, 2));
|
|
139
|
+
console.error(`\n✓ Snapshot → ${outPath}\n`);
|
|
140
|
+
|
|
141
|
+
function lastDayOfMonth(yyyyMm) {
|
|
142
|
+
const [y, m] = yyyyMm.split('-').map(Number);
|
|
143
|
+
return new Date(Date.UTC(y, m, 0)).toISOString().slice(0, 10);
|
|
144
|
+
}
|