diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 4591cda5..138ea16e 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -23,6 +23,7 @@ import cl_sii.contribuyente.constants import cl_sii.rut.constants from cl_sii.libs import encoding_utils +from cl_sii.libs import tz_utils from cl_sii.rut import Rut from . import constants @@ -95,6 +96,13 @@ def validate_non_empty_bytes(value: bytes) -> None: raise ValueError("Bytes value length (stripped) is 0.") +def validate_correct_tz(value: datetime, tz: tz_utils.PytzTimezone) -> None: + if not tz_utils.dt_is_aware(value): + raise ValueError("Value must be a timezone-aware datetime.", value) + if value.tzinfo.zone != tz.zone: # type: ignore + raise ValueError(f"Timezone of datetime value must be '{tz.zone!s}'.", value) + + @dataclasses.dataclass(frozen=True) class DteNaturalKey: @@ -318,6 +326,16 @@ class DteDataL2(DteDataL1): """ + ########################################################################### + # constants + ########################################################################### + + DATETIME_FIELDS_TZ = tz_utils.TZ_CL_SANTIAGO + + ########################################################################### + # fields + ########################################################################### + emisor_razon_social: str = dc_field() """ "Razón social" (legal name) of the "emisor" of the DTE. @@ -333,7 +351,7 @@ class DteDataL2(DteDataL1): "Fecha de vencimiento (pago)" of the DTE. """ - firma_documento_dt_naive: Optional[datetime] = dc_field(default=None) + firma_documento_dt: Optional[datetime] = dc_field(default=None) """ Datetime on which the "documento" was digitally signed. """ @@ -386,9 +404,10 @@ def __post_init__(self) -> None: if not isinstance(self.fecha_vencimiento_date, date): raise TypeError("Inappropriate type of 'fecha_vencimiento_date'.") - if self.firma_documento_dt_naive is not None: - if not isinstance(self.firma_documento_dt_naive, datetime): - raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.") + if self.firma_documento_dt is not None: + if not isinstance(self.firma_documento_dt, datetime): + raise TypeError("Inappropriate type of 'firma_documento_dt'.") + validate_correct_tz(self.firma_documento_dt, self.DATETIME_FIELDS_TZ) if self.signature_value is not None: if not isinstance(self.signature_value, bytes): diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index 5cd5831f..77b5cc79 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -24,6 +24,7 @@ from typing import Tuple from cl_sii.libs import encoding_utils +from cl_sii.libs import tz_utils from cl_sii.libs import xml_utils from cl_sii.libs.xml_utils import XmlElement, XmlElementTree from cl_sii.rut import Rut @@ -447,7 +448,9 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: monto_total_value = int(monto_total_em.text.strip()) - tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text) + tmst_firma_value = tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime.fromisoformat(tmst_firma_em.text), + tz=data_models.DteDataL2.DATETIME_FIELDS_TZ) signature_signature_value = encoding_utils.decode_base64_strict( signature_signature_value_em.text.strip()) @@ -464,7 +467,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: emisor_razon_social=emisor_razon_social_value, receptor_razon_social=receptor_razon_social_value, fecha_vencimiento_date=fecha_vencimiento_value, - firma_documento_dt_naive=tmst_firma_value, + firma_documento_dt=tmst_firma_value, signature_value=signature_signature_value, signature_x509_cert_pem=signature_key_info_x509_cert_pem, emisor_giro=emisor_giro_value, diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index d0d4acad..03fdcdc0 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -2,6 +2,7 @@ import unittest from datetime import date, datetime +from cl_sii.libs import tz_utils from cl_sii.rut import Rut # noqa: F401 from cl_sii.dte.constants import TipoDteEnum # noqa: F401 @@ -150,7 +151,9 @@ def setUp(self) -> None: emisor_razon_social='INGENIERIA ENACON SPA', receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=None, signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', @@ -175,7 +178,9 @@ def test_as_dict(self) -> None: emisor_razon_social='INGENIERIA ENACON SPA', receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=None, signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', @@ -197,3 +202,23 @@ def test_validate_dte_folio(self) -> None: def test_validate_dte_monto_total(self) -> None: # TODO: implement for 'validate_dte_monto_total' pass + + def test_validate_clean_str(self) -> None: + # TODO: implement for 'validate_clean_str' + pass + + def test_validate_clean_bytes(self) -> None: + # TODO: implement for 'validate_clean_bytes' + pass + + def test_validate_non_empty_str(self) -> None: + # TODO: implement for 'validate_non_empty_str' + pass + + def test_validate_non_empty_bytes(self) -> None: + # TODO: implement for 'validate_non_empty_bytes' + pass + + def test_validate_correct_tz(self) -> None: + # TODO: implement for 'validate_correct_tz' + pass diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index 1944f17d..f18b4396 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -4,8 +4,10 @@ from datetime import date, datetime import cl_sii.dte.constants +from cl_sii.dte.data_models import DteDataL2 from cl_sii.libs import crypto_utils from cl_sii.libs import encoding_utils +from cl_sii.libs import tz_utils from cl_sii.libs import xml_utils from cl_sii.rut import Rut @@ -341,7 +343,9 @@ def test_parse_dte_xml_ok_1(self) -> None: emisor_razon_social='INGENIERIA ENACON SPA', receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=self._TEST_DTE_1_SIGNATURE_VALUE, signature_x509_cert_pem=self.dte_clean_xml_1_cert_pem_bytes, emisor_giro='Ingenieria y Construccion', @@ -365,7 +369,9 @@ def test_parse_dte_xml_ok_2(self) -> None: emisor_razon_social='COMERCIALIZADORA INNOVA MOBEL SPA', receptor_razon_social='EMPRESAS LA POLAR S.A.', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 3, 28, 13, 59, 52), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 3, 28, 13, 59, 52), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=self._TEST_DTE_2_SIGNATURE_VALUE, signature_x509_cert_pem=self.dte_clean_xml_2_cert_pem_bytes, emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR',