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)