From 4e2784fd45c5980f619eb2ce55c181a6ae7c297f Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Wed, 24 Dec 2025 11:15:25 -0300 Subject: [PATCH 1/4] fix(rcv): Update RV fields optional in schemas and data models - Some valid RCV files were missing previously required fields - Updated various monetary and other fields (e.g., `monto_neto`, `monto_iva`) to be nullable. - Adjusted test cases and test data to reflect schema changes. Ref: https://app.shortcut.com/cordada/story/17695/ --- src/cl_sii/rcv/data_models.py | 26 +- src/cl_sii/rcv/parse_csv.py | 39 ++- .../RCV-venta-missing-required-fields.csv | 1 + src/tests/test_rcv_parse_csv.py | 254 +++++++++++++++++- 4 files changed, 285 insertions(+), 35 deletions(-) diff --git a/src/cl_sii/rcv/data_models.py b/src/cl_sii/rcv/data_models.py index acdf4ba6..8188b0cf 100644 --- a/src/cl_sii/rcv/data_models.py +++ b/src/cl_sii/rcv/data_models.py @@ -344,17 +344,17 @@ class RvDetalleEntry(RcvDetalleEntry): Fecha Reclamo (must be timezone aware) """ - monto_exento: int + monto_exento: Optional[int] """ Monto Exento """ - monto_neto: int + monto_neto: Optional[int] """ Monto Neto """ - monto_iva: int + monto_iva: Optional[int] """ Monto IVA """ @@ -389,22 +389,22 @@ class RvDetalleEntry(RcvDetalleEntry): RUT Emisor Liquid. Factura """ - neto_comision_liquidacion_factura: int + neto_comision_liquidacion_factura: Optional[int] """ Neto Comision Liquid. Factura """ - exento_comision_liquidacion_factura: int + exento_comision_liquidacion_factura: Optional[int] """ Exento Comision Liquid. Factura """ - iva_comision_liquidacion_factura: int + iva_comision_liquidacion_factura: Optional[int] """ IVA Comision Liquid. Factura """ - iva_fuera_de_plazo: int + iva_fuera_de_plazo: Optional[int] """ IVA fuera de plazo """ @@ -429,7 +429,7 @@ class RvDetalleEntry(RcvDetalleEntry): Nacionalidad Receptor Extranjero """ - credito_empresa_constructora: int + credito_empresa_constructora: Optional[int] """ Credito empresa constructora """ @@ -439,27 +439,27 @@ class RvDetalleEntry(RcvDetalleEntry): Impto. Zona Franca (Ley 18211) """ - garantia_dep_envases: int + garantia_dep_envases: Optional[int] """ Garantia Dep. Envases """ - indicador_venta_sin_costo: int + indicador_venta_sin_costo: Optional[int] """ Indicador Venta sin Costo """ - indicador_servicio_periodico: int + indicador_servicio_periodico: Optional[int] """ Indicador Servicio Periodico """ - monto_no_facturable: int + monto_no_facturable: Optional[int] """ Monto No facturable """ - total_monto_periodo: int + total_monto_periodo: Optional[int] """ Total Monto Periodo """ diff --git a/src/cl_sii/rcv/parse_csv.py b/src/cl_sii/rcv/parse_csv.py index 47a64287..674b75d6 100644 --- a/src/cl_sii/rcv/parse_csv.py +++ b/src/cl_sii/rcv/parse_csv.py @@ -523,15 +523,18 @@ class RcvVentaCsvRowSchema(_RcvCsvRowSchemaBase): data_key='Fecha Reclamo', ) monto_exento = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Monto Exento', ) monto_neto = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Monto Neto', ) monto_iva = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Monto IVA', ) monto_total = marshmallow.fields.Integer( @@ -569,19 +572,23 @@ class RcvVentaCsvRowSchema(_RcvCsvRowSchemaBase): data_key='RUT Emisor Liquid. Factura', ) neto_comision_liquidacion_factura = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Neto Comision Liquid. Factura', ) exento_comision_liquidacion_factura = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Exento Comision Liquid. Factura', ) iva_comision_liquidacion_factura = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='IVA Comision Liquid. Factura', ) iva_fuera_de_plazo = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='IVA fuera de plazo', ) tipo_documento_referencia = marshmallow.fields.Integer( @@ -605,7 +612,8 @@ class RcvVentaCsvRowSchema(_RcvCsvRowSchemaBase): data_key='Nacionalidad Receptor Extranjero', ) credito_empresa_constructora = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Credito empresa constructora', ) impuesto_zona_franca_ley_18211 = marshmallow.fields.Integer( @@ -614,23 +622,28 @@ class RcvVentaCsvRowSchema(_RcvCsvRowSchemaBase): data_key='Impto. Zona Franca (Ley 18211)', ) garantia_dep_envases = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Garantia Dep. Envases', ) indicador_venta_sin_costo = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Indicador Venta sin Costo', ) indicador_servicio_periodico = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Indicador Servicio Periodico', ) monto_no_facturable = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Monto No facturable', ) total_monto_periodo = marshmallow.fields.Integer( - required=True, + required=False, + allow_none=True, data_key='Total Monto Periodo', ) venta_pasajes_transporte_nacional = marshmallow.fields.Integer( diff --git a/src/tests/test_data/sii-rcv/RCV-venta-missing-required-fields.csv b/src/tests/test_data/sii-rcv/RCV-venta-missing-required-fields.csv index 60718859..bff6f576 100644 --- a/src/tests/test_data/sii-rcv/RCV-venta-missing-required-fields.csv +++ b/src/tests/test_data/sii-rcv/RCV-venta-missing-required-fields.csv @@ -1,3 +1,4 @@ Nro;Tipo Doc;Tipo Venta;Rut cliente;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse Recibo;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA;Monto total;IVA Retenido Total;IVA Retenido Parcial;IVA no retenido;IVA propio;IVA Terceros;RUT Emisor Liquid. Factura;Neto Comision Liquid. Factura;Exento Comision Liquid. Factura;IVA Comision Liquid. Factura;IVA fuera de plazo;Tipo Docto. Referencia;Folio Docto. Referencia;Num. Ident. Receptor Extranjero;Nacionalidad Receptor Extranjero;Credito empresa constructora;Impto. Zona Franca (Ley 18211);Garantia Dep. Envases;Indicador Venta sin Costo;Indicador Servicio Periodico;Monto No facturable;Total Monto Periodo;Venta Pasajes Transporte Nacional;Venta Pasajes Transporte Internacional;Numero Interno;Codigo Sucursal;NCE o NDE sobre Fact. de Compra;Codigo Otro Imp.;Valor Otro Imp.;Tasa Otro Imp. 1;"";Del Giro;12345678-5;Fake Company S.A. ;506;04/06/2019;"";;;0;1750181;332534;2082715;0;0;0;0;0;-;0;0;0;0;;;;;0;;0;2;0;0;0;;;;0;;;;; 23;33;Del Giro;12345678-5; Fake Company S.A.;508;28/06/2019;01/07/2019 13:49:42;;;0;2209597;419823;2629420;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;0;;;;; +3;30;Del Giro;4954153-8;Faker Company;88;04/08/2017;13/09/2017 10:18:59;;;;15915315;3023910;18939225;;;;;;-;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/src/tests/test_rcv_parse_csv.py b/src/tests/test_rcv_parse_csv.py index e81260b6..0cc713ea 100644 --- a/src/tests/test_rcv_parse_csv.py +++ b/src/tests/test_rcv_parse_csv.py @@ -531,15 +531,251 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: ) assert isinstance(items, Iterable) and isinstance(items, Iterator) - entry_struct, row_ix, row_data, row_parsing_errors = next(items) - self.assertIsNone(entry_struct) - self.assertIn('validation', row_parsing_errors) - self.assertIn('Fecha Recepcion', row_parsing_errors['validation']) - self.assertIn('Tipo Doc', row_parsing_errors['validation']) - self.assertEqual( - row_parsing_errors['validation']['Fecha Recepcion'], - ['Missing data for required field.'], - ) + items_list = list(items) + + expected_entries_list: list[ + tuple[Optional[RvDetalleEntry], int, dict[str, object], dict[str, object]] + ] + expected_entries_list = [ + ( + None, + 1, + { + 'Tipo Venta': 'DEL_GIRO', + 'Rut cliente': '12345678-5', + 'Razon Social': 'Fake Company S.A. ', + 'Folio': '506', + 'Fecha Docto': '04/06/2019', + 'Fecha Acuse Recibo': None, + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '1750181', + 'Monto IVA': '332534', + 'Monto total': '2082715', + 'IVA Retenido Total': '0', + 'IVA Retenido Parcial': '0', + 'IVA no retenido': '0', + 'IVA propio': '0', + 'IVA Terceros': '0', + 'RUT Emisor Liquid. Factura': None, + 'Neto Comision Liquid. Factura': '0', + 'Exento Comision Liquid. Factura': '0', + 'IVA Comision Liquid. Factura': '0', + 'IVA fuera de plazo': '0', + 'Tipo Docto. Referencia': None, + 'Folio Docto. Referencia': None, + 'Num. Ident. Receptor Extranjero': None, + 'Nacionalidad Receptor Extranjero': None, + 'Credito empresa constructora': '0', + 'Impto. Zona Franca (Ley 18211)': None, + 'Garantia Dep. Envases': '0', + 'Indicador Venta sin Costo': '2', + 'Indicador Servicio Periodico': '0', + 'Monto No facturable': '0', + 'Total Monto Periodo': '0', + 'Venta Pasajes Transporte Nacional': None, + 'Venta Pasajes Transporte Internacional': None, + 'Numero Interno': None, + 'Codigo Sucursal': '0', + 'NCE o NDE sobre Fact. de Compra': None, + 'Otros Impuestos': None, + 'contribuyente_rut': Rut('1-9'), + }, + { + 'validation': { + 'Tipo Doc': ['Missing data for required field.'], + 'Fecha Recepcion': ['Missing data for required field.'], + } + }, + ), + ( + RvDetalleEntry( + contribuyente_rut=Rut('1-9'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=508, + fecha_emision_date=datetime.date(2019, 6, 28), + monto_total=2629420, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2019, 7, 1, 13, 49, 42), + tz=SII_OFFICIAL_TZ, + ), + cliente_rut=Rut('12345678-5'), + tipo_venta='DEL_GIRO', + cliente_razon_social='Fake Company S.A.', + fecha_acuse_dt=None, + fecha_reclamo_dt=None, + monto_exento=0, + monto_neto=2209597, + monto_iva=419823, + iva_retenido_total=0, + iva_retenido_parcial=0, + iva_no_retenido=0, + iva_propio=0, + iva_terceros=0, + liquidacion_factura_emisor_rut=None, + neto_comision_liquidacion_factura=0, + exento_comision_liquidacion_factura=0, + iva_comision_liquidacion_factura=0, + iva_fuera_de_plazo=0, + tipo_documento_referencia=0, + folio_documento_referencia=None, + num_ident_receptor_extranjero=None, + nacionalidad_receptor_extranjero=None, + credito_empresa_constructora=0, + impuesto_zona_franca_ley_18211=None, + garantia_dep_envases=0, + indicador_venta_sin_costo=2, + indicador_servicio_periodico=0, + monto_no_facturable=0, + total_monto_periodo=0, + venta_pasajes_transporte_nacional=None, + venta_pasajes_transporte_internacional=None, + numero_interno=None, + codigo_sucursal='0', + nce_o_nde_sobre_factura_de_compra=None, + otros_impuestos=None, + ), + 2, + { + 'Tipo Doc': '33', + 'Tipo Venta': 'DEL_GIRO', + 'Rut cliente': '12345678-5', + 'Razon Social': ' Fake Company S.A.', + 'Folio': '508', + 'Fecha Docto': '28/06/2019', + 'Fecha Recepcion': '01/07/2019 13:49:42', + 'Fecha Acuse Recibo': None, + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '2209597', + 'Monto IVA': '419823', + 'Monto total': '2629420', + 'IVA Retenido Total': '0', + 'IVA Retenido Parcial': '0', + 'IVA no retenido': '0', + 'IVA propio': '0', + 'IVA Terceros': '0', + 'RUT Emisor Liquid. Factura': None, + 'Neto Comision Liquid. Factura': '0', + 'Exento Comision Liquid. Factura': '0', + 'IVA Comision Liquid. Factura': '0', + 'IVA fuera de plazo': '0', + 'Tipo Docto. Referencia': '0', + 'Folio Docto. Referencia': None, + 'Num. Ident. Receptor Extranjero': None, + 'Nacionalidad Receptor Extranjero': None, + 'Credito empresa constructora': '0', + 'Impto. Zona Franca (Ley 18211)': None, + 'Garantia Dep. Envases': '0', + 'Indicador Venta sin Costo': '2', + 'Indicador Servicio Periodico': '0', + 'Monto No facturable': '0', + 'Total Monto Periodo': '0', + 'Venta Pasajes Transporte Nacional': None, + 'Venta Pasajes Transporte Internacional': None, + 'Numero Interno': None, + 'Codigo Sucursal': '0', + 'NCE o NDE sobre Fact. de Compra': None, + 'Otros Impuestos': None, + 'contribuyente_rut': Rut('1-9'), + }, + {}, + ), + ( + RvDetalleEntry( + contribuyente_rut=Rut('1-9'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA, + folio=88, + fecha_emision_date=datetime.date(2017, 8, 4), + monto_total=18939225, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2017, 9, 13, 10, 18, 59), + tz=SII_OFFICIAL_TZ, + ), + cliente_rut=Rut('4954153-8'), + tipo_venta='DEL_GIRO', + cliente_razon_social='Faker Company', + fecha_acuse_dt=None, + fecha_reclamo_dt=None, + monto_exento=None, + monto_neto=15915315, + monto_iva=3023910, + iva_retenido_total=None, + iva_retenido_parcial=None, + iva_no_retenido=None, + iva_propio=None, + iva_terceros=None, + liquidacion_factura_emisor_rut=None, + neto_comision_liquidacion_factura=None, + exento_comision_liquidacion_factura=None, + iva_comision_liquidacion_factura=None, + iva_fuera_de_plazo=None, + tipo_documento_referencia=None, + folio_documento_referencia=None, + num_ident_receptor_extranjero=None, + nacionalidad_receptor_extranjero=None, + credito_empresa_constructora=None, + impuesto_zona_franca_ley_18211=None, + garantia_dep_envases=None, + indicador_venta_sin_costo=None, + indicador_servicio_periodico=None, + monto_no_facturable=None, + total_monto_periodo=None, + venta_pasajes_transporte_nacional=None, + venta_pasajes_transporte_internacional=None, + numero_interno=None, + codigo_sucursal=None, + nce_o_nde_sobre_factura_de_compra=None, + otros_impuestos=None, + ), + 3, + { + 'Codigo Sucursal': None, + 'Credito empresa constructora': None, + 'Exento Comision Liquid. Factura': None, + 'Fecha Acuse Recibo': None, + 'Fecha Docto': '04/08/2017', + 'Fecha Recepcion': '13/09/2017 10:18:59', + 'Fecha Reclamo': None, + 'Folio': '88', + 'Folio Docto. Referencia': None, + 'Garantia Dep. Envases': None, + 'IVA Comision Liquid. Factura': None, + 'IVA Retenido Parcial': None, + 'IVA Retenido Total': None, + 'IVA Terceros': None, + 'IVA fuera de plazo': None, + 'IVA no retenido': None, + 'IVA propio': None, + 'Impto. Zona Franca (Ley 18211)': None, + 'Indicador Servicio Periodico': None, + 'Indicador Venta sin Costo': None, + 'Monto Exento': None, + 'Monto IVA': '3023910', + 'Monto Neto': '15915315', + 'Monto No facturable': None, + 'Monto total': '18939225', + 'NCE o NDE sobre Fact. de Compra': None, + 'Nacionalidad Receptor Extranjero': None, + 'Neto Comision Liquid. Factura': None, + 'Num. Ident. Receptor Extranjero': None, + 'Numero Interno': None, + 'Otros Impuestos': None, + 'RUT Emisor Liquid. Factura': None, + 'Razon Social': 'Faker Company', + 'Rut cliente': '4954153-8', + 'Tipo Doc': '30', + 'Tipo Docto. Referencia': None, + 'Tipo Venta': 'DEL_GIRO', + 'Total Monto Periodo': None, + 'Venta Pasajes Transporte Internacional': None, + 'Venta Pasajes Transporte Nacional': None, + 'contribuyente_rut': Rut('1-9'), + }, + {}, + ), + ] + self.assertEqual(items_list, expected_entries_list) def _test_parse_rcv_compra_csv_file_proveedor_rz_leading_trailing_whitespace( self, From f5e710d39fb1ebfdbc0ca404d8bbe33a7dba6083 Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Tue, 23 Dec 2025 14:02:48 -0300 Subject: [PATCH 2/4] feat(rcv): Introduce DocumentoReferencias for improved data handling - Add a new DocumentoReferencias type to encapsulate the tipo_documento_referencia and folio_documento_referencia fields. - This change enhances the data model by allowing multiple references to be stored in a structured format, improving the parsing and validation processes. - Updated function `get_documento_referencia_dte_natural_key` to handle retrieve keys of multiple referencias Ref: https://app.shortcut.com/cordada/story/17694/ --- src/cl_sii/rcv/data_models.py | 75 ++- src/cl_sii/rcv/parse_csv.py | 115 +++- .../RCV-venta-multiple-dte-referencias.csv | 5 + src/tests/test_rcv_data_models.py | 57 +- src/tests/test_rcv_parse_csv.py | 514 ++++++++++++++++-- 5 files changed, 667 insertions(+), 99 deletions(-) create mode 100644 src/tests/test_data/sii-rcv/RCV-venta-multiple-dte-referencias.csv diff --git a/src/cl_sii/rcv/data_models.py b/src/cl_sii/rcv/data_models.py index 8188b0cf..a1bf38a5 100644 --- a/src/cl_sii/rcv/data_models.py +++ b/src/cl_sii/rcv/data_models.py @@ -121,6 +121,18 @@ class OtrosImpuestos(TypedDict): """ +class DocumentoReferencia(TypedDict): + tipo_documento_referencia: int + """ + Tipo Docto. Referencia + """ + + folio_documento_referencia: int + """ + Folio Docto. Referencia + """ + + @pydantic.dataclasses.dataclass( frozen=True, config=pydantic.ConfigDict( @@ -409,14 +421,11 @@ class RvDetalleEntry(RcvDetalleEntry): IVA fuera de plazo """ - tipo_documento_referencia: Optional[int] - """ - Tipo Docto. Referencia - """ - - folio_documento_referencia: Optional[int] + documento_referencias: Optional[Sequence[DocumentoReferencia]] """ - Folio Docto. Referencia + List of: + - Tipo Docto. Referencia + - Folio Docto. Referencia """ num_ident_receptor_extranjero: Optional[str] @@ -495,33 +504,43 @@ class RvDetalleEntry(RcvDetalleEntry): # Custom Methods ########################################################################### - def get_documento_referencia_dte_natural_key( + def get_documento_referencia_dte_natural_keys( self, - ) -> cl_sii.dte.data_models.DteNaturalKey | None: - if self.tipo_documento_referencia is None or self.folio_documento_referencia is None: + ) -> Sequence[cl_sii.dte.data_models.DteNaturalKey] | None: + if self.documento_referencias is None: return None - try: - tipo_documento_referencia = RcvTipoDocto(self.tipo_documento_referencia) - except ValueError: - # Not a valid RCV Tipo de Documento, but it could still be a valid Tipo de DTE. - try: - tipo_dte_referencia = cl_sii.dte.constants.TipoDte(self.tipo_documento_referencia) - except ValueError: - # Not a DTE. - return None - else: + documento_referencia_natural_keys = [] + + for documento_referencia in self.documento_referencias: + tipo_documento_referencia = documento_referencia['tipo_documento_referencia'] + folio_documento_referencia = documento_referencia['folio_documento_referencia'] + try: - tipo_dte_referencia = tipo_documento_referencia.as_tipo_dte() + tipo_documento_referencia = RcvTipoDocto(tipo_documento_referencia) except ValueError: - # Not a DTE. - return None + # Not a valid RCV Tipo de Documento, but it could still be a valid Tipo de DTE. + try: + tipo_dte_referencia = cl_sii.dte.constants.TipoDte(tipo_documento_referencia) + except ValueError: + # Not a DTE. + return None + else: + try: + tipo_dte_referencia = tipo_documento_referencia.as_tipo_dte() + except ValueError: + # Not a DTE. + return None + + documento_referencia_natural_keys.append( + cl_sii.dte.data_models.DteNaturalKey( + emisor_rut=self.contribuyente_rut, + tipo_dte=tipo_dte_referencia, + folio=folio_documento_referencia, + ) + ) - return cl_sii.dte.data_models.DteNaturalKey( - emisor_rut=self.contribuyente_rut, - tipo_dte=tipo_dte_referencia, - folio=self.folio_documento_referencia, - ) + return documento_referencia_natural_keys ########################################################################### # Validators diff --git a/src/cl_sii/rcv/parse_csv.py b/src/cl_sii/rcv/parse_csv.py index 674b75d6..82a61680 100644 --- a/src/cl_sii/rcv/parse_csv.py +++ b/src/cl_sii/rcv/parse_csv.py @@ -9,7 +9,18 @@ import logging from collections.abc import MutableMapping from datetime import date, datetime -from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple, TypedDict, TypeVar +from typing import ( + Any, + Callable, + Dict, + Iterable, + Optional, + Sequence, + Tuple, + TypedDict, + TypeVar, + Union, +) import marshmallow import marshmallow.experimental.context @@ -591,15 +602,17 @@ class RcvVentaCsvRowSchema(_RcvCsvRowSchemaBase): allow_none=True, data_key='IVA fuera de plazo', ) - tipo_documento_referencia = marshmallow.fields.Integer( - required=False, - allow_none=True, - data_key='Tipo Docto. Referencia', - ) - folio_documento_referencia = marshmallow.fields.Integer( + documento_referencias = marshmallow.fields.List( required=False, allow_none=True, - data_key='Folio Docto. Referencia', + data_key='Documento Referencias', + cls_or_instance=marshmallow.fields.Dict( + keys=marshmallow.fields.String(), + values=marshmallow.fields.Raw( + required=True, + allow_none=True, + ), + ), ) num_ident_receptor_extranjero = marshmallow.fields.String( required=False, @@ -821,8 +834,7 @@ def to_detalle_entry(self, data: dict) -> RvDetalleEntry: exento_comision_liquidacion_factura = data['exento_comision_liquidacion_factura'] iva_comision_liquidacion_factura = data['iva_comision_liquidacion_factura'] iva_fuera_de_plazo = data['iva_fuera_de_plazo'] - tipo_documento_referencia = data['tipo_documento_referencia'] - folio_documento_referencia = data['folio_documento_referencia'] + documento_referencias = data.get('documento_referencias', None) num_ident_receptor_extranjero = data['num_ident_receptor_extranjero'] nacionalidad_receptor_extranjero = data['nacionalidad_receptor_extranjero'] credito_empresa_constructora = data['credito_empresa_constructora'] @@ -867,8 +879,7 @@ def to_detalle_entry(self, data: dict) -> RvDetalleEntry: exento_comision_liquidacion_factura=exento_comision_liquidacion_factura, iva_comision_liquidacion_factura=iva_comision_liquidacion_factura, iva_fuera_de_plazo=iva_fuera_de_plazo, - tipo_documento_referencia=tipo_documento_referencia, - folio_documento_referencia=folio_documento_referencia, + documento_referencias=documento_referencias, num_ident_receptor_extranjero=num_ident_receptor_extranjero, nacionalidad_receptor_extranjero=nacionalidad_receptor_extranjero, credito_empresa_constructora=credito_empresa_constructora, @@ -1642,6 +1653,16 @@ def _parse_rcv_csv_file( tipo_docto = row_data.get('Tipo Doc') rut = row_data.get(rut_key) + if isinstance(input_csv_row_schema, RcvVentaCsvRowSchema): + tipo_docto_referencia = row_data.pop('Tipo Docto. Referencia') + folio_docto_referencia = row_data.pop('Folio Docto. Referencia') + + documento_referencias = _parse_rv_documento_referencias( + tipo_documento_referencia=tipo_docto_referencia, + folio_documento_referencia=folio_docto_referencia, + ) + row_data['Documento Referencias'] = documento_referencias + # Concatenate folio, tipo_docto, and rut to create unique entry key entry_key = f"{folio}_{tipo_docto}_{rut}" @@ -1725,3 +1746,73 @@ def _parse_rcv_csv_file( row_errors['conversion_errors'] = conversion_error yield entry, row_ix, row_data, row_errors + + +def _parse_rv_documento_referencias( + tipo_documento_referencia: Union[str, int, None], folio_documento_referencia: Optional[str] +) -> Optional[Sequence[MutableMapping[str, Any]]]: + """ + Parse RvDocumentoReferencia from tipo and folio values. + + Parameters + ---------- + tipo_documento_referencia : Union[str, int, None] + Document type. Accepts string or integer. Empty values: None, "", "0". + folio_documento_referencia : Optional[str] + Folio number(s). Single folio ("10370") or hyphen-separated multiple + folios ("10370-10371"). Empty values: None, "", "0". + + Returns + ------- + Optional[Sequence[MutableMapping[str, Any]]] + None if both parameters are empty. Otherwise, list of mappings with + 'tipo_documento_referencia' and 'folio_documento_referencia' keys. + + Raises + ------ + ValueError + If only one parameter is provided or folio contains non-numeric parts. + """ + documento_referencias = [] + + if tipo_documento_referencia in (None, '', '0') and folio_documento_referencia in ( + None, + '', + '0', + ): + return None + + if tipo_documento_referencia in (None, '', '0') or folio_documento_referencia in ( + None, + '', + '0', + ): + raise ValueError( + f"Both 'tipo_documento_referencia' ({tipo_documento_referencia}) " + f"and 'folio_documento_referencia' ({folio_documento_referencia}) must be provided." + ) + + if isinstance(tipo_documento_referencia, str): + tipo_documento_referencia = int(tipo_documento_referencia) + + if isinstance(folio_documento_referencia, str): + folios = folio_documento_referencia.split('-') + for folio in folios: + if not folio.isdigit(): + raise ValueError( + f"Invalid 'folio_documento_referencia': {folio_documento_referencia}." + ) + documento_referencias.append( + { + 'tipo_documento_referencia': tipo_documento_referencia, + 'folio_documento_referencia': int(folio), + } + ) + else: + documento_referencias.append( + { + 'tipo_documento_referencia': tipo_documento_referencia, + 'folio_documento_referencia': folio_documento_referencia, + } + ) + return documento_referencias diff --git a/src/tests/test_data/sii-rcv/RCV-venta-multiple-dte-referencias.csv b/src/tests/test_data/sii-rcv/RCV-venta-multiple-dte-referencias.csv new file mode 100644 index 00000000..b6f139e4 --- /dev/null +++ b/src/tests/test_data/sii-rcv/RCV-venta-multiple-dte-referencias.csv @@ -0,0 +1,5 @@ +Nro;Tipo Doc;Tipo Venta;Rut cliente;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse Recibo;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA;Monto total;IVA Retenido Total;IVA Retenido Parcial;IVA no retenido;IVA propio;IVA Terceros;RUT Emisor Liquid. Factura;Neto Comision Liquid. Factura;Exento Comision Liquid. Factura;IVA Comision Liquid. Factura;IVA fuera de plazo;Tipo Docto. Referencia;Folio Docto. Referencia;Num. Ident. Receptor Extranjero;Nacionalidad Receptor Extranjero;Credito empresa constructora;Impto. Zona Franca (Ley 18211);Garantia Dep. Envases;Indicador Venta sin Costo;Indicador Servicio Periodico;Monto No facturable;Total Monto Periodo;Venta Pasajes Transporte Nacional;Venta Pasajes Transporte Internacional;Numero Interno;Codigo Sucursal;NCE o NDE sobre Fact. de Compra;Codigo Otro Imp.;Valor Otro Imp.;Tasa Otro Imp. +1;33;Del Giro;12345678-5;Fake Company S.A. ;506;04/06/2019;18/06/2019 17:01:06;;;0;1750181;332534;2082715;0;0;0;0;0;-;0;0;0;0;;;;;0;;0;2;0;0;0;;;;0;;;;; +2;33;Del Giro;77697060-3;DIRECT LTDA;14239;09/12/2025;09/12/2025 08:50:45;10/12/2025 13:14:17;;0;553180;105104;658284;0;0;0;0;0;-;0;0;0;0;52;10370-10371;;;0;;0;2;0;0;0;;;;0;;;;; +3;33;Del Giro;54213736-3;THE COMPANY SPA;3210;01/09/2025;01/09/2025 10:58:51;08/09/2025 14:15:23;;0;9999;3899347;30471437;0;0;0;0;0;-;0;0;0;0;33;200-300-400;;;0;;0;2;0;0;0;;;;12354;;24;6049210;31.5; +;33;Del Giro;54213736-3;THE COMPANY SPA;3210;01/09/2025;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;271;701395;18;;;;;; diff --git a/src/tests/test_rcv_data_models.py b/src/tests/test_rcv_data_models.py index 4b351401..1b35bd0d 100644 --- a/src/tests/test_rcv_data_models.py +++ b/src/tests/test_rcv_data_models.py @@ -10,6 +10,7 @@ from cl_sii.libs import tz_utils from cl_sii.rcv.constants import RcvTipoDocto from cl_sii.rcv.data_models import ( + DocumentoReferencia, PeriodoTributario, RcNoIncluirDetalleEntry, RcPendienteDetalleEntry, @@ -245,8 +246,7 @@ def setUp(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=None, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -407,14 +407,13 @@ def test_validate_datetime_tz(self) -> None: self.assertEqual(len(validation_errors), len(expected_validation_errors)) self.assertEqual(validation_errors, expected_validation_errors) - def test_get_documento_referencia_dte_natural_key(self) -> None: + def test_get_documento_referencia_dte_natural_keys(self) -> None: rv_detalle_entry = self.rv_detalle_entry_1 # Detalle Entry that does not reference another documento: - self.assertIsNone(rv_detalle_entry.tipo_documento_referencia) - self.assertIsNone(rv_detalle_entry.folio_documento_referencia) - self.assertIsNone(rv_detalle_entry.get_documento_referencia_dte_natural_key()) + self.assertIsNone(rv_detalle_entry.documento_referencias) + self.assertIsNone(rv_detalle_entry.get_documento_referencia_dte_natural_keys()) # Detalle Entry that references a DTE: @@ -422,8 +421,12 @@ def test_get_documento_referencia_dte_natural_key(self) -> None: rv_detalle_entry, tipo_docto=RcvTipoDocto.NOTA_CREDITO_ELECTRONICA, folio=12345, - tipo_documento_referencia=RcvTipoDocto.FACTURA_ELECTRONICA, - folio_documento_referencia=170, + documento_referencias=[ + DocumentoReferencia( + tipo_documento_referencia=RcvTipoDocto.FACTURA_ELECTRONICA, + folio_documento_referencia=170, + ) + ], ) self.assertEqual( cl_sii.dte.data_models.DteNaturalKey( @@ -434,13 +437,15 @@ def test_get_documento_referencia_dte_natural_key(self) -> None: rv_detalle_entry_with_doc_ref.as_dte_data_l2().natural_key, ) - expected_doc_ref_dte_natural_key = cl_sii.dte.data_models.DteNaturalKey( - emisor_rut=Rut('76354771-K'), - tipo_dte=cl_sii.dte.constants.TipoDte.FACTURA_ELECTRONICA, - folio=170, - ) + expected_doc_ref_dte_natural_key = [ + cl_sii.dte.data_models.DteNaturalKey( + emisor_rut=Rut('76354771-K'), + tipo_dte=cl_sii.dte.constants.TipoDte.FACTURA_ELECTRONICA, + folio=170, + ) + ] actual_doc_ref_dte_natural_key = ( - rv_detalle_entry_with_doc_ref.get_documento_referencia_dte_natural_key() + rv_detalle_entry_with_doc_ref.get_documento_referencia_dte_natural_keys() ) self.assertEqual(expected_doc_ref_dte_natural_key, actual_doc_ref_dte_natural_key) @@ -450,21 +455,27 @@ def test_get_documento_referencia_dte_natural_key(self) -> None: rv_detalle_entry_with_non_rcv_doc_ref = dataclasses.replace( rv_detalle_entry, - tipo_documento_referencia=cl_sii.dte.constants.TipoDte.GUIA_DESPACHO_ELECTRONICA, - folio_documento_referencia=12345, + documento_referencias=[ + DocumentoReferencia( + tipo_documento_referencia=cl_sii.dte.constants.TipoDte.GUIA_DESPACHO_ELECTRONICA, # noqa: E501 + folio_documento_referencia=12345, + ) + ], ) with self.assertRaisesRegex(ValueError, r'^52 is not a valid RcvTipoDocto$'): RcvTipoDocto( - rv_detalle_entry_with_non_rcv_doc_ref.tipo_documento_referencia # type: ignore[arg-type] # noqa: E501 + rv_detalle_entry_with_non_rcv_doc_ref.documento_referencias[0]['tipo_documento_referencia'] # type: ignore[index] # noqa: E501 ) - expected_doc_ref_dte_natural_key = cl_sii.dte.data_models.DteNaturalKey( - emisor_rut=Rut('76354771-K'), - tipo_dte=cl_sii.dte.constants.TipoDte.GUIA_DESPACHO_ELECTRONICA, - folio=12345, - ) + expected_doc_ref_dte_natural_key = [ + cl_sii.dte.data_models.DteNaturalKey( + emisor_rut=Rut('76354771-K'), + tipo_dte=cl_sii.dte.constants.TipoDte.GUIA_DESPACHO_ELECTRONICA, + folio=12345, + ) + ] actual_doc_ref_dte_natural_key = ( - rv_detalle_entry_with_non_rcv_doc_ref.get_documento_referencia_dte_natural_key() + rv_detalle_entry_with_non_rcv_doc_ref.get_documento_referencia_dte_natural_keys() ) self.assertEqual(expected_doc_ref_dte_natural_key, actual_doc_ref_dte_natural_key) diff --git a/src/tests/test_rcv_parse_csv.py b/src/tests/test_rcv_parse_csv.py index 0cc713ea..27f4cfd7 100644 --- a/src/tests/test_rcv_parse_csv.py +++ b/src/tests/test_rcv_parse_csv.py @@ -11,6 +11,7 @@ from cl_sii.base.constants import SII_OFFICIAL_TZ from cl_sii.libs.tz_utils import convert_naive_dt_to_tz_aware from cl_sii.rcv.data_models import ( + DocumentoReferencia, OtrosImpuestos, RcNoIncluirDetalleEntry, RcPendienteDetalleEntry, @@ -25,6 +26,7 @@ RcvCompraRegistroCsvRowSchema, RcvVentaCsvRowSchema, _parse_rcv_csv_file, + _parse_rv_documento_referencias, _RcvCompraCsvRowContext, _RcvCompraCsvRowSchemaContext, _RcvVentaCsvRowContext, @@ -70,8 +72,7 @@ def test_parse_rcv_ventas_row(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '', - 'Folio Docto. Referencia': '', + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': '', 'Nacionalidad Receptor Extranjero': '', 'Credito empresa constructora': '0', @@ -126,8 +127,7 @@ def test_parse_rcv_ventas_row(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=None, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -468,8 +468,7 @@ def test_parse_rcv_venta_csv_file(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=None, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -562,8 +561,7 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': None, - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -617,8 +615,7 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=0, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -660,8 +657,7 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '0', - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -710,8 +706,7 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: exento_comision_liquidacion_factura=None, iva_comision_liquidacion_factura=None, iva_fuera_de_plazo=None, - tipo_documento_referencia=None, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=None, @@ -732,13 +727,13 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: { 'Codigo Sucursal': None, 'Credito empresa constructora': None, + 'Documento Referencias': None, 'Exento Comision Liquid. Factura': None, 'Fecha Acuse Recibo': None, 'Fecha Docto': '04/08/2017', 'Fecha Recepcion': '13/09/2017 10:18:59', 'Fecha Reclamo': None, 'Folio': '88', - 'Folio Docto. Referencia': None, 'Garantia Dep. Envases': None, 'IVA Comision Liquid. Factura': None, 'IVA Retenido Parcial': None, @@ -765,7 +760,6 @@ def test_parse_rcv_venta_csv_file_missing_required_fields(self) -> None: 'Razon Social': 'Faker Company', 'Rut cliente': '4954153-8', 'Tipo Doc': '30', - 'Tipo Docto. Referencia': None, 'Tipo Venta': 'DEL_GIRO', 'Total Monto Periodo': None, 'Venta Pasajes Transporte Internacional': None, @@ -883,8 +877,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=0, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -937,8 +930,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '0', - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -998,8 +990,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=0, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -1047,8 +1038,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '0', - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -1106,8 +1096,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=0, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -1155,8 +1144,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '0', - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -1214,8 +1202,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=0, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -1263,8 +1250,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '0', - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -1322,8 +1308,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: exento_comision_liquidacion_factura=0, iva_comision_liquidacion_factura=0, iva_fuera_de_plazo=0, - tipo_documento_referencia=0, - folio_documento_referencia=None, + documento_referencias=None, num_ident_receptor_extranjero=None, nacionalidad_receptor_extranjero=None, credito_empresa_constructora=0, @@ -1376,8 +1361,7 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: 'Exento Comision Liquid. Factura': '0', 'IVA Comision Liquid. Factura': '0', 'IVA fuera de plazo': '0', - 'Tipo Docto. Referencia': '0', - 'Folio Docto. Referencia': None, + 'Documento Referencias': None, 'Num. Ident. Receptor Extranjero': None, 'Nacionalidad Receptor Extranjero': None, 'Credito empresa constructora': '0', @@ -1411,6 +1395,369 @@ def test_parse_rcv_venta_csv_file_empty_otros_impuestos_rows(self) -> None: ] self.assertEqual(items_list, expected_entries_list) + def test_parse_rcv_venta_csv_file_multiple_dte_referencias(self) -> None: + rcv_file_path = get_test_file_path( + 'test_data/sii-rcv/RCV-venta-multiple-dte-referencias.csv', + ) + + items = parse_rcv_venta_csv_file( + rut=Rut('1-9'), + input_file_path=rcv_file_path, + ) + items_list = list(items) + + expected_entries_list: list[ + tuple[Optional[RvDetalleEntry], int, dict[str, object], dict[str, object]] + ] + expected_entries_list = [ + ( + RvDetalleEntry( + contribuyente_rut=Rut('1-9'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=506, + fecha_emision_date=datetime.date(2019, 6, 4), + cliente_rut=Rut('12345678-5'), + monto_total=2082715, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2019, 6, 18, 17, 1, 6), + tz=SII_OFFICIAL_TZ, + ), + tipo_venta='DEL_GIRO', + cliente_razon_social='Fake Company S.A.', + fecha_acuse_dt=None, + fecha_reclamo_dt=None, + monto_exento=0, + monto_neto=1750181, + monto_iva=332534, + iva_retenido_total=0, + iva_retenido_parcial=0, + iva_no_retenido=0, + iva_propio=0, + iva_terceros=0, + liquidacion_factura_emisor_rut=None, + neto_comision_liquidacion_factura=0, + exento_comision_liquidacion_factura=0, + iva_comision_liquidacion_factura=0, + iva_fuera_de_plazo=0, + documento_referencias=None, + num_ident_receptor_extranjero=None, + nacionalidad_receptor_extranjero=None, + credito_empresa_constructora=0, + impuesto_zona_franca_ley_18211=None, + garantia_dep_envases=0, + indicador_venta_sin_costo=2, + indicador_servicio_periodico=0, + monto_no_facturable=0, + total_monto_periodo=0, + venta_pasajes_transporte_nacional=None, + venta_pasajes_transporte_internacional=None, + numero_interno=None, + codigo_sucursal='0', + nce_o_nde_sobre_factura_de_compra=None, + otros_impuestos=None, + ), + 1, + { + 'Tipo Doc': '33', + 'Tipo Venta': 'DEL_GIRO', + 'Rut cliente': '12345678-5', + 'Razon Social': 'Fake Company S.A. ', + 'Folio': '506', + 'Fecha Docto': '04/06/2019', + 'Fecha Recepcion': '18/06/2019 17:01:06', + 'Fecha Acuse Recibo': None, + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '1750181', + 'Monto IVA': '332534', + 'Monto total': '2082715', + 'IVA Retenido Total': '0', + 'IVA Retenido Parcial': '0', + 'IVA no retenido': '0', + 'IVA propio': '0', + 'IVA Terceros': '0', + 'RUT Emisor Liquid. Factura': None, + 'Neto Comision Liquid. Factura': '0', + 'Exento Comision Liquid. Factura': '0', + 'IVA Comision Liquid. Factura': '0', + 'IVA fuera de plazo': '0', + 'Num. Ident. Receptor Extranjero': None, + 'Nacionalidad Receptor Extranjero': None, + 'Credito empresa constructora': '0', + 'Impto. Zona Franca (Ley 18211)': None, + 'Garantia Dep. Envases': '0', + 'Indicador Venta sin Costo': '2', + 'Indicador Servicio Periodico': '0', + 'Monto No facturable': '0', + 'Total Monto Periodo': '0', + 'Venta Pasajes Transporte Nacional': None, + 'Venta Pasajes Transporte Internacional': None, + 'Numero Interno': None, + 'Codigo Sucursal': '0', + 'NCE o NDE sobre Fact. de Compra': None, + 'Documento Referencias': None, + 'Otros Impuestos': None, + 'contribuyente_rut': Rut('1-9'), + }, + {}, + ), + ( + RvDetalleEntry( + contribuyente_rut=Rut('1-9'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=14239, + fecha_emision_date=datetime.date(2025, 12, 9), + cliente_rut=Rut('77697060-3'), + monto_total=658284, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2025, 12, 9, 8, 50, 45), + tz=SII_OFFICIAL_TZ, + ), + tipo_venta='DEL_GIRO', + cliente_razon_social='DIRECT LTDA', + fecha_acuse_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2025, 12, 10, 13, 14, 17), + tz=SII_OFFICIAL_TZ, + ), + fecha_reclamo_dt=None, + monto_exento=0, + monto_neto=553180, + monto_iva=105104, + iva_retenido_total=0, + iva_retenido_parcial=0, + iva_no_retenido=0, + iva_propio=0, + iva_terceros=0, + liquidacion_factura_emisor_rut=None, + neto_comision_liquidacion_factura=0, + exento_comision_liquidacion_factura=0, + iva_comision_liquidacion_factura=0, + iva_fuera_de_plazo=0, + documento_referencias=[ + DocumentoReferencia( + tipo_documento_referencia=52, + folio_documento_referencia=10370, + ), + DocumentoReferencia( + tipo_documento_referencia=52, + folio_documento_referencia=10371, + ), + ], + num_ident_receptor_extranjero=None, + nacionalidad_receptor_extranjero=None, + credito_empresa_constructora=0, + impuesto_zona_franca_ley_18211=None, + garantia_dep_envases=0, + indicador_venta_sin_costo=2, + indicador_servicio_periodico=0, + monto_no_facturable=0, + total_monto_periodo=0, + venta_pasajes_transporte_nacional=None, + venta_pasajes_transporte_internacional=None, + numero_interno=None, + codigo_sucursal='0', + nce_o_nde_sobre_factura_de_compra=None, + otros_impuestos=None, + ), + 2, + { + 'Tipo Doc': '33', + 'Tipo Venta': 'DEL_GIRO', + 'Rut cliente': '77697060-3', + 'Razon Social': 'DIRECT LTDA', + 'Folio': '14239', + 'Fecha Docto': '09/12/2025', + 'Fecha Recepcion': '09/12/2025 08:50:45', + 'Fecha Acuse Recibo': '10/12/2025 13:14:17', + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '553180', + 'Monto IVA': '105104', + 'Monto total': '658284', + 'IVA Retenido Total': '0', + 'IVA Retenido Parcial': '0', + 'IVA no retenido': '0', + 'IVA propio': '0', + 'IVA Terceros': '0', + 'RUT Emisor Liquid. Factura': None, + 'Neto Comision Liquid. Factura': '0', + 'Exento Comision Liquid. Factura': '0', + 'IVA Comision Liquid. Factura': '0', + 'IVA fuera de plazo': '0', + 'Num. Ident. Receptor Extranjero': None, + 'Nacionalidad Receptor Extranjero': None, + 'Credito empresa constructora': '0', + 'Impto. Zona Franca (Ley 18211)': None, + 'Garantia Dep. Envases': '0', + 'Indicador Venta sin Costo': '2', + 'Indicador Servicio Periodico': '0', + 'Monto No facturable': '0', + 'Total Monto Periodo': '0', + 'Venta Pasajes Transporte Nacional': None, + 'Venta Pasajes Transporte Internacional': None, + 'Numero Interno': None, + 'Codigo Sucursal': '0', + 'NCE o NDE sobre Fact. de Compra': None, + 'Documento Referencias': [ + { + 'tipo_documento_referencia': 52, + 'folio_documento_referencia': 10370, + }, + { + 'tipo_documento_referencia': 52, + 'folio_documento_referencia': 10371, + }, + ], + 'Otros Impuestos': None, + 'contribuyente_rut': Rut('1-9'), + }, + {}, + ), + ( + RvDetalleEntry( + contribuyente_rut=Rut('1-9'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=3210, + fecha_emision_date=datetime.date(2025, 9, 1), + cliente_rut=Rut('54213736-3'), + monto_total=30471437, + fecha_recepcion_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2025, 9, 1, 10, 58, 51), + tz=SII_OFFICIAL_TZ, + ), + tipo_venta='DEL_GIRO', + cliente_razon_social='THE COMPANY SPA', + fecha_acuse_dt=convert_naive_dt_to_tz_aware( + dt=datetime.datetime(2025, 9, 8, 14, 15, 23), + tz=SII_OFFICIAL_TZ, + ), + fecha_reclamo_dt=None, + monto_exento=0, + monto_neto=9999, + monto_iva=3899347, + iva_retenido_total=0, + iva_retenido_parcial=0, + iva_no_retenido=0, + iva_propio=0, + iva_terceros=0, + liquidacion_factura_emisor_rut=None, + neto_comision_liquidacion_factura=0, + exento_comision_liquidacion_factura=0, + iva_comision_liquidacion_factura=0, + iva_fuera_de_plazo=0, + documento_referencias=[ + DocumentoReferencia( + tipo_documento_referencia=33, + folio_documento_referencia=200, + ), + DocumentoReferencia( + tipo_documento_referencia=33, + folio_documento_referencia=300, + ), + DocumentoReferencia( + tipo_documento_referencia=33, + folio_documento_referencia=400, + ), + ], + num_ident_receptor_extranjero=None, + nacionalidad_receptor_extranjero=None, + credito_empresa_constructora=0, + impuesto_zona_franca_ley_18211=None, + garantia_dep_envases=0, + indicador_venta_sin_costo=2, + indicador_servicio_periodico=0, + monto_no_facturable=0, + total_monto_periodo=0, + venta_pasajes_transporte_nacional=None, + venta_pasajes_transporte_internacional=None, + numero_interno=None, + codigo_sucursal='12354', + nce_o_nde_sobre_factura_de_compra=None, + otros_impuestos=[ + { + 'codigo_otro_impuesto': '24', + 'valor_otro_impuesto': 6049210, + 'tasa_otro_impuesto': Decimal('31.5'), + }, + { + 'codigo_otro_impuesto': '271', + 'valor_otro_impuesto': 701395, + 'tasa_otro_impuesto': Decimal('18'), + }, + ], + ), + 3, + { + 'Tipo Doc': '33', + 'Tipo Venta': 'DEL_GIRO', + 'Rut cliente': '54213736-3', + 'Razon Social': 'THE COMPANY SPA', + 'Folio': '3210', + 'Fecha Docto': '01/09/2025', + 'Fecha Recepcion': '01/09/2025 10:58:51', + 'Fecha Acuse Recibo': '08/09/2025 14:15:23', + 'Fecha Reclamo': None, + 'Monto Exento': '0', + 'Monto Neto': '9999', + 'Monto IVA': '3899347', + 'Monto total': '30471437', + 'IVA Retenido Total': '0', + 'IVA Retenido Parcial': '0', + 'IVA no retenido': '0', + 'IVA propio': '0', + 'IVA Terceros': '0', + 'RUT Emisor Liquid. Factura': None, + 'Neto Comision Liquid. Factura': '0', + 'Exento Comision Liquid. Factura': '0', + 'IVA Comision Liquid. Factura': '0', + 'IVA fuera de plazo': '0', + 'Num. Ident. Receptor Extranjero': None, + 'Nacionalidad Receptor Extranjero': None, + 'Credito empresa constructora': '0', + 'Impto. Zona Franca (Ley 18211)': None, + 'Garantia Dep. Envases': '0', + 'Indicador Venta sin Costo': '2', + 'Indicador Servicio Periodico': '0', + 'Monto No facturable': '0', + 'Total Monto Periodo': '0', + 'Venta Pasajes Transporte Nacional': None, + 'Venta Pasajes Transporte Internacional': None, + 'Numero Interno': None, + 'Codigo Sucursal': '12354', + 'NCE o NDE sobre Fact. de Compra': None, + 'Documento Referencias': [ + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 200, + }, + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 300, + }, + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 400, + }, + ], + 'Otros Impuestos': [ + { + 'codigo_otro_impuesto': '24', + 'valor_otro_impuesto': '6049210', + 'tasa_otro_impuesto': '31.5', + }, + { + 'codigo_otro_impuesto': '271', + 'valor_otro_impuesto': '701395', + 'tasa_otro_impuesto': '18', + }, + ], + 'contribuyente_rut': Rut('1-9'), + }, + {}, + ), + ] + self.assertEqual(items_list, expected_entries_list) + def test_parse_rcv_compra_registro_csv_file(self) -> None: # TODO: implement for 'parse_rcv_compra_registro_csv_file'. pass @@ -1798,3 +2145,98 @@ class DummyRcvKind: with self.assertRaises(Exception): get_rcv_csv_file_parser(DummyRcvKind(), None) # type: ignore[arg-type] + + def test__parse_rv_documento_referencias(self) -> None: + """Test _parse_rv_documento_referencias function with all possible scenarios.""" + + # Test case 1: Both parameters are None - should return None + with self.subTest("Both None"): + result = _parse_rv_documento_referencias(None, None) + self.assertIsNone(result) + + # Test case 2: Both parameters are empty strings - should return None + with self.subTest("Mixed empty values"): + result = _parse_rv_documento_referencias("0", "") + self.assertIsNone(result) + + # Test case 3: Only tipo provided (folio empty) - should raise ValueError + with self.subTest("Only tipo provided"): + with self.assertRaises(ValueError) as cm: + _parse_rv_documento_referencias("33", None) + self.assertIn("Both 'tipo_documento_referencia'", str(cm.exception)) + + with self.assertRaises(ValueError): + _parse_rv_documento_referencias(33, "") + + with self.assertRaises(ValueError): + _parse_rv_documento_referencias("33", "0") + + # Test case 4: Only folio provided (tipo empty) - should raise ValueError + with self.subTest("Only folio provided"): + with self.assertRaises(ValueError) as cm: + _parse_rv_documento_referencias(None, "12345") + self.assertIn("Both 'tipo_documento_referencia'", str(cm.exception)) + + with self.assertRaises(ValueError): + _parse_rv_documento_referencias("", "12345") + + with self.assertRaises(ValueError): + _parse_rv_documento_referencias("0", "12345") + + # Test case 5: Valid single folio with string tipo + with self.subTest("Valid single folio - string tipo"): + result = _parse_rv_documento_referencias("33", "12345") + expected = [ + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 12345, + } + ] + self.assertEqual(result, expected) + + # Test case 6: Valid single folio with integer tipo + with self.subTest("Valid single folio - integer tipo"): + result = _parse_rv_documento_referencias(52, "67890") + expected = [ + { + 'tipo_documento_referencia': 52, + 'folio_documento_referencia': 67890, + } + ] + self.assertEqual(result, expected) + + # Test case 7: Valid multiple folios - three folios + with self.subTest("Valid three folios"): + result = _parse_rv_documento_referencias("33", "200-300-400") + expected = [ + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 200, + }, + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 300, + }, + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 400, + }, + ] + self.assertEqual(result, expected) + + # Test case 8: Invalid folio - non-numeric + with self.subTest("Invalid folio - non-numeric"): + with self.assertRaises(ValueError) as cm: + _parse_rv_documento_referencias("33", "abc") + self.assertIn("Invalid 'folio_documento_referencia'", str(cm.exception)) + + # Test case 9: Non-string folio parameter (should work for integer) + with self.subTest("Non-string folio"): + result = _parse_rv_documento_referencias("33", 12345) # type: ignore[arg-type] + expected = [ + { + 'tipo_documento_referencia': 33, + 'folio_documento_referencia': 12345, + } + ] + self.assertEqual(result, expected) From f63989d0467674daee30fcb5233a7cfd63ae29ec Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Mon, 29 Dec 2025 13:29:15 -0300 Subject: [PATCH 3/4] chore: Update history for new version --- HISTORY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index a7f13674..50312649 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # History +## 0.68.0 (2025-12-29) + +- (PR #962, 2025-12-29) rcv: Update RV fields optional in schemas and data models +- (PR #961, 2025-12-29) rcv: Introduce DocumentoReferencias for improved data handling + ## 0.67.0 (2025-12-02) - (PR #956, 2025-12-02) rcv: `RvDetalleEntry.get_documento_referencia_dte_natural_key()` From 16d0cefe5d6e3cf86b02780ec9fe8b33ecb5d580 Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Mon, 29 Dec 2025 13:29:17 -0300 Subject: [PATCH 4/4] chore: Bump version from 0.67.0 to 0.68.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 f43f7d59..acea0719 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.67.0 +current_version = 0.68.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 8c0fb0e2..fdd6d401 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.67.0' +__version__ = '0.68.0'