diff --git a/cl_sii/rtc/data_models.py b/cl_sii/rtc/data_models.py index bb5241b2..fd7c03e9 100644 --- a/cl_sii/rtc/data_models.py +++ b/cl_sii/rtc/data_models.py @@ -47,6 +47,23 @@ def validate_cesion_seq(value: int) -> None: raise ValueError("Value is out of the valid range.", value) +def validate_cesion_fecha(value: datetime, tz: tz_utils.PytzTimezone) -> None: + """ + Validate value of date and time when the "cesión" happened. + + :raises ValueError: + """ + + tz_utils.validate_dt_tz(value, tz) + + now_tz_aware = tz_utils.get_now_tz_aware() + + if not (value.date() <= now_tz_aware.date()): + raise ValueError( + 'Value of "fecha_cesion_dt" must be before or equal to the current day.', value + ) + + def validate_cesion_monto(value: int) -> None: """ Validate amount of the "cesión". @@ -245,9 +262,9 @@ def validate_dte_tipo_dte(cls, v: object) -> object: return v @pydantic.validator('fecha_cesion_dt') - def validate_datetime_tz(cls, v: object) -> object: + def validate_fecha_cesion_dt(cls, v: object) -> object: if isinstance(v, datetime): - tz_utils.validate_dt_tz(v, cls.DATETIME_FIELDS_TZ) + validate_cesion_fecha(v, cls.DATETIME_FIELDS_TZ) return v @pydantic.validator('fecha_cesion_dt') @@ -321,6 +338,9 @@ class CesionL0: - Same timestamp as the "Registro AoR DTE" event ``DTE Cedido``. - The above statements were empirically verified for ``CesionNaturalKey(dte_key=DteNaturalKey(Rut('99***140-4'), 33, 3105), seq=2)``. + - When receiving an XML AEC document, the SII validates this date is before or + equal to the current day. + Source: (https://www.sii.cl/factura_electronica/ins_tecnico.pdf) .. warning:: The timestamp is generated by the signer of the AEC so it cannot be fully trusted. It is not clear how much validation is @@ -392,9 +412,9 @@ def validate_seq(cls, v: object) -> object: return v @pydantic.validator('fecha_cesion_dt') - def validate_datetime_tz(cls, v: object) -> object: + def validate_fecha_cesion_dt(cls, v: object) -> object: if isinstance(v, datetime): - tz_utils.validate_dt_tz(v, cls.DATETIME_FIELDS_TZ) + validate_cesion_fecha(v, cls.DATETIME_FIELDS_TZ) return v @@ -676,10 +696,13 @@ def as_dte_data_l2(self) -> dte_data_models.DteDataL2: # TODO: Validate value of 'fecha_ultimo_vencimiento' in relation to the DTE data. - @pydantic.validator( - 'fecha_cesion_dt', - 'fecha_firma_dt', - ) + @pydantic.validator('fecha_cesion_dt') + def validate_fecha_cesion_dt(cls, v: object) -> object: + if isinstance(v, datetime): + validate_cesion_fecha(v, cls.DATETIME_FIELDS_TZ) + return v + + @pydantic.validator('fecha_firma_dt') def validate_datetime_tz(cls, v: object) -> object: if isinstance(v, datetime): tz_utils.validate_dt_tz(v, cls.DATETIME_FIELDS_TZ) diff --git a/cl_sii/rtc/data_models_aec.py b/cl_sii/rtc/data_models_aec.py index 99ee7475..c2c3ec4e 100644 --- a/cl_sii/rtc/data_models_aec.py +++ b/cl_sii/rtc/data_models_aec.py @@ -324,9 +324,9 @@ def validate_contribuyente_razon_social(cls, v: object) -> object: return v @pydantic.validator('fecha_cesion_dt') - def validate_datetime_tz(cls, v: object) -> object: + def validate_fecha_cesion_dt(cls, v: object) -> object: if isinstance(v, datetime): - tz_utils.validate_dt_tz(v, cls.DATETIME_FIELDS_TZ) + data_models.validate_cesion_fecha(v, cls.DATETIME_FIELDS_TZ) return v @pydantic.root_validator(skip_on_failure=True) diff --git a/tests/test_rtc_data_models.py b/tests/test_rtc_data_models.py index f5e36807..774653c8 100644 --- a/tests/test_rtc_data_models.py +++ b/tests/test_rtc_data_models.py @@ -2,7 +2,7 @@ import dataclasses import unittest -from datetime import date, datetime +from datetime import date, datetime, timedelta import pydantic @@ -223,7 +223,7 @@ def test_validate_dte_tipo_dte(self) -> None: validation_errors = assert_raises_cm.exception.errors() self.assertIn(expected_validation_error, validation_errors) - def test_validate_datetime_tz(self) -> None: + def test_validate_fecha_cesion_dt(self) -> None: self._set_obj_1() obj = self.obj_1 @@ -269,6 +269,33 @@ def test_validate_datetime_tz(self) -> None: validation_errors = assert_raises_cm.exception.errors() self.assertIn(expected_validation_error, validation_errors) + # Test value constraints: + + tomorrow_tz_aware = tz_utils.get_now_tz_aware().astimezone( + CesionAltNaturalKey.DATETIME_FIELDS_TZ + ).replace(microsecond=0) + timedelta(days=1) + + expected_validation_error = { + 'loc': ('fecha_cesion_dt',), + 'msg': + '(' + '''\'Value of "fecha_cesion_dt" must be before or equal to the current day.\',''' + ' datetime.datetime(' + f'{tomorrow_tz_aware.strftime("%Y, %-m, %-d, %-H, %-M, %-S")},' + ' tzinfo=)' + ')', + 'type': 'value_error', + } + + with self.assertRaises(pydantic.ValidationError) as assert_raises_cm: + dataclasses.replace( + obj, + fecha_cesion_dt=tomorrow_tz_aware, + ) + + validation_errors = assert_raises_cm.exception.errors() + self.assertIn(expected_validation_error, validation_errors) + def test_truncate_fecha_cesion_dt_to_minutes(self) -> None: self._set_obj_1()