diff --git a/Dockerfile b/Dockerfile index 7894872..36f18b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,34 +7,40 @@ RUN apt install software-properties-common -y \ && add-apt-repository ppa:deadsnakes/ppa RUN apt-get install -y --no-install-recommends \ - python3.9 python3.9-distutils python3.9-dev \ python3.10 python3.10-distutils python3.10-dev \ python3.11 python3.11-distutils python3.11-dev \ python3.12 python3-setuptools python3.12-dev \ + python3.13 python3-setuptools python3.13-dev \ wget \ ca-certificates RUN wget https://bootstrap.pypa.io/get-pip.py \ - && python3.9 get-pip.py pip==23.2.1 --break-system-packages \ && python3.10 get-pip.py pip==23.2.1 --break-system-packages \ && python3.11 get-pip.py pip==23.2.1 --break-system-packages \ && python3.12 get-pip.py pip==23.2.1 --break-system-packages \ + && python3.13 get-pip.py pip==23.2.1 --break-system-packages \ && rm get-pip.py RUN apt-get install -y --no-install-recommends \ - libxml2-dev \ - libxmlsec1-dev \ - build-essential \ + gcc \ + build-essential \ + pkg-config \ + libxml2-dev \ + libxmlsec1-dev \ + xmlsec1 \ + libxml2-dev \ + libxslt-dev \ zip -RUN python3.9 --version RUN python3.10 --version RUN python3.11 --version RUN python3.12 --version +RUN python3.13 --version + -RUN pip3.9 install setuptools setuptools-rust RUN pip3.10 install setuptools setuptools-rust RUN pip3.11 install setuptools setuptools-rust --break-system-packages RUN pip3.12 install setuptools setuptools-rust --break-system-packages +RUN pip3.13 install setuptools setuptools-rust --break-system-packages RUN pip3 install tox pytest --break-system-packages diff --git a/Makefile.dev b/Makefile.dev index ae90ab9..03e6a40 100644 --- a/Makefile.dev +++ b/Makefile.dev @@ -15,7 +15,7 @@ dev-shell: docker run --rm -ti -v "$(PWD):/app" -w /app --name facho-cli facho bash test: - docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.12 setup.py test' + docker run -t -v $(PWD):/app -w /app facho sh -c 'cd /app; python3.13 -m pytest' tox: docker run -it -v $(PWD)/:/app -w /app facho tox diff --git a/facho/facho.py b/facho/facho.py index 0970c2c..19dc8e6 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -140,12 +140,12 @@ class LXMLBuilder: attrs['pretty_print'] = attrs.pop('pretty_print', False) attrs['encoding'] = attrs.pop('encoding', 'UTF-8') - for el in elem.getiterator(): + for el in elem.iter(): keys = filter(lambda key: key.startswith('facho_'), el.keys()) self.remove_attributes(el, keys, exclude=['facho_optional']) is_optional = el.get('facho_optional', 'False') == 'True' - if is_optional and el.getchildren() == [] and el.keys() == [ + if is_optional and list(el) == [] and el.keys() == [ 'facho_optional']: el.getparent().remove(el) @@ -291,7 +291,7 @@ class FachoXML: # se fuerza la adicion como un nuevo elemento if append: last_slibing = None - for child in parent.getchildren(): + for child in list(parent): if child.tag == node_tag: last_slibing = child diff --git a/facho/fe/client/dian.py b/facho/fe/client/dian.py index 3da2cbf..fdd3611 100644 --- a/facho/fe/client/dian.py +++ b/facho/fe/client/dian.py @@ -8,7 +8,7 @@ import xmlsec import urllib.request from datetime import datetime from dataclasses import dataclass, asdict, field -from typing import List + import http.client import hashlib import secrets @@ -47,7 +47,7 @@ class GetNumberingRangeResponse: ValidateDateTo: str TechnicalKey: str - NumberRangeResponse: List[NumberRangeResponse] + NumberRangeResponse: list[NumberRangeResponse] @classmethod @@ -91,7 +91,7 @@ class SendBillAsync(SOAPService): @dataclass class SendTestSetAsyncResponse: ZipKey: str - ErrorMessageList: List[str] + ErrorMessageList: list[str] @classmethod def fromdict(cls, data): @@ -134,7 +134,7 @@ class GetStatusResponse: IsValid: bool StatusDescription: str StatusCode: int - ErrorMessage: List[str] + ErrorMessage: list[str] @classmethod def fromdict(cls, data): diff --git a/facho/fe/client/wsse/signature.py b/facho/fe/client/wsse/signature.py index cc42a3d..6d69adc 100644 --- a/facho/fe/client/wsse/signature.py +++ b/facho/fe/client/wsse/signature.py @@ -9,7 +9,7 @@ module. """ import pytz -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from lxml import etree from lxml.etree import QName @@ -222,7 +222,7 @@ def sign_envelope( return _sign_envelope_with_key(envelope, key, signature_method, digest_method) def get_timestamp(timestamp = None, delta=None): - timestamp = timestamp or datetime.utcnow() + timestamp = timestamp or datetime.now(timezone.utc) if delta: timestamp += delta diff --git a/facho/fe/data/dian/codelist/__init__.py b/facho/fe/data/dian/codelist/__init__.py index 837009d..4c8d9bf 100644 --- a/facho/fe/data/dian/codelist/__init__.py +++ b/facho/fe/data/dian/codelist/__init__.py @@ -35,8 +35,8 @@ class CodeList: row = {} #construir registro... - for value in xmlrow.getchildren(): - row[value.attrib['ColumnRef']] = value.getchildren()[0].text + for value in list(xmlrow): + row[value.attrib['ColumnRef']] = list(value)[0].text return row diff --git a/facho/fe/fe.py b/facho/fe/fe.py index a703606..1c6163e 100644 --- a/facho/fe/fe.py +++ b/facho/fe/fe.py @@ -5,7 +5,6 @@ import uuid import xmlsig import xades from datetime import datetime -import OpenSSL import zipfile # import warnings import hashlib @@ -18,6 +17,21 @@ from dateutil import tz from cryptography.hazmat.primitives.serialization import pkcs12 +# Monkey-patch xades/xmlsig para compatibilidad con pyOpenSSL >= 24 +# (PKCS12 fue eliminado de OpenSSL.crypto, pero ambas librerías lo referencian) +import OpenSSL +import xades.xades_context + +if not hasattr(OpenSSL.crypto, 'PKCS12'): + def _patched_load_pkcs12(self, key): + if isinstance(key, tuple): + self.x509 = key[1] + self.public_key = key[1].public_key() + self.private_key = key[0] + else: + raise NotImplementedError("unsupported key type") + xades.xades_context.XAdESContext.load_pkcs12 = _patched_load_pkcs12 + AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] @@ -444,9 +458,6 @@ class DianXMLExtensionSigner: self._pkcs12_data, self._passphrase)) - # ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12( - # self._pkcs12_data, - # self._passphrase)) if self._localpolicy: with mock_xades_policy(): ctx.sign(signature) @@ -590,8 +601,8 @@ class DianXMLExtensionSignerVerifier: if isinstance(self._pkcs12_path_or_bytes, str): pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read() ctx = xades.XAdESContext() - ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data, - self._passphrase)) + ctx.load_pkcs12(pkcs12.load_key_and_certificates( + pkcs12_data, self._passphrase)) try: if self._localpolicy: with mock_xades_policy(): diff --git a/facho/fe/form/__init__.py b/facho/fe/form/__init__.py index 026eef8..267b8f8 100644 --- a/facho/fe/form/__init__.py +++ b/facho/fe/form/__init__.py @@ -11,7 +11,6 @@ from datetime import datetime, date # from collections import defaultdict import decimal from decimal import Decimal -import typing from ..data.dian import codelist DECIMAL_PRECISION = 6 @@ -60,7 +59,7 @@ class AmountCollection(Collection): class Amount: def __init__( - self, amount: typing.Union[int, float, str, "Amount"], + self, amount: int | float | str | "Amount", currency: Currency = Currency('COP')): # DIAN 1.7.-2020: 1.2.3.1 @@ -294,7 +293,7 @@ class TaxScheme: class Party: name: str ident: str - responsability_code: typing.List[Responsability] + responsability_code: list[Responsability] responsability_regime_code: str organization_code: str tax_scheme: TaxScheme = field(default_factory=lambda: TaxScheme('01')) @@ -327,7 +326,7 @@ class TaxScheme: @dataclass class TaxSubTotal: percent: float - scheme: typing.Optional[TaxScheme] = None + scheme: TaxScheme | None = None tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) def calculate(self, invline): @@ -359,7 +358,7 @@ class TaxTotalOmit(TaxTotal): @dataclass class WithholdingTaxSubTotal: percent: float - scheme: typing.Optional[TaxScheme] = None + scheme: TaxScheme | None = None tax_amount: Amount = field(default_factory=lambda: Amount(0.0)) def calculate(self, invline): @@ -491,7 +490,7 @@ class AllowanceCharge: reason: AllowanceChargeReason = None # Valor Base para calcular el descuento o el cargo - base_amount: typing.Optional[Amount] = field( + base_amount: Amount | None = field( default_factory=lambda: Amount(0.0)) # Porcentaje: Porcentaje que aplicar. @@ -536,9 +535,9 @@ class InvoiceLine: # ya que al reportar los totales es sobre # la factura y el percent es unico por type_code # de subtotal - tax: typing.Optional[TaxTotal] - withholding: typing.Optional[WithholdingTaxTotal] - allowance_charge: typing.List[AllowanceCharge] = dataclasses.field( + tax: TaxTotal | None + withholding: WithholdingTaxTotal | None + allowance_charge: list[AllowanceCharge] = dataclasses.field( default_factory=list) def add_allowance_charge(self, charge): diff --git a/facho/fe/nomina/__init__.py b/facho/fe/nomina/__init__.py index a1f523a..bb66ad0 100644 --- a/facho/fe/nomina/__init__.py +++ b/facho/fe/nomina/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import datetime import hashlib -import typing + from .. import fe from .. import form @@ -86,13 +86,13 @@ class NumeroSecuencia: @dataclass class Periodo: - fecha_ingreso: typing.Union[str, Fecha] - fecha_liquidacion_inicio: typing.Union[str, Fecha] - fecha_liquidacion_fin: typing.Union[str, Fecha] - fecha_generacion: typing.Union[str, Fecha] + fecha_ingreso: str | Fecha + fecha_liquidacion_inicio: str | Fecha + fecha_liquidacion_fin: str | Fecha + fecha_generacion: str | Fecha tiempo_laborado: int = 1 - fecha_retiro: typing.Union[str, Fecha] = None + fecha_retiro: str | Fecha | None = None def __post_init__(self): self.fecha_ingreso = Fecha.cast(self.fecha_ingreso) @@ -259,7 +259,7 @@ class InformacionGeneral: def __str__(self): self.valor - fecha_generacion: typing.Union[str, Fecha] + fecha_generacion: str | Fecha hora_generacion: str periodo_nomina: PeriodoNomina tipo_moneda: TipoMoneda diff --git a/facho/fe/nomina/devengado/horas_extras.py b/facho/fe/nomina/devengado/horas_extras.py index de21772..c591781 100644 --- a/facho/fe/nomina/devengado/horas_extras.py +++ b/facho/fe/nomina/devengado/horas_extras.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List + from ..amount import Amount from .devengado import Devengado @@ -30,7 +30,7 @@ class DevengadoHoraExtra: @dataclass class DevengadoHorasExtrasDiarias(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HEDs') @@ -39,7 +39,7 @@ class DevengadoHorasExtrasDiarias(Devengado): @dataclass class DevengadoHorasExtrasNocturnas(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HENs') @@ -49,7 +49,7 @@ class DevengadoHorasExtrasNocturnas(Devengado): @dataclass class DevengadoHorasRecargoNocturno(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HRNs') @@ -58,7 +58,7 @@ class DevengadoHorasRecargoNocturno(Devengado): @dataclass class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HEDDFs') @@ -67,7 +67,7 @@ class DevengadoHorasExtrasDiariasDominicalesYFestivos(Devengado): @dataclass class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HRDDFs') @@ -76,7 +76,7 @@ class DevengadoHorasRecargoDiariasDominicalesYFestivos(Devengado): @dataclass class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HENDFs') @@ -85,7 +85,7 @@ class DevengadoHorasExtrasNocturnasDominicalesYFestivos(Devengado): @dataclass class DevengadoHorasRecargoNocturnoDominicalesYFestivos(Devengado): - horas_extras: List[DevengadoHoraExtra] + horas_extras: list[DevengadoHoraExtra] def apply(self, fragment): hora_extra_xml = fragment.fragment('./HRNDFs') diff --git a/requirements_dev.txt b/requirements_dev.txt index e96da27..47ca138 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,17 +1,15 @@ -attrs==22.1.0 -distlib==0.3.6 -filelock==3.8.0 -iniconfig==1.1.1 -packaging==21.3 -platformdirs==2.5.2 -pluggy==1.0.0 -py==1.11.0 -pyparsing==3.0.9 -pytest==7.1.3 +attrs==24.2.0 +distlib==0.3.9 +filelock==3.16.1 +iniconfig==2.0.0 +packaging==24.1 +platformdirs==4.3.6 +pluggy==1.5.0 +pyparsing==3.2.0 +pytest==8.3.4 semantic-version==2.10.0 -setuptools-rust==1.5.2 +setuptools-rust==1.10.2 six==1.16.0 -tomli==2.0.1 -tox==3.26.0 -typing_extensions==4.4.0 -virtualenv==20.16.5 +tomli==2.2.1 +tox==4.23.2 +virtualenv==20.28.0 diff --git a/setup.py b/setup.py index 226c7f8..b355046 100644 --- a/setup.py +++ b/setup.py @@ -14,14 +14,14 @@ with open('HISTORY.rst') as history_file: history = history_file.read() requirements = ['Click>=8.1.7', - 'zeep==4.2.1', - 'lxml==5.2.2', - 'cryptography==3.3.2', - 'pyOpenSSL==20.0.1', - 'xmlsig==0.1.7', - 'xades==1.0.0', - 'xmlsec==1.3.14', - 'python-dateutil==2.9.0.post0', + 'zeep>=4.2.1', + 'lxml>=5.2.2', + 'cryptography>=41.0.0', + 'pyOpenSSL>=23.2.0', + 'xmlsig>=0.1.9', + 'xades>=1.0.0', + 'xmlsec>=1.3.12', + 'python-dateutil>=2.9.0', # usamos esta dependencia en runtime # para forzar uso de policy_id de archivo local 'mock>=5.1.0', @@ -57,10 +57,10 @@ setup( 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], description="Facturacion Electronica Colombia", entry_points={ diff --git a/tox.ini b/tox.ini index f06bea6..89f9089 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312, flake8 +envlist = py39, py310, py311, py312, py313, flake8 [travis] python = @@ -7,6 +7,7 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [testenv:flake8] basepython = python