@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +96 -1
  2. package/config/bank-gl-map.json +93 -0
  3. package/config/bank-keywords.js +35 -0
  4. package/lib/bc-client.js +73 -13
  5. package/package.json +3 -2
  6. package/scripts/diagnose-closing.js +144 -0
  7. package/scripts/generate-month-closing-xlsx.py +325 -0
  8. package/scripts/test-closing-flow.js +71 -0
  9. package/scripts/test-closing-fq01-dec25.js +78 -0
  10. package/server.js +77 -0
  11. package/tools/auditoria/bank-ledger-entries.js +3 -5
  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 +23 -12
  15. package/tools/auditoria/pm-receipts.js +4 -4
  16. package/tools/auditoria/reconcile-pos-sales.js +46 -21
  17. package/tools/auditoria/unmatched-ledger-entries.js +3 -3
  18. package/tools/auditoria/unmatched-statement-lines.js +2 -2
  19. package/tools/cierre-mensual/fetch-ledger.js +69 -0
  20. package/tools/cierre-mensual/generate-closing-journal.js +111 -0
  21. package/tools/cierre-mensual/get-match-results.js +151 -0
  22. package/tools/cierre-mensual/get-questionnaire.js +283 -0
  23. package/tools/cierre-mensual/index.js +6 -0
  24. package/tools/cierre-mensual/journal-builder.js +219 -0
  25. package/tools/cierre-mensual/matchers/index.js +161 -0
  26. package/tools/cierre-mensual/matchers/match-cross-bank.js +168 -0
  27. package/tools/cierre-mensual/matchers/match-draft-payments.js +178 -0
  28. package/tools/cierre-mensual/matchers/match-feedback-loop.js +24 -0
  29. package/tools/cierre-mensual/matchers/match-keywords.js +65 -0
  30. package/tools/cierre-mensual/matchers/match-open-arap.js +66 -0
  31. package/tools/cierre-mensual/matchers/match-pos-terminal.js +53 -0
  32. package/tools/cierre-mensual/reconcile-with-bc.js +116 -0
  33. package/tools/cierre-mensual/start-month-closing.js +234 -0
  34. package/tools/cierre-mensual/state-store.js +211 -0
  35. package/tools/cierre-mensual/submit-answers.js +106 -0
  36. package/tools/cobranzas/customer-ledger.js +4 -4
  37. package/tools/cobranzas/customer-list.js +63 -0
  38. package/tools/cobranzas/vendor-ledger.js +4 -4
  39. package/tools/financials/aggregator.js +360 -0
  40. package/tools/financials/cash-flow-html.js +459 -0
  41. package/tools/financials/cash-flow.js +471 -0
  42. package/tools/financials/html-template.js +674 -0
  43. package/tools/financials/index.js +79 -0
  44. package/tools/financials/statements.js +296 -0
  45. package/tools/inventario/index.js +3 -0
  46. package/tools/inventario/inventory-by-location.js +212 -0
  47. package/tools/inventario/inventory-change.js +386 -0
  48. package/tools/inventario/inventory-levels.js +214 -0
  49. package/tools/inventario/item-card.js +1 -50
  50. package/tools/inventario/item-ledger-entries.js +4 -4
  51. package/tools/inventario/item-value-entries.js +2 -2
  52. package/tools/inventario/shared/cost-calculator.js +64 -0
  53. package/tools/ventas/index.js +1 -0
  54. package/tools/ventas/item-sales-detail.js +366 -0
  55. package/utils/rate-limiter.js +84 -0
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env python3
2
+ """Generate month-end bank closing workbook from generate_closing_journal JSON.
3
+
4
+ Usage:
5
+ python3 generate-month-closing-xlsx.py <input.json> <output.xlsx>
6
+
7
+ Input JSON shape: { state: {...}, journal: {...} } (written by generate-closing-journal.js)
8
+ Sheets:
9
+ - Resumen
10
+ - Asiento_<BANK> per bank in journal.banks
11
+ - Cuestionario (unknown line classifications)
12
+ - Confirmaciones (POS, intercompany, vendors confirmed/declined)
13
+ """
14
+
15
+ import json
16
+ import sys
17
+
18
+ try:
19
+ from openpyxl import Workbook
20
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
21
+ from openpyxl.utils import get_column_letter
22
+ except ImportError:
23
+ print("Error: openpyxl required. pip3 install openpyxl", file=sys.stderr)
24
+ sys.exit(1)
25
+
26
+ BOLD = Font(bold=True)
27
+ BOLD_W = Font(bold=True, color="FFFFFF")
28
+ HEADER_FILL = PatternFill(start_color="305496", end_color="305496", fill_type="solid")
29
+ RESIDUAL_FILL = PatternFill(start_color="FFE699", end_color="FFE699", fill_type="solid")
30
+ WARN_FILL = PatternFill(start_color="F4B084", end_color="F4B084", fill_type="solid")
31
+ TOTAL_FILL = PatternFill(start_color="D9E2F3", end_color="D9E2F3", fill_type="solid")
32
+ THIN = Border(bottom=Side(style="thin", color="BFBFBF"))
33
+ NUM = '#,##0.00'
34
+
35
+
36
+ def autosize(ws, widths):
37
+ for i, w in enumerate(widths, start=1):
38
+ ws.column_dimensions[get_column_letter(i)].width = w
39
+
40
+
41
+ def write_header(ws, row, headers):
42
+ for i, h in enumerate(headers, start=1):
43
+ c = ws.cell(row=row, column=i, value=h)
44
+ c.font = BOLD_W
45
+ c.fill = HEADER_FILL
46
+ c.alignment = Alignment(horizontal="center")
47
+
48
+
49
+ def sheet_resumen(wb, state, journal):
50
+ ws = wb.create_sheet("Resumen")
51
+ ws["A1"] = f"Cierre Mensual — {state.get('store')} {state.get('month')}"
52
+ ws["A1"].font = Font(bold=True, size=14)
53
+ ws["A2"] = f"Statement date: {state.get('statement_date')}"
54
+ ws["A3"] = f"Status: {state.get('status')}"
55
+ ws["A4"] = f"Generated: {state.get('updated_at')}"
56
+
57
+ row = 6
58
+ ws.cell(row=row, column=1, value="Banco").font = BOLD
59
+ ws.cell(row=row, column=2, value="GL").font = BOLD
60
+ ws.cell(row=row, column=3, value="Apertura").font = BOLD
61
+ ws.cell(row=row, column=4, value="Cierre Statement").font = BOLD
62
+ ws.cell(row=row, column=5, value="Cierre Calculado").font = BOLD
63
+ ws.cell(row=row, column=6, value="Diferencia").font = BOLD
64
+ ws.cell(row=row, column=7, value="Filas").font = BOLD
65
+ ws.cell(row=row, column=8, value="Balanceado").font = BOLD
66
+ row += 1
67
+ for b in journal.get("banks", []):
68
+ ws.cell(row=row, column=1, value=b["bank_account"])
69
+ ws.cell(row=row, column=2, value=b["gl_account"])
70
+ ws.cell(row=row, column=3, value=b["opening_balance"]).number_format = NUM
71
+ ws.cell(row=row, column=4, value=b["statement_ending"]).number_format = NUM
72
+ ws.cell(row=row, column=5, value=b["computed_ending"]).number_format = NUM
73
+ gap = ws.cell(row=row, column=6, value=b["gap_to_clearing"])
74
+ gap.number_format = NUM
75
+ if abs(b["gap_to_clearing"] or 0) > 0.5:
76
+ gap.fill = RESIDUAL_FILL
77
+ ws.cell(row=row, column=7, value=b["summary"]["rows"])
78
+ ws.cell(row=row, column=8, value="Sí" if b["summary"]["balanced"] else "NO")
79
+ row += 1
80
+
81
+ row += 1
82
+ ws.cell(row=row, column=1, value="Total Débito").font = BOLD
83
+ ws.cell(row=row, column=2, value=journal["totals"]["debit"]).number_format = NUM
84
+ row += 1
85
+ ws.cell(row=row, column=1, value="Total Crédito").font = BOLD
86
+ ws.cell(row=row, column=2, value=journal["totals"]["credit"]).number_format = NUM
87
+ row += 1
88
+ ws.cell(row=row, column=1, value="Balanceado global").font = BOLD
89
+ ws.cell(row=row, column=2, value="Sí" if journal.get("balanced") else "NO")
90
+
91
+ if journal.get("warnings"):
92
+ row += 2
93
+ ws.cell(row=row, column=1, value="Advertencias").font = BOLD
94
+ row += 1
95
+ for w in journal["warnings"]:
96
+ c = ws.cell(row=row, column=1, value=w)
97
+ c.fill = WARN_FILL
98
+ row += 1
99
+
100
+ autosize(ws, [22, 8, 16, 18, 18, 14, 8, 12])
101
+
102
+
103
+ def sheet_journal_per_bank(wb, bank):
104
+ safe = bank["bank_account"].replace("-", "_")[:25]
105
+ ws = wb.create_sheet(f"Asiento_{safe}")
106
+ ws["A1"] = f"Asiento Propuesto — {bank['bank_account']} (GL {bank['gl_account']})"
107
+ ws["A1"].font = Font(bold=True, size=12)
108
+ ws["A2"] = (
109
+ f"Apertura: {bank['opening_balance']:,.2f} "
110
+ f"Cierre statement: {bank['statement_ending']:,.2f} "
111
+ f"Diferencia: {bank['gap_to_clearing']:,.2f}"
112
+ )
113
+ headers = [
114
+ "Fecha", "Doc No", "Tipo Cuenta", "Cuenta", "Contraparte",
115
+ "Descripción", "Débito VES", "Crédito VES", "Banco",
116
+ ]
117
+ write_header(ws, 4, headers)
118
+ row = 5
119
+ for r in bank.get("rows", []):
120
+ cells = [
121
+ r.get("posting_date"),
122
+ r.get("document_no"),
123
+ r.get("account_type"),
124
+ r.get("account_no"),
125
+ r.get("counterparty", ""),
126
+ r.get("description", ""),
127
+ r.get("debit_ves", 0),
128
+ r.get("credit_ves", 0),
129
+ r.get("bank_account", ""),
130
+ ]
131
+ for i, v in enumerate(cells, start=1):
132
+ c = ws.cell(row=row, column=i, value=v)
133
+ c.border = THIN
134
+ if i in (7, 8):
135
+ c.number_format = NUM
136
+ if r.get("is_residual"):
137
+ c.fill = RESIDUAL_FILL
138
+ row += 1
139
+ # totals
140
+ ws.cell(row=row + 1, column=6, value="Total").font = BOLD
141
+ td = ws.cell(row=row + 1, column=7, value=bank["summary"]["total_debit"])
142
+ tc = ws.cell(row=row + 1, column=8, value=bank["summary"]["total_credit"])
143
+ td.number_format = NUM
144
+ tc.number_format = NUM
145
+ td.fill = TOTAL_FILL
146
+ tc.fill = TOTAL_FILL
147
+ autosize(ws, [12, 18, 14, 12, 18, 50, 14, 14, 16])
148
+
149
+
150
+ def sheet_cuestionario(wb, state):
151
+ ws = wb.create_sheet("Cuestionario")
152
+ q = state.get("questionnaire", {})
153
+ headers = [
154
+ "Banco", "Fecha", "Descripción", "Monto VES", "Categoría",
155
+ "Sugerencia GL", "Confianza", "Clasificación usuario", "Memo", "Guardar feedback",
156
+ ]
157
+ write_header(ws, 1, headers)
158
+ row = 2
159
+ for ln in q.get("unknown_lines", []):
160
+ sug = ln.get("suggestion") or {}
161
+ cls = ln.get("user_classification") or {}
162
+ cells = [
163
+ ln.get("bank_account"),
164
+ ln.get("date"),
165
+ ln.get("description"),
166
+ ln.get("amount"),
167
+ ln.get("category"),
168
+ sug.get("gl_account") or "",
169
+ sug.get("confidence") or sug.get("source") or "",
170
+ cls.get("gl_account") or "",
171
+ cls.get("memo") or "",
172
+ "Sí" if cls.get("save_for_future") else "",
173
+ ]
174
+ for i, v in enumerate(cells, start=1):
175
+ c = ws.cell(row=row, column=i, value=v)
176
+ c.border = THIN
177
+ if i == 4:
178
+ c.number_format = NUM
179
+ row += 1
180
+ autosize(ws, [16, 12, 50, 14, 22, 12, 12, 12, 30, 14])
181
+
182
+
183
+ def sheet_confirmaciones(wb, state):
184
+ ws = wb.create_sheet("Confirmaciones")
185
+ q = state.get("questionnaire", {})
186
+ headers = [
187
+ "Grupo", "Contraparte", "Nombre", "Documento", "Fecha",
188
+ "Monto VES", "Monto USD", "Días", "Premisa", "Confirmado", "Banco asignado",
189
+ ]
190
+ write_header(ws, 1, headers)
191
+ row = 2
192
+ routing = q.get("routing", {}) or {}
193
+
194
+ def add(group, e):
195
+ nonlocal row
196
+ cells = [
197
+ group,
198
+ e.get("counterparty_no"),
199
+ e.get("counterparty_name") or "",
200
+ e.get("document_number") or "",
201
+ e.get("posting_date"),
202
+ e.get("amount_ves"),
203
+ e.get("amount_usd"),
204
+ e.get("days_old"),
205
+ e.get("premise"),
206
+ (
207
+ "Sí" if e.get("confirmed") is True
208
+ else "No" if e.get("confirmed") is False
209
+ else "(pendiente)"
210
+ ),
211
+ routing.get(e.get("counterparty_no"), ""),
212
+ ]
213
+ for i, v in enumerate(cells, start=1):
214
+ c = ws.cell(row=row, column=i, value=v)
215
+ c.border = THIN
216
+ if i in (6, 7) and isinstance(v, (int, float)):
217
+ c.number_format = NUM
218
+ row += 1
219
+
220
+ for e in q.get("pos_entries", []):
221
+ add("POS", e)
222
+ for e in q.get("intercompany_entries", []):
223
+ add("Intercompañía", e)
224
+ for e in q.get("vendor_entries", []):
225
+ add("Proveedor", e)
226
+
227
+ autosize(ws, [14, 18, 36, 18, 12, 16, 14, 8, 22, 14, 18])
228
+
229
+
230
+ def sheet_detalle_lineas(wb, state):
231
+ """One row per bank statement line with full match metadata for manual review.
232
+
233
+ Use this sheet to verify that auto-matches are correct before approving.
234
+ Filter by Banco column to focus on one account at a time.
235
+ """
236
+ ws = wb.create_sheet("Detalle Líneas Banco")
237
+ q = state.get("questionnaire", {}) or {}
238
+ headers = [
239
+ "Banco", "Fecha", "Doc No", "Tipo",
240
+ "Monto VES", "Categoría Original",
241
+ "Match Source", "Confianza",
242
+ "GL Sugerida", "Categoría Match", "Acción Sugerida",
243
+ "GL Usuario", "Memo Usuario",
244
+ "Grupo Match", "Razón", "Descripción Completa",
245
+ ]
246
+ write_header(ws, 1, headers)
247
+ row = 2
248
+ # Sort by bank then date for review-friendly order
249
+ lines = sorted(
250
+ q.get("unknown_lines", []),
251
+ key=lambda l: ((l.get("bank_account") or ""), (l.get("date") or "")),
252
+ )
253
+ for ln in lines:
254
+ sug = ln.get("match_suggestion") or {}
255
+ target = sug.get("target") or {}
256
+ cls = ln.get("user_classification") or {}
257
+ # Grupo Match: aggregate_pattern (UBII) or counterparty / target line id
258
+ grupo = (
259
+ target.get("aggregate_pattern")
260
+ or target.get("counterparty_no")
261
+ or target.get("invoice_no")
262
+ or target.get("mp_no")
263
+ or target.get("line_id")
264
+ or ""
265
+ )
266
+ cells = [
267
+ ln.get("bank_account"),
268
+ ln.get("date"),
269
+ ln.get("document_no") or "",
270
+ ln.get("type") or "",
271
+ ln.get("amount"),
272
+ ln.get("category") or "",
273
+ sug.get("source") or "",
274
+ sug.get("confidence") or "",
275
+ target.get("gl_account") or "",
276
+ target.get("category") or target.get("label") or "",
277
+ sug.get("recommended_action") or "",
278
+ cls.get("gl_account") or "",
279
+ cls.get("memo") or "",
280
+ grupo,
281
+ (sug.get("reason") or "")[:200],
282
+ ln.get("description") or "",
283
+ ]
284
+ for i, v in enumerate(cells, start=1):
285
+ c = ws.cell(row=row, column=i, value=v)
286
+ c.border = THIN
287
+ if i == 5 and isinstance(v, (int, float)):
288
+ c.number_format = NUM
289
+ # Highlight: residual to clearing or apply_in_bc
290
+ if (sug.get("recommended_action") in ("apply_in_bc", "apply_mp_draft")):
291
+ c.fill = TOTAL_FILL
292
+ elif not sug:
293
+ c.fill = RESIDUAL_FILL # unclassified
294
+ row += 1
295
+ # Freeze the header
296
+ ws.freeze_panes = "A2"
297
+ # Auto-filter so the user can sort/filter in Excel
298
+ if row > 2:
299
+ ws.auto_filter.ref = f"A1:{chr(ord('A') + len(headers) - 1)}{row - 1}"
300
+ autosize(ws, [16, 12, 18, 8, 14, 22, 14, 11, 12, 30, 18, 12, 28, 22, 50, 60])
301
+
302
+
303
+ def main(in_path, out_path):
304
+ with open(in_path, "r", encoding="utf-8") as f:
305
+ data = json.load(f)
306
+ state = data.get("state", {})
307
+ journal = data.get("journal", {})
308
+
309
+ wb = Workbook()
310
+ wb.remove(wb.active)
311
+ sheet_resumen(wb, state, journal)
312
+ for b in journal.get("banks", []):
313
+ sheet_journal_per_bank(wb, b)
314
+ sheet_detalle_lineas(wb, state)
315
+ sheet_cuestionario(wb, state)
316
+ sheet_confirmaciones(wb, state)
317
+ wb.save(out_path)
318
+ print(f"Wrote {out_path}")
319
+
320
+
321
+ if __name__ == "__main__":
322
+ if len(sys.argv) != 3:
323
+ print("Usage: generate-month-closing-xlsx.py <input.json> <output.xlsx>", file=sys.stderr)
324
+ sys.exit(2)
325
+ main(sys.argv[1], sys.argv[2])
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // End-to-end integration test for the auto-matching closing workflow.
3
+ // start_month_closing → get_closing_match_results → submit_closing_answers
4
+ // (bulk approvals + mark_apply_in_bc) → reconcile_closing_with_bc →
5
+ // generate_closing_journal
6
+ //
7
+ // Usage: node scripts/test-closing-flow.js [--store FQ01] [--month 2025-12]
8
+
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+ import { existsSync } from 'fs';
12
+ import dotenv from 'dotenv';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const envPath = join(__dirname, '..', '.env');
16
+ if (existsSync(envPath)) dotenv.config({ path: envPath });
17
+
18
+ const { BCClient } = await import('../lib/bc-client.js');
19
+ const mod = await import('../tools/cierre-mensual/index.js');
20
+
21
+ const STORE = (process.argv.indexOf('--store') >= 0 && process.argv[process.argv.indexOf('--store') + 1]) || 'FQ01';
22
+ const MONTH = (process.argv.indexOf('--month') >= 0 && process.argv[process.argv.indexOf('--month') + 1]) || '2025-12';
23
+
24
+ const bc = new BCClient();
25
+
26
+ console.error(`\n=== ${STORE} ${MONTH} — full closing flow test ===\n`);
27
+
28
+ console.error('[1/5] start_month_closing (force_refresh=true)');
29
+ const start = await mod.handleStartMonthClosing(bc, { store: STORE, month: MONTH, force_refresh: true });
30
+ console.error(' match_results_summary:', JSON.stringify(start.match_results_summary, null, 2));
31
+
32
+ console.error('\n[2/5] get_closing_match_results by_category (top 6)');
33
+ const cats = await mod.handleGetClosingMatchResults(null, { store: STORE, month: MONTH, list: 'by_category' });
34
+ for (const c of cats.by_category.slice(0, 6)) {
35
+ console.error(` ${c.category.padEnd(50)} count=${String(c.count).padStart(5)} VES=${c.total_abs_ves.toLocaleString().padStart(15)} sources=${c.sources.join(',')}`);
36
+ }
37
+
38
+ console.error('\n[3/5] submit: approve top categories + mark apply_in_bc + classify a few unclassified');
39
+ const topCategories = cats.by_category.slice(0, 6).map((c) => c.category);
40
+ const apply = await mod.handleGetClosingMatchResults(null, { store: STORE, month: MONTH, list: 'apply_in_bc', limit: 200 });
41
+ const unclassified = await mod.handleGetClosingMatchResults(null, { store: STORE, month: MONTH, list: 'unclassified', limit: 5 });
42
+ const lineClassifications = unclassified.items.map((x) => ({
43
+ line_id: x.line_id, gl_account: '67100', memo: 'Manual: ' + (x.description || '').slice(0, 40), save_for_future: true,
44
+ }));
45
+ const subRes = await mod.handleSubmitClosingAnswers(null, {
46
+ store: STORE, month: MONTH, user: 'test@fullqueso.com',
47
+ answers: {
48
+ approve_category_bulk: topCategories,
49
+ mark_apply_in_bc: apply.items.map((x) => x.line_id),
50
+ line_classifications: lineClassifications,
51
+ },
52
+ });
53
+ console.error(' status=' + subRes.status, 'pending_count=' + subRes.pending_count);
54
+ console.error(' pending_breakdown:', subRes.pending_breakdown);
55
+
56
+ console.error('\n[4/5] reconcile_closing_with_bc');
57
+ const recon = await mod.handleReconcileWithBc(bc, { store: STORE, month: MONTH });
58
+ console.error(' drift summary:', recon.summary);
59
+
60
+ console.error('\n[5/5] generate_closing_journal (allow_partial=true)');
61
+ const j = await mod.handleGenerateClosingJournal(bc, { store: STORE, month: MONTH, output_dir: '/tmp', allow_partial: true });
62
+ console.error(' xlsx_path:', j.xlsx_path);
63
+ console.error(' balanced:', j.journal_summary?.balanced);
64
+ console.error(' totals: debit=' + j.journal_summary?.total_debit, 'credit=' + j.journal_summary?.total_credit);
65
+ console.error(' banks:');
66
+ for (const b of j.journal_summary?.banks || []) {
67
+ console.error(` ${b.bank_account} rows=${b.rows} debit=${b.total_debit} credit=${b.total_credit} gap=${b.gap_to_clearing}`);
68
+ }
69
+ console.error(' apply_in_bc roster size:', j.journal_summary?.apply_in_bc_count);
70
+
71
+ console.error('\n✓ End-to-end test complete.\n');
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // End-to-end integration test for the entry-level month-end closing workflow.
3
+
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+ import { existsSync } from 'fs';
7
+ import dotenv from 'dotenv';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const envPath = join(__dirname, '..', '.env');
11
+ if (existsSync(envPath)) dotenv.config({ path: envPath });
12
+
13
+ const { BCClient } = await import('../lib/bc-client.js');
14
+ const {
15
+ handleStartMonthClosing,
16
+ handleSubmitClosingAnswers,
17
+ handleGenerateClosingJournal,
18
+ handleGetClosingQuestionnaire,
19
+ } = await import('../tools/cierre-mensual/index.js');
20
+
21
+ const bc = new BCClient();
22
+
23
+ console.error('\n=== TEST: start_month_closing FQ01 (force_refresh) ===');
24
+ const startRes = await handleStartMonthClosing(bc, { store: 'FQ01', month: '2025-12', force_refresh: true });
25
+ console.error(JSON.stringify({
26
+ status: startRes.status, month: startRes.month,
27
+ banks_count: startRes.banks?.length,
28
+ q: startRes.questionnaire_summary,
29
+ }, null, 2));
30
+
31
+ console.error('\n=== TEST: get_closing_questionnaire summary ===');
32
+ const sumRes = await handleGetClosingQuestionnaire(bc, { store: 'FQ01', month: '2025-12', bucket: 'summary' });
33
+ console.error(JSON.stringify({ buckets: sumRes.buckets, unknown_lines: sumRes.unknown_lines }, null, 2));
34
+
35
+ console.error('\n=== TEST: counterparties view ===');
36
+ const cpRes = await handleGetClosingQuestionnaire(bc, { store: 'FQ01', month: '2025-12', bucket: 'counterparties' });
37
+ console.error('POS:', JSON.stringify(cpRes.counterparties.pos_entries.map((g) => ({ cp: g.counterparty_no, name: g.counterparty_name, entries: g.entries, ves: g.total_ves, usd: g.total_usd, oldest: g.oldest_doc_date, latest: g.latest_doc_date })), null, 2));
38
+ console.error('Intercompany:', JSON.stringify(cpRes.counterparties.intercompany_entries.map((g) => ({ cp: g.counterparty_no, name: g.counterparty_name, entries: g.entries, ves: g.total_ves, usd: g.total_usd })), null, 2));
39
+ console.error('Vendors:', cpRes.counterparties.vendor_entries.length, 'groups');
40
+
41
+ console.error('\n=== TEST: pos_entries first 5 ===');
42
+ const posRes = await handleGetClosingQuestionnaire(bc, { store: 'FQ01', month: '2025-12', bucket: 'pos_entries', limit: 5, sort: 'amount_ves_desc' });
43
+ console.error(JSON.stringify({ total: posRes.total, entries: posRes.entries }, null, 2));
44
+
45
+ // Confirm all POS via counterparty_bulk shortcut, route to MN0001
46
+ console.error('\n=== TEST: submit (bulk confirm POS to MN0001) ===');
47
+ const posCPs = cpRes.counterparties.pos_entries.map((g) => g.counterparty_no);
48
+ const bulk = {};
49
+ for (const cp of posCPs) bulk[cp] = true;
50
+ const routing = {};
51
+ for (const cp of posCPs) routing[cp] = 'FQ01-MN0001';
52
+ const subRes = await handleSubmitClosingAnswers(bc, {
53
+ store: 'FQ01', month: '2025-12',
54
+ answers: { counterparty_bulk: bulk, routing },
55
+ });
56
+ console.error(JSON.stringify({ status: subRes.status, pending_count: subRes.pending_count, pending_breakdown: subRes.pending_breakdown }, null, 2));
57
+
58
+ console.error('\n=== TEST: summary after POS confirmation ===');
59
+ const sumRes2 = await handleGetClosingQuestionnaire(bc, { store: 'FQ01', month: '2025-12', bucket: 'summary' });
60
+ console.error(JSON.stringify({ buckets: sumRes2.buckets }, null, 2));
61
+
62
+ console.error('\n=== TEST: generate_closing_journal (allow_partial=true) ===');
63
+ const genRes = await handleGenerateClosingJournal(bc, {
64
+ store: 'FQ01', month: '2025-12', output_dir: '/tmp', allow_partial: true,
65
+ });
66
+ console.error(JSON.stringify({
67
+ status: genRes.status,
68
+ xlsx_path: genRes.xlsx_path,
69
+ xlsx_error: genRes.xlsx_error?.slice(0, 300),
70
+ summary: {
71
+ balanced: genRes.journal_summary.balanced,
72
+ total_debit: genRes.journal_summary.total_debit,
73
+ total_credit: genRes.journal_summary.total_credit,
74
+ banks: genRes.journal_summary.banks,
75
+ warnings_count: genRes.journal_summary.warnings.length,
76
+ unassigned_rows: genRes.journal_summary.unassigned_rows,
77
+ },
78
+ }, null, 2));
package/server.js CHANGED
@@ -54,6 +54,7 @@ import { openReceivablesTool, handleOpenReceivables } from './tools/cobranzas/op
54
54
  import { collectionStatusTool, handleCollectionStatus } from './tools/cobranzas/collection-status.js';
55
55
  import { vendorLedgerTool, handleVendorLedger } from './tools/cobranzas/vendor-ledger.js';
56
56
  import { openPayablesTool, handleOpenPayables } from './tools/cobranzas/open-payables.js';
57
+ import { customerListTool, handleCustomerList } from './tools/cobranzas/customer-list.js';
57
58
 
58
59
  // Multi-Payment draft visibility tools
59
60
  import {
@@ -75,6 +76,9 @@ import {
75
76
  itemLedgerEntriesTool, handleItemLedgerEntries,
76
77
  itemValueEntriesTool, handleItemValueEntries,
77
78
  itemCostTrendTool, handleItemCostTrend,
79
+ inventoryLevelsTool, handleInventoryLevels,
80
+ inventoryChangeTool, handleInventoryChange,
81
+ inventoryByLocationTool, handleInventoryByLocation,
78
82
  } from './tools/inventario/index.js';
79
83
 
80
84
  // Ventas tools (sales analysis)
@@ -82,12 +86,29 @@ import {
82
86
  salesAnalysisTool, handleSalesAnalysis,
83
87
  productPerformanceTool, handleProductPerformance,
84
88
  salesStoreComparisonTool, handleSalesStoreComparison,
89
+ itemSalesDetailTool, handleItemSalesDetail,
85
90
  } from './tools/ventas/index.js';
86
91
 
87
92
  // Reports tools
88
93
  import { managerReportTool, handleGenerateManagerReport } from './tools/reports/manager-report.js';
89
94
  import { cxpReportTool, handleGenerateCxpReport } from './tools/reports/cxp-report.js';
90
95
 
96
+ // Cierre mensual (month-end bank reconciliation closing)
97
+ import {
98
+ startMonthClosingTool, handleStartMonthClosing,
99
+ submitClosingAnswersTool, handleSubmitClosingAnswers,
100
+ generateClosingJournalTool, handleGenerateClosingJournal,
101
+ getClosingQuestionnaireTool, handleGetClosingQuestionnaire,
102
+ getClosingMatchResultsTool, handleGetClosingMatchResults,
103
+ reconcileWithBcTool, handleReconcileWithBc,
104
+ } from './tools/cierre-mensual/index.js';
105
+
106
+ // Financials (P&L / Financial Statements) + Cash Flow (FCF)
107
+ import {
108
+ financialStatementsTool, handleFinancialStatements,
109
+ cashFlowTool, handleCashFlow,
110
+ } from './tools/financials/index.js';
111
+
91
112
  const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
92
113
  const bcClient = new BCClient();
93
114
 
@@ -136,6 +157,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
136
157
  collectionStatusTool,
137
158
  vendorLedgerTool,
138
159
  openPayablesTool,
160
+ customerListTool,
139
161
  // Multi-Payment draft visibility
140
162
  draftReceivablesTool,
141
163
  draftPayablesTool,
@@ -149,13 +171,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
149
171
  itemLedgerEntriesTool,
150
172
  itemValueEntriesTool, // get_item_cost_analysis
151
173
  itemCostTrendTool,
174
+ inventoryLevelsTool,
175
+ inventoryChangeTool,
176
+ inventoryByLocationTool,
152
177
  // Ventas (sales analysis)
153
178
  salesAnalysisTool,
154
179
  productPerformanceTool,
155
180
  salesStoreComparisonTool,
181
+ itemSalesDetailTool,
156
182
  // Reports
157
183
  managerReportTool,
158
184
  cxpReportTool,
185
+ // Cierre mensual (month-end bank reconciliation closing)
186
+ startMonthClosingTool,
187
+ submitClosingAnswersTool,
188
+ generateClosingJournalTool,
189
+ getClosingQuestionnaireTool,
190
+ getClosingMatchResultsTool,
191
+ reconcileWithBcTool,
192
+ // Financials (P&L / Financial Statements) + Cash Flow (FCF)
193
+ financialStatementsTool,
194
+ cashFlowTool,
159
195
  ],
160
196
  };
161
197
  });
@@ -251,6 +287,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
251
287
  case 'get_open_payables':
252
288
  result = await handleOpenPayables(bcClient, args);
253
289
  break;
290
+ case 'get_customer_list':
291
+ result = await handleCustomerList(bcClient, args);
292
+ break;
254
293
  // Multi-Payment draft visibility
255
294
  case 'get_draft_receivables':
256
295
  result = await handleDraftReceivables(bcClient, args);
@@ -284,6 +323,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
284
323
  case 'get_item_cost_trend':
285
324
  result = await handleItemCostTrend(bcClient, args);
286
325
  break;
326
+ case 'get_inventory_levels':
327
+ result = await handleInventoryLevels(bcClient, args);
328
+ break;
329
+ case 'get_inventory_change':
330
+ result = await handleInventoryChange(bcClient, args);
331
+ break;
332
+ case 'get_inventory_by_location':
333
+ result = await handleInventoryByLocation(bcClient, args);
334
+ break;
287
335
  // Ventas (sales analysis)
288
336
  case 'get_sales_analysis':
289
337
  result = await handleSalesAnalysis(bcClient, args);
@@ -294,6 +342,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
294
342
  case 'compare_sales_by_store':
295
343
  result = await handleSalesStoreComparison(bcClient, args);
296
344
  break;
345
+ case 'get_item_sales_detail':
346
+ result = await handleItemSalesDetail(bcClient, args);
347
+ break;
297
348
  // Reports
298
349
  case 'generate_manager_report':
299
350
  result = await handleGenerateManagerReport(bcClient, args);
@@ -301,6 +352,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
301
352
  case 'generate_cxp_report':
302
353
  result = await handleGenerateCxpReport(bcClient, args);
303
354
  break;
355
+ // Cierre mensual (month-end bank reconciliation closing)
356
+ case 'start_month_closing':
357
+ result = await handleStartMonthClosing(bcClient, args);
358
+ break;
359
+ case 'submit_closing_answers':
360
+ result = await handleSubmitClosingAnswers(bcClient, args);
361
+ break;
362
+ case 'generate_closing_journal':
363
+ result = await handleGenerateClosingJournal(bcClient, args);
364
+ break;
365
+ case 'get_closing_questionnaire':
366
+ result = await handleGetClosingQuestionnaire(bcClient, args);
367
+ break;
368
+ case 'get_closing_match_results':
369
+ result = await handleGetClosingMatchResults(bcClient, args);
370
+ break;
371
+ case 'reconcile_closing_with_bc':
372
+ result = await handleReconcileWithBc(bcClient, args);
373
+ break;
374
+ // Financials (P&L / Financial Statements) + Cash Flow (FCF)
375
+ case 'get_financial_statements':
376
+ result = await handleFinancialStatements(bcClient, args);
377
+ break;
378
+ case 'get_cash_flow':
379
+ result = await handleCashFlow(bcClient, args);
380
+ break;
304
381
  default:
305
382
  return {
306
383
  content: [{ type: 'text', text: `Herramienta desconocida: ${name}` }],
@@ -1,4 +1,3 @@
1
- import { DateTime } from 'luxon';
2
1
  import { resolveStores } from '../../config/company-config.js';
3
2
 
4
3
  export const bankLedgerEntriesTool = {
@@ -46,9 +45,9 @@ export async function handleBankLedgerEntries(bcClient, args) {
46
45
  const storeInfo = stores[0];
47
46
 
48
47
  const filterParts = [
49
- `Bank_Account_No eq '${bank_account}'`,
50
- `Posting_Date ge ${date_from}`,
51
- `Posting_Date le ${date_to}`,
48
+ `Bank_Account_No eq '${bcClient._sanitizeOData(bank_account)}'`,
49
+ `Posting_Date ge ${bcClient._sanitizeOData(date_from)}`,
50
+ `Posting_Date le ${bcClient._sanitizeOData(date_to)}`,
52
51
  ];
53
52
  if (openOnly) {
54
53
  filterParts.push('Open eq true');
@@ -60,7 +59,6 @@ export async function handleBankLedgerEntries(bcClient, args) {
60
59
  });
61
60
  const entries = await bcClient.odataCallAllPages(url);
62
61
 
63
- const today = DateTime.now();
64
62
  const formatted = entries.map((e) => {
65
63
  const amount = e.Amount || 0;
66
64
  const postingDate = e.Posting_Date || null;
@@ -83,7 +83,7 @@ export async function handleFindPotentialMatches(bcClient, args) {
83
83
 
84
84
  // Fetch open bank ledger entries within date range
85
85
  const url = bcClient.buildODataUrl(store, 'BankAccountLedgerEntries', {
86
- $filter: `Bank_Account_No eq '${bankAccount}' and Open eq true and Posting_Date ge ${dateFrom} and Posting_Date le ${dateTo}`,
86
+ $filter: `Bank_Account_No eq '${bcClient._sanitizeOData(bankAccount)}' and Open eq true and Posting_Date ge ${bcClient._sanitizeOData(dateFrom)} and Posting_Date le ${bcClient._sanitizeOData(dateTo)}`,
87
87
  $orderby: 'Posting_Date desc',
88
88
  });
89
89
 
@@ -39,7 +39,7 @@ export async function handleGLAccountEntries(bcClient, args) {
39
39
  const storeInfo = stores[0];
40
40
 
41
41
  const url = bcClient.buildApiUrl(storeInfo.companyId, 'generalLedgerEntries', {
42
- $filter: `postingDate ge ${date_from} and postingDate le ${date_to} and accountNumber eq '${gl_account}'`,
42
+ $filter: `postingDate ge ${bcClient._sanitizeOData(date_from)} and postingDate le ${bcClient._sanitizeOData(date_to)} and accountNumber eq '${bcClient._sanitizeOData(gl_account)}'`,
43
43
  $select: 'entryNumber,postingDate,documentNumber,accountNumber,description,debitAmount,creditAmount',
44
44
  $orderby: 'postingDate asc',
45
45
  });