From 494dac6ef0efd031d493b5f9232132177f8cd299 Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Wed, 10 Sep 2025 21:14:20 -0300 Subject: [PATCH 1/6] chore(tests): Refactor and improve constants tests Refactor several test files for enum and constant classes to improve maintainability, consistency, and clarity. The main changes involve shifting from direct imports and usage of module-level constants/enums to storing them as class variables set up in `setUpClass()`, and enhancing the tests to check type consistency and equality for enum members. Key improvements include: - Consistent use of `setUpClass` to initialize class variables for constants and enums. - Enhanced type and equality checks for enum members in test cases. - Refactoring tests to use class variables instead of direct imports or module references. The most important changes are: **Test structure and setup improvements:** - Refactored all test classes in `test_dte_constants.py`, `test_rcv_constants.py`, `test_rtc_constants.py`, and `test_rut_constants.py` to use `setUpClass()` and store constants/enums as class variables, promoting consistency and easier maintenance. **Enum and constant validation enhancements:** - Updated enum tests to include checks for member value types (e.g., ensuring all values are of type `int` or `str`) and to verify that enum members are equal to their values or names where appropriate. - Improved tests for enums with integer values (like `TipoDte` and `RcvTipoDocto`) to confirm that enum members are also instances of `int`, and that casting their values to `int` yields the member itself. - For enums with string values, added checks to ensure that member values match their names. **Test code simplification:** - Replaced repetitive direct constant access in test methods with class variables, simplifying test code and reducing import clutter. **Documentation and clarity:** - Added docstrings and comments to clarify test purposes, such as in `TipoDteCediblesTest` for `TIPO_DTE_CEDIBLES`. These changes collectively make the test suite more robust, readable, and maintainable. --- src/tests/test_dte_constants.py | 52 ++++---- src/tests/test_rcv_constants.py | 208 +++++++++++++++----------------- src/tests/test_rtc_constants.py | 23 +++- src/tests/test_rut_constants.py | 27 +++-- 4 files changed, 166 insertions(+), 144 deletions(-) diff --git a/src/tests/test_dte_constants.py b/src/tests/test_dte_constants.py index 3fdc00dd..fc509aeb 100644 --- a/src/tests/test_dte_constants.py +++ b/src/tests/test_dte_constants.py @@ -1,26 +1,34 @@ +from __future__ import annotations + import unittest +from typing import ClassVar -from cl_sii.dte import constants # noqa: F401 -from cl_sii.dte.constants import TipoDte +from cl_sii.dte import constants class TipoDteTest(unittest.TestCase): - def test_members(self) -> None: - self.assertSetEqual( - {x for x in TipoDte}, - { - TipoDte.FACTURA_ELECTRONICA, - TipoDte.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA, - TipoDte.LIQUIDACION_FACTURA_ELECTRONICA, - TipoDte.FACTURA_COMPRA_ELECTRONICA, - TipoDte.GUIA_DESPACHO_ELECTRONICA, - TipoDte.NOTA_DEBITO_ELECTRONICA, - TipoDte.NOTA_CREDITO_ELECTRONICA, - }, - ) + TipoDte: ClassVar[type[constants.TipoDte]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.TipoDte = constants.TipoDte + + def test_enum_member_value_types(self) -> None: + expected_type = int + for member in self.TipoDte: + with self.subTest(name=member.name): + self.assertIsInstance(member.value, expected_type) + + def test_enum_members_are_also_integers(self) -> None: + for member in self.TipoDte: + with self.subTest(name=member.name): + self.assertEqual(int(member.value), member) + self.assertIsInstance(member, int) def test_FACTURA_ELECTRONICA(self) -> None: - value = TipoDte.FACTURA_ELECTRONICA + value = self.TipoDte.FACTURA_ELECTRONICA self.assertEqual(value.name, 'FACTURA_ELECTRONICA') self.assertEqual(value.value, 33) @@ -38,7 +46,7 @@ def test_FACTURA_ELECTRONICA(self) -> None: self.assertEqual(result, expected) def test_FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA(self) -> None: - value = TipoDte.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA + value = self.TipoDte.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA self.assertEqual(value.name, 'FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA') self.assertEqual(value.value, 34) @@ -56,7 +64,7 @@ def test_FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA(self) -> None: self.assertTrue(result is expected) def test_LIQUIDACION_FACTURA_ELECTRONICA(self) -> None: - value = TipoDte.LIQUIDACION_FACTURA_ELECTRONICA + value = self.TipoDte.LIQUIDACION_FACTURA_ELECTRONICA self.assertEqual(value.name, 'LIQUIDACION_FACTURA_ELECTRONICA') self.assertEqual(value.value, 43) @@ -74,7 +82,7 @@ def test_LIQUIDACION_FACTURA_ELECTRONICA(self) -> None: self.assertEqual(result, expected) def test_FACTURA_COMPRA_ELECTRONICA(self) -> None: - value = TipoDte.FACTURA_COMPRA_ELECTRONICA + value = self.TipoDte.FACTURA_COMPRA_ELECTRONICA self.assertEqual(value.name, 'FACTURA_COMPRA_ELECTRONICA') self.assertEqual(value.value, 46) @@ -92,7 +100,7 @@ def test_FACTURA_COMPRA_ELECTRONICA(self) -> None: self.assertTrue(result is expected) def test_GUIA_DESPACHO_ELECTRONICA(self) -> None: - value = TipoDte.GUIA_DESPACHO_ELECTRONICA + value = self.TipoDte.GUIA_DESPACHO_ELECTRONICA self.assertEqual(value.name, 'GUIA_DESPACHO_ELECTRONICA') self.assertEqual(value.value, 52) @@ -110,7 +118,7 @@ def test_GUIA_DESPACHO_ELECTRONICA(self) -> None: self.assertTrue(result is expected) def test_NOTA_DEBITO_ELECTRONICA(self) -> None: - value = TipoDte.NOTA_DEBITO_ELECTRONICA + value = self.TipoDte.NOTA_DEBITO_ELECTRONICA self.assertEqual(value.name, 'NOTA_DEBITO_ELECTRONICA') self.assertEqual(value.value, 56) @@ -128,7 +136,7 @@ def test_NOTA_DEBITO_ELECTRONICA(self) -> None: self.assertTrue(result is expected) def test_NOTA_CREDITO_ELECTRONICA(self) -> None: - value = TipoDte.NOTA_CREDITO_ELECTRONICA + value = self.TipoDte.NOTA_CREDITO_ELECTRONICA self.assertEqual(value.name, 'NOTA_CREDITO_ELECTRONICA') self.assertEqual(value.value, 61) diff --git a/src/tests/test_rcv_constants.py b/src/tests/test_rcv_constants.py index ab5ede8d..2c3c2e32 100644 --- a/src/tests/test_rcv_constants.py +++ b/src/tests/test_rcv_constants.py @@ -1,24 +1,36 @@ +from __future__ import annotations + import unittest +from typing import ClassVar -from cl_sii.dte.constants import TipoDte # noqa: F401 -from cl_sii.rcv import constants # noqa: F401 -from cl_sii.rcv.constants import RcEstadoContable, RcTipoCompra, RcvKind, RcvTipoDocto, RvTipoVenta +from cl_sii.dte.constants import TipoDte +from cl_sii.rcv import constants class RcvKindTest(unittest.TestCase): - def test_members(self) -> None: - self.assertSetEqual( - {x for x in RcvKind}, - { - RcvKind.COMPRAS, - RcvKind.VENTAS, - }, - ) + RcvKind: ClassVar[type[constants.RcvKind]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() - def test_values_type(self) -> None: - self.assertSetEqual({type(x.value) for x in RcvKind}, {str}) + cls.RcvKind = constants.RcvKind + + def test_enum_member_value_types(self) -> None: + expected_type = str + for member in self.RcvKind: + with self.subTest(name=member.name): + self.assertIsInstance(member.value, expected_type) + + def test_enum_members_equal_names_and_values(self) -> None: + for member in self.RcvKind: + with self.subTest(name=member.name): + self.assertEqual(member.value, member.name) def test_is_estado_contable_compatible(self) -> None: + RcvKind = self.RcvKind + RcEstadoContable = constants.RcEstadoContable + self.assertTrue(RcvKind.VENTAS.is_estado_contable_compatible(None)) self.assertTrue(RcvKind.COMPRAS.is_estado_contable_compatible(RcEstadoContable.REGISTRO)) self.assertTrue(RcvKind.COMPRAS.is_estado_contable_compatible(RcEstadoContable.NO_INCLUIR)) @@ -33,129 +45,103 @@ def test_is_estado_contable_compatible(self) -> None: class RcEstadoContableTest(unittest.TestCase): - def test_members(self) -> None: - self.assertSetEqual( - {x for x in RcEstadoContable}, - { - RcEstadoContable.REGISTRO, - RcEstadoContable.NO_INCLUIR, - RcEstadoContable.RECLAMADO, - RcEstadoContable.PENDIENTE, - }, - ) + RcEstadoContable: ClassVar[type[constants.RcEstadoContable]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.RcEstadoContable = constants.RcEstadoContable - def test_values_type(self) -> None: - self.assertSetEqual({type(x.value) for x in RcEstadoContable}, {str}) + def test_enum_member_value_types(self) -> None: + expected_type = str + for member in self.RcEstadoContable: + with self.subTest(name=member.name): + self.assertIsInstance(member.value, expected_type) + + def test_enum_members_equal_names_and_values(self) -> None: + for member in self.RcEstadoContable: + with self.subTest(name=member.name): + self.assertEqual(member.value, member.name) class RcTipoCompraTest(unittest.TestCase): - def test_members(self) -> None: - self.assertSetEqual( - {x for x in RcTipoCompra}, - { - RcTipoCompra.DEL_GIRO, - RcTipoCompra.SUPERMERCADOS, - RcTipoCompra.BIENES_RAICES, - RcTipoCompra.ACTIVO_FIJO, - RcTipoCompra.IVA_USO_COMUN, - RcTipoCompra.IVA_NO_RECUPERABLE, - RcTipoCompra.NO_CORRESPONDE_INCLUIR, - }, - ) + RcTipoCompra: ClassVar[type[constants.RcTipoCompra]] - def test_values_type(self) -> None: - self.assertSetEqual({type(x.value) for x in RcTipoCompra}, {str}) + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.RcTipoCompra = constants.RcTipoCompra + + def test_enum_member_value_types(self) -> None: + expected_type = str + for member in self.RcTipoCompra: + with self.subTest(name=member.name): + self.assertIsInstance(member.value, expected_type) + + def test_enum_members_equal_names_and_values(self) -> None: + for member in self.RcTipoCompra: + with self.subTest(name=member.name): + self.assertEqual(member.value, member.name) class RcvTipoDoctoTest(unittest.TestCase): - def test_members(self) -> None: - self.assertSetEqual( - {x for x in RcvTipoDocto}, - { - RcvTipoDocto.FACTURA_INICIO, - RcvTipoDocto.FACTURA, - RcvTipoDocto.FACTURA_ELECTRONICA, - RcvTipoDocto.FACTURA_NO_AFECTA_O_EXENTA, - RcvTipoDocto.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA, - RcvTipoDocto.FACTURA_COMPRA, - RcvTipoDocto.FACTURA_COMPRA_ELECTRONICA, - RcvTipoDocto.FACTURA_EXPORTACION, - RcvTipoDocto.FACTURA_EXPORTACION_ELECTRONICA, - # - RcvTipoDocto.NOTA_DEBITO, - RcvTipoDocto.NOTA_DEBITO_ELECTRONICA, - RcvTipoDocto.NOTA_CREDITO, - RcvTipoDocto.NOTA_CREDITO_ELECTRONICA, - RcvTipoDocto.NOTA_DEBITO_EXPORTACION, - RcvTipoDocto.NOTA_DEBITO_EXPORTACION_ELECTRONICA, - RcvTipoDocto.NOTA_CREDITO_EXPORTACION, - RcvTipoDocto.NOTA_CREDITO_EXPORTACION_ELECTRONICA, - # - RcvTipoDocto.LIQUIDACION_FACTURA, - RcvTipoDocto.LIQUIDACION_FACTURA_ELECTRONICA, - # - RcvTipoDocto.TOTAL_OP_DEL_MES_BOLETA_AFECTA, - RcvTipoDocto.TOTAL_OP_DEL_MES_BOLETA_EXENTA, - RcvTipoDocto.TOTAL_OP_DEL_MES_BOLETA_EXENTA_ELECTR, - RcvTipoDocto.TOTAL_OP_DEL_MES_BOLETA_ELECTR, - # - RcvTipoDocto.TIPO_47, - RcvTipoDocto.TIPO_48, - RcvTipoDocto.TIPO_102, - RcvTipoDocto.TIPO_103, - RcvTipoDocto.TIPO_105, - RcvTipoDocto.TIPO_108, - RcvTipoDocto.TIPO_109, - RcvTipoDocto.TIPO_901, - RcvTipoDocto.TIPO_902, - RcvTipoDocto.TIPO_903, - RcvTipoDocto.TIPO_904, - RcvTipoDocto.TIPO_905, - RcvTipoDocto.TIPO_906, - RcvTipoDocto.TIPO_907, - RcvTipoDocto.TIPO_909, - RcvTipoDocto.TIPO_910, - RcvTipoDocto.TIPO_911, - RcvTipoDocto.TIPO_914, - RcvTipoDocto.TIPO_919, - RcvTipoDocto.TIPO_920, - RcvTipoDocto.TIPO_922, - RcvTipoDocto.TIPO_924, - }, - ) + RcvTipoDocto: ClassVar[type[constants.RcvTipoDocto]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.RcvTipoDocto = constants.RcvTipoDocto - def test_values_type(self) -> None: - self.assertSetEqual({type(x.value) for x in RcvTipoDocto}, {int}) + def test_enum_member_value_types(self) -> None: + expected_type = int + for member in self.RcvTipoDocto: + with self.subTest(name=member.name): + self.assertIsInstance(member.value, expected_type) + + def test_enum_members_are_also_integers(self) -> None: + for member in self.RcvTipoDocto: + with self.subTest(name=member.name): + self.assertEqual(int(member.value), member) + self.assertIsInstance(member, int) def test_of_some_member(self) -> None: - value = RcvTipoDocto.FACTURA_ELECTRONICA + value = self.RcvTipoDocto.FACTURA_ELECTRONICA self.assertEqual(value.name, 'FACTURA_ELECTRONICA') self.assertEqual(value.value, 33) def test_as_tipo_dte(self) -> None: self.assertEqual( - RcvTipoDocto.FACTURA_ELECTRONICA.as_tipo_dte(), + self.RcvTipoDocto.FACTURA_ELECTRONICA.as_tipo_dte(), TipoDte.FACTURA_ELECTRONICA, ) with self.assertRaises(ValueError) as cm: - RcvTipoDocto.FACTURA.as_tipo_dte() + self.RcvTipoDocto.FACTURA.as_tipo_dte() self.assertEqual( cm.exception.args, ("There is no equivalent 'TipoDte' for 'RcvTipoDocto.FACTURA'.",) ) class RvTipoVentaTest(unittest.TestCase): - def test_members(self) -> None: - self.assertSetEqual( - {x for x in RvTipoVenta}, - { - RvTipoVenta.DEL_GIRO, - RvTipoVenta.BIENES_RAICES, - RvTipoVenta.ACTIVO_FIJO, - }, - ) + RvTipoVenta: ClassVar[type[constants.RvTipoVenta]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.RvTipoVenta = constants.RvTipoVenta + + def test_enum_member_value_types(self) -> None: + expected_type = str + for member in self.RvTipoVenta: + with self.subTest(name=member.name): + self.assertIsInstance(member.value, expected_type) - def test_values_type(self) -> None: - self.assertSetEqual({type(x.value) for x in RvTipoVenta}, {str}) + def test_enum_members_equal_names_and_values(self) -> None: + for member in self.RvTipoVenta: + with self.subTest(name=member.name): + self.assertEqual(member.value, member.name) diff --git a/src/tests/test_rtc_constants.py b/src/tests/test_rtc_constants.py index 056a74cb..1d773f8f 100644 --- a/src/tests/test_rtc_constants.py +++ b/src/tests/test_rtc_constants.py @@ -1,14 +1,29 @@ +from __future__ import annotations + import unittest +from typing import ClassVar -from cl_sii.rtc.constants import TIPO_DTE_CEDIBLES +from cl_sii.dte.constants import TipoDte +from cl_sii.rtc import constants class TipoDteCediblesTest(unittest.TestCase): - # For 'TIPO_DTE_CEDIBLES' + """ + Tests for `TIPO_DTE_CEDIBLES`. + """ + + TIPO_DTE_CEDIBLES: ClassVar[frozenset[TipoDte]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.TIPO_DTE_CEDIBLES = constants.TIPO_DTE_CEDIBLES def test_all_are_factura(self) -> None: - for element in TIPO_DTE_CEDIBLES: - self.assertTrue(element.is_factura) + for element in self.TIPO_DTE_CEDIBLES: + with self.subTest(name=element.name): + self.assertTrue(element.is_factura) # TODO: implement test that check that the values correspond to those defined in # XML type 'SiiDte:DTEFacturasType' in official schema 'SiiTypes_v10.xsd'. diff --git a/src/tests/test_rut_constants.py b/src/tests/test_rut_constants.py index 12e90a79..84e62a7e 100644 --- a/src/tests/test_rut_constants.py +++ b/src/tests/test_rut_constants.py @@ -1,23 +1,36 @@ from __future__ import annotations import unittest +from typing import ClassVar from cl_sii.rut import constants class RutDigitsConstantsTestCase(unittest.TestCase): + RUT_DIGITS_MIN_VALUE: ClassVar[int] + RUT_DIGITS_MAX_VALUE: ClassVar[int] + PERSONA_JURIDICA_MIN_RUT_DIGITS: ClassVar[int] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.RUT_DIGITS_MIN_VALUE = constants.RUT_DIGITS_MIN_VALUE + cls.RUT_DIGITS_MAX_VALUE = constants.RUT_DIGITS_MAX_VALUE + cls.PERSONA_JURIDICA_MIN_RUT_DIGITS = constants.PERSONA_JURIDICA_MIN_RUT_DIGITS + def test_min_value(self) -> None: - min_rut_digits = constants.RUT_DIGITS_MIN_VALUE + min_rut_digits = self.RUT_DIGITS_MIN_VALUE - self.assertLessEqual(min_rut_digits, constants.RUT_DIGITS_MAX_VALUE) + self.assertLessEqual(min_rut_digits, self.RUT_DIGITS_MAX_VALUE) def test_max_value(self) -> None: - max_rut_digits = constants.RUT_DIGITS_MAX_VALUE + max_rut_digits = self.RUT_DIGITS_MAX_VALUE - self.assertGreaterEqual(max_rut_digits, constants.RUT_DIGITS_MIN_VALUE) + self.assertGreaterEqual(max_rut_digits, self.RUT_DIGITS_MIN_VALUE) def test_persona_juridica_min_value(self) -> None: - min_rut_digits = constants.PERSONA_JURIDICA_MIN_RUT_DIGITS + min_rut_digits = self.PERSONA_JURIDICA_MIN_RUT_DIGITS - self.assertGreaterEqual(min_rut_digits, constants.RUT_DIGITS_MIN_VALUE) - self.assertLessEqual(min_rut_digits, constants.RUT_DIGITS_MAX_VALUE) + self.assertGreaterEqual(min_rut_digits, self.RUT_DIGITS_MIN_VALUE) + self.assertLessEqual(min_rut_digits, self.RUT_DIGITS_MAX_VALUE) From 871f662795b83599327b1c025e624a21819424c7 Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Thu, 11 Sep 2025 18:32:33 -0300 Subject: [PATCH 2/6] =?UTF-8?q?feat(cte):=20Add=20parser=20for=20"Carpeta?= =?UTF-8?q?=20Tributaria=20Electr=C3=B3nica"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented `parse_taxpayer_provided_info` to parse taxpayer-provided information from CTE HTML. - Added `TaxpayerProvidedInfo` and `LegalRepresentative` data models. - Created tests to validate parser functionality with sample HTML input. Ref: https://app.shortcut.com/cordada/story/16535/ --- src/cl_sii/cte/data_models.py | 40 ++++ src/cl_sii/cte/parsers.py | 91 +++++++++ src/tests/test_cte_parsers.py | 51 ++++++ .../sii-cte/cte_taxpayer_provided_info.html | 173 ++++++++++++++++++ 4 files changed, 355 insertions(+) create mode 100644 src/cl_sii/cte/data_models.py create mode 100644 src/cl_sii/cte/parsers.py create mode 100644 src/tests/test_cte_parsers.py create mode 100644 src/tests/test_data/sii-cte/cte_taxpayer_provided_info.html diff --git a/src/cl_sii/cte/data_models.py b/src/cl_sii/cte/data_models.py new file mode 100644 index 00000000..91155774 --- /dev/null +++ b/src/cl_sii/cte/data_models.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from collections.abc import Sequence + +import pydantic + + +@pydantic.dataclasses.dataclass( + frozen=True, + config=pydantic.ConfigDict( + arbitrary_types_allowed=True, + extra='forbid', + ), +) +class TaxpayerProvidedInfo: + """ + Información proporcionada por el contribuyente para fines tributarios (1) + """ + + legal_representatives: Sequence[LegalRepresentative] + company_formation: Sequence[LegalRepresentative] + participation_in_existing_companies: Sequence[LegalRepresentative] + + +@pydantic.dataclasses.dataclass( + frozen=True, +) +class LegalRepresentative: + name: str + """ + Nombre o Razón social. + """ + rut: str + """ + RUT. + """ + incorporation_date: str + """ + Fecha de incorporación. + """ diff --git a/src/cl_sii/cte/parsers.py b/src/cl_sii/cte/parsers.py new file mode 100644 index 00000000..bbcd6a5a --- /dev/null +++ b/src/cl_sii/cte/parsers.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from bs4 import BeautifulSoup + +from .data_models import LegalRepresentative, TaxpayerProvidedInfo + + +def parse_taxpayer_provided_info(html_content: str) -> TaxpayerProvidedInfo: + """ + Parse the CTE HTML content to extract the content of the section: + "Información proporcionada por el contribuyente para fines tributarios (1)" + + Args: + html_content: HTML string containing the taxpayer information table + + Returns: + TaxpayerProvidedInfo instance with the parsed data + """ + soup = BeautifulSoup(html_content, 'html.parser') + + # Find the main table with id="tbl_sociedades" + table = soup.find('table', id='tbl_sociedades') + + if not table: + raise ValueError("Could not find taxpayer information table in HTML") + + # Initialize lists for each section + legal_representatives = [] + company_formation = [] + participation_in_companies = [] + + # Current section being parsed + current_section = None + + # Iterate through rows to extract data + rows = table.find_all('tr') # type: ignore[attr-defined] + for row in rows: + section_header = row.find( + 'span', class_='textof', string=lambda s: s and 'Representante(s) Legal(es)' in s + ) + if section_header: + current_section = 'legal_representatives' + continue + + section_header = row.find( + 'span', + class_='textof', + string=lambda s: s and 'Conformación de la sociedad' in s, + ) + if section_header: + current_section = 'company_formation' + continue + + section_header = row.find( + 'span', + class_='textof', + string=lambda s: s and 'Participación en sociedades vigentes' in s, + ) + if section_header: + current_section = 'participation_in_companies' + continue + + # Skip rows without useful data + cells = row.find_all('td') + if len(cells) < 3: + continue + + name_cell = cells[1].find('span', class_='textof') + rut_cell = cells[2].find('span', class_='textof') + date_cell = cells[3].find('span', class_='textof') + + # If this is a data row with person information + if name_cell and rut_cell and date_cell and name_cell.text.strip(): + name = name_cell.text.strip() + rut = rut_cell.text.strip() + incorporation_date = date_cell.text.strip() + + person = LegalRepresentative(name=name, rut=rut, incorporation_date=incorporation_date) + + if current_section == 'legal_representatives': + legal_representatives.append(person) + elif current_section == 'company_formation': + company_formation.append(person) + elif current_section == 'participation_in_companies': + participation_in_companies.append(person) + + return TaxpayerProvidedInfo( + legal_representatives=legal_representatives, + company_formation=company_formation, + participation_in_existing_companies=participation_in_companies, + ) diff --git a/src/tests/test_cte_parsers.py b/src/tests/test_cte_parsers.py new file mode 100644 index 00000000..c2fb8005 --- /dev/null +++ b/src/tests/test_cte_parsers.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from unittest import TestCase + +from cl_sii.cte import data_models, parsers +from .utils import read_test_file_str_utf8 + + +class ParsersTest(TestCase): + def test_parse_taxpayer_provided_info(self) -> None: + html_content = read_test_file_str_utf8('test_data/sii-cte/cte_taxpayer_provided_info.html') + + with self.subTest("Parsing ok"): + result = parsers.parse_taxpayer_provided_info(html_content) + expected_obj = data_models.TaxpayerProvidedInfo( + legal_representatives=[ + data_models.LegalRepresentative( + name='DAVID USUARIO DE PRUEBA', + rut='76354771-K', + incorporation_date='20-09-2023', + ), + data_models.LegalRepresentative( + name='JAVIERA USUARIO DE PRUEBA', + rut='38855667-6', + incorporation_date='20-09-2023', + ), + ], + company_formation=[ + data_models.LegalRepresentative( + name='JAVIERA USUARIO DE PRUEBA', + rut='38855667-6', + incorporation_date='20-09-2023', + ), + data_models.LegalRepresentative( + name='MARÍA USUARIO DE PRUEBA', + rut='34413183-k', + incorporation_date='23-02-2024', + ), + ], + participation_in_existing_companies=[], + ) + self.assertEqual(result, expected_obj) + + with self.subTest("Parsing emtpy content"): + with self.assertRaises(ValueError) as assert_raises_cm: + parsers.parse_taxpayer_provided_info("") + + self.assertEqual( + assert_raises_cm.exception.args, + ("Could not find taxpayer information table in HTML",), + ) diff --git a/src/tests/test_data/sii-cte/cte_taxpayer_provided_info.html b/src/tests/test_data/sii-cte/cte_taxpayer_provided_info.html new file mode 100644 index 00000000..9ca38888 --- /dev/null +++ b/src/tests/test_data/sii-cte/cte_taxpayer_provided_info.html @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + +
 
+

Revise los datos contenidos en esta Carpeta Tributaria Electrónica y, si están correctos, seleccione el botón "Continuar". Si detecta información incorrecta, reporte esta situación llamando a nuestra Mesa de Ayuda Telefónica, al (02) 395 11 15, o ingresando a nuestra página Web, www.sii.cl, menú Contáctenos, opción Problemas y quejas.

+
+ + + + + + + + + + + + + + + + + +
+ CARPETA TRIBUTARIA ELECTRÓNICA
PARA SOLICITAR CRÉDITOS
+

+

+ Importante: Esta información es válida para la fecha y hora en que se generó la carpeta.

+ Toda declaración y pago que sea presentada en papel retrasa la actualización de las bases de datos del SII, por lo que, eventualmente, podrían no aparecer en esta carpeta.
+

+

+ + + + + + + + + + + + + +
Nombre del emisor: INVERSIONES SPA
RUT del emisor:47797573 - 9
Fecha de generación de la carpeta:25/07/2025 07:21
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Datos del Contribuyente
Fecha de Inicio de Actividades:15-11-2023
Actividades Económicas: SERVICIOS DE ASESORIA Y CONSULTORIA EN MATERIA DE ADMINISTRACION DE EMPRESAS Y OTROS SERVICIOS DE ASESORIA ADMINISTRATIVA Y DE NEGOCIOS N.C.P.
ACTIVIDADES DE OTRAS ORGANIZACIONES EMPRESARIALES N.C.P.
OTRAS ACTIVIDADES DE SERVICIOS PERSONALES N.C.P.
Categoría tributaria:Primera categoría
Domicilio:AV REAL, LAS CONDES
Sucursales:
Últimos documentos timbrados:FACTURA ELECTRONICA
FACTURA NO AFECTA O EXENTA ELECTRONICA
GUIA DESPACHO ELECTRONICA
NOTA CREDITO ELECTRONICA
24-07-2025
17-07-2025
14-05-2025
18-07-2025
Observaciones tributarias: + + + + +

No tiene observaciones.

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Información proporcionada por el contribuyente para fines tributarios (1)
 Nombre o Razón SocialRUTFecha de Incorporación
Representante(s) Legal(es)   
 DAVID USUARIO DE PRUEBA76354771-K20-09-2023
 JAVIERA USUARIO DE PRUEBA38855667-620-09-2023
Conformación de la sociedad
JAVIERA USUARIO DE PRUEBA38855667-620-09-2023
MARÍA USUARIO DE PRUEBA34413183-k23-02-2024
Participación en sociedades vigentes(2)   
 - No existen sociedades para el RUT -

(1): Información declarada por el contribuyente y que puede haber sufrido modificaciones. +
(2): La vigencia de estas sociedades está asociada a la existencia de un Inicio de Actividades, sin Término de Giro. +

+
From b471dfcbd393b0abe26de945d85ef24592d7265a Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Mon, 15 Sep 2025 11:33:17 -0300 Subject: [PATCH 3/6] fix(rcv): Make `iva_no_retenido` optional in data models and parsers - Updated `iva_no_retenido` to be optional in `RcDetalleEntry` and its variants. - Adjusted CSV parser to handle cases where `iva_no_retenido` is missing or `None`. - Updated tests to reflect changes in `iva_no_retenido` handling. Ref: https://app.shortcut.com/cordada/story/16600/ --- src/cl_sii/rcv/data_models.py | 2 +- src/cl_sii/rcv/parse_csv.py | 8 ++++---- src/tests/test_rcv_data_models.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cl_sii/rcv/data_models.py b/src/cl_sii/rcv/data_models.py index b76d1956..ef0fbe9b 100644 --- a/src/cl_sii/rcv/data_models.py +++ b/src/cl_sii/rcv/data_models.py @@ -526,7 +526,7 @@ class RcDetalleEntry(RcvDetalleEntry): Impto. Sin Derecho a Credito """ - iva_no_retenido: int + iva_no_retenido: Optional[int] """ IVA No Retenido """ diff --git a/src/cl_sii/rcv/parse_csv.py b/src/cl_sii/rcv/parse_csv.py index 41267720..28a40176 100644 --- a/src/cl_sii/rcv/parse_csv.py +++ b/src/cl_sii/rcv/parse_csv.py @@ -1074,7 +1074,7 @@ def to_detalle_entry(self, data: dict) -> RcRegistroDetalleEntry: iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo') iva_uso_comun: Optional[int] = data.get('iva_uso_comun') impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito') - iva_no_retenido: int = data['iva_no_retenido'] + iva_no_retenido: Optional[int] = data.get('iva_no_retenido') nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get( 'nce_o_nde_sobre_factura_de_compra' ) @@ -1194,7 +1194,7 @@ def to_detalle_entry(self, data: dict) -> RcNoIncluirDetalleEntry: iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo') iva_uso_comun: Optional[int] = data.get('iva_uso_comun') impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito') - iva_no_retenido: int = data['iva_no_retenido'] + iva_no_retenido: Optional[int] = data.get('iva_no_retenido') nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get( 'nce_o_nde_sobre_factura_de_compra' ) @@ -1314,7 +1314,7 @@ def to_detalle_entry(self, data: dict) -> RcReclamadoDetalleEntry: iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo') iva_uso_comun: Optional[int] = data.get('iva_uso_comun') impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito') - iva_no_retenido: int = data['iva_no_retenido'] + iva_no_retenido: Optional[int] = data.get('iva_no_retenido') nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get( 'nce_o_nde_sobre_factura_de_compra' ) @@ -1399,7 +1399,7 @@ def to_detalle_entry(self, data: dict) -> RcPendienteDetalleEntry: iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo') iva_uso_comun: Optional[int] = data.get('iva_uso_comun') impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito') - iva_no_retenido: int = data['iva_no_retenido'] + iva_no_retenido: Optional[int] = data.get('iva_no_retenido') nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get( 'nce_o_nde_sobre_factura_de_compra' ) diff --git a/src/tests/test_rcv_data_models.py b/src/tests/test_rcv_data_models.py index d6a1a37f..870d959a 100644 --- a/src/tests/test_rcv_data_models.py +++ b/src/tests/test_rcv_data_models.py @@ -440,7 +440,7 @@ def setUp(self) -> None: iva_activo_fijo=0, iva_uso_comun=0, impto_sin_derecho_a_credito=0, - iva_no_retenido=0, + iva_no_retenido=None, tabacos_puros=0, tabacos_cigarrillos=0, tabacos_elaborados=0, From 9096a3a70bb94214387ecd55708eea009ed44839 Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Mon, 15 Sep 2025 12:39:13 -0300 Subject: [PATCH 4/6] fix(rcv): Improve checks for RCV Reclamado parser workaround - Updated `RcvCompraReclamadoCsvRowSchema.preprocess` workaround for `Fecha Reclamo` to check if variable is already `None` - Implemented test `test_parse_rcv_compra_reclamado_csv_file` to check multiple use cases for parser `RcvCompraReclamadoCsvRowSchema` Ref: https://app.shortcut.com/cordada/story/16601/ --- src/cl_sii/rcv/parse_csv.py | 5 +- .../sii-rcv/RCV-compra-reclamado.csv | 6 + src/tests/test_rcv_parse_csv.py | 281 +++++++++++++++++- 3 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 src/tests/test_data/sii-rcv/RCV-compra-reclamado.csv diff --git a/src/cl_sii/rcv/parse_csv.py b/src/cl_sii/rcv/parse_csv.py index 28a40176..e29deb2b 100644 --- a/src/cl_sii/rcv/parse_csv.py +++ b/src/cl_sii/rcv/parse_csv.py @@ -1263,8 +1263,9 @@ def preprocess(self, in_data: dict, **kwargs: Any) -> dict: # note: for some reason the rows with 'tipo_docto' equal to # '' (and maybe others as well) do not # have this field set (always? we do not know). - if 'Fecha Reclamo' in in_data: - if in_data['Fecha Reclamo'] == '' or 'null' in in_data['Fecha Reclamo']: + if 'Fecha Reclamo' in in_data and in_data['Fecha Reclamo'] is not None: + value = in_data['Fecha Reclamo'] + if isinstance(value, str) and (value == '' or 'null' in value): in_data['Fecha Reclamo'] = None return in_data diff --git a/src/tests/test_data/sii-rcv/RCV-compra-reclamado.csv b/src/tests/test_data/sii-rcv/RCV-compra-reclamado.csv new file mode 100644 index 00000000..6e08cf8d --- /dev/null +++ b/src/tests/test_data/sii-rcv/RCV-compra-reclamado.csv @@ -0,0 +1,6 @@ +Nro;Tipo Doc;Tipo Compra;RUT Proveedor;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA Recuperable;Monto Iva No Recuperable;Codigo IVA No Rec.;Monto Total;Monto Neto Activo Fijo;IVA Activo Fijo;IVA uso Comun;Impto. Sin Derecho a Credito;IVA No Retenido;NCE o NDE sobre Fact. de Compra;Codigo Otro Impuesto;Valor Otro Impuesto;Tasa Otro Impuesto +1;33;Del Giro;12345678-5;Fake Company S.A. ;1000055;05/06/2019;05/06/2019 21:58:49;12/06/2019 09:47:23;0;970894;184470;;;1155364;;;;;0;0;;;; +2;61;Del Giro;12345678-5; Fake Company S.A.;70013;24/06/2019;24/06/2019 15:24:41;null;0;1652840;314040;;;1966880;;;;;0;0;;;; +3;33;Del Giro;76354771-K;Fake Company S.A. ;789456;05/06/2019;05/06/2019 21:58:49;;0;970894;184470;;;1155364;;;;;0;0;;;; +4;33;Del Giro;INVALID-RUT;Fake Company S.A.;notanumber;invalid-date;invalid-datetime;invalid-datetime;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber +5;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/src/tests/test_rcv_parse_csv.py b/src/tests/test_rcv_parse_csv.py index f03137ba..f37dd520 100644 --- a/src/tests/test_rcv_parse_csv.py +++ b/src/tests/test_rcv_parse_csv.py @@ -597,8 +597,285 @@ def test_parse_rcv_compra_no_incluir_csv_file_emisor_rz_leading_trailing_whitesp ) def test_parse_rcv_compra_reclamado_csv_file(self) -> None: - # TODO: implement for 'parse_rcv_compra_reclamado_csv_file'. - pass + rcv_file_path = get_test_file_path('test_data/sii-rcv/RCV-compra-reclamado.csv') + + items = parse_rcv_compra_reclamado_csv_file( + rut=Rut('1-9'), + input_file_path=rcv_file_path, + n_rows_offset=0, + max_n_rows=None, + ) + result = list(items) + + # Expected output: list of RcReclamadoDetalleEntry instances matching the CSV + expected_result = [ + ( + RcReclamadoDetalleEntry( + emisor_rut=Rut('12345678-5'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=1000055, + fecha_emision_date=datetime.date(2019, 6, 5), + receptor_rut=Rut('1-9'), + monto_total=1155364, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2019, 6, 5, 21, 58, 49), + tz=SII_OFFICIAL_TZ, + ), + tipo_compra='DEL_GIRO', + emisor_razon_social='Fake Company S.A.', + monto_exento=0, + monto_neto=970894, + monto_iva_recuperable=184470, + monto_iva_no_recuperable=None, + codigo_iva_no_rec=None, + monto_neto_activo_fijo=None, + iva_activo_fijo=None, + iva_uso_comun=None, + impto_sin_derecho_a_credito=None, + iva_no_retenido=0, + nce_o_nde_sobre_factura_de_compra='0', + codigo_otro_impuesto=None, + valor_otro_impuesto=None, + tasa_otro_impuesto=None, + fecha_reclamo_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2019, 6, 12, 9, 47, 23), + tz=SII_OFFICIAL_TZ, + ), + ), + 1, + { + 'Tipo Doc': '33', + 'Tipo Compra': 'DEL_GIRO', + 'RUT Proveedor': '12345678-5', + 'Razon Social': 'Fake Company S.A. ', + 'Folio': '1000055', + 'Fecha Docto': '05/06/2019', + 'Fecha Recepcion': '05/06/2019 21:58:49', + 'Fecha Reclamo': '12/06/2019 09:47:23', + 'Monto Exento': '0', + 'Monto Neto': '970894', + 'Monto IVA Recuperable': '184470', + 'Monto Iva No Recuperable': None, + 'Codigo IVA No Rec.': None, + 'Monto Total': '1155364', + 'Monto Neto Activo Fijo': None, + 'IVA Activo Fijo': None, + 'IVA uso Comun': None, + 'Impto. Sin Derecho a Credito': None, + 'IVA No Retenido': '0', + 'NCE o NDE sobre Fact. de Compra': '0', + 'Codigo Otro Impuesto': None, + 'Valor Otro Impuesto': None, + 'Tasa Otro Impuesto': None, + 'receptor_rut': Rut('1-9'), + }, + {}, + ), + ( + RcReclamadoDetalleEntry( + emisor_rut=Rut('12345678-5'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.NOTA_CREDITO_ELECTRONICA, + folio=70013, + fecha_emision_date=datetime.date(2019, 6, 24), + receptor_rut=Rut('1-9'), + monto_total=1966880, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2019, 6, 24, 15, 24, 41), + tz=SII_OFFICIAL_TZ, + ), + tipo_compra='DEL_GIRO', + emisor_razon_social='Fake Company S.A.', + monto_exento=0, + monto_neto=1652840, + monto_iva_recuperable=314040, + monto_iva_no_recuperable=None, + codigo_iva_no_rec=None, + monto_neto_activo_fijo=None, + iva_activo_fijo=None, + iva_uso_comun=None, + impto_sin_derecho_a_credito=None, + iva_no_retenido=0, + nce_o_nde_sobre_factura_de_compra='0', + codigo_otro_impuesto=None, + valor_otro_impuesto=None, + tasa_otro_impuesto=None, + fecha_reclamo_dt=None, + ), + 2, + { + 'Tipo Doc': '61', + 'Tipo Compra': 'DEL_GIRO', + 'RUT Proveedor': '12345678-5', + 'Razon Social': ' Fake Company S.A.', + 'Folio': '70013', + 'Fecha Docto': '24/06/2019', + 'Fecha Recepcion': '24/06/2019 15:24:41', + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '1652840', + 'Monto IVA Recuperable': '314040', + 'Monto Iva No Recuperable': None, + 'Codigo IVA No Rec.': None, + 'Monto Total': '1966880', + 'Monto Neto Activo Fijo': None, + 'IVA Activo Fijo': None, + 'IVA uso Comun': None, + 'Impto. Sin Derecho a Credito': None, + 'IVA No Retenido': '0', + 'NCE o NDE sobre Fact. de Compra': '0', + 'Codigo Otro Impuesto': None, + 'Valor Otro Impuesto': None, + 'Tasa Otro Impuesto': None, + 'receptor_rut': Rut('1-9'), + }, + {}, + ), + ( + RcReclamadoDetalleEntry( + emisor_rut=Rut('76354771-K'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=789456, + fecha_emision_date=datetime.date(2019, 6, 5), + receptor_rut=Rut('1-9'), + monto_total=1155364, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2019, 6, 5, 21, 58, 49), + tz=SII_OFFICIAL_TZ, + ), + tipo_compra='DEL_GIRO', + emisor_razon_social='Fake Company S.A.', + monto_exento=0, + monto_neto=970894, + monto_iva_recuperable=184470, + monto_iva_no_recuperable=None, + codigo_iva_no_rec=None, + monto_neto_activo_fijo=None, + iva_activo_fijo=None, + iva_uso_comun=None, + impto_sin_derecho_a_credito=None, + iva_no_retenido=0, + nce_o_nde_sobre_factura_de_compra='0', + codigo_otro_impuesto=None, + valor_otro_impuesto=None, + tasa_otro_impuesto=None, + fecha_reclamo_dt=None, + ), + 3, + { + 'Tipo Doc': '33', + 'Tipo Compra': 'DEL_GIRO', + 'RUT Proveedor': '76354771-K', + 'Razon Social': 'Fake Company S.A. ', + 'Folio': '789456', + 'Fecha Docto': '05/06/2019', + 'Fecha Recepcion': '05/06/2019 21:58:49', + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '970894', + 'Monto IVA Recuperable': '184470', + 'Monto Iva No Recuperable': None, + 'Codigo IVA No Rec.': None, + 'Monto Total': '1155364', + 'Monto Neto Activo Fijo': None, + 'IVA Activo Fijo': None, + 'IVA uso Comun': None, + 'Impto. Sin Derecho a Credito': None, + 'IVA No Retenido': '0', + 'NCE o NDE sobre Fact. de Compra': '0', + 'Codigo Otro Impuesto': None, + 'Valor Otro Impuesto': None, + 'Tasa Otro Impuesto': None, + 'receptor_rut': Rut('1-9'), + }, + {}, + ), + ( + None, + 4, + { + 'Tipo Doc': '33', + 'Tipo Compra': 'DEL_GIRO', + 'RUT Proveedor': 'INVALID-RUT', + 'Razon Social': 'Fake Company S.A.', + 'Folio': 'notanumber', + 'Fecha Docto': 'invalid-date', + 'Fecha Recepcion': 'invalid-datetime', + 'Fecha Reclamo': 'invalid-datetime', + 'Monto Exento': 'notanumber', + 'Monto Neto': 'notanumber', + 'Monto IVA Recuperable': 'notanumber', + 'Monto Iva No Recuperable': 'notanumber', + 'Codigo IVA No Rec.': 'notanumber', + 'Monto Total': 'notanumber', + 'Monto Neto Activo Fijo': 'notanumber', + 'IVA Activo Fijo': 'notanumber', + 'IVA uso Comun': 'notanumber', + 'Impto. Sin Derecho a Credito': 'notanumber', + 'IVA No Retenido': 'notanumber', + 'NCE o NDE sobre Fact. de Compra': 'notanumber', + 'Codigo Otro Impuesto': 'notanumber', + 'Valor Otro Impuesto': 'notanumber', + 'Tasa Otro Impuesto': 'notanumber', + 'receptor_rut': Rut('1-9'), + }, + { + 'validation': { + 'RUT Proveedor': ['Not a syntactically valid RUT.'], + 'Folio': ['Not a valid integer.'], + 'Fecha Docto': ['Not a valid date.'], + 'Monto Total': ['Not a valid integer.'], + 'Fecha Recepcion': ['Not a valid datetime.'], + 'Monto Exento': ['Not a valid integer.'], + 'Monto Neto': ['Not a valid integer.'], + 'Monto IVA Recuperable': ['Not a valid integer.'], + 'Monto Iva No Recuperable': ['Not a valid integer.'], + 'Monto Neto Activo Fijo': ['Not a valid integer.'], + 'IVA Activo Fijo': ['Not a valid integer.'], + 'IVA uso Comun': ['Not a valid integer.'], + 'Impto. Sin Derecho a Credito': ['Not a valid integer.'], + 'IVA No Retenido': ['Not a valid integer.'], + 'Valor Otro Impuesto': ['Not a valid integer.'], + 'Tasa Otro Impuesto': ['Not a valid number.'], + 'Fecha Reclamo': ['Not a valid datetime.'], + } + }, + ), + ( + None, + 5, + { + 'Fecha Reclamo': None, + 'Monto IVA Recuperable': None, + 'Monto Iva No Recuperable': None, + 'Codigo IVA No Rec.': None, + 'Monto Neto Activo Fijo': None, + 'IVA Activo Fijo': None, + 'IVA uso Comun': None, + 'Impto. Sin Derecho a Credito': None, + 'IVA No Retenido': None, + 'NCE o NDE sobre Fact. de Compra': None, + 'Codigo Otro Impuesto': None, + 'Valor Otro Impuesto': None, + 'Tasa Otro Impuesto': None, + 'receptor_rut': Rut('1-9'), + }, + { + 'validation': { + 'RUT Proveedor': ['Missing data for required field.'], + 'Tipo Doc': ['Missing data for required field.'], + 'Tipo Compra': ['Missing data for required field.'], + 'Folio': ['Missing data for required field.'], + 'Fecha Docto': ['Missing data for required field.'], + 'Monto Total': ['Missing data for required field.'], + 'Razon Social': ['Missing data for required field.'], + 'Fecha Recepcion': ['Missing data for required field.'], + 'Monto Exento': ['Missing data for required field.'], + 'Monto Neto': ['Missing data for required field.'], + } + }, + ), + ] + self.assertEqual(result, expected_result) def test_parse_rcv_compra_reclamado_csv_file_emisor_rz_leading_trailing_whitespace( self, From 87daed63c24fb4c981c88e02bd693a756f16813a Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Mon, 15 Sep 2025 13:49:27 -0300 Subject: [PATCH 5/6] chore: Update history for new version --- HISTORY.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 70521948..9af257ef 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # History +## 0.57.0 (2025-09-15) + +- (PR #888, 2025-09-10) tests: Refactor and improve constants tests +- (PR #889, 2025-09-12) cte: Add parser for "Carpeta Tributaria Electrónica" +- (PR #891, 2025-09-15) rcv: Make `iva_no_retenido` optional in data models and parsers +- (PR #892, 2025-09-15) rcv: Improve checks for RCV Reclamado parser workaround + ## 0.56.0 (2025-09-10) - (PR #883, 2025-09-10) extras: Add more tests for Django form field for `Rut` From efdf8476c2b867c44ae0d4e3ab77de68414dc3cb Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Mon, 15 Sep 2025 13:49:30 -0300 Subject: [PATCH 6/6] chore: Bump version from 0.56.0 to 0.57.0 --- .bumpversion.cfg | 2 +- src/cl_sii/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5963bc5f..b5f0115c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.56.0 +current_version = 0.57.0 commit = True tag = False message = chore: Bump version from {current_version} to {new_version} diff --git a/src/cl_sii/__init__.py b/src/cl_sii/__init__.py index c7410bcf..f595f3ce 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.56.0' +__version__ = '0.57.0'