@fullqueso/mcp-bc-gastos 1.19.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 +51 -0
- package/config/bank-gl-map.json +5 -5
- package/config/bank-keywords.js +35 -0
- package/lib/bc-client.js +71 -11
- 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 +57 -0
- package/tools/auditoria/bank-ledger-entries.js +3 -3
- 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 +11 -9
- 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/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/item-ledger-entries.js +4 -4
- package/tools/inventario/item-value-entries.js +2 -2
- 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,5 +1,56 @@
|
|
|
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
|
+
|
|
3
54
|
## [1.19.0] - 2026-04-13
|
|
4
55
|
|
|
5
56
|
### Added
|
package/config/bank-gl-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.0",
|
|
3
|
-
"last_updated": "2026-
|
|
3
|
+
"last_updated": "2026-05-17",
|
|
4
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
5
|
"stores": {
|
|
6
6
|
"FQ01": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"accounts": {
|
|
26
26
|
"MN0001": { "gl": "18210", "name": "BDV 8139 Bs.", "currency": "VES", "category": "bancos_nacionales" },
|
|
27
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" },
|
|
29
|
-
"MN0029": { "gl": "18291", "name": "UBII 6442
|
|
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
30
|
"MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
31
31
|
"MN0031": { "gl": "18160", "name": "Caja transitoria tienda ventas Bs.", "currency": "VES", "category": "caja" },
|
|
32
|
-
"MN0032": { "gl": "18292", "name": "UBII 4
|
|
33
|
-
"MN0033": { "gl": "18295", "name": "UBII 7
|
|
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
34
|
"MN0098": { "gl": "18298", "name": "Operaciones en Tránsito", "currency": "VES", "category": "bancos_nacionales" },
|
|
35
35
|
"ME0002": { "gl": "18310", "name": "Bancrecer $", "currency": "USD", "category": "bancos_nacionales_divisas" },
|
|
36
36
|
"ME0005": { "gl": "18410", "name": "Zelle Bofa $ *", "currency": "USD", "category": "bancos_extranjeros" },
|
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
|
});
|
|
@@ -572,6 +616,22 @@ export class BCClient {
|
|
|
572
616
|
return this.apiCallAllPages(url);
|
|
573
617
|
}
|
|
574
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',
|
|
631
|
+
});
|
|
632
|
+
return this.apiCallAllPages(url);
|
|
633
|
+
}
|
|
634
|
+
|
|
575
635
|
async getSalesStoreData(storeCode, startDate, endDate, exchangeRates = null) {
|
|
576
636
|
const stores = resolveStores([storeCode]);
|
|
577
637
|
const store = stores[0];
|
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
|
+
}
|