@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
|
@@ -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
|
});
|