Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 99 additions & 9 deletions cl_sii/cte/f29/parse_datos_obj.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import copy
import json
from datetime import datetime
from decimal import Decimal
from pathlib import Path
Expand All @@ -23,10 +25,17 @@
)
CTE_F29_DATOS_OBJ_SCHEMA = read_json_schema(_CTE_F29_DATOS_OBJ_SCHEMA_PATH)

_CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES_PATH = (
Path(__file__).parent.parent.parent / 'data' / 'cte' / 'f29_datos_obj_missing_key_fixes.json'
)
CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES: SiiCteF29DatosObjType = json.load(
open(_CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES_PATH)
)


def parse_sii_cte_f29_datos_obj(
datos_obj: SiiCteF29DatosObjType,
schema_validator: Optional[Callable[[SiiCteF29DatosObjType], None]] = None,
schema_validator: Optional[Callable[[SiiCteF29DatosObjType], SiiCteF29DatosObjType]] = None,
campo_deserializer: Optional[Callable[[object, str], object]] = None,
) -> CteForm29:
"""
Expand Down Expand Up @@ -55,7 +64,7 @@ def parse_sii_cte_f29_datos_obj(

def _parse_sii_cte_f29_datos_obj_to_dict(
datos_obj: SiiCteF29DatosObjType,
schema_validator: Callable[[SiiCteF29DatosObjType], None],
schema_validator: Callable[[SiiCteF29DatosObjType], SiiCteF29DatosObjType],
campo_deserializer: Callable[[object, str], object],
) -> Mapping[str, object]:
"""
Expand All @@ -67,17 +76,17 @@ def _parse_sii_cte_f29_datos_obj_to_dict(
:param campo_deserializer:
:raises JsonSchemaValidationError: If schema validation fails.
"""
schema_validator(datos_obj)
validated_datos_obj = schema_validator(datos_obj)

datos_obj_campos: Mapping[int, str] = {
int(code): str(value) for code, value in datos_obj['campos'].items()
int(code): str(value) for code, value in validated_datos_obj['campos'].items()
}
datos_obj_extras: Mapping[str, object] = datos_obj['extras']
datos_obj_extras: Mapping[str, object] = validated_datos_obj['extras']
datos_obj_glosa: Mapping[int, str] = { # noqa: F841
int(code): str(value) for code, value in datos_obj['glosa'].items()
int(code): str(value) for code, value in validated_datos_obj['glosa'].items()
}
datos_obj_tipos: Mapping[int, str] = {
int(code): str(value) for code, value in datos_obj['tipos'].items()
int(code): str(value) for code, value in validated_datos_obj['tipos'].items()
}

deserialized_datos_obj_campos = {
Expand Down Expand Up @@ -156,12 +165,14 @@ def cte_f29_datos_obj_campo_best_effort_deserializer(campo_value: object, tipo:
return deserialized_value


def cte_f29_datos_schema_default_validator(datos_obj: SiiCteF29DatosObjType) -> None:
def cte_f29_datos_schema_default_validator(
datos_obj: SiiCteF29DatosObjType,
) -> SiiCteF29DatosObjType:
"""
Validate the ``datos`` object against the schema.

:raises JsonSchemaValidationError: If schema validation fails.
:returns: ``None`` if schema validation passed.
:returns: Validated ``datos`` object if schema validation passed.
"""
try:
jsonschema.validate(datos_obj, schema=CTE_F29_DATOS_OBJ_SCHEMA)
Expand All @@ -172,3 +183,82 @@ def cte_f29_datos_schema_default_validator(datos_obj: SiiCteF29DatosObjType) ->
raise JsonSchemaValidationError("The keys of 'campos' and 'tipos' are not exactly the same")
if datos_obj['campos'].keys() != datos_obj['glosa'].keys():
raise JsonSchemaValidationError("The keys of 'campos' and 'tipos' are not exactly the same")

return datos_obj


def cte_f29_datos_schema_best_effort_validator(
datos_obj: SiiCteF29DatosObjType,
) -> SiiCteF29DatosObjType:
"""
Validate the ``datos`` object against the schema.

If there are missing keys in the `tipos` or `glosa` dicts, it will try to get them
from a list of default values.

:raises JsonSchemaValidationError: If schema validation fails.
:returns: Validated ``datos`` object if schema validation passed.
"""
try:
validated_datos_obj = cte_f29_datos_schema_default_validator(datos_obj)
except JsonSchemaValidationError as exc:
if exc.__cause__ is jsonschema.exceptions.ValidationError:
# We will not try to fix this kind of error.
raise
elif exc.__cause__ is None:
# Let's try to fix this.
new_datos_obj = try_fix_cte_f29_datos(datos_obj)

# Let's try again.
cte_f29_datos_schema_default_validator(new_datos_obj)
return new_datos_obj
else:
raise
else:
return validated_datos_obj


def try_fix_cte_f29_datos(datos_obj: SiiCteF29DatosObjType) -> SiiCteF29DatosObjType:
"""
Try to fix the ``datos`` object.

If there are missing keys in the `tipos` or `glosa` dicts, it will try to get them
from a list of default values.

:raises JsonSchemaValidationError: If an unfixable issue is found.
:returns: A possibly fixed ``datos`` object.
"""
new_datos_obj: Mapping[str, MutableMapping[str, object]]
new_datos_obj = copy.deepcopy(datos_obj) # type: ignore[arg-type]

campos_tipos_keys_diff = datos_obj['campos'].keys() - datos_obj['tipos'].keys()
remaining_campos_tipos_diff = (
campos_tipos_keys_diff - CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES['tipos'].keys()
)
if remaining_campos_tipos_diff:
raise JsonSchemaValidationError(
"The keys of 'campos' and 'tipos' differ for the following codes: "
f"{remaining_campos_tipos_diff}"
)
else:
for missing_key in campos_tipos_keys_diff:
new_datos_obj['tipos'][missing_key] = CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES['tipos'][
missing_key
]

campos_glosa_keys_diff = datos_obj['campos'].keys() - datos_obj['glosa'].keys()
remaining_campos_glosa_diff = (
campos_glosa_keys_diff - CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES['glosa'].keys()
)
if remaining_campos_glosa_diff:
raise JsonSchemaValidationError(
"The keys of 'campos' and 'glosa' differ for the following codes: "
f"{remaining_campos_glosa_diff}"
)
else:
for missing_key in campos_glosa_keys_diff:
new_datos_obj['glosa'][missing_key] = CTE_F29_DATOS_OBJ_MISSING_KEY_FIXES['glosa'][
missing_key
]

return new_datos_obj
8 changes: 8 additions & 0 deletions cl_sii/data/cte/f29_datos_obj_missing_key_fixes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"glosa": {
"049": "(Desconocido)"
},
"tipos": {
"049": "M"
}
}
107 changes: 107 additions & 0 deletions tests/test_cte_f29_parse_datos_obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,110 @@ def test_full_2(self) -> None:
"fecha_presentacion": datetime.date(2019, 1, 22),
}
self.assertEqual(dict(obj), expected_dict)

def test_full_3_file_with_missing_value(self) -> None:
datos_obj: Mapping[str, Any] = read_test_file_json_dict(
'test_data/sii-cte/f29/cte--61002000-3--f29-6600000016-datos-obj-fake-missing-code-49.json', # noqa: E501
)
obj = parse_datos_obj.parse_sii_cte_f29_datos_obj(
datos_obj=datos_obj,
schema_validator=parse_datos_obj.cte_f29_datos_schema_best_effort_validator,
)

self.assertIsInstance(obj, data_models.CteForm29)
self.assertEqual(obj.contribuyente_rut, Rut('61002000-3'))
self.assertEqual(obj.periodo_tributario, PeriodoTributario(year=2017, month=7))
self.assertEqual(obj.folio, 6600000016)

expected_codes_dict = {
1: "SERVICIO DE REGISTRO CIVIL E IDENTIFICACION",
3: Rut('61002000-3'),
6: "HUERFANOS 1570",
7: 6600000016,
8: "SANTIAGO",
15: PeriodoTributario(year=2017, month=7),
30: 723122062,
48: 1409603,
49: 211345,
60: 70,
89: 0,
77: 11429763,
91: 1536269,
92: 0,
93: 224400,
94: 1603589,
151: 126666,
315: datetime.date(2017, 8, 28),
502: 6588397,
503: 9,
504: 17037344,
511: 980816,
519: 30,
520: 1481178,
527: 2,
528: 512805,
537: 18018160,
538: 6588397,
547: 1536269,
562: 17296416,
584: 10,
595: 1536269,
761: 1,
762: 12443,
795: 157080,
915: datetime.date(2017, 10, 31),
922: "013-2015",
}
self.assertEqual(obj.as_codes_dict(include_none=False), expected_codes_dict)

expected_dict = {
"contribuyente_rut": Rut('61002000-3'),
"periodo_tributario": PeriodoTributario(year=2017, month=7),
"folio": 6600000016,
"apellido_paterno_o_razon_social": "SERVICIO DE REGISTRO CIVIL E IDENTIFICACION",
"apellido_materno": None,
"nombres": None,
"calle_direccion": "HUERFANOS 1570",
"numero_direccion": None,
"comuna_direccion": "SANTIAGO",
"telefono": None,
"correo_electronico": None,
"representante_legal_rut": None,
"extra": {
30: 723122062,
48: 1409603,
49: 211345,
77: 11429763,
89: 0,
92: 0,
93: 224400,
151: 126666,
502: 6588397,
503: 9,
504: 17037344,
511: 980816,
519: 30,
520: 1481178,
527: 2,
528: 512805,
537: 18018160,
538: 6588397,
547: 1536269,
562: 17296416,
584: 10,
595: 1536269,
761: 1,
762: 12443,
795: 157080,
},
"total_a_pagar_en_plazo_legal": 1536269,
"total_a_pagar_con_recargo": 1603589,
"pct_condonacion": 70,
"num_res_condonacion": "013-2015",
"fecha_condonacion": datetime.date(2017, 10, 31),
"tipo_declaracion": "Primitiva",
"banco": "Banco del Patito Amarillo",
"medio_pago": "PEL",
"fecha_presentacion": datetime.date(2017, 8, 28),
}
self.assertEqual(dict(obj), expected_dict)
Loading