Update fill_template.py
Browse files- fill_template.py +445 -11
fill_template.py
CHANGED
|
@@ -232,6 +232,87 @@ STOPWORDS = {
|
|
| 232 |
|
| 233 |
PERMITIDAS_CORTAS = {'Y', 'DE', 'LA', 'EL', 'SA', 'CV', 'RL', 'SC', 'AC', 'S'}
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
def _con_sustancia(r):
|
| 237 |
return any(len(t) >= 3 and t not in PERMITIDAS_CORTAS for t in r)
|
|
@@ -272,6 +353,14 @@ def extraer_nombre(concepto, tipo=''):
|
|
| 272 |
"""
|
| 273 |
raw = quitar_acentos(str(concepto or '').upper())
|
| 274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
# 1) contraparte explicita por NOMBRE (Santander/Banorte SPEI: 'DEL CLIENTE X',
|
| 276 |
# 'AL CLIENTE X', 'A FAVOR DE X').
|
| 277 |
m = re.search(r'\b(?:DEL CLIENTE|AL CLIENTE|A FAVOR DE)\s+(.*)$', raw)
|
|
@@ -311,6 +400,18 @@ def extraer_nombre(concepto, tipo=''):
|
|
| 311 |
if cand:
|
| 312 |
return cand
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
# 3) heuristica general: ultima corrida con 2+ palabras (si no, la mas larga)
|
| 315 |
s = raw
|
| 316 |
for b in BANCOS_TOKENS:
|
|
@@ -330,7 +431,10 @@ def extraer_nombre(concepto, tipo=''):
|
|
| 330 |
runs.append(run)
|
| 331 |
runs = [r for r in runs if _con_sustancia(r)]
|
| 332 |
if not runs:
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
| 334 |
candidata = None
|
| 335 |
for r in reversed(runs):
|
| 336 |
if len(r) >= 2:
|
|
@@ -339,7 +443,8 @@ def extraer_nombre(concepto, tipo=''):
|
|
| 339 |
if candidata is None:
|
| 340 |
candidata = max(runs, key=len)
|
| 341 |
candidata = candidata[-6:]
|
| 342 |
-
return ' '.join(candidata)[:60].strip()
|
|
|
|
| 343 |
|
| 344 |
|
| 345 |
# ---------------------------------------------------------------- principal
|
|
@@ -402,10 +507,33 @@ def fill(template_path, output_path, data):
|
|
| 402 |
for t in txs:
|
| 403 |
if t[clave_monto] > 0:
|
| 404 |
tot[t['nombre']] = tot.get(t['nombre'], 0.0) + t[clave_monto]
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
mapa_dep = _mapa_nombres('deposito', 'OTROS DEPOSITANTES')
|
| 411 |
mapa_ret = _mapa_nombres('retiro', 'OTROS BENEFICIARIOS')
|
|
@@ -764,19 +892,325 @@ def fill(template_path, output_path, data):
|
|
| 764 |
}
|
| 765 |
|
| 766 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
def main():
|
| 768 |
if len(sys.argv) < 2:
|
| 769 |
-
print(json.dumps({"error": "Uso: fill_template.py <output_path> [template_path]"}))
|
| 770 |
sys.exit(1)
|
| 771 |
-
|
| 772 |
-
|
|
|
|
|
|
|
| 773 |
try:
|
| 774 |
data = json.load(sys.stdin)
|
| 775 |
except Exception as e:
|
| 776 |
print(json.dumps({"error": "JSON invalido por stdin: %s" % e}))
|
| 777 |
sys.exit(1)
|
| 778 |
try:
|
| 779 |
-
result = fill(template_path, output_path, data)
|
| 780 |
print(json.dumps(result, ensure_ascii=False))
|
| 781 |
except Exception as e:
|
| 782 |
import traceback
|
|
@@ -785,4 +1219,4 @@ def main():
|
|
| 785 |
|
| 786 |
|
| 787 |
if __name__ == '__main__':
|
| 788 |
-
main()
|
|
|
|
| 232 |
|
| 233 |
PERMITIDAS_CORTAS = {'Y', 'DE', 'LA', 'EL', 'SA', 'CV', 'RL', 'SC', 'AC', 'S'}
|
| 234 |
|
| 235 |
+
# --- Movimientos SIN contraparte: se resumen por su NATURALEZA, no por nombre ---
|
| 236 |
+
# El usuario pide que todo lo que aluda a efectivo diga 'EFECTIVO' (antes caia en
|
| 237 |
+
# 'SIN IDENTIFICAR' porque EFECTIVO/DEPOSITO/CAJERO estan en STOPWORDS: en
|
| 238 |
+
# SANTANDER eran 63 movimientos, entre ellos 'DEPOSITO EN EFECTIVO' x36).
|
| 239 |
+
EFECTIVO_RE = re.compile(
|
| 240 |
+
r'RETIRO\s+SIN\s+TARJETA|SIN\s+TARJETA|EN\s+EFECTIVO|DE\s+EFECTIVO|'
|
| 241 |
+
r'\bEFECTIVO\b|DISPOSICION\s+(?:DE\s+)?EFECTIVO|RETIRO\s+CAJERO|'
|
| 242 |
+
r'\bCAJERO\b|\bATM\b|\bPRACTICAJA\b|PRACTIC\b|\bVENTANILLA\b|'
|
| 243 |
+
r'RETIRO\s+EN\s+SUCURSAL|DEP[O0]SITO\s+SUCURSAL', re.I)
|
| 244 |
+
# Otros movimientos que tampoco tienen contraparte (son del propio banco/SAT).
|
| 245 |
+
# El orden importa: se evalua de arriba hacia abajo.
|
| 246 |
+
NATURALEZA = [
|
| 247 |
+
('EFECTIVO', EFECTIVO_RE),
|
| 248 |
+
('ISR', re.compile(r'\bI\s*S\s*R\b|IMPUESTO\s+SOBRE\s+LA\s+RENTA|RETENCION\s+ISR', re.I)),
|
| 249 |
+
('IVA', re.compile(r'\bI\s*V\s*A\b|\bVV\s+AA\b', re.I)),
|
| 250 |
+
('COMISIONES', re.compile(r'COMISION|MEMBRESIA|ANUALIDAD|MANEJO\s+DE\s+CUENTA', re.I)),
|
| 251 |
+
('INTERESES', re.compile(r'\bINTERES(?:ES)?\b|RENDIMIENTO(?:S)?\s+PAGADO', re.I)),
|
| 252 |
+
('TRASPASO PROPIO', re.compile(r'MISMO\s+TITULAR|TRASPASO\s+ENTRE\s+CUENTAS|CUENTA\s+PROPIA', re.I)),
|
| 253 |
+
]
|
| 254 |
+
# OJO: aqui NO van 'PAGO DE SERVICIOS' ni 'DOMICILIACION'. Esos SI tienen
|
| 255 |
+
# contraparte (CFE, TELMEX, la aseguradora...) y el usuario quiere saber DE QUIEN
|
| 256 |
+
# fue el movimiento: colapsarlos en una etiqueta generica escondia 128 y 63
|
| 257 |
+
# movimientos de SANTANDER bajo un mismo nombre.
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _naturaleza(raw):
|
| 261 |
+
"""Etiqueta corta para movimientos sin contraparte. '' si no aplica."""
|
| 262 |
+
for etiqueta, rx in NATURALEZA:
|
| 263 |
+
if rx.search(raw):
|
| 264 |
+
return etiqueta
|
| 265 |
+
return ''
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _ultimo_recurso(raw):
|
| 269 |
+
"""Etiqueta para movimientos donde el banco NO da contraparte. Se aplica solo
|
| 270 |
+
DESPUES de que fallaron todas las reglas de nombre, para que un nombre real
|
| 271 |
+
siempre gane. Casos medidos en BBVA (60 de 686 caian en 'SIN IDENTIFICAR')."""
|
| 272 |
+
# Pago de impuestos: el SAT es la contraparte, pero 'S.A.T.' se pierde al
|
| 273 |
+
# quitar la puntuacion (queda 'S A T', tres tokens de 1 letra).
|
| 274 |
+
if re.search(r'\bS\.A\.T\.?|\bSAT\b|SERVICIO\s+DE\s+ADMINISTRACION\s+TRIBUTARIA', raw):
|
| 275 |
+
return 'SAT'
|
| 276 |
+
# Pago a la tarjeta de credito del propio cliente: no hay tercero. (22 casos)
|
| 277 |
+
if re.search(r'PAGO\s+TARJETA\s+DE\s+CREDITO|PAGO\s+A\s+TARJETA', raw):
|
| 278 |
+
return 'TARJETA DE CREDITO'
|
| 279 |
+
# Cobro automatico de un prestamo: el numero de recibo identifica el credito,
|
| 280 |
+
# asi que se conserva (se repite mes con mes y agrupa solo).
|
| 281 |
+
m = re.search(r'RECIBO\s+PREST\.?\s*(\d{6,})', raw)
|
| 282 |
+
if m:
|
| 283 |
+
return 'PRESTAMO ' + m.group(1)
|
| 284 |
+
m = re.search(r'\bPREST\w*\s+(\d{6,})', raw)
|
| 285 |
+
if m:
|
| 286 |
+
return 'PRESTAMO ' + m.group(1)
|
| 287 |
+
# BBVA 'PAGO CUENTA DE TERCERO BNET <ref> pago': el ordenante no escribio
|
| 288 |
+
# nombre. La referencia BNET es del movimiento, no del tercero, asi que usarla
|
| 289 |
+
# crearia un nombre distinto por operacion; se agrupan en una sola etiqueta.
|
| 290 |
+
if re.search(r'CUENTA\s+DE\s+TERCERO', raw):
|
| 291 |
+
return 'TERCERO SIN NOMBRE'
|
| 292 |
+
# Si al menos se sabe el banco contraparte, decirlo es mejor que no decir nada.
|
| 293 |
+
for b in BANCOS_TOKENS:
|
| 294 |
+
if b in raw:
|
| 295 |
+
return b
|
| 296 |
+
return ''
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _resumen_crudo(raw):
|
| 300 |
+
"""Ultimo recurso: resume el concepto en algo LEGIBLE en vez de devolver
|
| 301 |
+
'SIN IDENTIFICAR'. Quita bancos, digitos y puntuacion, tira las stopwords y
|
| 302 |
+
se queda con las palabras con sustancia. Solo devuelve '' si de verdad no
|
| 303 |
+
quedo nada (concepto vacio o puros numeros)."""
|
| 304 |
+
s = raw
|
| 305 |
+
for b in BANCOS_TOKENS:
|
| 306 |
+
s = s.replace(b, ' ')
|
| 307 |
+
s = re.sub(r'[^A-ZÑ&\s]', ' ', s)
|
| 308 |
+
toks, vistos = [], set()
|
| 309 |
+
for t in s.split():
|
| 310 |
+
if len(t) < 3 or t in STOPWORDS or t in vistos:
|
| 311 |
+
continue
|
| 312 |
+
vistos.add(t)
|
| 313 |
+
toks.append(t)
|
| 314 |
+
return ' '.join(toks[:6])[:60].strip()
|
| 315 |
+
|
| 316 |
|
| 317 |
def _con_sustancia(r):
|
| 318 |
return any(len(t) >= 3 and t not in PERMITIDAS_CORTAS for t in r)
|
|
|
|
| 353 |
"""
|
| 354 |
raw = quitar_acentos(str(concepto or '').upper())
|
| 355 |
|
| 356 |
+
# 0) Movimientos SIN contraparte (efectivo, comisiones, IVA/ISR, intereses,
|
| 357 |
+
# servicios): se resumen por su NATURALEZA. Va PRIMERO porque un
|
| 358 |
+
# 'RETIRO SIN TARJETA' o un 'DEPOSITO EN EFECTIVO' no tienen de quien
|
| 359 |
+
# sacar un nombre y antes terminaban en 'SIN IDENTIFICAR'.
|
| 360 |
+
nat = _naturaleza(raw)
|
| 361 |
+
if nat:
|
| 362 |
+
return nat
|
| 363 |
+
|
| 364 |
# 1) contraparte explicita por NOMBRE (Santander/Banorte SPEI: 'DEL CLIENTE X',
|
| 365 |
# 'AL CLIENTE X', 'A FAVOR DE X').
|
| 366 |
m = re.search(r'\b(?:DEL CLIENTE|AL CLIENTE|A FAVOR DE)\s+(.*)$', raw)
|
|
|
|
| 400 |
if cand:
|
| 401 |
return cand
|
| 402 |
|
| 403 |
+
# 6) SPEI generico: el banco imprime primero las REFERENCIAS (clave de rastreo,
|
| 404 |
+
# CLABE, folio: bloques largos de digitos) y AL FINAL el nombre de la
|
| 405 |
+
# contraparte. Se toma el texto que sigue al ULTIMO bloque de 10+ digitos.
|
| 406 |
+
# Ej: 'SPEI RECIBIDOMULTIVA BANC 0000121PAGO HONORARIOS LOMAS
|
| 407 |
+
# 00132180000009445645 26021595130297 ARUAL MEDICINA DE REANIMACION'
|
| 408 |
+
# -> 'ARUAL MEDICINA DE REANIMACION'.
|
| 409 |
+
if 'SPEI' in raw or 'TRANSFERENCIA' in raw:
|
| 410 |
+
cola = re.split(r'\d{10,}', raw)[-1]
|
| 411 |
+
cand = _primer_nombre(cola)
|
| 412 |
+
if cand and len(cand.split()) >= 2:
|
| 413 |
+
return cand
|
| 414 |
+
|
| 415 |
# 3) heuristica general: ultima corrida con 2+ palabras (si no, la mas larga)
|
| 416 |
s = raw
|
| 417 |
for b in BANCOS_TOKENS:
|
|
|
|
| 431 |
runs.append(run)
|
| 432 |
runs = [r for r in runs if _con_sustancia(r)]
|
| 433 |
if not runs:
|
| 434 |
+
# Antes se devolvia 'SIN IDENTIFICAR' aunque el concepto TUVIERA texto util
|
| 435 |
+
# (el Sheet lo mostraba bien y la maqueta no). Ahora solo se rinde si de
|
| 436 |
+
# verdad no quedo nada legible.
|
| 437 |
+
return _resumen_crudo(raw) or _ultimo_recurso(raw) or 'SIN IDENTIFICAR'
|
| 438 |
candidata = None
|
| 439 |
for r in reversed(runs):
|
| 440 |
if len(r) >= 2:
|
|
|
|
| 443 |
if candidata is None:
|
| 444 |
candidata = max(runs, key=len)
|
| 445 |
candidata = candidata[-6:]
|
| 446 |
+
return (' '.join(candidata)[:60].strip()
|
| 447 |
+
or _resumen_crudo(raw) or _ultimo_recurso(raw) or 'SIN IDENTIFICAR')
|
| 448 |
|
| 449 |
|
| 450 |
# ---------------------------------------------------------------- principal
|
|
|
|
| 507 |
for t in txs:
|
| 508 |
if t[clave_monto] > 0:
|
| 509 |
tot[t['nombre']] = tot.get(t['nombre'], 0.0) + t[clave_monto]
|
| 510 |
+
|
| 511 |
+
# --- Fusion de variantes del MISMO nombre -------------------------------
|
| 512 |
+
# El PDF corta la razon social en distinto punto segun cuanto texto la
|
| 513 |
+
# precede, asi que el mismo cliente aparece como 'EXPORTACIONES TEXTIL' y
|
| 514 |
+
# 'EXPORTACIONES TEXTILES MEXICANAS'. Se fusionan solo cuando la clave
|
| 515 |
+
# canonica de uno es PREFIJO de la del otro y mide >= 12 caracteres: es
|
| 516 |
+
# conservador a proposito (fusionar de mas atribuiria dinero a la
|
| 517 |
+
# contraparte equivocada, que es peor que dejar dos filas separadas).
|
| 518 |
+
canon = {n: re.sub(r'[^A-Z0-9]', '', quitar_acentos(n).upper()) for n in tot}
|
| 519 |
+
rep = {}
|
| 520 |
+
for n in sorted(tot, key=lambda x: (-len(canon[x]), x)): # del mas largo al mas corto
|
| 521 |
+
cn = canon[n]
|
| 522 |
+
destino = n
|
| 523 |
+
if len(cn) >= 12:
|
| 524 |
+
for m in rep:
|
| 525 |
+
if canon[m].startswith(cn) or cn.startswith(canon[m]):
|
| 526 |
+
destino = rep[m]
|
| 527 |
+
break
|
| 528 |
+
rep[n] = destino
|
| 529 |
+
fusionado = {}
|
| 530 |
+
for n, dest in rep.items():
|
| 531 |
+
fusionado[dest] = fusionado.get(dest, 0.0) + tot[n]
|
| 532 |
+
|
| 533 |
+
if len(fusionado) <= DESGLOSE_CAPACIDAD:
|
| 534 |
+
return {n: rep[n] for n in tot}
|
| 535 |
+
principales = set(sorted(fusionado, key=lambda n: -fusionado[n])[:DESGLOSE_CAPACIDAD - 1])
|
| 536 |
+
return {n: (rep[n] if rep[n] in principales else etiqueta_otros) for n in tot}
|
| 537 |
|
| 538 |
mapa_dep = _mapa_nombres('deposito', 'OTROS DEPOSITANTES')
|
| 539 |
mapa_ret = _mapa_nombres('retiro', 'OTROS BENEFICIARIOS')
|
|
|
|
| 892 |
}
|
| 893 |
|
| 894 |
|
| 895 |
+
def _hojas_por_nombre(contenido):
|
| 896 |
+
"""{nombre de hoja -> 'xl/worksheets/sheetN.xml'} resuelto por los rels.
|
| 897 |
+
|
| 898 |
+
NO se puede asumir que la hoja N-esima del libro sea sheetN.xml: el orden de
|
| 899 |
+
las pestanas y el numero de archivo son independientes (en la plantilla de
|
| 900 |
+
microempresa '00_VisitaFisica' es sheet7.xml y '07_EC_Raw' es sheet8.xml).
|
| 901 |
+
"""
|
| 902 |
+
wbx = contenido['xl/workbook.xml'].decode('utf-8')
|
| 903 |
+
relsx = contenido['xl/_rels/workbook.xml.rels'].decode('utf-8')
|
| 904 |
+
rels = {}
|
| 905 |
+
for m in re.finditer(r'<Relationship\b[^>]*>', relsx):
|
| 906 |
+
tag = m.group(0)
|
| 907 |
+
rid = re.search(r'Id="([^"]+)"', tag)
|
| 908 |
+
tgt = re.search(r'Target="([^"]+)"', tag)
|
| 909 |
+
if rid and tgt:
|
| 910 |
+
rels[rid.group(1)] = tgt.group(1).lstrip('/')
|
| 911 |
+
hojas = {}
|
| 912 |
+
for m in re.finditer(r'<sheet\b[^>]*>', wbx):
|
| 913 |
+
tag = m.group(0)
|
| 914 |
+
nom = re.search(r'name="([^"]+)"', tag)
|
| 915 |
+
rid = re.search(r'r:id="([^"]+)"', tag)
|
| 916 |
+
if nom and rid and rid.group(1) in rels:
|
| 917 |
+
destino = rels[rid.group(1)]
|
| 918 |
+
if not destino.startswith('xl/'):
|
| 919 |
+
destino = 'xl/' + destino
|
| 920 |
+
hojas[unescape_xml(nom.group(1))] = destino
|
| 921 |
+
return hojas
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
def unescape_xml(s):
|
| 925 |
+
from xml.sax.saxutils import unescape
|
| 926 |
+
return unescape(s, {'"': '"', ''': "'"})
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
# Columnas de 07_EC_Raw en la plantilla de microempresa. Ojo: Categoria(E),
|
| 930 |
+
# Deposito(F) y Retiro(G) son COLUMNAS CALCULADAS de Tabla_EC (derivan de Tipo y
|
| 931 |
+
# Monto), y Mes(I)/Anio(J)/MesAnio(K)/Saldo(M) tambien son formulas. Excel solo
|
| 932 |
+
# las autorrellena al editar de forma interactiva, asi que al escribir el XML hay
|
| 933 |
+
# que emitir la formula en CADA fila o esas columnas quedan vacias y toda la
|
| 934 |
+
# maqueta lee ceros.
|
| 935 |
+
MICRO_RAW_HOJA = '07_EC_Raw'
|
| 936 |
+
MICRO_EC_HOJA = '04_EstadosCuenta'
|
| 937 |
+
MICRO_PORTADA_HOJA = '01_Portada'
|
| 938 |
+
MICRO_F = { # formulas por fila de 07_EC_Raw
|
| 939 |
+
'E': 'IF(Tabla_EC[[#This Row],[Tipo]]="Depósito","Ingreso","Egreso")',
|
| 940 |
+
'F': 'IF(Tabla_EC[[#This Row],[Categoria]]="Ingreso",Tabla_EC[[#This Row],[Monto]],0)',
|
| 941 |
+
'G': 'IF(Tabla_EC[[#This Row],[Categoria]]="Egreso",Tabla_EC[[#This Row],[Monto]],0)',
|
| 942 |
+
'I': 'IF(Tabla_EC[[#This Row],[Fecha]]="","",TEXT(Tabla_EC[[#This Row],[Fecha]], "mmm"))',
|
| 943 |
+
'J': 'IF(Tabla_EC[[#This Row],[Fecha]]="","",YEAR(Tabla_EC[[#This Row],[Fecha]]))',
|
| 944 |
+
'K': ('IF(Tabla_EC[[#This Row],[Fecha]]="","",TEXT(Tabla_EC[[#This Row],[Fecha]], "mm")'
|
| 945 |
+
' & " " & YEAR(Tabla_EC[[#This Row],[Fecha]]))'),
|
| 946 |
+
}
|
| 947 |
+
# Filas de 04_EstadosCuenta que se escriben (el resto son formulas SUMIFS/COUNTIFS
|
| 948 |
+
# que leen 07_EC_Raw y se recalculan solas).
|
| 949 |
+
MICRO_ROW_MES = 7 # C7 = primer mes de la ventana (D7..H7 son EDATE)
|
| 950 |
+
MICRO_ROW_SALDO_INI = 8 # C8 = saldo inicial (D8..H8 = saldo final anterior)
|
| 951 |
+
MICRO_ROW_SALDO_PROM = 12 # C12:H12 = saldo promedio del mes (vacias en la plantilla)
|
| 952 |
+
MICRO_COLS = ['C', 'D', 'E', 'F', 'G', 'H']
|
| 953 |
+
|
| 954 |
+
|
| 955 |
+
def fill_microempresa(template_path, output_path, data):
|
| 956 |
+
"""Llena EXPEDIENTE_FINANCIERO_V1.5 MICROEMPRESA.xlsx.
|
| 957 |
+
|
| 958 |
+
A diferencia de la maqueta V3 anterior, esta plantilla SI tiene hoja de datos
|
| 959 |
+
crudos ('07_EC_Raw', con Tabla_EC A1:M) y su hoja 04_EstadosCuenta calcula
|
| 960 |
+
todo con SUMIFS/COUNTIFS sobre ella. Asi que el llenado es el mismo concepto
|
| 961 |
+
que la maqueta estandar: se vuelcan TODOS los movimientos y la maqueta se
|
| 962 |
+
calcula sola. Solo se escriben ademas el primer mes de la ventana, el saldo
|
| 963 |
+
inicial, el saldo promedio por mes y los datos del solicitante.
|
| 964 |
+
"""
|
| 965 |
+
cliente = data.get('cliente', 'CLIENTE_SIN_NOMBRE')
|
| 966 |
+
cliente_raw = data.get('cliente_raw', cliente)
|
| 967 |
+
rfc = str(data.get('rfc', '') or '').strip()
|
| 968 |
+
transacciones = data.get('transacciones', []) or []
|
| 969 |
+
saldos_in = data.get('saldos', []) or []
|
| 970 |
+
if not isinstance(transacciones, list):
|
| 971 |
+
raise ValueError("'transacciones' debe ser una lista")
|
| 972 |
+
|
| 973 |
+
src = Path(template_path)
|
| 974 |
+
if not src.exists():
|
| 975 |
+
raise FileNotFoundError('Template no existe: %s' % template_path)
|
| 976 |
+
shutil.copy(str(src), output_path)
|
| 977 |
+
|
| 978 |
+
# ===== 1) Normalizar (sin filtrar, igual que la maqueta estandar) =====
|
| 979 |
+
txs = []
|
| 980 |
+
for t in transacciones:
|
| 981 |
+
f = parse_fecha(t.get('Fecha'))
|
| 982 |
+
if not f:
|
| 983 |
+
continue
|
| 984 |
+
dep = abs(parse_num(t.get('Deposito')))
|
| 985 |
+
ret = abs(parse_num(t.get('Retiro')))
|
| 986 |
+
concepto = str(t.get('Concepto', '') or '').strip()
|
| 987 |
+
tipo = str(t.get('Tipo', '') or '').strip()
|
| 988 |
+
txs.append({
|
| 989 |
+
'fecha': f,
|
| 990 |
+
'nombre': extraer_nombre(concepto),
|
| 991 |
+
# La columna calculada Categoria compara Tipo="Depósito". Se normaliza
|
| 992 |
+
# para que el acento y la forma coincidan siempre; los demas tipos
|
| 993 |
+
# (COMISION, DEVOLUCION, SIN_IMPORTE) caen en "Egreso", que es correcto.
|
| 994 |
+
'tipo': 'Depósito' if (dep > 0 and ret == 0) else (tipo or 'Retiro'),
|
| 995 |
+
'banco': str(t.get('Banco', '') or 'N/A').strip(),
|
| 996 |
+
'monto': round(dep if dep > 0 else ret, 2),
|
| 997 |
+
'dep': round(dep, 2),
|
| 998 |
+
'ret': round(ret, 2),
|
| 999 |
+
'moneda': (str(t.get('Moneda', '') or 'MXN').strip().upper() or 'MXN'),
|
| 1000 |
+
})
|
| 1001 |
+
txs.sort(key=lambda x: x['fecha'])
|
| 1002 |
+
|
| 1003 |
+
# tope de nombres distintos, igual que en la maqueta estandar
|
| 1004 |
+
def _mapa(clave, etiqueta):
|
| 1005 |
+
tot = {}
|
| 1006 |
+
for t in txs:
|
| 1007 |
+
if t[clave] > 0:
|
| 1008 |
+
tot[t['nombre']] = tot.get(t['nombre'], 0.0) + t[clave]
|
| 1009 |
+
canon = {n: re.sub(r'[^A-Z0-9]', '', quitar_acentos(n).upper()) for n in tot}
|
| 1010 |
+
rep = {}
|
| 1011 |
+
for n in sorted(tot, key=lambda x: (-len(canon[x]), x)):
|
| 1012 |
+
destino = n
|
| 1013 |
+
if len(canon[n]) >= 12:
|
| 1014 |
+
for m in rep:
|
| 1015 |
+
if canon[m].startswith(canon[n]) or canon[n].startswith(canon[m]):
|
| 1016 |
+
destino = rep[m]
|
| 1017 |
+
break
|
| 1018 |
+
rep[n] = destino
|
| 1019 |
+
fus = {}
|
| 1020 |
+
for n, dst in rep.items():
|
| 1021 |
+
fus[dst] = fus.get(dst, 0.0) + tot[n]
|
| 1022 |
+
if len(fus) <= DESGLOSE_CAPACIDAD:
|
| 1023 |
+
return {n: rep[n] for n in tot}
|
| 1024 |
+
top = set(sorted(fus, key=lambda n: -fus[n])[:DESGLOSE_CAPACIDAD - 1])
|
| 1025 |
+
return {n: (rep[n] if rep[n] in top else etiqueta) for n in tot}
|
| 1026 |
+
|
| 1027 |
+
mapa_dep = _mapa('dep', 'OTROS DEPOSITANTES')
|
| 1028 |
+
mapa_ret = _mapa('ret', 'OTROS BENEFICIARIOS')
|
| 1029 |
+
|
| 1030 |
+
# ===== 2) Ventana de 6 meses y saldo inicial ancla =====
|
| 1031 |
+
meses = sorted({t['fecha'].strftime('%Y-%m') for t in txs})
|
| 1032 |
+
ventana = meses[-6:]
|
| 1033 |
+
fuera = meses[:-6]
|
| 1034 |
+
sal_por_mes = {}
|
| 1035 |
+
prom_por_mes = {}
|
| 1036 |
+
for s in saldos_in:
|
| 1037 |
+
m = str(s.get('mes', ''))
|
| 1038 |
+
if not re.match(r'^\d{4}-\d{2}$', m):
|
| 1039 |
+
continue
|
| 1040 |
+
if s.get('saldo_inicial') not in (None, ''):
|
| 1041 |
+
sal_por_mes[m] = parse_num(s.get('saldo_inicial'))
|
| 1042 |
+
if s.get('saldo_promedio') not in (None, ''):
|
| 1043 |
+
prom_por_mes[m] = parse_num(s.get('saldo_promedio'))
|
| 1044 |
+
ancla = None
|
| 1045 |
+
for m in ventana:
|
| 1046 |
+
if m in sal_por_mes:
|
| 1047 |
+
ancla = sal_por_mes[m]
|
| 1048 |
+
break
|
| 1049 |
+
# saldo promedio por mes: el del banco si lo reporta, si no el punto medio
|
| 1050 |
+
# del saldo corrido de ese mes.
|
| 1051 |
+
por_mes = {m: [0.0, 0.0] for m in ventana}
|
| 1052 |
+
for t in txs:
|
| 1053 |
+
m = t['fecha'].strftime('%Y-%m')
|
| 1054 |
+
if m in por_mes:
|
| 1055 |
+
por_mes[m][0] += t['dep']
|
| 1056 |
+
por_mes[m][1] += t['ret']
|
| 1057 |
+
corrido = ancla if ancla is not None else 0.0
|
| 1058 |
+
prom_calc = {}
|
| 1059 |
+
for m in ventana:
|
| 1060 |
+
ini = corrido
|
| 1061 |
+
fin = round(ini + por_mes[m][0] - por_mes[m][1], 2)
|
| 1062 |
+
prom_calc[m] = round((ini + fin) / 2, 2)
|
| 1063 |
+
corrido = fin
|
| 1064 |
+
|
| 1065 |
+
# ===== 3) Abrir el xlsx y resolver hojas POR NOMBRE =====
|
| 1066 |
+
zin = zipfile.ZipFile(output_path, 'r')
|
| 1067 |
+
contenido = {i.filename: zin.read(i.filename) for i in zin.infolist()}
|
| 1068 |
+
infos = {i.filename: i for i in zin.infolist()}
|
| 1069 |
+
zin.close()
|
| 1070 |
+
hojas = _hojas_por_nombre(contenido)
|
| 1071 |
+
for req in (MICRO_RAW_HOJA, MICRO_EC_HOJA, MICRO_PORTADA_HOJA):
|
| 1072 |
+
if req not in hojas:
|
| 1073 |
+
raise ValueError('La plantilla no tiene la hoja %r (hojas: %s)'
|
| 1074 |
+
% (req, sorted(hojas)))
|
| 1075 |
+
|
| 1076 |
+
# ===== 4) 07_EC_Raw: TODOS los movimientos =====
|
| 1077 |
+
raw = contenido[hojas[MICRO_RAW_HOJA]].decode('utf-8')
|
| 1078 |
+
est = {c: get_style(raw, '%s2' % c) for c in 'ABCDEFGHIJKLM'}
|
| 1079 |
+
filas_xml = []
|
| 1080 |
+
for i, t in enumerate(txs, start=2):
|
| 1081 |
+
nombre = (mapa_dep if t['dep'] > 0 else mapa_ret).get(t['nombre'], t['nombre'])
|
| 1082 |
+
celdas = [
|
| 1083 |
+
build_cell('A%d' % i, s=est['A'], num=str(serial(t['fecha']))),
|
| 1084 |
+
build_cell('B%d' % i, s=est['B'], text=nombre[:250]),
|
| 1085 |
+
build_cell('C%d' % i, s=est['C'], text=t['tipo']),
|
| 1086 |
+
build_cell('D%d' % i, s=est['D'], text=t['banco']),
|
| 1087 |
+
build_cell('E%d' % i, s=est['E'], formula=MICRO_F['E']),
|
| 1088 |
+
build_cell('F%d' % i, s=est['F'], formula=MICRO_F['F']),
|
| 1089 |
+
build_cell('G%d' % i, s=est['G'], formula=MICRO_F['G']),
|
| 1090 |
+
build_cell('H%d' % i, s=est['H'], num=t['monto']),
|
| 1091 |
+
build_cell('I%d' % i, s=est['I'], formula=MICRO_F['I']),
|
| 1092 |
+
build_cell('J%d' % i, s=est['J'], formula=MICRO_F['J']),
|
| 1093 |
+
build_cell('K%d' % i, s=est['K'], formula=MICRO_F['K']),
|
| 1094 |
+
build_cell('L%d' % i, s=est['L'], text=t['moneda']),
|
| 1095 |
+
# Saldo corrido: la primera fila arranca del saldo inicial del banco.
|
| 1096 |
+
build_cell('M%d' % i, s=est['M'],
|
| 1097 |
+
formula=('%s+Tabla_EC[[#This Row],[Deposito]]'
|
| 1098 |
+
'-Tabla_EC[[#This Row],[Retiro]]'
|
| 1099 |
+
% (num_str(ancla) if (i == 2 and ancla is not None)
|
| 1100 |
+
else ('M%d' % (i - 1)) if i > 2 else '0'))),
|
| 1101 |
+
]
|
| 1102 |
+
filas_xml.append('<row r="%d">%s</row>' % (i, ''.join(celdas)))
|
| 1103 |
+
raw = replace_sheet_data_rows(raw, filas_xml, keep_row1=True)
|
| 1104 |
+
last_row = max(2, 1 + len(txs))
|
| 1105 |
+
raw = re.sub(r'<dimension ref="[^"]*"/>', '<dimension ref="A1:M%d"/>' % last_row, raw)
|
| 1106 |
+
contenido[hojas[MICRO_RAW_HOJA]] = raw.encode('utf-8')
|
| 1107 |
+
|
| 1108 |
+
# Tabla_EC al numero real de filas
|
| 1109 |
+
tbl = contenido[TABLE_EC].decode('utf-8')
|
| 1110 |
+
tbl = re.sub(r'(<table [^>]*?ref=")[^"]*(")', r'\g<1>A1:M%d\g<2>' % last_row, tbl)
|
| 1111 |
+
tbl = re.sub(r'(<autoFilter [^>]*?ref=")[^"]*(")', r'\g<1>A1:M%d\g<2>' % last_row, tbl)
|
| 1112 |
+
contenido[TABLE_EC] = tbl.encode('utf-8')
|
| 1113 |
+
|
| 1114 |
+
# ===== 5) 04_EstadosCuenta =====
|
| 1115 |
+
ec = desharear_formulas(contenido[hojas[MICRO_EC_HOJA]].decode('utf-8'))
|
| 1116 |
+
# DEFECTO DE LA PLANTILLA: los SUMIFS de Depositos/Retiros/Efectivo cierran el
|
| 1117 |
+
# mes con '"<=" & EDATE(C$7,1)', o sea INCLUYEN el dia 1 del mes siguiente, asi
|
| 1118 |
+
# que todo movimiento fechado el dia 1 se suma en DOS meses. Los COUNTIFS de
|
| 1119 |
+
# al lado (# Depositos / # Retiros) ya usan '"<"', que es lo correcto.
|
| 1120 |
+
# Medido con 6 estados de BANAMEX: +$517,344.11 en depositos y +$192,332.71 en
|
| 1121 |
+
# retiros contra lo que reporta el banco. Se corrige aqui para que la maqueta
|
| 1122 |
+
# cuadre; conviene arreglarlo tambien en la plantilla de origen.
|
| 1123 |
+
n_fix = ec.count('"<=" & EDATE(')
|
| 1124 |
+
ec = ec.replace('"<=" & EDATE(', '"<" & EDATE(')
|
| 1125 |
+
# los SUMIFS/COUNTIFS escanean 07_EC_Raw hasta $2167: se amplia si hay mas filas
|
| 1126 |
+
def _ampliar(m):
|
| 1127 |
+
return ':$%s$%d' % (m.group(1), max(int(m.group(2)), last_row))
|
| 1128 |
+
ec = re.sub(r':\$([A-Z]{1,3})\$(\d{3,6})\b', _ampliar, ec)
|
| 1129 |
+
if ventana:
|
| 1130 |
+
y0, m0 = int(ventana[0][:4]), int(ventana[0][5:7])
|
| 1131 |
+
ec = set_row_cells(ec, MICRO_ROW_MES, {
|
| 1132 |
+
'C': build_cell('C%d' % MICRO_ROW_MES,
|
| 1133 |
+
s=get_style(ec, 'C%d' % MICRO_ROW_MES),
|
| 1134 |
+
num=str(serial(date(y0, m0, 1))))})
|
| 1135 |
+
if ancla is not None:
|
| 1136 |
+
ec = set_row_cells(ec, MICRO_ROW_SALDO_INI, {
|
| 1137 |
+
'C': build_cell('C%d' % MICRO_ROW_SALDO_INI,
|
| 1138 |
+
s=get_style(ec, 'C%d' % MICRO_ROW_SALDO_INI),
|
| 1139 |
+
num=round(ancla, 2))})
|
| 1140 |
+
for i, m in enumerate(ventana):
|
| 1141 |
+
col = MICRO_COLS[i]
|
| 1142 |
+
ec = set_row_cells(ec, MICRO_ROW_SALDO_PROM, {
|
| 1143 |
+
col: build_cell('%s%d' % (col, MICRO_ROW_SALDO_PROM),
|
| 1144 |
+
s=get_style(ec, '%s%d' % (col, MICRO_ROW_SALDO_PROM)),
|
| 1145 |
+
num=prom_por_mes.get(m, prom_calc[m]))})
|
| 1146 |
+
for i in range(len(ventana), 6): # meses sin estado -> vacio
|
| 1147 |
+
col = MICRO_COLS[i]
|
| 1148 |
+
ec = set_row_cells(ec, MICRO_ROW_SALDO_PROM, {
|
| 1149 |
+
col: build_cell('%s%d' % (col, MICRO_ROW_SALDO_PROM),
|
| 1150 |
+
s=get_style(ec, '%s%d' % (col, MICRO_ROW_SALDO_PROM)))})
|
| 1151 |
+
contenido[hojas[MICRO_EC_HOJA]] = ec.encode('utf-8')
|
| 1152 |
+
|
| 1153 |
+
# ===== 6) 01_Portada: C9 nombre, C10 RFC, C14 fecha de analisis =====
|
| 1154 |
+
por = desharear_formulas(contenido[hojas[MICRO_PORTADA_HOJA]].decode('utf-8'))
|
| 1155 |
+
por = set_row_cells(por, 9, {'C': build_cell('C9', s=get_style(por, 'C9'),
|
| 1156 |
+
text=str(cliente_raw).upper())})
|
| 1157 |
+
# El RFC se escribe SIEMPRE (aunque venga vacio) para no dejar el de ejemplo.
|
| 1158 |
+
por = set_row_cells(por, 10, {'C': build_cell('C10', s=get_style(por, 'C10'),
|
| 1159 |
+
text=rfc.upper() if rfc else '')})
|
| 1160 |
+
por = set_row_cells(por, 14, {'C': build_cell('C14', s=get_style(por, 'C14'),
|
| 1161 |
+
num=str(serial(date.today())))})
|
| 1162 |
+
contenido[hojas[MICRO_PORTADA_HOJA]] = por.encode('utf-8')
|
| 1163 |
+
|
| 1164 |
+
# ===== 7) recalculo al abrir =====
|
| 1165 |
+
contenido.pop('xl/calcChain.xml', None)
|
| 1166 |
+
wbx = contenido['xl/workbook.xml'].decode('utf-8')
|
| 1167 |
+
if '<calcPr' in wbx:
|
| 1168 |
+
wbx = re.sub(r'<calcPr[^>]*/>', '<calcPr calcId="191029" fullCalcOnLoad="1"/>', wbx)
|
| 1169 |
+
else:
|
| 1170 |
+
wbx = wbx.replace('</workbook>',
|
| 1171 |
+
'<calcPr calcId="191029" fullCalcOnLoad="1"/></workbook>')
|
| 1172 |
+
contenido['xl/workbook.xml'] = wbx.encode('utf-8')
|
| 1173 |
+
ct = contenido['[Content_Types].xml'].decode('utf-8')
|
| 1174 |
+
ct = re.sub(r'<Override[^>]*calcChain[^>]*/>', '', ct)
|
| 1175 |
+
contenido['[Content_Types].xml'] = ct.encode('utf-8')
|
| 1176 |
+
|
| 1177 |
+
zout = zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED)
|
| 1178 |
+
for nombre, blob in contenido.items():
|
| 1179 |
+
if nombre == 'xl/calcChain.xml':
|
| 1180 |
+
continue
|
| 1181 |
+
zi = infos.get(nombre)
|
| 1182 |
+
zout.writestr(zi if zi is not None else nombre, blob)
|
| 1183 |
+
zout.close()
|
| 1184 |
+
|
| 1185 |
+
return {
|
| 1186 |
+
'output': output_path,
|
| 1187 |
+
'plantilla': 'microempresa',
|
| 1188 |
+
'rows_written': len(txs),
|
| 1189 |
+
'ventana_meses': ventana,
|
| 1190 |
+
'meses_fuera_ventana': fuera,
|
| 1191 |
+
'saldo_inicial_ancla': ancla,
|
| 1192 |
+
'ordenantes_detectados': len(set(mapa_dep.values())),
|
| 1193 |
+
'beneficiarios_detectados': len(set(mapa_ret.values())),
|
| 1194 |
+
'sumifs_corregidos': n_fix, # '<=' -> '<' en el cierre de mes (ver nota)
|
| 1195 |
+
'cliente': cliente,
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
|
| 1199 |
def main():
|
| 1200 |
if len(sys.argv) < 2:
|
| 1201 |
+
print(json.dumps({"error": "Uso: fill_template.py <output_path> [template_path] [--microempresa]"}))
|
| 1202 |
sys.exit(1)
|
| 1203 |
+
argv = [a for a in sys.argv[1:] if a != '--microempresa']
|
| 1204 |
+
micro = '--microempresa' in sys.argv
|
| 1205 |
+
output_path = argv[0]
|
| 1206 |
+
template_path = argv[1] if len(argv) > 1 else DEFAULT_TEMPLATE
|
| 1207 |
try:
|
| 1208 |
data = json.load(sys.stdin)
|
| 1209 |
except Exception as e:
|
| 1210 |
print(json.dumps({"error": "JSON invalido por stdin: %s" % e}))
|
| 1211 |
sys.exit(1)
|
| 1212 |
try:
|
| 1213 |
+
result = (fill_microempresa if micro else fill)(template_path, output_path, data)
|
| 1214 |
print(json.dumps(result, ensure_ascii=False))
|
| 1215 |
except Exception as e:
|
| 1216 |
import traceback
|
|
|
|
| 1219 |
|
| 1220 |
|
| 1221 |
if __name__ == '__main__':
|
| 1222 |
+
main()
|