Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.56.0
current_version = 0.57.0
commit = True
tag = False
message = chore: Bump version from {current_version} to {new_version}
Expand Down
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# History

## 0.57.0 (2025-09-15)

- (PR #888, 2025-09-10) tests: Refactor and improve constants tests
- (PR #889, 2025-09-12) cte: Add parser for "Carpeta Tributaria Electrónica"
- (PR #891, 2025-09-15) rcv: Make `iva_no_retenido` optional in data models and parsers
- (PR #892, 2025-09-15) rcv: Improve checks for RCV Reclamado parser workaround

## 0.56.0 (2025-09-10)

- (PR #883, 2025-09-10) extras: Add more tests for Django form field for `Rut`
Expand Down
2 changes: 1 addition & 1 deletion src/cl_sii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""

__version__ = '0.56.0'
__version__ = '0.57.0'
40 changes: 40 additions & 0 deletions src/cl_sii/cte/data_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from collections.abc import Sequence

import pydantic


@pydantic.dataclasses.dataclass(
frozen=True,
config=pydantic.ConfigDict(
arbitrary_types_allowed=True,
extra='forbid',
),
)
class TaxpayerProvidedInfo:
"""
Información proporcionada por el contribuyente para fines tributarios (1)
"""

legal_representatives: Sequence[LegalRepresentative]
company_formation: Sequence[LegalRepresentative]
participation_in_existing_companies: Sequence[LegalRepresentative]


@pydantic.dataclasses.dataclass(
frozen=True,
)
class LegalRepresentative:
name: str
"""
Nombre o Razón social.
"""
rut: str
"""
RUT.
"""
incorporation_date: str
"""
Fecha de incorporación.
"""
91 changes: 91 additions & 0 deletions src/cl_sii/cte/parsers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import annotations

from bs4 import BeautifulSoup

from .data_models import LegalRepresentative, TaxpayerProvidedInfo


def parse_taxpayer_provided_info(html_content: str) -> TaxpayerProvidedInfo:
"""
Parse the CTE HTML content to extract the content of the section:
"Información proporcionada por el contribuyente para fines tributarios (1)"

Args:
html_content: HTML string containing the taxpayer information table

Returns:
TaxpayerProvidedInfo instance with the parsed data
"""
soup = BeautifulSoup(html_content, 'html.parser')

# Find the main table with id="tbl_sociedades"
table = soup.find('table', id='tbl_sociedades')

if not table:
raise ValueError("Could not find taxpayer information table in HTML")

# Initialize lists for each section
legal_representatives = []
company_formation = []
participation_in_companies = []

# Current section being parsed
current_section = None

# Iterate through rows to extract data
rows = table.find_all('tr') # type: ignore[attr-defined]
for row in rows:
section_header = row.find(
'span', class_='textof', string=lambda s: s and 'Representante(s) Legal(es)' in s
)
if section_header:
current_section = 'legal_representatives'
continue

section_header = row.find(
'span',
class_='textof',
string=lambda s: s and 'Conformación de la sociedad' in s,
)
if section_header:
current_section = 'company_formation'
continue

section_header = row.find(
'span',
class_='textof',
string=lambda s: s and 'Participación en sociedades vigentes' in s,
)
if section_header:
current_section = 'participation_in_companies'
continue

# Skip rows without useful data
cells = row.find_all('td')
if len(cells) < 3:
continue

name_cell = cells[1].find('span', class_='textof')
rut_cell = cells[2].find('span', class_='textof')
date_cell = cells[3].find('span', class_='textof')

# If this is a data row with person information
if name_cell and rut_cell and date_cell and name_cell.text.strip():
name = name_cell.text.strip()
rut = rut_cell.text.strip()
incorporation_date = date_cell.text.strip()

person = LegalRepresentative(name=name, rut=rut, incorporation_date=incorporation_date)

if current_section == 'legal_representatives':
legal_representatives.append(person)
elif current_section == 'company_formation':
company_formation.append(person)
elif current_section == 'participation_in_companies':
participation_in_companies.append(person)

return TaxpayerProvidedInfo(
legal_representatives=legal_representatives,
company_formation=company_formation,
participation_in_existing_companies=participation_in_companies,
)
2 changes: 1 addition & 1 deletion src/cl_sii/rcv/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ class RcDetalleEntry(RcvDetalleEntry):
Impto. Sin Derecho a Credito
"""

iva_no_retenido: int
iva_no_retenido: Optional[int]
"""
IVA No Retenido
"""
Expand Down
13 changes: 7 additions & 6 deletions src/cl_sii/rcv/parse_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ def to_detalle_entry(self, data: dict) -> RcRegistroDetalleEntry:
iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo')
iva_uso_comun: Optional[int] = data.get('iva_uso_comun')
impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito')
iva_no_retenido: int = data['iva_no_retenido']
iva_no_retenido: Optional[int] = data.get('iva_no_retenido')
nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get(
'nce_o_nde_sobre_factura_de_compra'
)
Expand Down Expand Up @@ -1194,7 +1194,7 @@ def to_detalle_entry(self, data: dict) -> RcNoIncluirDetalleEntry:
iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo')
iva_uso_comun: Optional[int] = data.get('iva_uso_comun')
impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito')
iva_no_retenido: int = data['iva_no_retenido']
iva_no_retenido: Optional[int] = data.get('iva_no_retenido')
nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get(
'nce_o_nde_sobre_factura_de_compra'
)
Expand Down Expand Up @@ -1263,8 +1263,9 @@ def preprocess(self, in_data: dict, **kwargs: Any) -> dict:
# note: for some reason the rows with 'tipo_docto' equal to
# '<RcvTipoDocto.NOTA_CREDITO_ELECTRONICA: 61>' (and maybe others as well) do not
# have this field set (always? we do not know).
if 'Fecha Reclamo' in in_data:
if in_data['Fecha Reclamo'] == '' or 'null' in in_data['Fecha Reclamo']:
if 'Fecha Reclamo' in in_data and in_data['Fecha Reclamo'] is not None:
value = in_data['Fecha Reclamo']
if isinstance(value, str) and (value == '' or 'null' in value):
in_data['Fecha Reclamo'] = None

return in_data
Expand Down Expand Up @@ -1314,7 +1315,7 @@ def to_detalle_entry(self, data: dict) -> RcReclamadoDetalleEntry:
iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo')
iva_uso_comun: Optional[int] = data.get('iva_uso_comun')
impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito')
iva_no_retenido: int = data['iva_no_retenido']
iva_no_retenido: Optional[int] = data.get('iva_no_retenido')
nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get(
'nce_o_nde_sobre_factura_de_compra'
)
Expand Down Expand Up @@ -1399,7 +1400,7 @@ def to_detalle_entry(self, data: dict) -> RcPendienteDetalleEntry:
iva_activo_fijo: Optional[int] = data.get('iva_activo_fijo')
iva_uso_comun: Optional[int] = data.get('iva_uso_comun')
impto_sin_derecho_a_credito: Optional[int] = data.get('impto_sin_derecho_a_credito')
iva_no_retenido: int = data['iva_no_retenido']
iva_no_retenido: Optional[int] = data.get('iva_no_retenido')
nce_o_nde_sobre_factura_de_compra: Optional[str] = data.get(
'nce_o_nde_sobre_factura_de_compra'
)
Expand Down
51 changes: 51 additions & 0 deletions src/tests/test_cte_parsers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from unittest import TestCase

from cl_sii.cte import data_models, parsers
from .utils import read_test_file_str_utf8


class ParsersTest(TestCase):
def test_parse_taxpayer_provided_info(self) -> None:
html_content = read_test_file_str_utf8('test_data/sii-cte/cte_taxpayer_provided_info.html')

with self.subTest("Parsing ok"):
result = parsers.parse_taxpayer_provided_info(html_content)
expected_obj = data_models.TaxpayerProvidedInfo(
legal_representatives=[
data_models.LegalRepresentative(
name='DAVID USUARIO DE PRUEBA',
rut='76354771-K',
incorporation_date='20-09-2023',
),
data_models.LegalRepresentative(
name='JAVIERA USUARIO DE PRUEBA',
rut='38855667-6',
incorporation_date='20-09-2023',
),
],
company_formation=[
data_models.LegalRepresentative(
name='JAVIERA USUARIO DE PRUEBA',
rut='38855667-6',
incorporation_date='20-09-2023',
),
data_models.LegalRepresentative(
name='MARÍA USUARIO DE PRUEBA',
rut='34413183-k',
incorporation_date='23-02-2024',
),
],
participation_in_existing_companies=[],
)
self.assertEqual(result, expected_obj)

with self.subTest("Parsing emtpy content"):
with self.assertRaises(ValueError) as assert_raises_cm:
parsers.parse_taxpayer_provided_info("")

self.assertEqual(
assert_raises_cm.exception.args,
("Could not find taxpayer information table in HTML",),
)
Loading
Loading