From b20cbc20928c1f224c754e0822c85126fc3c74cf Mon Sep 17 00:00:00 2001 From: SatCFDI Date: Mon, 8 Apr 2024 22:39:30 -0600 Subject: [PATCH] Added Contabilidad functions --- .gitignore | 1 + satcfdi/accounting/contabilidad.py | 195 ++++++++++++++++++ satcfdi/accounting/contabilidad_print.py | 51 +++++ satcfdi/accounting/process.py | 4 +- satcfdi/create/contabilidad/__init__.py | 1 + tests/MOLE870717DRA202402BN.xlsx | Bin 0 -> 5594 bytes tests/test_contabilidad.py | 19 +- .../ref/MOLE870717DRA202402BN.xml | 2 + .../ref/MOLE870717DRA202402CT.xml | 2 + .../ref/MOLE870717DRA202402PL.xml | 2 + .../ref/MOLE870717DRA202402XC.xml | 2 + .../ref/MOLE870717DRA202402XF.xml | 2 + tests/utils.py | 30 +++ 13 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 satcfdi/accounting/contabilidad.py create mode 100644 satcfdi/accounting/contabilidad_print.py create mode 100644 tests/MOLE870717DRA202402BN.xlsx create mode 100644 tests/test_contabilidad_electronica/ref/MOLE870717DRA202402BN.xml create mode 100644 tests/test_contabilidad_electronica/ref/MOLE870717DRA202402CT.xml create mode 100644 tests/test_contabilidad_electronica/ref/MOLE870717DRA202402PL.xml create mode 100644 tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XC.xml create mode 100644 tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XF.xml diff --git a/.gitignore b/.gitignore index 24e3e8c..e9578f1 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,4 @@ dmypy.json /satcfdi/create/*/*_z.py /docs/generated/ /satcfdi.svg +/tests/test_contabilidad_electronica/out/ diff --git a/satcfdi/accounting/contabilidad.py b/satcfdi/accounting/contabilidad.py new file mode 100644 index 0000000..68afa4a --- /dev/null +++ b/satcfdi/accounting/contabilidad.py @@ -0,0 +1,195 @@ +import os +from typing import Sequence + +from satcfdi.create.contabilidad.AuxiliarCtas13 import AuxiliarCtas, Cuenta, DetalleAux +from satcfdi.create.contabilidad.BCE13 import Balanza +from satcfdi.create.contabilidad.PLZ13 import Polizas, CompNal, Poliza +from satcfdi.create.contabilidad.RepAux13 import RepAuxFol, DetAuxFol +from satcfdi.create.contabilidad.catalogocuentas13 import Catalogo, Ctas +from .contabilidad_print import imprimir_contablidad + +from .. import render + +from ..models import DatePeriod + + +def filename(file): + if file.tag == '{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/BalanzaComprobacion}Balanza': + return file["RFC"] + str(file["Anio"]) + file["Mes"] + "B" + file["TipoEnvio"] + ".xml" + if file.tag == '{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/CatalogoCuentas}Catalogo': + return file["RFC"] + str(file["Anio"]) + file["Mes"] + "CT.xml" + if file.tag == '{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/AuxiliarCtas}AuxiliarCtas': + return file["RFC"] + str(file["Anio"]) + file["Mes"] + "XC.xml" + if file.tag == '{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/PolizasPeriodo}Polizas': + return file["RFC"] + str(file["Anio"]) + file["Mes"] + "PL.xml" + if file.tag == '{http://www.sat.gob.mx/esquemas/ContabilidadE/1_3/AuxiliarFolios}RepAuxFol': + return file["RFC"] + str(file["Anio"]) + file["Mes"] + "XF.xml" + raise ValueError(f"Unknown file type: {file.tag}") + + +def output_file(file, folder, fiel=None, generate_pdf=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 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: + pass + + return output_file + + +def generar_contabilidad( + dp: DatePeriod, + rfc_emisor: str, + ctas: dict, + polizas: Sequence[Poliza], + tipo_envio='N', + fecha_mod_bal=None, + tipo_solicitud='', + numero_orden=None, + numero_tramite=None, + folder=None, + fiel=None, + generate_pdf=False): + + plz = Polizas( + rfc=rfc_emisor, + mes=str(dp.month).zfill(2), + anio=dp.year, + tipo_solicitud=tipo_solicitud, + num_orden=numero_orden, + num_tramite=numero_tramite, + poliza=polizas + ) + output_file(plz, folder, fiel, generate_pdf=generate_pdf) + + cat = Catalogo( + rfc=rfc_emisor, + mes=str(dp.month).zfill(2), + anio=dp.year, + ctas=[ + Ctas( + cod_agrup=v["CodAgrup"].split("_")[0], + num_cta=k, + desc=v["Desc"], + nivel=v["Nivel"], + natur=v["Natur"], + sub_cta_de=v['SubCtaDe'], + ) for k, v in ctas.items() + ] + ) + output_file(cat, folder, fiel) + + ban = Balanza( + rfc=rfc_emisor, + mes=str(dp.month).zfill(2), + anio=dp.year, + tipo_envio=tipo_envio, + fecha_mod_bal=fecha_mod_bal, + ctas=[{ + "NumCta": k, + **v, + } for k, v in ctas.items() if v["SaldoIni"] or v["Debe"] or v["Haber"] or v["SaldoFin"]], + ) + output_file(ban, folder, fiel) + + aux_detalles = group_aux_cuentas(polizas) + aux = AuxiliarCtas( + rfc=rfc_emisor, + mes=str(dp.month).zfill(2), + anio=dp.year, + tipo_solicitud=tipo_solicitud, + num_orden=numero_orden, + num_tramite=numero_tramite, + cuenta=[ + Cuenta( + num_cta=k, + des_cta=v["Desc"], + saldo_ini=v["SaldoIni"], + saldo_fin=v["SaldoFin"], + detalle_aux=aux_detalles[k] + ) for k, v in ctas.items() if k in aux_detalles + ] + ) + output_file(aux, folder, fiel, generate_pdf=generate_pdf) + + auxf = RepAuxFol( + rfc=rfc_emisor, + mes=str(dp.month).zfill(2), + anio=dp.year, + tipo_solicitud=tipo_solicitud, + num_orden=numero_orden, + num_tramite=numero_tramite, + det_aux_fol=list(group_aux_folios(polizas)) + ) + output_file(auxf, folder, fiel, generate_pdf=generate_pdf) + + imprimir_contablidad( + catalogo_cuentas=cat, + balanza_comprobacion=ban, + archivo_excel=filename(ban)[:-4] + ".xlsx" + ) + + validate_saldos(ctas) + + +def group_aux_cuentas(polizas): + cta_polizas = {} + for p in polizas: + for t in p["Transaccion"]: + detalles = cta_polizas.setdefault(t["NumCta"], []) + detalles.append( + DetalleAux( + fecha=p["Fecha"], + num_un_iden_pol=p["NumUnIdenPol"], + concepto=t["Concepto"], + debe=t["Debe"], + haber=t["Haber"], + ) + ) + return cta_polizas + + +def group_aux_folios(polizas): + for p in polizas: + yield DetAuxFol( + num_un_iden_pol=p["NumUnIdenPol"], + fecha=p["Fecha"], + compr_nal=p.comp_nal, + ) + + +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'] + + assert total == 0 + for k, v in totales.items(): + if cuentas[k]['Natur'] == 'D': + assert v == cuentas[k]['SaldoFin'] + else: + assert v == -cuentas[k]['SaldoFin'] diff --git a/satcfdi/accounting/contabilidad_print.py b/satcfdi/accounting/contabilidad_print.py new file mode 100644 index 0000000..1013d37 --- /dev/null +++ b/satcfdi/accounting/contabilidad_print.py @@ -0,0 +1,51 @@ +import xlsxwriter +from satcfdi.accounting.process import excel_export +from satcfdi.cfdi import CFDI + +EXCEL_COLUMNS = { + 'NumCta': (12, False, lambda i: i['NumCta']), + 'Desc': (62, False, lambda i: ' ' * (i['Nivel'] - 1) + i['Desc']), + 'Natur': (5, False, lambda i: i['Natur']), + + 'SaldoIni': (12, False, lambda i: i.get('SaldoIni')), + 'Debe': (12, False, lambda i: i.get('Debe')), + 'Haber': (12, False, lambda i: i.get('Haber')), + 'SaldoFin': (12, False, lambda i: i.get('SaldoFin')), + + 'CodAgrup': (12, False, lambda i: i['CodAgrup'].code), + 'CodDesc': (120, False, lambda i: i['CodAgrup'].description), +} + + +def imprimir_contablidad( + catalogo_cuentas, + balanza_comprobacion, + archivo_excel +): + # ct = CFDI.from_file(catalogo_cuentas) + # bc = CFDI.from_file(balanza_comprobacion) + ct = catalogo_cuentas + bc = balanza_comprobacion + + ctas = { + c['NumCta']: { + 'Desc': c['Desc'], + 'Natur': c['Natur'], + 'CodAgrup': c['CodAgrup'], + 'Nivel': c['Nivel'], + } + for c in ct['Ctas'] + } + for r in bc['Ctas']: + ctv = ctas[r['NumCta']] + r.update(ctv) + + workbook = xlsxwriter.Workbook(archivo_excel) + excel_export( + workbook=workbook, + name=bc['RFC'] + str(bc['Anio']) + str(bc['Mes']), + invoices=bc['Ctas'], + columns=EXCEL_COLUMNS, + row_height=1 + ) + workbook.close() diff --git a/satcfdi/accounting/process.py b/satcfdi/accounting/process.py index 88553b4..f2f6c24 100644 --- a/satcfdi/accounting/process.py +++ b/satcfdi/accounting/process.py @@ -248,7 +248,7 @@ def num2col(n): return name -def excel_export(workbook: xlsxwriter.workbook.Workbook, name, invoices, columns): +def excel_export(workbook: xlsxwriter.workbook.Workbook, name, invoices, columns, row_heigth=3): worksheet = workbook.add_worksheet(name) for n, (w, s, f) in enumerate(columns.values()): @@ -275,7 +275,7 @@ def excel_export(workbook: xlsxwriter.workbook.Workbook, name, invoices, columns number_format = workbook.add_format({'num_format': '0.00'}) number_format.set_align('top') - row_height = 20 * 3 + row_height = 20 * row_heigth r = 0 for r, i in enumerate(invoices): worksheet.set_row_pixels(1 + r, row_height) diff --git a/satcfdi/create/contabilidad/__init__.py b/satcfdi/create/contabilidad/__init__.py index e69de29..8b13789 100644 --- a/satcfdi/create/contabilidad/__init__.py +++ b/satcfdi/create/contabilidad/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/MOLE870717DRA202402BN.xlsx b/tests/MOLE870717DRA202402BN.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3c70e2903d2cc24da2ea2628108227338507ffc4 GIT binary patch literal 5594 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 Q0~_%TLd03&*XS1TKNek^!T + diff --git a/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402CT.xml b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402CT.xml new file mode 100644 index 0000000..b6389c5 --- /dev/null +++ b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402CT.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402PL.xml b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402PL.xml new file mode 100644 index 0000000..7864ada --- /dev/null +++ b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402PL.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XC.xml b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XC.xml new file mode 100644 index 0000000..cdcacfd --- /dev/null +++ b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XC.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XF.xml b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XF.xml new file mode 100644 index 0000000..e7f251d --- /dev/null +++ b/tests/test_contabilidad_electronica/ref/MOLE870717DRA202402XF.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/utils.py b/tests/utils.py index 25658a3..bfd1c8b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +import filecmp import inspect import os import uuid @@ -97,6 +98,35 @@ class XElementPrettyPrinter(PrettyPrinter): _dispatch[defaultdict.__repr__] = PrettyPrinter._pprint_dict +def compare_directories(dir1, dir2): + # Check if both paths are directories + if not (os.path.isdir(dir1) and os.path.isdir(dir2)): + return False + + # Use filecmp to compare directories + dcmp = filecmp.dircmp(dir1, dir2) + + # Check for common files that are different + if dcmp.diff_files: + print(f"Files that are different: {dcmp.diff_files} in {dir2}") + return False + + # Check for files present in one directory but not in the other + if dcmp.left_only or dcmp.right_only: + print(f"Files present only in one directory: {dcmp.left_only + dcmp.right_only} in {dir2}") + return False + + # Check for subdirectories that are not present in both directories + if dcmp.subdirs: + for subdir in dcmp.subdirs: + subdir1 = os.path.join(dir1, subdir) + subdir2 = os.path.join(dir2, subdir) + if not compare_directories(subdir1, subdir2): + return False + + return True + + if __name__ == "__main__": import os