diff --git a/satcfdi/accounting/contabilidad.py b/satcfdi/accounting/contabilidad.py index 10e941c..fe382ee 100644 --- a/satcfdi/accounting/contabilidad.py +++ b/satcfdi/accounting/contabilidad.py @@ -1,6 +1,10 @@ import os from typing import Sequence +from satcfdi.zip import zip_create, ZipData, zip_file + +from satcfdi.catalogs import select, catalog_code + from satcfdi.utils import iterate from satcfdi.create.contabilidad.AuxiliarCtas13 import AuxiliarCtas, Cuenta, DetalleAux @@ -29,21 +33,28 @@ def filename(file): raise ValueError(f"Unknown file type: {file.tag}") -def output_file(file, folder, fiel=None, generate_pdf=False): +def output_file(file, folder, fiel=None, generate_pdf=False, zip_xml=False): if fiel: file.sign(fiel) output_file = os.path.join(folder, filename(file)) - file.xml_write( - output_file, - pretty_print=True, - xml_declaration=True - ) + if zip_xml: + zip_file(output_file[:-4] + '.zip', [ + ZipData( + filename(file), + file.xml_bytes(xml_declaration=True) + ) + ]) + else: + file.xml_write( + output_file, + pretty_print=True, + xml_declaration=True + ) + if generate_pdf: - # render.html_write(file, output_file[:-4] + ".html") render.pdf_write(file, output_file[:-4] + ".pdf") else: - # delete file try: os.remove(output_file[:-4] + ".pdf") except FileNotFoundError: @@ -52,40 +63,6 @@ def output_file(file, folder, fiel=None, generate_pdf=False): return output_file -def calcular_saldos(cuentas, polizas): - max_level = 1 - for c in cuentas.values(): - # c['SaldoIni'] = 0 - c['Debe'] = 0 - c['Haber'] = 0 - c['SaldoFin'] = 0 - max_level = max(max_level, c['Nivel']) - - for p in polizas: - for t in p["Transaccion"]: - num_cta = t["NumCta"] - cuenta = cuentas[num_cta] - cuenta["Debe"] += t["Debe"] - cuenta["Haber"] += t["Haber"] - - # Fill Parents - for level in range(max_level, 1, -1): - for k, v in cuentas.items(): - if v['Nivel'] == level: - parent = v['SubCtaDe'] - if parent: - p_cuenta = cuentas[parent] - p_cuenta['Debe'] += v['Debe'] - p_cuenta['Haber'] += v['Haber'] - - # Fill SaldoFin - for c in cuentas.values(): - if c["Natur"] == "D": - c["SaldoFin"] += c["SaldoIni"] + c["Debe"] - c["Haber"] - else: - c["SaldoFin"] += c["SaldoIni"] + c["Haber"] - c["Debe"] - - def generar_contabilidad( dp: DatePeriod, rfc_emisor: str, @@ -98,8 +75,10 @@ def generar_contabilidad( numero_tramite=None, folder=None, fiel=None, - generate_pdf=False): - + generate_pdf=False, + zip_xml=False +): + validate_cuentas(cuentas) validate_polizas(polizas) calcular_saldos(cuentas, polizas) @@ -112,7 +91,7 @@ def generar_contabilidad( num_tramite=numero_tramite, poliza=polizas ) - output_file(plz, folder, fiel, generate_pdf=generate_pdf) + output_file(plz, folder, fiel, generate_pdf=generate_pdf, zip_xml=zip_xml) cat = Catalogo( rfc=rfc_emisor, @@ -120,7 +99,7 @@ def generar_contabilidad( anio=dp.year, ctas=[ Ctas( - cod_agrup=v["CodAgrup"].split("_")[0], + cod_agrup=v["CodAgrup"], num_cta=k, desc=v["Desc"], nivel=v["Nivel"], @@ -129,7 +108,7 @@ def generar_contabilidad( ) for k, v in cuentas.items() ] ) - cato = output_file(cat, folder, fiel) + output_file(cat, folder, fiel, zip_xml=zip_xml) ban = Balanza( rfc=rfc_emisor, @@ -142,7 +121,7 @@ def generar_contabilidad( **v, } for k, v in cuentas.items() if v["SaldoIni"] or v["Debe"] or v["Haber"] or v["SaldoFin"]], ) - bano = output_file(ban, folder, fiel) + output_file(ban, folder, fiel, zip_xml=zip_xml) aux_detalles = group_aux_cuentas(polizas) aux = AuxiliarCtas( @@ -162,7 +141,7 @@ def generar_contabilidad( ) for k, v in cuentas.items() if k in aux_detalles ] ) - output_file(aux, folder, fiel, generate_pdf=generate_pdf) + output_file(aux, folder, fiel, generate_pdf=generate_pdf, zip_xml=zip_xml) auxf = RepAuxFol( rfc=rfc_emisor, @@ -173,11 +152,11 @@ def generar_contabilidad( num_tramite=numero_tramite, det_aux_fol=list(group_aux_folios(polizas)) ) - output_file(auxf, folder, fiel, generate_pdf=generate_pdf) + output_file(auxf, folder, fiel, generate_pdf=generate_pdf, zip_xml=zip_xml) imprimir_contablidad( - catalogo_cuentas=cato, - balanza_comprobacion=bano, + catalogo_cuentas=cat, + balanza_comprobacion=ban, archivo_excel=os.path.join(folder, filename(ban)[:-4] + ".xlsx") ) @@ -230,30 +209,45 @@ def group_aux_folios(polizas): ) +def validate_cuentas(cuentas): + # validar cuentas + for k, v in cuentas.items(): + assert k + v['_Lowest'] = True + assert v['Natur'] in ['A', 'D'] + if v['SubCtaDe']: + assert v['SubCtaDe'] in cuentas, f"Parent account {v['SubCtaDe']} not found for {k}" + v['Nivel'] = cuentas[v['SubCtaDe']]['Nivel'] + 1 + else: + v['Nivel'] = 1 + + v['CodAgrup'] = catalog_code('Cb9f_c_CodAgrup', v['CodAgrup']) + assert v['CodAgrup'].description, f"Unknown CodAgrup: {v['CodAgrup']}" + + for k, v in cuentas.items(): + if v['SubCtaDe']: + cuentas[v['SubCtaDe']]['_Lowest'] = False + + +def sign(cta): + if cta['Natur'] == 'D': + return 1 + return -1 + + def validate_saldos(cuentas): - total = 0 totales = {} for k, v in cuentas.items(): - if v['Nivel'] == 1: - if v['Natur'] == 'D': - total += v['SaldoFin'] - else: - total -= v['SaldoFin'] - else: - totales.setdefault(v['SubCtaDe'], 0) - if v['Natur'] == 'D': - totales[v['SubCtaDe']] += v['SaldoFin'] - else: - totales[v['SubCtaDe']] -= v['SaldoFin'] + sub_cta = v.get('SubCtaDe') + totales.setdefault(sub_cta, 0) + totales[sub_cta] += v['SaldoFin'] * sign(v) - assert total == 0 for k, v in totales.items(): - if cuentas[k]['Natur'] == 'D': - if v != cuentas[k]['SaldoFin']: + if k: + if v != cuentas[k]['SaldoFin'] * sign(cuentas[k]): raise ValueError(f"Error in {k}: {v} != {cuentas[k]['SaldoFin']}") else: - if v != -cuentas[k]['SaldoFin']: - raise ValueError(f"Error in {k}: {v} != {cuentas[k]['SaldoFin']}") + assert v == 0 def validate_polizas(polizas): @@ -263,3 +257,36 @@ def validate_polizas(polizas): if u in num_un: raise ValueError(f"Repeated NumUnIdenPol: {u}") num_un.add(u) + + +def calcular_saldos(cuentas, polizas): + max_level = 1 + for c in cuentas.values(): + # c['SaldoIni'] = 0 + c['Debe'] = 0 + c['Haber'] = 0 + c['SaldoFin'] = 0 + max_level = max(max_level, c['Nivel']) + + for p in polizas: + for t in p["Transaccion"]: + num_cta = t["NumCta"] + cuenta = cuentas[num_cta] + assert cuenta["_Lowest"], f"Account {num_cta} is not a lowest level account" + cuenta["Debe"] += t["Debe"] + cuenta["Haber"] += t["Haber"] + + # Fill Parents + for level in range(max_level, 1, -1): + for k, v in cuentas.items(): + if v['Nivel'] == level: + parent = v['SubCtaDe'] + if parent: + p_cuenta = cuentas[parent] + p_cuenta['Debe'] += v['Debe'] + p_cuenta['Haber'] += v['Haber'] + + # Fill SaldoFin + for c in cuentas.values(): + s = sign(c) + c["SaldoFin"] += c["SaldoIni"] + c["Debe"] * s - c["Haber"] * s diff --git a/satcfdi/accounting/contabilidad_print.py b/satcfdi/accounting/contabilidad_print.py index 5d75a1a..eba2b5f 100644 --- a/satcfdi/accounting/contabilidad_print.py +++ b/satcfdi/accounting/contabilidad_print.py @@ -22,10 +22,8 @@ def imprimir_contablidad( balanza_comprobacion, archivo_excel ): - ct = CFDI.from_file(catalogo_cuentas) - bc = CFDI.from_file(balanza_comprobacion) - # ct = catalogo_cuentas - # bc = balanza_comprobacion + ct = catalogo_cuentas + bc = balanza_comprobacion ctas = { c['NumCta']: { diff --git a/satcfdi/zip.py b/satcfdi/zip.py index 9dbe1d4..45bf6ab 100644 --- a/satcfdi/zip.py +++ b/satcfdi/zip.py @@ -8,8 +8,8 @@ def zip_create(target: io.BytesIO, files: list[ZipData]): p = target.tell() - for f in files: - with ZipFile(target, "w") as myzip: + with ZipFile(target, "w") as myzip: + for f in files: zinfo = ZipInfo( filename=f.filename ) @@ -24,3 +24,19 @@ def zip_create(target: io.BytesIO, files: list[ZipData]): with target.getbuffer() as view: # change zip flag bytes view[p + 6:p + 8] = b"\x08\x08" + + +def zip_file(zipfile, files: list[ZipData]): + # Create a ZipFile object in write mode + with ZipFile(zipfile, 'w') as zipf: + # Add the input file to the zip archive with its base name + for f in files: + zinfo = ZipInfo( + filename=f.filename, + # date_time=datetime_to_tuple(datetime.now()) + ) + zinfo.compress_type = 8 + zinfo.create_system = 0 + + with zipf.open(zinfo, 'w') as stream: + stream.write(f.data) diff --git a/tests/contabilidad_electronica/cuentas.yaml b/tests/contabilidad_electronica/cuentas.yaml index f6bd679..12f1c2e 100644 --- a/tests/contabilidad_electronica/cuentas.yaml +++ b/tests/contabilidad_electronica/cuentas.yaml @@ -3,7 +3,6 @@ Desc: Activos Natur: D CodAgrup: '100' # Activo - Nivel: 1 SubCtaDe: SaldoIni: 0 @@ -13,21 +12,18 @@ Desc: Bancos Natur: D CodAgrup: '102' # Bancos nacionales - Nivel: 2 SubCtaDe: '1000' SaldoIni: 0 '1020.01': Desc: Bancos Nacionales Natur: D CodAgrup: '102.01' # Bancos nacionales - Nivel: 2 SubCtaDe: '1020' SaldoIni: 0 '1020.02': Desc: Bancos Extranjeros Natur: D CodAgrup: '102.02' # Bancos extranjeros - Nivel: 2 SubCtaDe: '1020' SaldoIni: 0 @@ -36,20 +32,17 @@ Desc: Clientes Natur: D CodAgrup: '105' # Clientes - Nivel: 2 SubCtaDe: '1000' SaldoIni: 0 '1050.01': Desc: Clientes Nacionales Natur: D CodAgrup: '105.01' # Clientes nacionales - Nivel: 2 SubCtaDe: '1050' SaldoIni: 0 '1050.02': Desc: Clientes Extranjeros Natur: D CodAgrup: '105.02' # Clientes extranjeros - Nivel: 2 SubCtaDe: '1050' SaldoIni: 0 diff --git a/tests/test_contabilidad.py b/tests/test_contabilidad.py index c8fac91..7d05498 100644 --- a/tests/test_contabilidad.py +++ b/tests/test_contabilidad.py @@ -1,7 +1,9 @@ import datetime import logging import os +from unittest import mock from uuid import UUID +from zipfile import ZipInfo import pytest import yaml @@ -69,59 +71,93 @@ def test_generate_contabilidad_empty(): ) +polizas = [ + Poliza( + num_un_iden_pol="1", + fecha=datetime.date(2024, 2, 1), + concepto="Compra de equipo de computo", + transaccion=[ + Transaccion( + num_cta="1020.01", + des_cta="Bancos", + concepto="Nal", + debe=10000, + haber=0, + comp_nal=[ + CompNal( + uuid_cfdi='a4f4fea5-e798-4ab3-a2e5-75f741f4ecca', + rfc="CACX7605101P8", + monto_total=10000 + ) + ] + ), + Transaccion( + num_cta="1020.02", + des_cta="Bancos", + concepto="Ext", + debe=0, + haber=10000, + comp_nal=[ + CompNal( + uuid_cfdi='a4f4fea5-e798-4ab3-a2e5-75f741f4ecca', + rfc="CACX7605101P8", + monto_total=10000 + ) + ] + ) + ] + ) +] + + def test_generate_contabilidad_simple(): - os.makedirs(os.path.join(current_dir, 'test_contabilidad_electronica/out/simple'), exist_ok=True) + path = 'simple' + os.makedirs(os.path.join(current_dir, 'test_contabilidad_electronica/out', path), exist_ok=True) with open(os.path.join(current_dir, 'contabilidad_electronica', 'cuentas.yaml'), 'r', encoding='utf-8') as f: cuentas = yaml.load(f, Loader=yaml.SafeLoader) - polizas = [ - Poliza( - num_un_iden_pol="1", - fecha=datetime.date(2024, 2, 1), - concepto="Compra de equipo de computo", - transaccion=[ - Transaccion( - num_cta="1020.01", - des_cta="Bancos", - concepto="Nal", - debe=10000, - haber=0, - comp_nal=[ - CompNal( - uuid_cfdi='a4f4fea5-e798-4ab3-a2e5-75f741f4ecca', - rfc="CACX7605101P8", - monto_total=10000 - ) - ] - ), - Transaccion( - num_cta="1020.02", - des_cta="Bancos", - concepto="Ext", - debe=0, - haber=10000, - comp_nal=[ - CompNal( - uuid_cfdi='a4f4fea5-e798-4ab3-a2e5-75f741f4ecca', - rfc="CACX7605101P8", - monto_total=10000 - ) - ] - ) - ] - ) - ] generar_contabilidad( dp=DatePeriod(2024, 2), rfc_emisor="CACX7605101P8", cuentas=cuentas, polizas=polizas, - folder=os.path.join(current_dir, 'test_contabilidad_electronica/out/simple'), - tipo_solicitud='AF' + folder=os.path.join(current_dir, 'test_contabilidad_electronica/out', path), + tipo_solicitud='AF', + zip_xml=False ) assert compare_directories( - os.path.join(current_dir, 'test_contabilidad_electronica/ref/simple'), - os.path.join(current_dir, 'test_contabilidad_electronica/out/simple') + os.path.join(current_dir, 'test_contabilidad_electronica/ref', path), + os.path.join(current_dir, 'test_contabilidad_electronica/out', path) + ) + + +def test_generate_contabilidad_zip(): + path = 'simple_zip' + os.makedirs(os.path.join(current_dir, 'test_contabilidad_electronica/out', path), exist_ok=True) + + with open(os.path.join(current_dir, 'contabilidad_electronica', 'cuentas.yaml'), 'r', encoding='utf-8') as f: + cuentas = yaml.load(f, Loader=yaml.SafeLoader) + + def zi(filename): + return ZipInfo( + filename=filename, + date_time=(2022, 11, 8, 11, 41, 8) # time.localtime(time.time())[:6] + ) + + with mock.patch(f'{module}.zip.ZipInfo', zi) as m: + generar_contabilidad( + dp=DatePeriod(2024, 2), + rfc_emisor="CACX7605101P8", + cuentas=cuentas, + polizas=polizas, + folder=os.path.join(current_dir, 'test_contabilidad_electronica/out', path), + tipo_solicitud='AF', + zip_xml=True + ) + + assert compare_directories( + os.path.join(current_dir, 'test_contabilidad_electronica/ref', path), + os.path.join(current_dir, 'test_contabilidad_electronica/out', path) ) diff --git a/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402BN.xml b/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402BN.xml index fcdf169..9cc4444 100644 --- a/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402BN.xml +++ b/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402BN.xml @@ -1,5 +1,6 @@ + diff --git a/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402CT.xml b/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402CT.xml index 2bf38a3..cd2dc12 100644 --- a/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402CT.xml +++ b/tests/test_contabilidad_electronica/ref/simple/CACX7605101P8202402CT.xml @@ -2,9 +2,9 @@ - - + + - - + + diff --git a/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402BN.xlsx b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402BN.xlsx new file mode 100644 index 0000000..833ca08 Binary files /dev/null and b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402BN.xlsx differ diff --git a/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402BN.zip b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402BN.zip new file mode 100644 index 0000000..a3085f1 Binary files /dev/null and b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402BN.zip differ diff --git a/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402CT.zip b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402CT.zip new file mode 100644 index 0000000..194f250 Binary files /dev/null and b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402CT.zip differ diff --git a/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402PL.zip b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402PL.zip new file mode 100644 index 0000000..b51b80f Binary files /dev/null and b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402PL.zip differ diff --git a/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402XC.zip b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402XC.zip new file mode 100644 index 0000000..2c0a32f Binary files /dev/null and b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402XC.zip differ diff --git a/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402XF.zip b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402XF.zip new file mode 100644 index 0000000..f3b0efc Binary files /dev/null and b/tests/test_contabilidad_electronica/ref/simple_zip/CACX7605101P8202402XF.zip differ