diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b5f0115c..50663019 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.57.0 +current_version = 0.58.0 commit = True tag = False message = chore: Bump version from {current_version} to {new_version} diff --git a/HISTORY.md b/HISTORY.md index 9af257ef..aa13d3e3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # History +## 0.58.0 (2025-09-22) + +- (PR #890, 2025-09-16) cte: Add parser for "Datos del Contribuyente" +- (PR #896, 2025-09-16) rut: Add regex for canonical RUT that is compatible with JSON Schema +- (PR #895, 2025-09-16) cte: Add parser for "Propiedades y Bienes Raíces" + ## 0.57.0 (2025-09-15) - (PR #888, 2025-09-10) tests: Refactor and improve constants tests diff --git a/src/cl_sii/__init__.py b/src/cl_sii/__init__.py index f595f3ce..fc6fdafc 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.57.0' +__version__ = '0.58.0' diff --git a/src/cl_sii/cte/data_models.py b/src/cl_sii/cte/data_models.py index 91155774..fb69782a 100644 --- a/src/cl_sii/cte/data_models.py +++ b/src/cl_sii/cte/data_models.py @@ -1,6 +1,9 @@ from __future__ import annotations from collections.abc import Sequence +from datetime import date +from decimal import Decimal +from typing import Optional import pydantic @@ -38,3 +41,135 @@ class LegalRepresentative: """ Fecha de incorporación. """ + + +@pydantic.dataclasses.dataclass( + frozen=True, + config=pydantic.ConfigDict( + arbitrary_types_allowed=True, + extra='forbid', + ), +) +class TaxpayerData: + start_of_activities_date: Optional[date] + """ + Fecha de inicio de actividades. + """ + economic_activities: str + """ + Actividades Económicas + """ + tax_category: str + """ + Categoría Tributaria + """ + address: str + """ + Domicilio + """ + branches: Sequence[str] + """ + Sucursales + """ + last_filed_documents: Sequence[LastFiledDocument] + """ + Últimos documentos timbrados + """ + tax_observations: Optional[str] = None + """ + Observaciones tributarias + """ + + +@pydantic.dataclasses.dataclass( + frozen=True, +) +class LastFiledDocument: + name: str + date: date + + +@pydantic.dataclasses.dataclass( + frozen=True, + config=pydantic.ConfigDict( + arbitrary_types_allowed=True, + extra='forbid', + ), +) +class TaxpayerProperties: + """ + Propiedades y Bienes Raíces (3) + """ + + properties: Sequence[Property] + + +@pydantic.dataclasses.dataclass( + frozen=True, +) +class Property: + commune: Optional[str] + """ + Comuna + """ + role: Optional[str] + """ + Rol + """ + address: Optional[str] + """ + Dirección + """ + purpose: Optional[str] + """ + Destino + """ + fiscal_valuation: Optional[Decimal] + """ + Avalúo Fiscal + """ + overdue_installments: Optional[bool] + """ + Cuotas vencidas por pagar + """ + current_installments: Optional[bool] + """ + Cuotas vigentes por pagar + """ + condition: Optional[str] + """ + Condición + """ + + ########################################################################### + # Validators + ########################################################################### + + @pydantic.field_validator('fiscal_valuation', mode='before') + @classmethod + def parse_fiscal_valuation(cls, v: Optional[str]) -> Optional[Decimal]: + if isinstance(v, str): + v = v.replace('.', '').replace(',', '.') + return Decimal(v) + return v + + @pydantic.field_validator('commune', 'role', 'address', 'purpose', 'condition') + @classmethod + def parse_str_fields(cls, v: Optional[str]) -> Optional[str]: + if isinstance(v, str) and not v.strip(): + return None + return v + + @pydantic.field_validator('current_installments', 'overdue_installments', mode='before') + @classmethod + def parse_boolean_fields(cls, v: Optional[str | bool]) -> Optional[bool]: + if isinstance(v, str): + if v == 'NO': + return False + elif v == 'SI': + return True + else: + return None + if isinstance(v, bool): + return v + return None diff --git a/src/cl_sii/cte/parsers.py b/src/cl_sii/cte/parsers.py index bbcd6a5a..e49a214d 100644 --- a/src/cl_sii/cte/parsers.py +++ b/src/cl_sii/cte/parsers.py @@ -1,8 +1,17 @@ from __future__ import annotations +from datetime import datetime + from bs4 import BeautifulSoup -from .data_models import LegalRepresentative, TaxpayerProvidedInfo +from .data_models import ( + LastFiledDocument, + LegalRepresentative, + Property, + TaxpayerData, + TaxpayerProperties, + TaxpayerProvidedInfo, +) def parse_taxpayer_provided_info(html_content: str) -> TaxpayerProvidedInfo: @@ -89,3 +98,139 @@ def parse_taxpayer_provided_info(html_content: str) -> TaxpayerProvidedInfo: company_formation=company_formation, participation_in_existing_companies=participation_in_companies, ) + + +def parse_taxpayer_data(html_content: str) -> TaxpayerData: + """ + Parse the CTE HTML content to extract the content of the section: + "Datos del Contribuyente" + + Args: + html_content: HTML string containing the taxpayer information table + + Returns: + TaxpayerData instance with the parsed data + """ + soup = BeautifulSoup(html_content, 'html.parser') + table = soup.find('table', id='tbl_dbcontribuyente') + if not table: + raise ValueError("Could not find 'Datos del Contribuyente' table in HTML") + + fecha_inicio_elem = table.find(id='td_fecha_inicio') # type: ignore[attr-defined] + if fecha_inicio_elem: + start_of_activities_date = ( + datetime.strptime(fecha_inicio_elem.get_text(strip=True), "%d-%m-%Y").date() + if fecha_inicio_elem.get_text(strip=True) + else None + ) + else: + start_of_activities_date = None + + actividades_elem = table.find(id='td_actividades') # type: ignore[attr-defined] + if actividades_elem: + economic_activities = actividades_elem.get_text(separator="\n", strip=True) + else: + economic_activities = "" + + categoria_elem = table.find(id='td_categoria') # type: ignore[attr-defined] + if categoria_elem: + tax_category = categoria_elem.get_text(strip=True) + else: + tax_category = "" + + domicilio_elem = table.find(id='td_domicilio') # type: ignore[attr-defined] + if domicilio_elem: + address = domicilio_elem.get_text(strip=True) + else: + address = "" + + # Sucursales + branches = [] + sucursales_row = table.find( # type: ignore[attr-defined] + 'td', + string=lambda s: s and 'Sucursales:' in s, + ) + if sucursales_row: + sucursales_td = sucursales_row.find_next_sibling('td') + if sucursales_td: + branches_text = sucursales_td.get_text(separator="\n", strip=True) + branches = [b for b in branches_text.split("\n") if b] + + # Últimos documentos timbrados + last_filed_documents = [] + tim_nombre_elem = table.find(id='td_tim_nombre') # type: ignore[attr-defined] + tim_fecha_elem = table.find(id='td_tim_fecha') # type: ignore[attr-defined] + if tim_nombre_elem and tim_fecha_elem: + names = tim_nombre_elem.get_text(separator="\n", strip=True).split("\n") + dates = tim_fecha_elem.get_text(separator="\n", strip=True).split("\n") + for name, date_str in zip(names, dates): + if name and date_str: + doc_date = datetime.strptime(date_str, "%d-%m-%Y").date() + last_filed_documents.append(LastFiledDocument(name=name, date=doc_date)) + + # Observaciones tributarias + tax_observations = None + observaciones_elem = table.find(id='td_observaciones') # type: ignore[attr-defined] + if observaciones_elem: + tax_observations = observaciones_elem.get_text(strip=True) + + return TaxpayerData( + start_of_activities_date=start_of_activities_date, + economic_activities=economic_activities, + tax_category=tax_category, + address=address, + branches=branches, + last_filed_documents=last_filed_documents, + tax_observations=tax_observations, + ) + + +def parse_taxpayer_properties(html_content: str) -> TaxpayerProperties: + """ + Parse the CTE HTML content to extract the content of the section: + "Propiedades y Bienes Raíces (3)" + + Args: + html_content: HTML string containing the taxpayer properties table + + Returns: + TaxpayerProperties instance with the parsed data + """ + soup = BeautifulSoup(html_content, 'html.parser') + + # Find the main table with id="tbl_propiedades" + table = soup.find('table', id='tbl_propiedades') + if not table: + raise ValueError("Could not find taxpayer information table in HTML") + + properties = [] + rows = table.find_all('tr') # type: ignore[attr-defined] + for row in rows[2:]: # Skip headers rows + + # Skip rows without useful data + cells = row.find_all('td') + if len(cells) < 8: + continue + + commune = cells[0].get_text(strip=True) or None + role = cells[1].get_text(strip=True) or None + address = cells[2].get_text(strip=True) or None + purpose = cells[3].get_text(strip=True) or None + fiscal_valuation = cells[4].get_text(strip=True) or None + overdue_installments = cells[5].get_text(strip=True) or None + current_installments = cells[6].get_text(strip=True) or None + condition = cells[7].get_text(strip=True) or None + + properties.append( + Property( + commune=commune, + role=role, + address=address, + purpose=purpose, + fiscal_valuation=fiscal_valuation, + overdue_installments=overdue_installments, + current_installments=current_installments, + condition=condition, + ) + ) + return TaxpayerProperties(properties=properties) diff --git a/src/cl_sii/extras/pydantic_types.py b/src/cl_sii/extras/pydantic_types.py index 86724434..66ae221d 100644 --- a/src/cl_sii/extras/pydantic_types.py +++ b/src/cl_sii/extras/pydantic_types.py @@ -4,7 +4,6 @@ from __future__ import annotations -import re import sys from typing import Any, ClassVar, Pattern @@ -79,12 +78,8 @@ class _RutPydanticAnnotation: >>> example_json_schema = example_type_adapter.json_schema() """ - RUT_CANONICAL_STRICT_REGEX: ClassVar[Pattern] = re.compile( - re.sub( - pattern=r'\?P<\w+>', - repl='', - string=cl_sii.rut.constants.RUT_CANONICAL_STRICT_REGEX.pattern, - ) + RUT_CANONICAL_STRICT_REGEX: ClassVar[Pattern] = ( + cl_sii.rut.constants.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX ) """ RUT (strict) regex for canonical format, without named groups. diff --git a/src/cl_sii/rut/constants.py b/src/cl_sii/rut/constants.py index b5086386..751c2e61 100644 --- a/src/cl_sii/rut/constants.py +++ b/src/cl_sii/rut/constants.py @@ -7,6 +7,7 @@ """ import re +from typing import Pattern import cryptography.x509 @@ -22,6 +23,17 @@ RUT_DIGITS_MIN_VALUE = 1 """RUT digits min value.""" +RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX: Pattern[str] = re.compile("^(\\d{1,8})-([\\dK])$") +""" +RUT (strict) JSON Schema regex for canonical format. + +This regex is compatible with JSON Schema and OpenAPI, which use the regular expression syntax from +JavaScript (ECMA 262), which does not support Python’s named groups. + +.. tip:: If you need the regex as a string, for example to use it in a JSON Schema or + OpenAPI schema, use ``RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX.pattern``. +""" + SII_CERT_TITULAR_RUT_OID = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.4.1.8321.1") """OID of the RUT of the certificate holder""" # - Organismo: MINISTERIO DE ECONOMÍA / SUBSECRETARIA DE ECONOMIA diff --git a/src/tests/test_cte_parsers.py b/src/tests/test_cte_parsers.py index c2fb8005..bdd09c02 100644 --- a/src/tests/test_cte_parsers.py +++ b/src/tests/test_cte_parsers.py @@ -1,5 +1,7 @@ from __future__ import annotations +from datetime import date +from decimal import Decimal from unittest import TestCase from cl_sii.cte import data_models, parsers @@ -41,7 +43,7 @@ def test_parse_taxpayer_provided_info(self) -> None: ) self.assertEqual(result, expected_obj) - with self.subTest("Parsing emtpy content"): + with self.subTest("Parsing empty content"): with self.assertRaises(ValueError) as assert_raises_cm: parsers.parse_taxpayer_provided_info("") @@ -49,3 +51,108 @@ def test_parse_taxpayer_provided_info(self) -> None: assert_raises_cm.exception.args, ("Could not find taxpayer information table in HTML",), ) + + def test_parse_taxpayer_data(self) -> None: + html_content = read_test_file_str_utf8('test_data/sii-cte/cte_empty_f29.html') + with self.subTest("Parsing ok"): + result = parsers.parse_taxpayer_data(html_content) + expected_obj = data_models.TaxpayerData( + start_of_activities_date=date(2023, 11, 15), + economic_activities=( + "SERVICIOS DE ASESORIA Y CONSULTORIA EN MATERIA DE ADMINISTRACION DE EMPRESAS " + "Y OTROS SERVICIOS DE ASESORIA ADMINISTRATIVA Y DE NEGOCIOS N.C.P.\n" + "ACTIVIDADES DE OTRAS ORGANIZACIONES EMPRESARIALES N.C.P.\n" + "OTRAS ACTIVIDADES DE SERVICIOS PERSONALES N.C.P." + ), + tax_category="Primera categoría", + address="AV REAL, LAS CONDES", + branches=[], + last_filed_documents=[ + data_models.LastFiledDocument( + name="FACTURA ELECTRONICA", date=date(2025, 7, 24) + ), + data_models.LastFiledDocument( + name="FACTURA NO AFECTA O EXENTA ELECTRONICA", date=date(2025, 7, 17) + ), + data_models.LastFiledDocument( + name="GUIA DESPACHO ELECTRONICA", date=date(2025, 5, 14) + ), + data_models.LastFiledDocument( + name="NOTA CREDITO ELECTRONICA", date=date(2025, 7, 18) + ), + ], + tax_observations="No tiene observaciones.", + ) + self.assertEqual(result, expected_obj) + + with self.subTest("Parsing empty content"): + with self.assertRaises(ValueError) as assert_raises_cm: + parsers.parse_taxpayer_data("") + self.assertEqual( + assert_raises_cm.exception.args, + ("Could not find 'Datos del Contribuyente' table in HTML",), + ) + + with self.subTest("Parsing content with empty table"): + html_content = read_test_file_str_utf8('test_data/sii-cte/cte_empty_table.html') + result = parsers.parse_taxpayer_data(html_content) + expected_obj = data_models.TaxpayerData( + start_of_activities_date=None, + economic_activities="", + tax_category="", + address="", + branches=[], + last_filed_documents=[], + tax_observations=None, + ) + self.assertEqual(result, expected_obj) + + def test_parse_taxpayer_properties(self) -> None: + html_content = read_test_file_str_utf8('test_data/sii-cte/cte_empty_f29.html') + + with self.subTest("Parsing ok"): + result = parsers.parse_taxpayer_properties(html_content) + expected_obj = data_models.TaxpayerProperties( + properties=[ + data_models.Property( + commune="LAS CONDES", + role="123-4", + address="Av. Apoquindo 1234", + purpose="HABITACIONAL", + fiscal_valuation=Decimal('46550332'), + overdue_installments=True, + current_installments=True, + condition="AFECTO", + ), + data_models.Property( + commune="PROVIDENCIA", + role="567-8", + address="Calle 10 #456", + purpose="COMERCIAL", + fiscal_valuation=None, + overdue_installments=False, + current_installments=False, + condition="EXENTO", + ), + data_models.Property( + commune="ÑUÑOA", + role=None, + address=None, + purpose="INDUSTRIAL", + fiscal_valuation=Decimal('78456789'), + overdue_installments=False, + current_installments=False, + condition="AFECTO", + ), + ], + ) + self.assertEqual(result, expected_obj) + + with self.subTest("Parsing empty content"): + with self.assertRaises(ValueError) as assert_raises_cm: + parsers.parse_taxpayer_properties("") + + self.assertEqual( + assert_raises_cm.exception.args, + ("Could not find taxpayer information table in HTML",), + ) diff --git a/src/tests/test_data/sii-cte/cte_empty_f29.html b/src/tests/test_data/sii-cte/cte_empty_f29.html new file mode 100644 index 00000000..0f057d65 --- /dev/null +++ b/src/tests/test_data/sii-cte/cte_empty_f29.html @@ -0,0 +1,1166 @@ + + + + + + + + + + + + + + + + +
 
+

Revise los datos contenidos en esta Carpeta Tributaria + Electrónica y, si están correctos, seleccione el botón "Continuar". Si detecta + información incorrecta, reporte esta situación llamando a nuestra Mesa de Ayuda + Telefónica, al (02) 395 11 15, o ingresando a nuestra página Web, www.sii.cl, menú + Contáctenos, opción Problemas y quejas.

+
+ + + + + + + + + + + + + + + + + + + +
+ CARPETA TRIBUTARIA ELECTRÓNICA
PARA SOLICITAR CRÉDITOS
+
+
+
+

+ Importante: Esta información es válida para la fecha y hora en que se generó la carpeta.

+ Toda declaración y pago que sea presentada en papel retrasa la actualización de las bases de datos del SII, por lo que, eventualmente, podrían no aparecer en esta carpeta.
+

+
+
+
+ + + + + + + + + + + + + + + +
Nombre del emisor: INVERSIONES SPA
RUT del emisor:47797573 - 9
Fecha de generación de la carpeta:25/07/2025 07:21
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Datos del Contribuyente +
Fecha de Inicio de Actividades: + 15-11-2023
Actividades Económicas: SERVICIOS DE ASESORIA Y CONSULTORIA EN MATERIA DE ADMINISTRACION DE EMPRESAS Y OTROS SERVICIOS DE ASESORIA ADMINISTRATIVA Y DE NEGOCIOS N.C.P.
ACTIVIDADES DE OTRAS ORGANIZACIONES EMPRESARIALES N.C.P.
OTRAS ACTIVIDADES DE SERVICIOS PERSONALES N.C.P.
+
Categoría tributaria:Primera categoría
Domicilio:AV REAL, LAS CONDES
Sucursales:
Últimos documentos timbrados:FACTURA ELECTRONICA
FACTURA NO AFECTA O EXENTA ELECTRONICA
GUIA DESPACHO ELECTRONICA
NOTA CREDITO ELECTRONICA
+
24-07-2025
17-07-2025
14-05-2025
18-07-2025
+
Observaciones tributarias: + + +

No tiene observaciones.

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Información proporcionada por el contribuyente para fines tributarios (1) +
 Nombre o Razón Social + RUTFecha de Incorporación +
Representante(s) Legal(es)   
 DAVID USUARIO DE PRUEBA76354771-K20-09-2023
 JAVIERA USUARIO DE PRUEBA38855667-620-09-2023 +
Conformación de la sociedad
JAVIERA USUARIO DE PRUEBA38855667-620-09-2023
MARÍA USUARIO DE PRUEBA34413183-k23-02-2024
Participación en sociedades vigentes(2) +    
 - No existen sociedades para el RUT -

(1): Información declarada por el contribuyente y que puede haber sufrido modificaciones. +
(2): La vigencia de estas sociedades está asociada a la existencia de un Inicio de Actividades, sin Término de Giro. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Propiedades y Bienes Raíces (3) +
ComunaRolDirecciónDestinoAvalúo FiscalCuotas vencidas por pagarCuotas vigentes por pagarCondición
(4)
LAS CONDES123-4Av. Apoquindo 1234HABITACIONAL46.550.332SISIAFECTO
PROVIDENCIA567-8Calle 10 #456COMERCIALNONOEXENTO
ÑUÑOAINDUSTRIAL78.456.789NONOAFECTO
+

+ (3): La presente información no acredita dominio de una propiedad. +
+ (4): La condición exento/afecto ha sido determinada de los datos actuales del catastro de Bienes Raíces, considerando las modificaciones recientes de la tasación, y no según la existencia de cuotas de contribuciones emitidas. +
+

+
+
+ + + + + + + + + + + + + + + + + + + + + +
Boletas de Honorarios Electrónicas (5) +
PeríodosHonorario brutoRetención de tercerosPPM de contribuyente
- No se registran Boletas de Honorarios Electrónicas emitidas en los últimos 12 meses - +
+

(5): Además de las Boletas de Honorarios Electrónicas, un contribuyente puede tener boletas de honorarios emitidas en papel, cuyo detalle no está disponible en forma electrónica. +

+
+
+
+ + + + + + + + + + + + + + + + +
Declaraciones de IVA (F29) +
JULIO 20251 / 1
+
+

+ - No se registra declaración para este período - +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Declaraciones de Renta (F22) +
Año Tributario 20251 / 3
+
+

+ - No se registra declaración para este período - +

+ + +
+
Año Tributario 20242 / 3
+
+
+
+
+ + + + + + + + +
+ + + + + + + + + + +
REPUBLICA DE CHILE
+ SERVICIO DE IMPUESTOS INTERNOS
+
FORM. 22 +
+

AÑO TRIBUTARIO + + 2024

+ IMPUESTOS ANUALES A LA RENTA + +
  07   +   + + + 329526074 + +
+ + + + + + + + + + + + + + + + + + +
ROL UNICO
+ TRIBUTARIO
+
01 + Apellido Paterno
+ o razón social
+
02 + Apellido Materno + 05 + Nombres +
03 + + + 76706365-2 + + INVERSIONES EL LIBANO SPA +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
06 + Calle    Nº         Of.Depto.    + 09 + Teléfono + 08 + Comuna +
  + + I LA CATOLICA 4196 null +   +   +   + + LAS CONDES +
13Actividad, profesión o giro del negocio14Código actividad económica903RUT. del Representante
  + + DESRATIZACION, DESINFECCION Y EXTERMINIO DE + PLAGAS NO AGRICOLAS +   + + 812901 +   + + 11738446-2 +
55Correo + Electrónico +
  + + roberto.opazo@xinerlink.cl +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
15Fecha + Vencimiento Declaración 04202418Base + imponible IDPC de empresas acogidas al régimen Pro Pyme, según art. + 14 letra D) N° 3 LIR 926
20IDPC + de empresas acogidas al régimen Pro Pyme, según art. 14 letra D) N° + 3 LIR 9336PPM + y remanente del IEAM 72300
53Región  + 1355Correo + Electrónico  + roberto.opazo@xinerlink.cl +
102Capital + Efectivo 10000000122Total + del Activo 11591162
123Total + del Pasivo 11590236301Nombre + Institución Bancaria BANCO ESTADO
305RESULTADO + LIQUIDACIÓN ANUAL IMPUESTO A LA RENTA (si el resultado es negativo o + cero, deberá declarar por Internet) -72207306Numero + de Cuenta 20470150531
315Fecha + Presentación 24/04/2024645CPT + positivo final 0
646CPT + negativo final 0780Tipo + de Cuenta C
784Saldo + cuenta corriente bancaria según, conciliación 111531521400Ingresos + del giro percibidos 7230000
1409Existencias, + insumos y servicios del negocio, pagados 15340741410Total + de ingresos anuales 7230000
1415Arriendos + pagados 45000001424Otros + gastos deducibles de los ingresos 1195000
1430Total + de egresos anuales 72290741440Base + Imponible afecta a IDPC (o pérdida tributaria antes de imputar + dividendos o retiros percibidos) del ejercicio 926
1494Capital + aportado, histórico (incluye aumentos y disminuciones efectivas)  + 100000001545CPTS + positivo final 926
1703CPTS + positivo 9261705Base + imponible afecta a IDPC del ejercicio 926
1720Subtotal  + 9261729Base + imponible antes de rebaja por incentivo al ahorro (art. 14 letra E) + LIR) y/o por pago de IDPC voluntario (art. 14 letra A) N°6 LIR y + art. 42° transitorio Ley N° 21.210) o pérdida + tributaria 926
1904a) + PPM arts. 84 letras a), c) , e), y h) y 14 D N° 3 letra (k) LIR  + 723008809Rut + Representante 767063652
8811Moneda + de la Declaración CLP8865Código + Emisión 3
99342023 + comienza registro de layout f22 (rnet) F22.3
+ + + + + + + + + + +
Folio Nº + + + 329526074 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
REMANENTE DE + CREDITO
66SALDO A FAVOR + + 85 + + 72207 + +
67Menos: Saldo puesto a + disposición +
de los socios (Según Recuadro N° 6) + + . +
86 +   + -
68DEVOLUCIÓN SOLICITADA + + 87 + + + 72207 + =
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IMPUESTO A PAGAR +
69Impuesto Adeudado90 +   + +
70Reajuste Art. 72 línea + 69: 0.9% + 39 +   + +
71TOTAL A PAGAR (Líneas + 69+70) + + 91 +   + =
RECARGOS POR + DECLARACIÓN
+ FUERA DE PLAZO
+
(RECARGOS POR MORA EN EL PAGO) +
72MAS: Reajustes + declaración fuera de plazo + 92  +   + +
73MAS: Intereses y Multas + declaración fuera de plazo + 93  +   + +
74TOTAL A PAGAR (Líneas + 71+72+73)94  +   + + =
+
+ + + + + + +
+ + Declaro bajo juramento que la información contenida en este + documento es la expresión + fiel de la verdad, por lo que asumo la responsabilidad + correspondiente. + +
+
+
+ +
+ + +
+
+
Año Tributario 20233 / 3
+
+

+ - No se registra declaración para este período - +

+ + +
+
+ + + + + + + +
+
+ + + + + + +
+ + +
+ + + + diff --git a/src/tests/test_data/sii-cte/cte_empty_table.html b/src/tests/test_data/sii-cte/cte_empty_table.html new file mode 100644 index 00000000..9a96da86 --- /dev/null +++ b/src/tests/test_data/sii-cte/cte_empty_table.html @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + + +
 
+

Revise los datos contenidos en esta Carpeta Tributaria + Electrónica y, si están correctos, seleccione el botón "Continuar". Si detecta + información incorrecta, reporte esta situación llamando a nuestra Mesa de Ayuda + Telefónica, al (02) 395 11 15, o ingresando a nuestra página Web, www.sii.cl, menú + Contáctenos, opción Problemas y quejas.

+
+ + + + + + + + + + + + + + + + + + + +
+ CARPETA TRIBUTARIA ELECTRÓNICA
PARA SOLICITAR CRÉDITOS
+
+
+
+

+ Importante: Esta información es válida para la fecha y hora en que se generó la carpeta.

+ Toda declaración y pago que sea presentada en papel retrasa la actualización de las bases de datos del SII, por lo que, eventualmente, podrían no aparecer en esta carpeta.
+

+
+
+
+ + + + + + + + + + + + + + + +
Nombre del emisor: INVERSIONES SPA
RUT del emisor:47797573 - 9
Fecha de generación de la carpeta:25/07/2025 07:21
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Datos del Contribuyente +
Fecha de Inicio de Actividades: +
Actividades Económicas:
Categoría tributaria:
Domicilio:
Sucursales:
Últimos documentos timbrados:
Observaciones tributarias:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Información proporcionada por el contribuyente para fines tributarios (1) +
 Nombre o Razón Social + RUTFecha de Incorporación +
Representante(s) Legal(es)   
    
    
Conformación de la sociedad
   
   
Participación en sociedades vigentes(2) +    
 - No existen sociedades para el RUT -

(1): Información declarada por el contribuyente y que puede haber sufrido modificaciones. +
(2): La vigencia de estas sociedades está asociada a la existencia de un Inicio de Actividades, sin Término de Giro. +

+
+ + + + + + + + + + + + + + + + + + + + + + +
Propiedades y Bienes Raíces (3) +
ComunaRolDirecciónDestinoAvalúo FiscalCuotas vencidas por pagarCuotas vigentes por pagarCondición
(4)
- No se registra información para este RUT - +
+

+ (3): La presente información no acredita dominio de una propiedad. +
+ (4): La condición exento/afecto ha sido determinada de los datos actuales del catastro de Bienes Raíces, considerando las modificaciones recientes de la tasación, y no según la existencia de cuotas de contribuciones emitidas. +
+

+
+
+ + + + + + + + + + + + + + + + + + +
Boletas de Honorarios Electrónicas (5) +
PeríodosHonorario brutoRetención de tercerosPPM de contribuyente
- No se registran Boletas de Honorarios Electrónicas emitidas en los últimos 12 meses - +
+

(5): Además de las Boletas de Honorarios Electrónicas, un contribuyente puede tener boletas de honorarios emitidas en papel, cuyo detalle no está disponible en forma electrónica. +

+
+
+
+ + + + + + + + + + + + + + + + +
Declaraciones de IVA (F29) +
JULIO 20251 / 1
+
+

+ - No se registra declaración para este período - +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Declaraciones de Renta (F22) +
Año Tributario 20251 / 3
+
+

+ - No se registra declaración para este período - +

+ + +
+
Año Tributario 20242 / 3
+
+

+ - No se registra declaración para este período - +

+ + +
+
Año Tributario 20233 / 3
+
+

+ - No se registra declaración para este período - +

+ + +
+
+ + + + + + + +
+
+ + + + + + +
+ + +
+ + + + diff --git a/src/tests/test_rut_constants.py b/src/tests/test_rut_constants.py index 84e62a7e..7031ff89 100644 --- a/src/tests/test_rut_constants.py +++ b/src/tests/test_rut_constants.py @@ -1,7 +1,10 @@ from __future__ import annotations +import re import unittest -from typing import ClassVar +from typing import ClassVar, Pattern + +import jsonschema from cl_sii.rut import constants @@ -34,3 +37,79 @@ def test_persona_juridica_min_value(self) -> None: self.assertGreaterEqual(min_rut_digits, self.RUT_DIGITS_MIN_VALUE) self.assertLessEqual(min_rut_digits, self.RUT_DIGITS_MAX_VALUE) + + +class RutRegexConstantsTestCase(unittest.TestCase): + RUT_CANONICAL_STRICT_REGEX: ClassVar[Pattern[str]] + RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX: ClassVar[Pattern[str]] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.RUT_CANONICAL_STRICT_REGEX = constants.RUT_CANONICAL_STRICT_REGEX + cls.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX = ( + constants.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX + ) + + def test_json_schema_regex_is_python_regex_without_named_groups(self) -> None: + # %% -----Arrange----- + + python_regex = self.RUT_CANONICAL_STRICT_REGEX + python_regex_without_named_groups = re.compile( + re.sub( + pattern=r'\?P<\w+>', + repl='', + string=python_regex.pattern, + ) + ) + expected = python_regex_without_named_groups + + # %% -----Act----- + + actual = self.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX + + # %% -----Assert----- + + self.assertEqual(expected, actual) + + # %% ----- + + def test_json_schema_regex_is_valid_schema(self) -> None: + # %% -----Arrange----- + + schema = { + "type": "string", + "pattern": self.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX.pattern, + } + valid_test_values = [ + '0-0', + '1-9', + '6-K', + '78773510-K', + ] + invalid_test_values = [ + '6K', + '6-k', + '78773510-k', + '78.773.510-K', + 78773510, + 1.9, + None, + ] + + # %% -----Act & Assert----- + + for test_value in valid_test_values: + with self.subTest(test_value=test_value): + try: + jsonschema.validate(instance=test_value, schema=schema) + except jsonschema.exceptions.ValidationError as exc: + self.fail(f'{exc.__class__.__name__} raised') + + for invalid_test_value in invalid_test_values: + with self.subTest(test_value=invalid_test_value): + with self.assertRaises(jsonschema.exceptions.ValidationError): + jsonschema.validate(instance=invalid_test_value, schema=schema) + + # %% -----