@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.
- package/CHANGELOG.md +137 -0
- package/config/bank-gl-map.json +93 -0
- package/lib/bc-client.js +143 -0
- package/package.json +1 -1
- package/scripts/generate-pending-invoices-xlsx.py +378 -0
- package/scripts/generate-pending-sales-invoices-xlsx.py +372 -0
- package/scripts/report-pending-invoices.js +250 -0
- package/scripts/report-pending-sales-invoices.js +253 -0
- package/server.js +61 -0
- package/tools/auditoria/bank-ledger-entries.js +104 -0
- package/tools/auditoria/gl-account-entries.js +75 -0
- package/tools/auditoria/pm-receipts.js +182 -0
- package/tools/auditoria/reconcile-pos-sales.js +39 -16
- package/tools/cobranzas/customer-list.js +63 -0
- package/tools/inventario/index.js +4 -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-cost-trend.js +185 -0
- package/tools/inventario/shared/cost-calculator.js +64 -0
- package/tools/ventas/index.js +3 -0
- package/tools/ventas/product-performance.js +182 -0
- package/tools/ventas/sales-analysis.js +211 -0
- package/tools/ventas/sales-store-comparison.js +192 -0
- package/utils/sales-aggregation.js +82 -0
- package/utils/sales-insights.js +167 -0
|
@@ -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()
|