@fullqueso/mcp-bc-gastos 1.14.2 → 1.19.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.
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate FQ28 Pending Invoices Excel — May/Jun/Jul 2025
4
+ 3 tabs: Sin Draft, Con Draft, Pago Parcial
5
+ Each tab lists posted purchase invoice numbers for auditing.
6
+
7
+ Usage: python3 generate-fq28-pending-xlsx.py <input.json> <output.xlsx>
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from datetime import datetime, date
13
+ from pathlib import Path
14
+
15
+ try:
16
+ from openpyxl import Workbook
17
+ from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
18
+ except ImportError:
19
+ print("Error: openpyxl required. Install: pip3 install openpyxl", file=sys.stderr)
20
+ sys.exit(1)
21
+
22
+ # ─── Styles ───
23
+ BOLD = Font(bold=True)
24
+ BOLD_14 = Font(bold=True, size=14)
25
+ BOLD_12 = Font(bold=True, size=12)
26
+ BOLD_11 = Font(bold=True, size=11)
27
+ HEADER_FILL = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
28
+ HEADER_FONT = Font(bold=True, color="FFFFFF", size=10)
29
+ SUBTOTAL_FILL = PatternFill(start_color="D9E2F3", end_color="D9E2F3", fill_type="solid")
30
+ SUBTOTAL_FONT = Font(bold=True, size=10)
31
+ NUM_FMT = '#,##0.00'
32
+
33
+ TIER_FILLS = {
34
+ "fully_pending": PatternFill(start_color="FCE4EC", end_color="FCE4EC", fill_type="solid"),
35
+ "draft_in_progress": PatternFill(start_color="FFF3E0", end_color="FFF3E0", fill_type="solid"),
36
+ "in_journal": PatternFill(start_color="E8F5E9", end_color="E8F5E9", fill_type="solid"),
37
+ }
38
+
39
+ MONTH_NAMES_ES = {
40
+ "01": "Enero", "02": "Febrero", "03": "Marzo", "04": "Abril",
41
+ "05": "Mayo", "06": "Junio", "07": "Julio", "08": "Agosto",
42
+ "09": "Septiembre", "10": "Octubre", "11": "Noviembre", "12": "Diciembre",
43
+ }
44
+
45
+ def month_label(ym):
46
+ """Convert '2025-05' to 'Mayo 2025'."""
47
+ if len(ym) >= 7:
48
+ mm = ym[5:7]
49
+ yyyy = ym[:4]
50
+ return f"{MONTH_NAMES_ES.get(mm, mm)} {yyyy}"
51
+ return ym
52
+
53
+ # Keep MONTH_LABELS as a fallback dict that uses the dynamic function
54
+ class _MonthLabels(dict):
55
+ def get(self, key, default=None):
56
+ return month_label(key) if key and len(key) >= 7 else (default or key)
57
+ def __getitem__(self, key):
58
+ return month_label(key)
59
+ def __contains__(self, key):
60
+ return key is not None and len(key) >= 7
61
+
62
+ MONTH_LABELS = _MonthLabels()
63
+
64
+
65
+ def set_col_widths(ws, widths):
66
+ for col_letter, width in widths.items():
67
+ ws.column_dimensions[col_letter].width = width
68
+
69
+
70
+ def write_header_row(ws, row, headers, start_col=1):
71
+ for i, header in enumerate(headers):
72
+ cell = ws.cell(row=row, column=start_col + i, value=header)
73
+ cell.font = HEADER_FONT
74
+ cell.fill = HEADER_FILL
75
+ cell.alignment = Alignment(horizontal="center")
76
+
77
+
78
+ def days_since(date_str, today):
79
+ try:
80
+ d = datetime.strptime(date_str, "%Y-%m-%d").date()
81
+ return max(0, (today - d).days)
82
+ except (ValueError, TypeError):
83
+ return 0
84
+
85
+
86
+ def get_month(date_str):
87
+ return date_str[:7] if date_str and len(date_str) >= 7 else ""
88
+
89
+
90
+ # ─── Tab: Sin Draft ───
91
+ def build_sin_draft(wb, invoices, today, ctx=None):
92
+ ctx = ctx or {}
93
+ ws = wb.active
94
+ ws.title = "Sin Draft"
95
+ tier_invs = [i for i in invoices if i.get("tier") == "fully_pending"]
96
+ tier_invs.sort(key=lambda i: i.get("posting_date", ""))
97
+ fill = TIER_FILLS["fully_pending"]
98
+
99
+ set_col_widths(ws, {"A": 20, "B": 16, "C": 16, "D": 14, "E": 35, "F": 16, "G": 14})
100
+
101
+ total_amount = sum(i.get("invoice_amount", 0) for i in tier_invs)
102
+ title = f"SIN DRAFT — {ctx.get('store','')} {ctx.get('store_name','')} — {ctx.get('period','')}"
103
+ ws.cell(row=1, column=1, value=title).font = BOLD_14
104
+ ws.cell(row=2, column=1, value=f"{len(tier_invs)} facturas pendientes sin ningún draft de pago | Total: ${total_amount:,.2f} USD").font = BOLD_11
105
+
106
+ headers = ["No. Factura (Posted PI)", "Mes", "Fecha Posting", "No. Proveedor", "Proveedor", "Monto (USD)", "Días Pendiente"]
107
+ write_header_row(ws, 4, headers)
108
+ ws.freeze_panes = "A5"
109
+
110
+ row = 5
111
+ current_month = None
112
+ month_start_row = row
113
+
114
+ for inv in tier_invs:
115
+ month = get_month(inv.get("posting_date", ""))
116
+ if current_month and month != current_month:
117
+ # Month subtotal
118
+ ws.cell(row=row, column=1, value=f"Subtotal {MONTH_LABELS.get(current_month, current_month)}").font = SUBTOTAL_FONT
119
+ ws.cell(row=row, column=6, value=f"=SUM(F{month_start_row}:F{row - 1})").number_format = NUM_FMT
120
+ ws.cell(row=row, column=6).font = SUBTOTAL_FONT
121
+ for c in range(1, 8):
122
+ ws.cell(row=row, column=c).fill = SUBTOTAL_FILL
123
+ row += 1
124
+ month_start_row = row
125
+
126
+ current_month = month
127
+
128
+ ws.cell(row=row, column=1, value=inv.get("invoice_no", ""))
129
+ ws.cell(row=row, column=2, value=MONTH_LABELS.get(month, month))
130
+ ws.cell(row=row, column=3, value=inv.get("posting_date", ""))
131
+ ws.cell(row=row, column=4, value=inv.get("vendor_no", ""))
132
+ ws.cell(row=row, column=5, value=inv.get("vendor_name", ""))
133
+ ws.cell(row=row, column=6, value=inv.get("invoice_amount", 0)).number_format = NUM_FMT
134
+ ws.cell(row=row, column=7, value=days_since(inv.get("posting_date", ""), today))
135
+ for c in range(1, 8):
136
+ ws.cell(row=row, column=c).fill = fill
137
+ row += 1
138
+
139
+ # Last month subtotal
140
+ if current_month and tier_invs:
141
+ ws.cell(row=row, column=1, value=f"Subtotal {MONTH_LABELS.get(current_month, current_month)}").font = SUBTOTAL_FONT
142
+ ws.cell(row=row, column=6, value=f"=SUM(F{month_start_row}:F{row - 1})").number_format = NUM_FMT
143
+ ws.cell(row=row, column=6).font = SUBTOTAL_FONT
144
+ for c in range(1, 8):
145
+ ws.cell(row=row, column=c).fill = SUBTOTAL_FILL
146
+ row += 1
147
+
148
+ # Grand total
149
+ if tier_invs:
150
+ row += 1
151
+ ws.cell(row=row, column=1, value="TOTAL SIN DRAFT").font = BOLD_12
152
+ ws.cell(row=row, column=5, value=f"{len(tier_invs)} facturas").font = BOLD
153
+ ws.cell(row=row, column=6, value=round(total_amount, 2)).number_format = NUM_FMT
154
+ ws.cell(row=row, column=6).font = BOLD_12
155
+
156
+ return len(tier_invs)
157
+
158
+
159
+ # ─── Tab: Con Draft ───
160
+ def build_con_draft(wb, invoices, today, ctx=None):
161
+ ctx = ctx or {}
162
+ ws = wb.create_sheet("Con Draft")
163
+ tier_invs = [i for i in invoices if i.get("tier") == "draft_in_progress"]
164
+ tier_invs.sort(key=lambda i: i.get("posting_date", ""))
165
+ fill = TIER_FILLS["draft_in_progress"]
166
+
167
+ set_col_widths(ws, {"A": 20, "B": 16, "C": 16, "D": 14, "E": 35, "F": 16, "G": 18, "H": 14, "I": 16, "J": 16})
168
+
169
+ total_amount = sum(i.get("invoice_amount", 0) for i in tier_invs)
170
+ total_drafted = sum((i.get("multi_payment") or {}).get("total_payment_amount", 0) for i in tier_invs)
171
+ title = f"CON DRAFT (No Posteado) — {ctx.get('store','')} {ctx.get('store_name','')} — {ctx.get('period','')}"
172
+ ws.cell(row=1, column=1, value=title).font = BOLD_14
173
+ ws.cell(row=2, column=1, value=f"{len(tier_invs)} facturas con draft de pago creado | Total: ${total_amount:,.2f} USD | Drafted: ${total_drafted:,.2f} USD").font = BOLD_11
174
+
175
+ headers = ["No. Factura (Posted PI)", "Mes", "Fecha Posting", "No. Proveedor", "Proveedor", "Monto (USD)", "No. Multi-Payment", "Status MP", "Drafted (USD)", "Pendiente Neto"]
176
+ write_header_row(ws, 4, headers)
177
+ ws.freeze_panes = "A5"
178
+
179
+ row = 5
180
+ current_month = None
181
+ month_start_row = row
182
+
183
+ for inv in tier_invs:
184
+ month = get_month(inv.get("posting_date", ""))
185
+ mp = inv.get("multi_payment") or {}
186
+
187
+ if current_month and month != current_month:
188
+ ws.cell(row=row, column=1, value=f"Subtotal {MONTH_LABELS.get(current_month, current_month)}").font = SUBTOTAL_FONT
189
+ ws.cell(row=row, column=6, value=f"=SUM(F{month_start_row}:F{row - 1})").number_format = NUM_FMT
190
+ ws.cell(row=row, column=6).font = SUBTOTAL_FONT
191
+ ws.cell(row=row, column=9, value=f"=SUM(I{month_start_row}:I{row - 1})").number_format = NUM_FMT
192
+ ws.cell(row=row, column=9).font = SUBTOTAL_FONT
193
+ ws.cell(row=row, column=10, value=f"=SUM(J{month_start_row}:J{row - 1})").number_format = NUM_FMT
194
+ ws.cell(row=row, column=10).font = SUBTOTAL_FONT
195
+ for c in range(1, 11):
196
+ ws.cell(row=row, column=c).fill = SUBTOTAL_FILL
197
+ row += 1
198
+ month_start_row = row
199
+
200
+ current_month = month
201
+
202
+ ws.cell(row=row, column=1, value=inv.get("invoice_no", ""))
203
+ ws.cell(row=row, column=2, value=MONTH_LABELS.get(month, month))
204
+ ws.cell(row=row, column=3, value=inv.get("posting_date", ""))
205
+ ws.cell(row=row, column=4, value=inv.get("vendor_no", ""))
206
+ ws.cell(row=row, column=5, value=inv.get("vendor_name", ""))
207
+ ws.cell(row=row, column=6, value=inv.get("invoice_amount", 0)).number_format = NUM_FMT
208
+ ws.cell(row=row, column=7, value=mp.get("mp_no", ""))
209
+ ws.cell(row=row, column=8, value=mp.get("status", ""))
210
+ ws.cell(row=row, column=9, value=mp.get("total_payment_amount", 0)).number_format = NUM_FMT
211
+ ws.cell(row=row, column=10, value=mp.get("net_pending", inv.get("invoice_amount", 0))).number_format = NUM_FMT
212
+ for c in range(1, 11):
213
+ ws.cell(row=row, column=c).fill = fill
214
+ row += 1
215
+
216
+ # Last month subtotal
217
+ if current_month and tier_invs:
218
+ ws.cell(row=row, column=1, value=f"Subtotal {MONTH_LABELS.get(current_month, current_month)}").font = SUBTOTAL_FONT
219
+ ws.cell(row=row, column=6, value=f"=SUM(F{month_start_row}:F{row - 1})").number_format = NUM_FMT
220
+ ws.cell(row=row, column=6).font = SUBTOTAL_FONT
221
+ ws.cell(row=row, column=9, value=f"=SUM(I{month_start_row}:I{row - 1})").number_format = NUM_FMT
222
+ ws.cell(row=row, column=9).font = SUBTOTAL_FONT
223
+ ws.cell(row=row, column=10, value=f"=SUM(J{month_start_row}:J{row - 1})").number_format = NUM_FMT
224
+ ws.cell(row=row, column=10).font = SUBTOTAL_FONT
225
+ for c in range(1, 11):
226
+ ws.cell(row=row, column=c).fill = SUBTOTAL_FILL
227
+ row += 1
228
+
229
+ # Grand total
230
+ if tier_invs:
231
+ row += 1
232
+ ws.cell(row=row, column=1, value="TOTAL CON DRAFT").font = BOLD_12
233
+ ws.cell(row=row, column=5, value=f"{len(tier_invs)} facturas").font = BOLD
234
+ ws.cell(row=row, column=6, value=round(total_amount, 2)).number_format = NUM_FMT
235
+ ws.cell(row=row, column=6).font = BOLD_12
236
+ ws.cell(row=row, column=9, value=round(total_drafted, 2)).number_format = NUM_FMT
237
+ ws.cell(row=row, column=9).font = BOLD_12
238
+
239
+ return len(tier_invs)
240
+
241
+
242
+ # ─── Tab: Pago Parcial ───
243
+ def build_pago_parcial(wb, invoices, today, ctx=None):
244
+ ctx = ctx or {}
245
+ ws = wb.create_sheet("Pago Parcial")
246
+ tier_invs = [i for i in invoices if i.get("tier") == "in_journal"]
247
+ tier_invs.sort(key=lambda i: i.get("posting_date", ""))
248
+ fill = TIER_FILLS["in_journal"]
249
+
250
+ set_col_widths(ws, {"A": 20, "B": 16, "C": 16, "D": 14, "E": 35, "F": 16, "G": 18, "H": 14, "I": 16, "J": 16})
251
+
252
+ total_amount = sum(i.get("invoice_amount", 0) for i in tier_invs)
253
+ total_drafted = sum((i.get("multi_payment") or {}).get("total_payment_amount", 0) for i in tier_invs)
254
+ total_net = sum((i.get("multi_payment") or {}).get("net_pending", 0) for i in tier_invs)
255
+ title = f"PAGO PARCIAL (Transferred) — {ctx.get('store','')} {ctx.get('store_name','')} — {ctx.get('period','')}"
256
+ ws.cell(row=1, column=1, value=title).font = BOLD_14
257
+ ws.cell(row=2, column=1, value=f"{len(tier_invs)} facturas con pago parcial | Total: ${total_amount:,.2f} USD | Pagado: ${total_drafted:,.2f} | Pendiente: ${total_net:,.2f}").font = BOLD_11
258
+
259
+ headers = ["No. Factura (Posted PI)", "Mes", "Fecha Posting", "No. Proveedor", "Proveedor", "Monto (USD)", "No. Multi-Payment", "Status MP", "Pagado (USD)", "Pendiente Neto"]
260
+ write_header_row(ws, 4, headers)
261
+ ws.freeze_panes = "A5"
262
+
263
+ row = 5
264
+ current_month = None
265
+ month_start_row = row
266
+
267
+ for inv in tier_invs:
268
+ month = get_month(inv.get("posting_date", ""))
269
+ mp = inv.get("multi_payment") or {}
270
+
271
+ if current_month and month != current_month:
272
+ ws.cell(row=row, column=1, value=f"Subtotal {MONTH_LABELS.get(current_month, current_month)}").font = SUBTOTAL_FONT
273
+ ws.cell(row=row, column=6, value=f"=SUM(F{month_start_row}:F{row - 1})").number_format = NUM_FMT
274
+ ws.cell(row=row, column=6).font = SUBTOTAL_FONT
275
+ ws.cell(row=row, column=9, value=f"=SUM(I{month_start_row}:I{row - 1})").number_format = NUM_FMT
276
+ ws.cell(row=row, column=9).font = SUBTOTAL_FONT
277
+ ws.cell(row=row, column=10, value=f"=SUM(J{month_start_row}:J{row - 1})").number_format = NUM_FMT
278
+ ws.cell(row=row, column=10).font = SUBTOTAL_FONT
279
+ for c in range(1, 11):
280
+ ws.cell(row=row, column=c).fill = SUBTOTAL_FILL
281
+ row += 1
282
+ month_start_row = row
283
+
284
+ current_month = month
285
+
286
+ ws.cell(row=row, column=1, value=inv.get("invoice_no", ""))
287
+ ws.cell(row=row, column=2, value=MONTH_LABELS.get(month, month))
288
+ ws.cell(row=row, column=3, value=inv.get("posting_date", ""))
289
+ ws.cell(row=row, column=4, value=inv.get("vendor_no", ""))
290
+ ws.cell(row=row, column=5, value=inv.get("vendor_name", ""))
291
+ ws.cell(row=row, column=6, value=inv.get("invoice_amount", 0)).number_format = NUM_FMT
292
+ ws.cell(row=row, column=7, value=mp.get("mp_no", ""))
293
+ ws.cell(row=row, column=8, value=mp.get("status", ""))
294
+ ws.cell(row=row, column=9, value=mp.get("total_payment_amount", 0)).number_format = NUM_FMT
295
+ ws.cell(row=row, column=10, value=mp.get("net_pending", inv.get("invoice_amount", 0))).number_format = NUM_FMT
296
+ for c in range(1, 11):
297
+ ws.cell(row=row, column=c).fill = fill
298
+ row += 1
299
+
300
+ # Last month subtotal
301
+ if current_month and tier_invs:
302
+ ws.cell(row=row, column=1, value=f"Subtotal {MONTH_LABELS.get(current_month, current_month)}").font = SUBTOTAL_FONT
303
+ ws.cell(row=row, column=6, value=f"=SUM(F{month_start_row}:F{row - 1})").number_format = NUM_FMT
304
+ ws.cell(row=row, column=6).font = SUBTOTAL_FONT
305
+ ws.cell(row=row, column=9, value=f"=SUM(I{month_start_row}:I{row - 1})").number_format = NUM_FMT
306
+ ws.cell(row=row, column=9).font = SUBTOTAL_FONT
307
+ ws.cell(row=row, column=10, value=f"=SUM(J{month_start_row}:J{row - 1})").number_format = NUM_FMT
308
+ ws.cell(row=row, column=10).font = SUBTOTAL_FONT
309
+ for c in range(1, 11):
310
+ ws.cell(row=row, column=c).fill = SUBTOTAL_FILL
311
+ row += 1
312
+
313
+ # Grand total
314
+ if tier_invs:
315
+ row += 1
316
+ ws.cell(row=row, column=1, value="TOTAL PAGO PARCIAL").font = BOLD_12
317
+ ws.cell(row=row, column=5, value=f"{len(tier_invs)} facturas").font = BOLD
318
+ ws.cell(row=row, column=6, value=round(total_amount, 2)).number_format = NUM_FMT
319
+ ws.cell(row=row, column=6).font = BOLD_12
320
+ ws.cell(row=row, column=9, value=round(total_drafted, 2)).number_format = NUM_FMT
321
+ ws.cell(row=row, column=9).font = BOLD_12
322
+ ws.cell(row=row, column=10, value=round(total_net, 2)).number_format = NUM_FMT
323
+ ws.cell(row=row, column=10).font = BOLD_12
324
+
325
+ return len(tier_invs)
326
+
327
+
328
+ # ─── Main ───
329
+ def main():
330
+ if len(sys.argv) < 3:
331
+ print("Usage: python3 generate-fq28-pending-xlsx.py <input.json> <output.xlsx>", file=sys.stderr)
332
+ sys.exit(1)
333
+
334
+ input_path = Path(sys.argv[1])
335
+ output_path = Path(sys.argv[2])
336
+
337
+ if not input_path.exists():
338
+ print(f"Error: Input not found: {input_path}", file=sys.stderr)
339
+ sys.exit(1)
340
+
341
+ with open(input_path, "r", encoding="utf-8") as f:
342
+ data = json.load(f)
343
+
344
+ invoices = data.get("invoices", [])
345
+ today = date.today()
346
+ if data.get("report_date"):
347
+ try:
348
+ today = datetime.strptime(data["report_date"], "%Y-%m-%d").date()
349
+ except ValueError:
350
+ pass
351
+
352
+ store = data.get("store", "")
353
+ store_name = data.get("store_name", store)
354
+ dr = data.get("date_range", {})
355
+ period = ""
356
+ if dr.get("start") and dr.get("end"):
357
+ s_label = month_label(dr["start"][:7])
358
+ e_label = month_label(dr["end"][:7])
359
+ period = f"{s_label}" if s_label == e_label else f"{s_label} — {e_label}"
360
+
361
+ header_ctx = {"store": store, "store_name": store_name, "period": period}
362
+
363
+ wb = Workbook()
364
+ n_sin = build_sin_draft(wb, invoices, today, header_ctx)
365
+ n_draft = build_con_draft(wb, invoices, today, header_ctx)
366
+ n_parcial = build_pago_parcial(wb, invoices, today, header_ctx)
367
+
368
+ wb.save(str(output_path))
369
+ result = {
370
+ "file": str(output_path),
371
+ "sheets": {"sin_draft": n_sin, "con_draft": n_draft, "pago_parcial": n_parcial},
372
+ "total": len(invoices),
373
+ }
374
+ print(json.dumps(result))
375
+
376
+
377
+ if __name__ == "__main__":
378
+ main()