From 9ac6a44ae582c46555bac7da71addb9309bef3dd Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Thu, 26 Sep 2024 20:28:26 -0300 Subject: [PATCH 1/2] feat(dte): Add option to trust the input when parsing DTE XML documents Ref: https://app.shortcut.com/cordada/story/9837 --- src/cl_sii/dte/data_models.py | 3 ++ src/cl_sii/dte/parse.py | 51 ++++++++++++++++++++----------- src/tests/test_dte_data_models.py | 11 +++---- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/cl_sii/dte/data_models.py b/src/cl_sii/dte/data_models.py index 73f1b9a4..673bf92c 100644 --- a/src/cl_sii/dte/data_models.py +++ b/src/cl_sii/dte/data_models.py @@ -918,3 +918,6 @@ def validate_referencias_codigo_ref_is_consistent_with_tipo_dte(self) -> DteXmlD ) return self + + +DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER = pydantic.TypeAdapter(DteXmlData) diff --git a/src/cl_sii/dte/parse.py b/src/cl_sii/dte/parse.py index 0b5ddf80..fae2912c 100644 --- a/src/cl_sii/dte/parse.py +++ b/src/cl_sii/dte/parse.py @@ -117,7 +117,7 @@ def validate_dte_xml(xml_doc: XmlElement) -> None: xml_utils.validate_xml_doc(DTE_XML_SCHEMA_OBJ, xml_doc) -def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: +def parse_dte_xml(xml_doc: XmlElement, trust_input: bool = False) -> data_models.DteXmlData: """ Parse data from a DTE XML doc. @@ -125,6 +125,16 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: It is assumed that ``xml_doc`` is an ``{http://www.sii.cl/SiiDte}/DTE`` XML element. + :param xml_doc: + DTE XML document. + :param trust_input: + If ``True``, the input data is trusted to be valid and + some validation errors are replaced by warnings. + + .. warning:: + Use this option *only* if the DTE XML document was obtained directly + from the SII *and* you need to work around some validation errors + that the SII should have caught, but let through. :raises ValueError: :raises TypeError: :raises NotImplementedError: @@ -511,23 +521,28 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: _text_strip_or_raise(signature_key_info_x509_cert_em) ) - return data_models.DteXmlData( - emisor_rut=emisor_rut_value, - tipo_dte=tipo_dte_value, - folio=folio_value, - fecha_emision_date=fecha_emision_value, - receptor_rut=receptor_rut_value, - monto_total=monto_total_value, - emisor_razon_social=emisor_razon_social_value, - receptor_razon_social=receptor_razon_social_value, - fecha_vencimiento_date=fecha_vencimiento_value, - firma_documento_dt=tmst_firma_value, - signature_value=signature_signature_value, - signature_x509_cert_der=signature_key_info_x509_cert_der, - emisor_giro=emisor_giro_value, - emisor_email=emisor_email_value, - receptor_email=receptor_email_value, - referencias=referencia_xml_list, + return data_models.DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.validate_python( + dict( + emisor_rut=emisor_rut_value, + tipo_dte=tipo_dte_value, + folio=folio_value, + fecha_emision_date=fecha_emision_value, + receptor_rut=receptor_rut_value, + monto_total=monto_total_value, + emisor_razon_social=emisor_razon_social_value, + receptor_razon_social=receptor_razon_social_value, + fecha_vencimiento_date=fecha_vencimiento_value, + firma_documento_dt=tmst_firma_value, + signature_value=signature_signature_value, + signature_x509_cert_der=signature_key_info_x509_cert_der, + emisor_giro=emisor_giro_value, + emisor_email=emisor_email_value, + receptor_email=receptor_email_value, + referencias=referencia_xml_list, + ), + context={ + data_models.VALIDATION_CONTEXT_TRUST_INPUT: trust_input, + }, ) diff --git a/src/tests/test_dte_data_models.py b/src/tests/test_dte_data_models.py index 94f39fd2..d88b48a9 100644 --- a/src/tests/test_dte_data_models.py +++ b/src/tests/test_dte_data_models.py @@ -14,6 +14,7 @@ TipoDte, ) from cl_sii.dte.data_models import ( # noqa: F401 + DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER, VALIDATION_CONTEXT_TRUST_INPUT, DteDataL0, DteDataL1, @@ -1062,8 +1063,6 @@ def setUpClass(cls) -> None: 'test_data/sii-crypto/DTE--96670340-7--61--110616-cert.der' ) - cls.dte_xml_data_pydantic_type_adapter = pydantic.TypeAdapter(DteXmlData) - def setUp(self) -> None: super().setUp() @@ -1788,7 +1787,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_tipo_dte_for_trusted_i ) invalid_but_trusted_obj: Mapping[str, object] = { - **self.dte_xml_data_pydantic_type_adapter.dump_python(obj), + **DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.dump_python(obj), **dict( referencias=[obj_referencia], ), @@ -1797,7 +1796,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_tipo_dte_for_trusted_i try: with self.assertLogs('cl_sii.dte.data_models', level='WARNING') as assert_logs_cm: - self.dte_xml_data_pydantic_type_adapter.validate_python( + DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.validate_python( invalid_but_trusted_obj, context=validation_context ) except pydantic.ValidationError as exc: @@ -1876,7 +1875,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_emisor_rut_for_trusted ) invalid_but_trusted_obj: Mapping[str, object] = { - **self.dte_xml_data_pydantic_type_adapter.dump_python(obj), + **DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.dump_python(obj), **dict( referencias=[obj_referencia], ), @@ -1885,7 +1884,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_emisor_rut_for_trusted try: with self.assertLogs('cl_sii.dte.data_models', level='WARNING') as assert_logs_cm: - self.dte_xml_data_pydantic_type_adapter.validate_python( + DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.validate_python( invalid_but_trusted_obj, context=validation_context ) except pydantic.ValidationError as exc: From 152a909237615f179cbeae770f20c33dd6174662 Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Thu, 26 Sep 2024 20:32:43 -0300 Subject: [PATCH 2/2] fix(rtc): Fully propagate value of `trust_input` in `parse_aec_xml()` Ref: https://app.shortcut.com/cordada/story/9837 --- src/cl_sii/rtc/data_models_aec.py | 3 +++ src/cl_sii/rtc/parse_aec.py | 38 ++++++++++++++++++------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/cl_sii/rtc/data_models_aec.py b/src/cl_sii/rtc/data_models_aec.py index 271af93e..94481e57 100644 --- a/src/cl_sii/rtc/data_models_aec.py +++ b/src/cl_sii/rtc/data_models_aec.py @@ -798,3 +798,6 @@ def validate_signature_value_and_signature_x509_cert_der_may_only_be_none_togeth ) return self + + +AEC_XML_PYDANTIC_TYPE_ADAPTER = pydantic.TypeAdapter(AecXml) diff --git a/src/cl_sii/rtc/parse_aec.py b/src/cl_sii/rtc/parse_aec.py index 54ee4f96..ffbdc65e 100644 --- a/src/cl_sii/rtc/parse_aec.py +++ b/src/cl_sii/rtc/parse_aec.py @@ -26,7 +26,7 @@ import cl_sii.dte.data_models import cl_sii.dte.parse from cl_sii.dte.constants import TipoDte -from cl_sii.dte.data_models import DteXmlData +from cl_sii.dte.data_models import DteXmlData, is_input_trusted_according_to_validation_context from cl_sii.dte.parse import DTE_XMLNS_MAP from cl_sii.libs import encoding_utils, tz_utils, xml_utils from cl_sii.libs.xml_utils import XmlElement @@ -88,7 +88,7 @@ def parse_aec_xml(xml_doc: XmlElement, trust_input: bool = False) -> data_models that the SII should have caught, but let through. """ aec_struct = _Aec.parse_xml(xml_doc, trust_input=trust_input) - return aec_struct.as_aec_xml() + return aec_struct.as_aec_xml(trust_input=trust_input) ############################################################################### @@ -656,10 +656,11 @@ def parse_xml_to_dict(xml_em: XmlElement) -> Mapping[str, object]: @pydantic.field_validator('dte', mode='before') @classmethod - def validate_dte(cls, v: object) -> object: + def validate_dte(cls, v: object, info: pydantic.ValidationInfo) -> object: if isinstance(v, XmlElement): cl_sii.dte.parse.validate_dte_xml(v) - v = cl_sii.dte.parse.parse_dte_xml(v) + trust_dte = is_input_trusted_according_to_validation_context(info.context) + v = cl_sii.dte.parse.parse_dte_xml(v, trust_input=trust_dte) return v # @pydantic.validator('tmst_firma') @@ -909,7 +910,7 @@ def parse_xml(cls, xml_doc: XmlElement, trust_input: bool = False) -> _Aec: }, ) - def as_aec_xml(self) -> data_models_aec.AecXml: + def as_aec_xml(self, trust_input: bool = False) -> data_models_aec.AecXml: doc_aec_struct = self.documento_aec signature_over_doc_aec_struct = self.signature @@ -922,17 +923,22 @@ def as_aec_xml(self) -> data_models_aec.AecXml: cesion_struct.as_cesion_aec_xml() for cesion_struct in cesion_struct_list ] - return data_models_aec.AecXml( - dte=dte, - cedente_rut=caratula_struct.rut_cedente, - cesionario_rut=caratula_struct.rut_cesionario, - fecha_firma_dt=caratula_struct.tmst_firmaenvio, - signature_value=signature_over_doc_aec_struct.signature_value, - signature_x509_cert_der=signature_over_doc_aec_struct.key_info_x509_data_x509_cert, - cesiones=aec_xml_cesion_list, - contacto_nombre=caratula_struct.nmb_contacto, - contacto_telefono=caratula_struct.fono_contacto, - contacto_email=caratula_struct.mail_contacto, + return data_models_aec.AEC_XML_PYDANTIC_TYPE_ADAPTER.validate_python( + dict( + dte=dte, + cedente_rut=caratula_struct.rut_cedente, + cesionario_rut=caratula_struct.rut_cesionario, + fecha_firma_dt=caratula_struct.tmst_firmaenvio, + signature_value=signature_over_doc_aec_struct.signature_value, + signature_x509_cert_der=signature_over_doc_aec_struct.key_info_x509_data_x509_cert, + cesiones=aec_xml_cesion_list, + contacto_nombre=caratula_struct.nmb_contacto, + contacto_telefono=caratula_struct.fono_contacto, + contacto_email=caratula_struct.mail_contacto, + ), + context={ + cl_sii.dte.data_models.VALIDATION_CONTEXT_TRUST_INPUT: trust_input, + }, ) @staticmethod