From 9155a2193800f8459397f91e196199f8ea8d039d Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Thu, 28 Aug 2025 15:04:22 -0400 Subject: [PATCH 1/2] chore(rcv): Add TypeVar for RCV CSV parser typing --- src/cl_sii/rcv/parse_csv.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cl_sii/rcv/parse_csv.py b/src/cl_sii/rcv/parse_csv.py index 9bc273eb..93b53a38 100644 --- a/src/cl_sii/rcv/parse_csv.py +++ b/src/cl_sii/rcv/parse_csv.py @@ -8,7 +8,7 @@ import csv import logging from datetime import date, datetime -from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple +from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple, TypeVar import marshmallow @@ -30,12 +30,13 @@ logger = logging.getLogger(__name__) +RcvDetalleEntryType = TypeVar('RcvDetalleEntryType', bound=RcvDetalleEntry) RcvCsvFileParserType = Callable[ [Rut, str, int, Optional[int]], Iterable[ Tuple[ - Optional[RcvDetalleEntry], + Optional[RcvDetalleEntryType], int, Dict[str, object], Dict[str, object], From c65bd6adb11b8adabd3e368561049d8119571307 Mon Sep 17 00:00:00 2001 From: Samuel Villegas Date: Thu, 28 Aug 2025 15:05:25 -0400 Subject: [PATCH 2/2] feat(rcv): Update "RCV Ventas" update preprocessing logic - Implemented test for `parse_rcv_venta_csv_file` ensuring correct CSV parsing. - Updated `_RcvCsvRowSchemaBase.preprocess` to handle empty and "-" values correctly. --- src/cl_sii/rcv/parse_csv.py | 25 ++++++------ src/tests/test_rcv_parse_csv.py | 67 ++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/cl_sii/rcv/parse_csv.py b/src/cl_sii/rcv/parse_csv.py index 93b53a38..41267720 100644 --- a/src/cl_sii/rcv/parse_csv.py +++ b/src/cl_sii/rcv/parse_csv.py @@ -432,17 +432,20 @@ def preprocess(self, in_data: dict, **kwargs: Any) -> dict: for name, field_item in self.fields.items(): data_key = field_item.data_key if data_key is not None and data_key in in_data.keys(): - if isinstance( - field_item, - ( - marshmallow.fields.Integer, - marshmallow.fields.Decimal, - marshmallow.fields.Float, - marshmallow.fields.Date, - marshmallow.fields.DateTime, - mm_fields.RutField, - ), - ) and in_data[data_key] in ('', '-'): + if in_data[data_key] == '' or ( + isinstance( + field_item, + ( + marshmallow.fields.Integer, + marshmallow.fields.Decimal, + marshmallow.fields.Float, + marshmallow.fields.Date, + marshmallow.fields.DateTime, + mm_fields.RutField, + ), + ) + and in_data[data_key] == '-' + ): in_data[data_key] = None return in_data diff --git a/src/tests/test_rcv_parse_csv.py b/src/tests/test_rcv_parse_csv.py index 0bf09e81..f03137ba 100644 --- a/src/tests/test_rcv_parse_csv.py +++ b/src/tests/test_rcv_parse_csv.py @@ -5,6 +5,7 @@ import cl_sii.rcv.constants 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 ( RcNoIncluirDetalleEntry, RcPendienteDetalleEntry, @@ -399,8 +400,70 @@ def test_parse_rcv_compra_pendiente_row(self) -> None: class FunctionsTest(unittest.TestCase): def test_parse_rcv_venta_csv_file(self) -> None: - # TODO: implement for 'parse_rcv_venta_csv_file'. - pass + rcv_file_path = get_test_file_path( + 'test_data/sii-rcv/RCV-venta-rz_leading_trailing_whitespace.csv', + ) + + items = parse_rcv_venta_csv_file( + rut=Rut('1-9'), + input_file_path=rcv_file_path, + n_rows_offset=0, + max_n_rows=None, + ) + + expected_entry_struct = RvDetalleEntry( + emisor_rut=Rut('1-9'), + tipo_docto=cl_sii.rcv.constants.RcvTipoDocto.FACTURA_ELECTRONICA, + folio=506, + fecha_emision_date=datetime.date(2019, 6, 4), + receptor_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', + receptor_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, + tipo_documento_referencia=None, + 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, + codigo_otro_imp=None, + valor_otro_imp=None, + tasa_otro_imp=None, + ) + # First row: + entry_struct, row_ix, row_data, row_parsing_errors = next(items) + self.assertEqual(row_ix, 1) + self.assertEqual(row_data['Razon Social'], 'Fake Company S.A. ') + self.assertEqual(entry_struct, expected_entry_struct) def test_parse_rcv_venta_csv_file_receptor_rz_leading_trailing_whitespace(self) -> None: rcv_file_path = get_test_file_path(