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/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` 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' 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/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..e29deb2b 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' ) @@ -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 @@ -1314,7 +1315,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 +1400,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_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. +

+
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_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_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, 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, 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)