diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index b940d94e..b2c02822 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.4.0
+current_version = 0.5.0
commit = True
tag = True
diff --git a/HISTORY.rst b/HISTORY.rst
index dac52651..086009e9 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,21 @@
History
-------
+0.5.0 (2019-04-25)
++++++++++++++++++++++++
+
+* (PR #29, 2019-04-25) dte.data_models: modify new fields of `DteDataL2`
+* (PR #28, 2019-04-25) libs: add module `crypto_utils`
+* (PR #27, 2019-04-25) libs: add module `encoding_utils`
+* (PR #26, 2019-04-25) test_data: add files
+* (PR #25, 2019-04-25) libs.xml_utils: fix class alias `XmlElementTree`
+* (PR #24, 2019-04-25) requirements: add and update packages
+* (PR #22, 2019-04-24) test_data: add files
+* (PR #21, 2019-04-22) dte: many improvements
+* (PR #20, 2019-04-22) libs.xml_utils: misc improvements
+* (PR #19, 2019-04-22) test_data: fix and add real SII DTE & AEC XML files
+* (PR #18, 2019-04-22) data.ref: add XML schemas for "Cesion" (RTC)
+
0.4.0 (2019-04-16)
+++++++++++++++++++++++
diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py
index 795a5582..43f5d1e4 100644
--- a/cl_sii/__init__.py
+++ b/cl_sii/__init__.py
@@ -5,4 +5,4 @@
"""
-__version__ = '0.4.0'
+__version__ = '0.5.0'
diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd
new file mode 100644
index 00000000..da646f8f
--- /dev/null
+++ b/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+ Archivo Electronico de Cesion
+
+
+
+
+
+ Documento de AEC
+
+
+
+
+
+ Informacion de AEC
+
+
+
+
+
+ RUT que Genera el Archivo de Transferencias
+
+
+
+
+ RUT a Quien Va Dirigido el Archivo de Transferencias
+
+
+
+
+ Persona de Contacto para aclarar dudas
+
+
+
+
+ Telefono de Contacto
+
+
+
+
+ Correo Electronico de Contacto
+
+
+
+
+ Fecha y Hora de la Firma del Archivo de Transferencias
+
+
+
+
+
+
+
+
+ Cesiones
+
+
+
+
+
+ Representacion XML y Grafica del DTE Cedido
+
+
+
+
+ Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor
+
+
+
+
+
+
+
+
+
+
+
+ Firma Digital sobre Transferencia
+
+
+
+
+
+
+
diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd
new file mode 100644
index 00000000..aef37cb1
--- /dev/null
+++ b/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+
+
+ Envio de Informacion de Transferencias Electronicas
+
+
+
+
+ Documento Tributario Electronico
+
+
+
+
+
+
+
+ Secuencia de Cesiones (1, 2, 3, ... )
+
+
+
+
+
+
+
+
+
+ Identificacion del DTE Cedido
+
+
+
+
+
+ Tipo de DTE
+
+
+
+
+ RUT Emisor del DTE
+
+
+
+
+ RUT Receptor del DTE
+
+
+
+
+ Folio del DTE
+
+
+
+
+ Fecha Emision Contable del DTE (AAAA-MM-DD)
+
+
+
+
+ Monto Total del DTE
+
+
+
+
+
+
+
+ Identificacion del Cedente
+
+
+
+
+
+ RUT del Cedente del DTE
+
+
+
+
+ Razon Social o Nombre del Cedente
+
+
+
+
+
+
+
+
+
+ Direccion del Cedente
+
+
+
+
+
+
+
+
+
+ Correo Electronico del Cedente
+
+
+
+
+
+
+
+
+
+ Lista de Personas Autorizadas por el Cedente a Firmar la Transferencia
+
+
+
+
+
+ RUT de Persona Autorizada
+
+
+
+
+ Nombre de Persona Autorizada
+
+
+
+
+
+
+
+ Declaracion Jurada de Disponibilidad de Documentacion No Electronica
+
+
+
+
+
+
+
+
+
+
+
+
+ Identificacion del Cesionario
+
+
+
+
+
+ RUT del Cesionario
+
+
+
+
+ Razon Social o Nombre del Cesionario
+
+
+
+
+
+
+
+
+
+ Direccion del Cesionario
+
+
+
+
+
+
+
+
+
+ Correo Electronico del Cesionario
+
+
+
+
+
+
+
+
+
+
+
+
+ Monto del Credito Cedido
+
+
+
+
+ Fecha de Ultimo Vencimiento
+
+
+
+
+ Otras Condiciones de la Cesion
+
+
+
+
+
+
+
+
+
+ Correo Electronico del Deudor del DTE
+
+
+
+
+ TimeStamp de la Cesion del DTE
+
+
+
+
+
+
+
+
+ Firmas Digitales sobre Cesion
+
+
+
+
+
+
diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd
new file mode 100644
index 00000000..f81b34fa
--- /dev/null
+++ b/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+ DTE con Imagen y Recibos
+
+
+
+
+
+
+
+
+
+ Representacion XML del DTE Cedido
+
+
+
+
+ Representacion PDF del DTE Cedido
+
+
+
+
+ Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor
+
+
+
+
+ Representacion PDF del los Acuse de Recibo
+
+
+
+
+
+
+
+
+
+ Fecha y Hora en que se Firmo Digitalmente el Documento Cedido AAAA-MM-DDTHH:MI:SS
+
+
+
+
+
+
+
+
+ Firma Digital sobre Documento
+
+
+
+
+
+
diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/README.md b/cl_sii/data/ref/factura_electronica/schemas-xml/README.md
index 04c5d36e..18dce65f 100644
--- a/cl_sii/data/ref/factura_electronica/schemas-xml/README.md
+++ b/cl_sii/data/ref/factura_electronica/schemas-xml/README.md
@@ -17,6 +17,8 @@ The most significant structures are:
Note:
- DTE means "Documento Tributario Electr贸nico".
+- RTC: "Registro Transferencia de Cr茅dito" aka RPETC; "Registro Electr贸nico de Cesi贸n de Cr茅ditos".
+- RPETC: "Registro P煤blico Electr贸nico de Transferencia de Cr茅dito" aka RTC.
- IECV means "Informaci贸n Electr贸nica de Libros de Compra y Venta".
- LCE means "Libros Contables Electr贸nicos".
@@ -41,6 +43,23 @@ as
"[Bajar schema XML de Documentos Tributarios Electr贸nicos](http://www.sii.cl/factura_electronica/schema_dte.zip) (Incluye Documentos de exportaci贸n)"
+#### Cesion (RTC)
+
+Archive [schema_cesion.zip](http://www.sii.cl/factura_electronica/schema_cesion.zip),
+referenced from official webpage
+[SII](http://www.sii.cl)
+/ [Servicios online](http://www.sii.cl/servicios_online/index.html)
+/ [Factura electr贸nica](http://www.sii.cl/servicios_online/1039-.html)
+/ [Sistema de facturaci贸n de mercado](http://www.sii.cl/servicios_online/1039-1184.html)
+/ [Registro electr贸nico de cesi贸n de cr茅ditos](https://palena.sii.cl/rtc/RTC/RTCMenu.html)
+/ [Formatos de archivos electr贸nicos](http://www.sii.cl/factura_electronica/form_ele.htm)
+as
+"[Formato XML del Archivo Electr贸nico de Cesi贸n](http://www.sii.cl/factura_electronica/schema_cesion.zip)"
+
+- Retrieval date: 2019-04-16
+- MD5 checksum: `82d426fc3bd5f3a29e61a1d07ed4d6dd`.
+
+
#### IECV
[schema_iecv.zip](http://www.sii.cl/factura_electronica/schema_iecv.zip) (2018-11-28),
@@ -137,6 +156,82 @@ Schema files will be updated as necessary, indicating the source in the correspo
- `PctType`: "Monto de Porcentaje ( 3 y 2)".
+#### Cesion (RTC)
+
+- `AEC_v10.xsd`: main schema; it includes (directly or indirectly) all the others of this section.
+ - XML target namespace: `http://www.sii.cl/SiiDte`.
+ - XML included/imported schemas: `Cesion_v10.xsd`, `DTECedido_v10.xsd`, `xmldsignature_v10.xsd`.
+ - XML elements:
+ - `AEC`: "Archivo Electronico de Cesion"
+ - `DocumentoAEC`: "Documento de AEC"
+ - `Caratula`: "Informacion de AEC"
+ - `Cesiones`: "Cesiones"
+ - ref `DTECedido`: "Representacion XML y Grafica del DTE Cedido"
+ - ref `Cesion` (1..N occurrences):
+ "Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor"
+ - XML data types: no explicit definitions.
+
+- `Cesion_v10.xsd`: ?
+ - XML target namespace: `http://www.sii.cl/SiiDte`.
+ - XML included/imported schemas: `SiiTypes_v10.xsd`, `xmldsignature_v10.xsd`.
+ - XML elements:
+ - `Cesion`: "Envio de Informacion de Transferencias Electronicas".
+ - XML data types:
+ - `CesionDefType`: "Documento Tributario Electronico" (sic).
+ Relevant elements:
+ - `DocumentoCesion`: (no description nor annotations)
+ - `SeqCesion`: "Secuencia de Cesiones (1, 2, 3, ... )".
+ - `IdDTE`: "Identificacion del DTE Cedido".
+ - `Cedente`: "Identificacion del Cedente".
+ - `Cesionario`: "Identificacion del Cesionario".
+ - `MontoCesion`: "Monto del Credito Cedido".
+ - `UltimoVencimiento`: "Fecha de Ultimo Vencimiento".
+ - `OtrasCondiciones`: "Otras Condiciones de la Cesion".
+ - `eMailDeudor`: "Correo Electronico del Deudor del DTE".
+ - `TmstCesion`: "TimeStamp de la Cesion del DTE".
+
+- `DTECedido_v10.xsd`: ?
+ - XML target namespace: `http://www.sii.cl/SiiDte`.
+ - XML included/imported schemas: `DTE_v10.xsd`, `Recibos_v10.xsd`, `xmldsignature_v10.xsd`.
+ - XML elements:
+ - `DTECedido`: "DTE con Imagen y Recibos".
+ - XML data types:
+ - `DTECedidoDefType`: "Documento Tributario Electronico".
+ Relevant elements:
+ - `DocumentoDTECedido`: (no description nor annotations)
+ - ref `DTE`: "Representacion XML del DTE Cedido".
+ - `ImagenDTE` (optional): "Representacion PDF del DTE Cedido" (binary as base64)
+ - ref `Recibo` (0..N occurrences):
+ "Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor".
+ - `ImagenAR` (optional):
+ "Representacion PDF del los Acuse de Recibo" (sic) (binary as base64)
+ - `TmstFirma`:
+ "Fecha y Hora en que se Firmo Digitalmente el Documento Cedido AAAA-MM-DDTHH:MI:SS".
+
+- `Recibos_v10.xsd`: ?
+ - XML target namespace: `http://www.sii.cl/SiiDte`.
+ - XML included/imported schemas: `SiiTypes_v10.xsd`, `xmldsignature_v10.xsd`.
+ - XML elements:
+ - `Recibo`:
+ doc 1: "Comprobante de Recepcion de Mercaderias o Servicios Prestados".
+ doc 2: "Recibos de Recepcion de Mercaderias o Servicios Prestados".
+ - XML data types:
+ - `ReciboDefType`: "Documento Tributario Electronico" (sic)
+ Relevant elements:
+ - `DocumentoRecibo`: "Identificacion del Documento Recibido" (sic)
+ - `TipoDoc`: "Tipo de Documento".
+ - `Folio`: "Folio del Documento".
+ - `FchEmis`: "Fecha Emision Contable del Documento (AAAA-MM-DD)".
+ - `RUTEmisor`: "RUT Emisor del Documento".
+ - `RUTRecep`: "RUT Receptor del Documento".
+ - `MntTotal`: "Monto Total del Documento".
+ - `Recinto`: "Lugar donde se materializa la recepci贸n conforme".
+ - `RutFirma`: "RUT de quien Firma el Recibo".
+ - `Declaracion` (fixed string):
+ "Texto Ley 19.983, acredita la recepcion mercader铆as o servicio.".
+ - `TmstFirmaRecibo`: "Fecha y Hora de la Firma del Recibo".
+
+
#### IECV
- `LceCal_v10.xsd`
diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd
new file mode 100644
index 00000000..c2ec588e
--- /dev/null
+++ b/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+ Comprobante de Recepcion de Mercaderias o Servicios Prestados
+ Recibos de Recepcion de Mercaderias o Servicios Prestados
+
+
+
+
+ Documento Tributario Electronico
+
+
+
+
+ Identificacion del Documento Recibido
+
+
+
+
+
+ Tipo de Documento
+
+
+
+
+ Folio del Documento
+
+
+
+
+ Fecha Emision Contable del Documento (AAAA-MM-DD)
+
+
+
+
+ RUT Emisor del Documento
+
+
+
+
+ RUT Receptor del Documento
+
+
+
+
+ Monto Total del Documento
+
+
+
+
+ Lugar donde se materializa la recepci髇 conforme
+
+
+
+
+
+
+
+
+
+ RUT de quien Firma el Recibo
+
+
+
+
+ Texto Ley 19.983, acredita la recepcion mercader韆s o servicio.
+
+
+
+
+
+
+
+
+
+ Fecha y Hora de la Firma del Recibo
+
+
+
+
+
+
+
+
+ Firma Digital sobre Documento
+
+
+
+
+
+
diff --git a/cl_sii/dte/constants.py b/cl_sii/dte/constants.py
index 4b489c1e..34ec8c1d 100644
--- a/cl_sii/dte/constants.py
+++ b/cl_sii/dte/constants.py
@@ -72,7 +72,7 @@
class TipoDteEnum(enum.IntEnum):
"""
- Enum of Tipo de DTE.
+ Enum of "Tipo de DTE".
Source: XML type ``DTEType`` (enum) in official schema ``SiiTypes_v10.xsd``.
https://github.com/fyndata/lib-cl-sii-python/blob/f57a326/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd#L63-L99
@@ -80,21 +80,78 @@ class TipoDteEnum(enum.IntEnum):
"""
FACTURA_ELECTRONICA = 33
- """Factura Electr贸nica."""
+ """Factura electr贸nica de venta."""
FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA = 34
- """Factura no Afecta o Exenta Electr贸nica."""
+ """Factura electr贸nica de venta, no afecta o exenta de IVA."""
+ # aka 'Factura no Afecta o Exenta Electr贸nica'
# aka 'Factura Electr贸nica de Venta de Bienes y Servicios No afectos o Exento de IVA'
FACTURA_COMPRA_ELECTRONICA = 46
- """Factura de Compra Electr贸nica."""
+ """Factura electr贸nica de compra."""
+ # aka 'Factura de Compra Electr贸nica'
# Name should have been 'Factura Electr贸nica de Compra'.
GUIA_DESPACHO_ELECTRONICA = 52
- """Gu铆a de Despacho Electr贸nica."""
+ """Gu铆a electr贸nica de despacho."""
+ # aka 'Gu铆a de Despacho Electr贸nica'
NOTA_DEBITO_ELECTRONICA = 56
- """Nota de D茅bito Electr贸nica."""
+ """Nota electr贸nica de d茅bito."""
+ # aka 'Nota de D茅bito Electr贸nica'
NOTA_CREDITO_ELECTRONICA = 61
- """Nota de Cr茅dito Electr贸nica."""
+ """Nota electr贸nica de cr茅dito."""
+ # aka 'Nota de Cr茅dito Electr贸nica'
+
+ @property
+ def is_factura(self) -> bool:
+ if self is TipoDteEnum.FACTURA_ELECTRONICA:
+ result = True
+ elif self is TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA:
+ result = True
+ elif self is TipoDteEnum.FACTURA_COMPRA_ELECTRONICA:
+ result = True
+ else:
+ result = False
+
+ return result
+
+ @property
+ def is_factura_venta(self) -> bool:
+ if self is TipoDteEnum.FACTURA_ELECTRONICA:
+ result = True
+ elif self is TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA:
+ result = True
+ else:
+ result = False
+
+ return result
+
+ @property
+ def is_factura_compra(self) -> bool:
+ if self is TipoDteEnum.FACTURA_COMPRA_ELECTRONICA:
+ result = True
+ else:
+ result = False
+
+ return result
+
+ @property
+ def is_nota(self) -> bool:
+ if self is TipoDteEnum.NOTA_DEBITO_ELECTRONICA:
+ result = True
+ elif self is TipoDteEnum.NOTA_CREDITO_ELECTRONICA:
+ result = True
+ else:
+ result = False
+
+ return result
+
+ @property
+ def emisor_is_vendedor(self) -> bool:
+ return self.is_factura_venta
+
+ @property
+ def receptor_is_vendedor(self) -> bool:
+ return self.is_factura_compra
diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py
index fa7ac32d..4591cda5 100644
--- a/cl_sii/dte/data_models.py
+++ b/cl_sii/dte/data_models.py
@@ -1,10 +1,28 @@
+"""
+DTE data models
+===============
+
+Concepts
+--------
+
+In the domain of a DTE, a:
+
+* "Vendedor": is who sold goods or services to "deudor" in a
+ transaction for which the DTE was issued.
+ It *usually* corresponds to the DTE's "emisor", but not always.
+* "Deudor": is who purchased goods or services from "vendedor" in a
+ transaction for which the DTE was issued.
+ It *usually* corresponds to the DTE's "receptor", but not always.
+
+"""
import dataclasses
from dataclasses import field as dc_field
-from datetime import date
+from datetime import date, datetime
from typing import Mapping, Optional
import cl_sii.contribuyente.constants
import cl_sii.rut.constants
+from cl_sii.libs import encoding_utils
from cl_sii.rut import Rut
from . import constants
@@ -57,6 +75,26 @@ def validate_contribuyente_razon_social(value: str) -> None:
raise ValueError("Value exceeds max allowed length.")
+def validate_clean_str(value: str) -> None:
+ if len(value.strip()) != len(value):
+ raise ValueError("Value has leading or trailing whitespace characters.", value)
+
+
+def validate_clean_bytes(value: bytes) -> None:
+ if len(value.strip()) != len(value):
+ raise ValueError("Value has leading or trailing whitespace characters.", value)
+
+
+def validate_non_empty_str(value: str) -> None:
+ if len(value.strip()) == 0:
+ raise ValueError("String value length (stripped) is 0.")
+
+
+def validate_non_empty_bytes(value: bytes) -> None:
+ if len(value.strip()) == 0:
+ raise ValueError("Bytes value length (stripped) is 0.")
+
+
@dataclasses.dataclass(frozen=True)
class DteNaturalKey:
@@ -230,8 +268,38 @@ def __post_init__(self) -> None:
validate_dte_monto_total(self.monto_total)
@property
- def natural_key(self) -> DteNaturalKey:
- return DteNaturalKey(emisor_rut=self.emisor_rut, tipo_dte=self.tipo_dte, folio=self.folio)
+ def vendedor_rut(self) -> Rut:
+ """
+ Return the RUT of the "vendedor".
+
+ :raises ValueError:
+ """
+ if self.tipo_dte.emisor_is_vendedor:
+ result = self.emisor_rut
+ elif self.tipo_dte.receptor_is_vendedor:
+ result = self.receptor_rut
+ else:
+ raise ValueError(
+ "Concept \"vendedor\" does not apply for this 'tipo_dte'.", self.tipo_dte)
+
+ return result
+
+ @property
+ def deudor_rut(self) -> Rut:
+ """
+ Return the RUT of the "deudor".
+
+ :raises ValueError:
+ """
+ if self.tipo_dte.emisor_is_vendedor:
+ result = self.receptor_rut
+ elif self.tipo_dte.receptor_is_vendedor:
+ result = self.emisor_rut
+ else:
+ raise ValueError(
+ "Concept \"deudor\" does not apply for this 'tipo_dte'.", self.tipo_dte)
+
+ return result
@dataclasses.dataclass(frozen=True)
@@ -265,6 +333,38 @@ class DteDataL2(DteDataL1):
"Fecha de vencimiento (pago)" of the DTE.
"""
+ firma_documento_dt_naive: Optional[datetime] = dc_field(default=None)
+ """
+ Datetime on which the "documento" was digitally signed.
+ """
+
+ signature_value: Optional[bytes] = dc_field(default=None)
+ """
+ DTE's digital signature's value (raw bytes, without base64 encoding).
+ """
+
+ signature_x509_cert_pem: Optional[bytes] = dc_field(default=None)
+ """
+ DTE's digital signature's PEM-encoded X.509 cert.
+
+ PEM-encoded implies base64-encoded.
+ """
+
+ emisor_giro: Optional[str] = dc_field(default=None)
+ """
+ "Giro" of the "emisor" of the DTE.
+ """
+
+ emisor_email: Optional[str] = dc_field(default=None)
+ """
+ Email address of the "emisor" of the DTE.
+ """
+
+ receptor_email: Optional[str] = dc_field(default=None)
+ """
+ Email address of the "receptor" of the DTE.
+ """
+
def __post_init__(self) -> None:
"""
Run validation automatically after setting the fields values.
@@ -285,3 +385,38 @@ def __post_init__(self) -> None:
if self.fecha_vencimiento_date is not None:
if not isinstance(self.fecha_vencimiento_date, date):
raise TypeError("Inappropriate type of 'fecha_vencimiento_date'.")
+
+ if self.firma_documento_dt_naive is not None:
+ if not isinstance(self.firma_documento_dt_naive, datetime):
+ raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.")
+
+ if self.signature_value is not None:
+ if not isinstance(self.signature_value, bytes):
+ raise TypeError("Inappropriate type of 'signature_value'.")
+ validate_clean_bytes(self.signature_value)
+ validate_non_empty_bytes(self.signature_value)
+
+ if self.signature_x509_cert_pem is not None:
+ if not isinstance(self.signature_x509_cert_pem, bytes):
+ raise TypeError("Inappropriate type of 'signature_x509_cert_pem'.")
+ validate_clean_bytes(self.signature_x509_cert_pem)
+ validate_non_empty_bytes(self.signature_x509_cert_pem)
+ encoding_utils.validate_base64(self.signature_x509_cert_pem)
+
+ if self.emisor_giro is not None:
+ if not isinstance(self.emisor_giro, str):
+ raise TypeError("Inappropriate type of 'emisor_giro'.")
+ validate_clean_str(self.emisor_giro)
+ validate_non_empty_str(self.emisor_giro)
+
+ if self.emisor_email is not None:
+ if not isinstance(self.emisor_email, str):
+ raise TypeError("Inappropriate type of 'emisor_email'.")
+ validate_clean_str(self.emisor_email)
+ validate_non_empty_str(self.emisor_email)
+
+ if self.receptor_email is not None:
+ if not isinstance(self.receptor_email, str):
+ raise TypeError("Inappropriate type of 'receptor_email'.")
+ validate_clean_str(self.receptor_email)
+ validate_non_empty_str(self.receptor_email)
diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py
index aac22134..5cd5831f 100644
--- a/cl_sii/dte/parse.py
+++ b/cl_sii/dte/parse.py
@@ -20,13 +20,12 @@
import io
import logging
import os
-from dataclasses import MISSING, _MISSING_TYPE
-from datetime import date
-from typing import Optional, Tuple, Union
-
-import lxml.etree
+from datetime import date, datetime
+from typing import Tuple
+from cl_sii.libs import encoding_utils
from cl_sii.libs import xml_utils
+from cl_sii.libs.xml_utils import XmlElement, XmlElementTree
from cl_sii.rut import Rut
from . import constants
from . import data_models
@@ -72,10 +71,10 @@
###############################################################################
def clean_dte_xml(
- xml_doc: lxml.etree.ElementBase,
+ xml_doc: XmlElement,
set_missing_xmlns: bool = False,
remove_doc_personalizado: bool = True,
-) -> Tuple[lxml.etree.ElementBase, bool]:
+) -> Tuple[XmlElement, bool]:
"""
Apply changes to ``xml_doc`` towards compliance to DTE XML schema.
@@ -103,7 +102,7 @@ def clean_dte_xml(
return xml_doc, modified
-def validate_dte_xml(xml_doc: lxml.etree.ElementBase) -> None:
+def validate_dte_xml(xml_doc: XmlElement) -> None:
"""
Validate ``xml_doc`` against DTE's XML schema.
@@ -114,39 +113,371 @@ def validate_dte_xml(xml_doc: lxml.etree.ElementBase) -> None:
xml_utils.validate_xml_doc(DTE_XML_SCHEMA_OBJ, xml_doc)
-def parse_dte_xml(xml_doc: lxml.etree.ElementBase) -> data_models.DteDataL2:
+# TODO: rename to 'parse_dte_xml_data'
+def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:
"""
- Parse and deserialize DTE data from ``xml_doc``.
+ Parse data from a DTE XML doc.
+
+ .. warning::
+ It is assumed that ``xml_doc`` is an
+ ``{http://www.sii.cl/SiiDte}/DTE`` XML element.
+
+ :raises ValueError:
+ :raises TypeError:
+ :raises NotImplementedError:
"""
+ # TODO: change response type to a dataclass like 'DteXmlData'.
# TODO: separate the XML parsing stage from the deserialization stage, which could be
# performed by XML-agnostic code (perhaps using Marshmallow or data clacases?).
# See :class:`cl_sii.rcv.parse.RcvCsvRowSchema`.
- xml_element_root_tree = xml_doc.getroottree()
-
- obj_struct = data_models.DteDataL2(
- emisor_rut=_get_emisor_rut(xml_element_root_tree),
- tipo_dte=_get_tipo_dte(xml_element_root_tree),
- folio=_get_folio(xml_element_root_tree),
- fecha_emision_date=_get_fecha_emision(xml_element_root_tree),
- receptor_rut=_get_receptor_rut(xml_element_root_tree),
- monto_total=_get_monto_total(xml_element_root_tree),
- emisor_razon_social=_get_emisor_razon_social(xml_element_root_tree),
- receptor_razon_social=_get_receptor_razon_social(xml_element_root_tree),
- fecha_vencimiento_date=_get_fecha_vencimiento(xml_element_root_tree, default=None),
+ if not isinstance(xml_doc, (XmlElement, XmlElementTree)):
+ raise TypeError("'xml_doc' must be an 'XmlElement'.")
+
+ xml_em = xml_doc
+
+ ###########################################################################
+ # XML elements finding
+ ###########################################################################
+
+ # Schema requires one, and only one, of these:
+ # a) 'Documento'
+ # b) 'Liquidacion'
+ # c) 'Exportaciones'
+ documento_em = xml_em.find(
+ 'sii-dte:Documento', # "Informacion Tributaria del DTE"
+ namespaces=DTE_XMLNS_MAP)
+ liquidacion_em = xml_em.find(
+ 'sii-dte:Liquidacion', # "Informacion Tributaria de Liquidaciones"
+ namespaces=DTE_XMLNS_MAP)
+ exportaciones_em = xml_em.find(
+ 'sii-dte:Exportaciones', # "Informacion Tributaria de exportaciones"
+ namespaces=DTE_XMLNS_MAP)
+ signature_em = xml_em.find(
+ 'ds:Signature', # "Firma Digital sobre Documento"
+ namespaces=xml_utils.XML_DSIG_NS_MAP)
+
+ if liquidacion_em is not None or exportaciones_em is not None:
+ raise NotImplementedError("XML element 'Documento' is the only one supported.")
+
+ if documento_em is None:
+ raise ValueError("Top level XML element 'Document' is required.")
+
+ # This value seems to be worthless (only useful for internal references in the XML doc).
+ # e.g. 'MiPE76354771-13419', 'MiPE76399752-6048'
+ # documento_em_id = documento_em.attrib['ID']
+
+ # 'Documento'
+ # Excluded elements (optional according to the XML schema but the SII may require some of these
+ # depending on 'tipo_dte' and other criteria):
+ # - 'Detalle': (occurrences: 0..60)
+ # "Detalle de Itemes del Documento"
+ # - 'SubTotInfo': (occurrences: 0..20)
+ # "Subtotales Informativos"
+ # - 'DscRcgGlobal': (occurrences: 0..20)
+ # "Descuentos y/o Recargos que afectan al total del Documento"
+ # - 'Referencia': (occurrences: 0..40)
+ # "Identificacion de otros documentos Referenciados por Documento"
+ # - 'Comisiones': (occurrences: 0..20)
+ # "Comisiones y otros cargos es obligatoria para Liquidaciones Factura"
+ encabezado_em = documento_em.find(
+ 'sii-dte:Encabezado', # "Identificacion y Totales del Documento"
+ namespaces=DTE_XMLNS_MAP)
+ # note: excluded because currently it is not useful.
+ # ted_em = documento_em.find(
+ # 'sii-dte:TED', # "Timbre Electronico de DTE"
+ # namespaces=DTE_XMLNS_MAP)
+ tmst_firma_em = documento_em.find(
+ 'sii-dte:TmstFirma', # "Fecha y Hora en que se Firmo Digitalmente el Documento"
+ namespaces=DTE_XMLNS_MAP)
+
+ # 'Documento.Encabezado'
+ # Excluded elements (optional according to the XML schema but the SII may require some of these
+ # depending on 'tipo_dte' and other criteria):
+ # - 'RUTMandante':
+ # "RUT a Cuenta de Quien se Emite el DTE"
+ # - 'RUTSolicita':
+ # "RUT que solicita el DTE en Venta a Publico"
+ # - 'Transporte':
+ # "Informacion de Transporte de Mercaderias"
+ # - 'OtraMoneda':
+ # "Otra Moneda"
+ id_doc_em = encabezado_em.find(
+ 'sii-dte:IdDoc', # "Identificacion del DTE"
+ namespaces=DTE_XMLNS_MAP)
+ emisor_em = encabezado_em.find(
+ 'sii-dte:Emisor', # "Datos del Emisor"
+ namespaces=DTE_XMLNS_MAP)
+ receptor_em = encabezado_em.find(
+ 'sii-dte:Receptor', # "Datos del Receptor"
+ namespaces=DTE_XMLNS_MAP)
+ totales_em = encabezado_em.find(
+ 'sii-dte:Totales', # "Montos Totales del DTE"
+ namespaces=DTE_XMLNS_MAP)
+
+ # 'Documento.Encabezado.IdDoc'
+ # Excluded elements (optional according to the XML schema but the SII may require some of these
+ # depending on 'tipo_dte' and other criteria):
+ # - 'IndNoRebaja':
+ # "Nota de Credito sin Derecho a Descontar Debito"
+ # - 'TipoDespacho':
+ # "Indica Modo de Despacho de los Bienes que Acompanan al DTE"
+ # - 'IndTraslado':
+ # "Incluido en Guias de Despacho para Especifiicar el Tipo de Traslado de Productos"
+ # - 'TpoImpresion':
+ # "Tipo de impresi贸n N (Normal) o T (Ticket)"
+ # - 'IndServicio':
+ # "Indica si Transaccion Corresponde a la Prestacion de un Servicio"
+ # - 'MntBruto':
+ # "Indica el Uso de Montos Brutos en Detalle"
+ # - 'TpoTranCompra':
+ # "Tipo de Transacci贸n para el comprador"
+ # - 'TpoTranVenta':
+ # "Tipo de Transacci贸n para el vendedor"
+ # - 'FmaPago':
+ # "Forma de Pago del DTE"
+ # - 'FmaPagExp':
+ # "Forma de Pago Exportaci贸n Tabla Formas de Pago de Aduanas"
+ # - 'FchCancel':
+ # "Fecha de Cancelacion del DTE"
+ # - 'MntCancel':
+ # "Monto Cancelado al emitirse el documento"
+ # - 'SaldoInsol':
+ # "Saldo Insoluto al emitirse el documento"
+ # - 'MntPagos': (occurrences: 0..30)
+ # "Tabla de Montos de Pago"
+ # - 'PeriodoDesde':
+ # "Periodo de Facturacion - Desde"
+ # - 'PeriodoHasta':
+ # "Periodo Facturacion - Hasta"
+ # - 'MedioPago':
+ # "Medio de Pago"
+ # - 'TpoCtaPago':
+ # "Tipo Cuenta de Pago"
+ # - 'NumCtaPago':
+ # "N煤mero de la cuenta del pago"
+ # - 'BcoPago':
+ # "Banco donde se realiza el pago"
+ # - 'TermPagoCdg':
+ # "Codigo del Termino de Pago Acordado"
+ # - 'TermPagoGlosa':
+ # "T茅rminos del Pago - glosa"
+ # - 'TermPagoDias':
+ # "Dias de Acuerdo al Codigo de Termino de Pago"
+ # (required):
+ tipo_dte_em = id_doc_em.find(
+ 'sii-dte:TipoDTE', # "Tipo de DTE"
+ namespaces=DTE_XMLNS_MAP)
+ folio_em = id_doc_em.find(
+ 'sii-dte:Folio', # "Folio del Documento Electronico"
+ namespaces=DTE_XMLNS_MAP)
+ fecha_emision_em = id_doc_em.find(
+ 'sii-dte:FchEmis', # "Fecha Emision Contable del DTE"
+ namespaces=DTE_XMLNS_MAP)
+ # (optional):
+ fecha_vencimiento_em = id_doc_em.find(
+ 'sii-dte:FchVenc', # "Fecha de Vencimiento del Pago"
+ namespaces=DTE_XMLNS_MAP)
+
+ # 'Documento.Encabezado.Emisor'
+ # Excluded elements (optional according to the XML schema but the SII may require some of these
+ # depending on 'tipo_dte' and other criteria):
+ # - 'Telefono': (occurrences: 0..2)
+ # "Telefono Emisor"
+ # - 'Acteco': (occurrences: 0..4)
+ # "Codigo de Actividad Economica del Emisor Relevante para el DTE"
+ # - 'GuiaExport':
+ # "Emisor de una Gu铆a de despacho para Exportaci贸n"
+ # - 'Sucursal':
+ # "Sucursal que Emite el DTE"
+ # - 'CdgSIISucur':
+ # "Codigo de Sucursal Entregado por el SII"
+ # - 'DirOrigen':
+ # "Direccion de Origen"
+ # - 'CmnaOrigen':
+ # "Comuna de Origen"
+ # - 'CiudadOrigen':
+ # "Ciudad de Origen"
+ # - 'CdgVendedor':
+ # "Codigo del Vendedor"
+ # - 'IdAdicEmisor':
+ # "Identificador Adicional del Emisor"
+ # (required):
+ emisor_rut_em = emisor_em.find(
+ 'sii-dte:RUTEmisor', # "RUT del Emisor del DTE"
+ namespaces=DTE_XMLNS_MAP)
+ emisor_razon_social_em = emisor_em.find(
+ 'sii-dte:RznSoc', # "Nombre o Razon Social del Emisor"
+ namespaces=DTE_XMLNS_MAP)
+ emisor_giro_em = emisor_em.find(
+ 'sii-dte:GiroEmis', # "Giro Comercial del Emisor Relevante para el DTE"
+ namespaces=DTE_XMLNS_MAP)
+ # (optional):
+ emisor_email_em = emisor_em.find(
+ 'sii-dte:CorreoEmisor', # "Correo Elect. de contacto en empresa del receptor" (wrong!)
+ namespaces=DTE_XMLNS_MAP)
+
+ # 'Documento.Encabezado.Receptor'
+ # Excluded elements (optional according to the XML schema but the SII may require some of these
+ # depending on 'tipo_dte' and other criteria):
+ # - 'CdgIntRecep':
+ # "Codigo Interno del Receptor"
+ # - 'Extranjero':
+ # "Receptor Extranjero"
+ # - 'GiroRecep':
+ # "Giro Comercial del Receptor"
+ # - 'Contacto':
+ # "Telefono o E-mail de Contacto del Receptor"
+ # - 'CorreoRecep':
+ # "Correo Elect. de contacto en empresa del receptor"
+ # - 'DirRecep':
+ # "Direccion en la Cual se Envian los Productos o se Prestan los Servicios"
+ # - 'CmnaRecep':
+ # "Comuna de Recepcion"
+ # - 'CiudadRecep':
+ # "Ciudad de Recepcion"
+ # - 'DirPostal':
+ # "Direccion Postal"
+ # - 'CmnaPostal':
+ # "Comuna Postal"
+ # - 'CiudadPostal':
+ # "Ciudad Postal"
+ # (required):
+ receptor_rut_em = receptor_em.find(
+ 'sii-dte:RUTRecep', # "RUT del Receptor del DTE"
+ namespaces=DTE_XMLNS_MAP)
+ receptor_razon_social_em = receptor_em.find(
+ 'sii-dte:RznSocRecep', # "Nombre o Razon Social del Receptor"
+ namespaces=DTE_XMLNS_MAP)
+ # (optional):
+ receptor_email_em = emisor_em.find(
+ 'sii-dte:CorreoRecep', # "Correo Elect. de contacto en empresa del receptor"
+ namespaces=DTE_XMLNS_MAP)
+
+ # 'Documento.Encabezado.Totales'
+ # Excluded elements (optional according to the XML schema but the SII may require some of these
+ # depending on 'tipo_dte' and other criteria):
+ # - 'MntNeto':
+ # "Monto Neto del DTE"
+ # - 'MntExe':
+ # "Monto Exento del DTE"
+ # - 'MntBase':
+ # "Monto Base Faenamiento Carne" (???)
+ # - 'MntMargenCom':
+ # "Monto Base de M谩rgenes de Comercializaci贸n. Monto informado"
+ # - 'TasaIVA':
+ # "Tasa de IVA" (percentage)
+ # - 'IVA':
+ # "Monto de IVA del DTE"
+ # - 'IVAProp':
+ # "Monto del IVA propio"
+ # - 'IVATerc':
+ # "Monto del IVA de Terceros"
+ # - 'ImptoReten': (occurrences: 0..20)
+ # "Impuestos y Retenciones Adicionales"
+ # - 'IVANoRet':
+ # "IVA No Retenido"
+ # - 'CredEC':
+ # "Credito Especial Empresas Constructoras"
+ # - 'GrntDep':
+ # "Garantia por Deposito de Envases o Embalajes"
+ # - 'Comisiones':
+ # "Comisiones y otros cargos es obligatoria para Liquidaciones Factura"
+ # - 'MontoNF':
+ # "Monto No Facturable - Corresponde a Bienes o Servicios Facturados Previamente"
+ # - 'MontoPeriodo':
+ # "Total de Ventas o Servicios del Periodo"
+ # - 'SaldoAnterior':
+ # "Saldo Anterior - Puede ser Negativo o Positivo"
+ # - 'VlrPagar':
+ # "Valor a Pagar Total del documento"
+ monto_total_em = totales_em.find(
+ 'sii-dte:MntTotal', # "Monto Total del DTE"
+ namespaces=DTE_XMLNS_MAP)
+
+ # 'Signature'
+ # signature_signed_info_em = signature_em.find(
+ # 'ds:SignedInfo', # "Descripcion de la Informacion Firmada y del Metodo de Firma"
+ # namespaces=xml_utils.XML_DSIG_NS_MAP)
+ # signature_signed_info_canonicalization_method_em = signature_signed_info_em.find(
+ # 'ds:CanonicalizationMethod', # "Algoritmo de Canonicalizacion"
+ # namespaces=xml_utils.XML_DSIG_NS_MAP)
+ # signature_signed_info_signature_method_em = signature_signed_info_em.find(
+ # 'ds:SignatureMethod', # "Algoritmo de Firma"
+ # namespaces=xml_utils.XML_DSIG_NS_MAP)
+ # signature_signed_info_reference_em = signature_signed_info_em.find(
+ # 'ds:Reference', # "Referencia a Elemento Firmado"
+ # namespaces=xml_utils.XML_DSIG_NS_MAP)
+ signature_signature_value_em = signature_em.find(
+ 'ds:SignatureValue', # "Valor de la Firma Digital"
+ namespaces=xml_utils.XML_DSIG_NS_MAP)
+ signature_key_info_em = signature_em.find(
+ 'ds:KeyInfo', # "Informacion de Claves Publicas y Certificado"
+ namespaces=xml_utils.XML_DSIG_NS_MAP)
+ # signature_key_info_key_value_em = signature_key_info_em.find(
+ # 'ds:KeyValue',
+ # namespaces=xml_utils.XML_DSIG_NS_MAP)
+ signature_key_info_x509_data_em = signature_key_info_em.find(
+ 'ds:X509Data', # "Informacion del Certificado Publico"
+ namespaces=xml_utils.XML_DSIG_NS_MAP)
+ signature_key_info_x509_cert_em = signature_key_info_x509_data_em.find(
+ 'ds:X509Certificate', # "Certificado Publico"
+ namespaces=xml_utils.XML_DSIG_NS_MAP)
+
+ ###########################################################################
+ # values parsing
+ ###########################################################################
+
+ tipo_dte_value = constants.TipoDteEnum(int(tipo_dte_em.text.strip()))
+ folio_value = int(folio_em.text.strip())
+ fecha_emision_value = date.fromisoformat(fecha_emision_em.text.strip())
+ fecha_vencimiento_value = None
+ if fecha_vencimiento_em is not None:
+ fecha_vencimiento_value = date.fromisoformat(fecha_vencimiento_em.text.strip())
+
+ emisor_rut_value = Rut(emisor_rut_em.text.strip())
+ emisor_razon_social_value = emisor_razon_social_em.text.strip()
+ emisor_giro_value = emisor_giro_em.text.strip()
+ emisor_email_value = emisor_email_em.text.strip() if emisor_email_em is not None else None
+
+ receptor_rut_value = Rut(receptor_rut_em.text.strip())
+ receptor_razon_social_value = receptor_razon_social_em.text.strip()
+ receptor_email_value = receptor_email_em.text.strip() if receptor_email_em is not None else None
+
+ monto_total_value = int(monto_total_em.text.strip())
+
+ tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text)
+
+ signature_signature_value = encoding_utils.decode_base64_strict(
+ signature_signature_value_em.text.strip())
+ signature_key_info_x509_cert_pem = encoding_utils.clean_base64(
+ signature_key_info_x509_cert_em.text.strip())
+
+ return data_models.DteDataL2(
+ emisor_rut=emisor_rut_value,
+ tipo_dte=tipo_dte_value,
+ folio=folio_value,
+ fecha_emision_date=fecha_emision_value,
+ receptor_rut=receptor_rut_value,
+ monto_total=monto_total_value,
+ emisor_razon_social=emisor_razon_social_value,
+ receptor_razon_social=receptor_razon_social_value,
+ fecha_vencimiento_date=fecha_vencimiento_value,
+ firma_documento_dt_naive=tmst_firma_value,
+ signature_value=signature_signature_value,
+ signature_x509_cert_pem=signature_key_info_x509_cert_pem,
+ emisor_giro=emisor_giro_value,
+ emisor_email=emisor_email_value,
+ receptor_email=receptor_email_value,
)
- return obj_struct
-
###############################################################################
# helpers
###############################################################################
-def _set_dte_xml_missing_xmlns(
- xml_doc: lxml.etree.ElementBase,
-) -> Tuple[lxml.etree.ElementBase, bool]:
+def _set_dte_xml_missing_xmlns(xml_doc: XmlElement) -> Tuple[XmlElement, bool]:
# source: name of the XML element without namespace.
# cl_sii/data/ref/factura_electronica/schemas-xml/DTE_v10.xsd#L22 (f57a326)
@@ -180,9 +511,7 @@ def _set_dte_xml_missing_xmlns(
return xml_doc, modified
-def _remove_dte_xml_doc_personalizado(
- xml_doc: lxml.etree.ElementBase,
-) -> Tuple[lxml.etree.ElementBase, bool]:
+def _remove_dte_xml_doc_personalizado(xml_doc: XmlElement) -> Tuple[XmlElement, bool]:
# Remove non-standard but popular element 'DocPersonalizado', it if exists.
modified = False
@@ -194,96 +523,3 @@ def _remove_dte_xml_doc_personalizado(
xml_doc.remove(xml_em)
return xml_doc, modified
-
-
-def _get_tipo_dte(xml_etree: lxml.etree.ElementTree) -> constants.TipoDteEnum:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:TipoDTE'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'TipoDTE' was not found in the XML document.")
- return constants.TipoDteEnum(int(value_str))
-
-
-def _get_folio(xml_etree: lxml.etree.ElementTree) -> int:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:Folio'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'Folio' was not found in the XML document.")
- return int(value_str)
-
-
-def _get_fecha_emision(xml_etree: lxml.etree.ElementTree) -> date:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:FchEmis'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'FchEmis' was not found in the XML document.")
- return date.fromisoformat(value_str)
-
-
-def _get_fecha_vencimiento(
- xml_etree: lxml.etree.ElementTree,
- default: Union[date, None, _MISSING_TYPE] = MISSING,
-) -> Optional[date]:
-
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:FchVenc'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- if default is None or isinstance(default, date):
- value = default
- elif default is MISSING:
- raise Exception("Element 'FchVenc' was not found in the XML document.")
- else:
- raise TypeError("Invalid type of 'default'.")
- else:
- value = date.fromisoformat(value_str)
-
- return value
-
-
-def _get_emisor_rut(xml_etree: lxml.etree.ElementTree) -> Rut:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Emisor/sii-dte:RUTEmisor'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'RUTEmisor' was not found in the XML document.")
- return Rut(value_str)
-
-
-def _get_emisor_razon_social(xml_etree: lxml.etree.ElementTree) -> str:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Emisor/sii-dte:RznSoc'
-
- value_str: str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'RznSoc' was not found in the XML document.")
- return value_str
-
-
-def _get_receptor_rut(xml_etree: lxml.etree.ElementTree) -> Rut:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Receptor/sii-dte:RUTRecep'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'RUTRecep' was not found in the XML document.")
- return Rut(value_str)
-
-
-def _get_receptor_razon_social(xml_etree: lxml.etree.ElementTree) -> str:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Receptor/sii-dte:RznSocRecep'
-
- value_str: str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'RznSocRecep' was not found in the XML document.")
- return value_str
-
-
-def _get_monto_total(xml_etree: lxml.etree.ElementTree) -> int:
- em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Totales/sii-dte:MntTotal'
-
- value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP)
- if value_str is None:
- raise Exception("Element 'MntTotal' was not found in the XML document.")
- return int(value_str)
diff --git a/cl_sii/libs/crypto_utils.py b/cl_sii/libs/crypto_utils.py
new file mode 100644
index 00000000..00452e1d
--- /dev/null
+++ b/cl_sii/libs/crypto_utils.py
@@ -0,0 +1,61 @@
+from typing import Union
+
+import cryptography.x509
+import signxml.util
+from cryptography.hazmat.backends.openssl import backend as _crypto_x509_backend
+from cryptography.x509 import Certificate as X509Cert
+from OpenSSL.crypto import X509 as _X509CertOpenSsl # noqa: F401
+
+
+def load_pem_x509_cert(pem_value: Union[str, bytes]) -> X509Cert:
+ """
+ Load an X.509 certificate from a PEM-formatted value.
+
+ .. seealso::
+ https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file
+
+ :raises TypeError:
+ :raises ValueError:
+
+ """
+ if isinstance(pem_value, str):
+ pem_value_bytes = pem_value.encode('ascii')
+ elif isinstance(pem_value, bytes):
+ pem_value_bytes = pem_value
+ else:
+ raise TypeError("Value must be str or bytes.")
+
+ mod_pem_value_bytes = add_pem_cert_header_footer(pem_value_bytes)
+ try:
+ x509_cert = cryptography.x509.load_pem_x509_certificate(
+ data=mod_pem_value_bytes,
+ backend=_crypto_x509_backend)
+ except ValueError:
+ # e.g.
+ # "Unable to load certificate. See
+ # https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file for more details."
+ raise
+
+ return x509_cert
+
+
+def add_pem_cert_header_footer(pem_cert: bytes) -> bytes:
+ """
+ Add certificate PEM header and footer (if not already present).
+ """
+ pem_value_str = pem_cert.decode('ascii')
+ # note: it would be great if 'add_pem_header' did not forcefully convert bytes to str.
+ mod_pem_value_str = signxml.util.add_pem_header(pem_value_str)
+ mod_pem_value: bytes = mod_pem_value_str.encode('ascii')
+ return mod_pem_value
+
+
+def remove_pem_cert_header_footer(pem_cert: bytes) -> bytes:
+ """
+ Remove certificate PEM header and footer (if they are present).
+ """
+ pem_value_str = pem_cert.decode('ascii')
+ # note: it would be great if 'strip_pem_header' did not expect input to be a str.
+ mod_pem_value_str = signxml.util.strip_pem_header(pem_value_str)
+ mod_pem_value: bytes = mod_pem_value_str.encode('ascii').strip()
+ return mod_pem_value
diff --git a/cl_sii/libs/encoding_utils.py b/cl_sii/libs/encoding_utils.py
new file mode 100644
index 00000000..9a6adb14
--- /dev/null
+++ b/cl_sii/libs/encoding_utils.py
@@ -0,0 +1,55 @@
+import base64
+import binascii
+from typing import Union
+
+
+def clean_base64(value: Union[str, bytes]) -> bytes:
+ """
+ Force bytes and remove line breaks and spaces.
+
+ Does not validate base64 format.
+
+ :raises ValueError:
+ :raises TypeError:
+
+ """
+ if isinstance(value, bytes):
+ value_base64_bytes = value
+ elif isinstance(value, str):
+ try:
+ value_base64_bytes = value.strip().encode(encoding='ascii', errors='strict')
+ except UnicodeEncodeError as exc:
+ raise ValueError("Only ASCII characters are accepted.", str(exc)) from exc
+ else:
+ raise TypeError("Value must be str or bytes.")
+
+ # remove line breaks and spaces
+ value_base64_bytes_cleaned = value_base64_bytes.replace(b'\n', b'').replace(b' ', b'')
+
+ return value_base64_bytes_cleaned
+
+
+def decode_base64_strict(value: Union[str, bytes]) -> bytes:
+ """
+ Strict conversion for str/bytes, tolerating only line breaks and spaces.
+
+ :raises ValueError: non-base64 input or non-ASCII characters included
+
+ """
+ value_base64_bytes_cleaned = clean_base64(value)
+ try:
+ value_bytes = base64.b64decode(value_base64_bytes_cleaned, validate=True)
+ except binascii.Error as exc:
+ raise ValueError("Input is not a valid base64 value.", str(exc)) from exc
+ return value_bytes
+
+
+def validate_base64(value: Union[str, bytes]) -> None:
+ """
+ Validate that ``value`` is base64-encoded data.
+
+ :raises ValueError:
+ :raises TypeError:
+
+ """
+ decode_base64_strict(value)
diff --git a/cl_sii/libs/xml_utils.py b/cl_sii/libs/xml_utils.py
index 83a9d69e..6ed10205 100644
--- a/cl_sii/libs/xml_utils.py
+++ b/cl_sii/libs/xml_utils.py
@@ -1,3 +1,24 @@
+"""
+XML utils
+=========
+
+
+XML (Digital) Signature
+-----------------------
+
+a.k.a. 'XMLDSig', 'XML-DSig', XML-Sig'
+
+XML Signature [..] defines an XML syntax for digital signatures and is
+defined in the W3C recommendation "XML Signature Syntax and Processing"
+(``xmldsig-core``). Functionally, it has much in common with ``PKCS#7 ``
+but is more extensible and geared towards signing XML documents.
+It is used by various Web technologies such as SOAP, SAML, and others.
+
+.. seealso::
+ https://en.wikipedia.org/wiki/XML_Signature
+
+
+"""
import logging
import os
from typing import IO
@@ -7,11 +28,33 @@
import lxml.etree
import xml.parsers.expat
import xml.parsers.expat.errors
+from lxml.etree import ElementBase as XmlElement # noqa: F401
+# note: 'lxml.etree.ElementTree' is a **function**, not a class.
+from lxml.etree import _ElementTree as XmlElementTree # noqa: F401
+from lxml.etree import XMLSchema as XmlSchema # noqa: F401
logger = logging.getLogger(__name__)
+XML_DSIG_NS_MAP = dict(
+ ds='http://www.w3.org/2000/09/xmldsig#',
+ dsig11='http://www.w3.org/2009/xmldsig11#',
+ dsig2='http://www.w3.org/2010/xmldsig2#',
+ ec='http://www.w3.org/2001/10/xml-exc-c14n#',
+ dsig_more='http://www.w3.org/2001/04/xmldsig-more#',
+ xenc='http://www.w3.org/2001/04/xmlenc#',
+ xenc11='http://www.w3.org/2009/xmlenc11#',
+)
+"""
+Mapping from XML namespace prefix to full name, for XML Signature.
+
+Source:
+``signxml.namespaces`` @ 16503242 (~ v2.6.0)
+https://github.com/XML-Security/signxml/blob/16503242/signxml/__init__.py#L23-L31
+"""
+
+
###############################################################################
# exceptions
###############################################################################
@@ -72,7 +115,7 @@ class XmlSchemaDocValidationError(Exception):
# functions
###############################################################################
-def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase:
+def parse_untrusted_xml(value: bytes) -> XmlElement:
"""
Parse XML-encoded content in value.
@@ -115,7 +158,7 @@ def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase:
base_url=None, # default: None
forbid_dtd=False, # default: False (allow Document Type Definition)
forbid_entities=True, # default: True (forbid Entity definitions/declarations)
- ) # type: lxml.etree.ElementBase
+ ) # type: XmlElement
except (defusedxml.DTDForbidden,
defusedxml.EntitiesForbidden,
@@ -192,7 +235,7 @@ def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase:
return xml_root_em
-def read_xml_schema(filename: str) -> lxml.etree.XMLSchema:
+def read_xml_schema(filename: str) -> XmlSchema:
"""
Instantiate an XML schema object from a file.
@@ -200,11 +243,11 @@ def read_xml_schema(filename: str) -> lxml.etree.XMLSchema:
"""
if os.path.exists(filename) and os.path.isfile(filename):
- return lxml.etree.XMLSchema(file=filename)
+ return XmlSchema(file=filename)
raise ValueError("XML schema file not found.", filename)
-def validate_xml_doc(xml_schema: lxml.etree.XMLSchema, xml_doc: lxml.etree.ElementBase) -> None:
+def validate_xml_doc(xml_schema: XmlSchema, xml_doc: XmlElement) -> None:
"""
Validate ``xml_doc`` against XML schema ``xml_schema``.
@@ -240,7 +283,7 @@ def validate_xml_doc(xml_schema: lxml.etree.XMLSchema, xml_doc: lxml.etree.Eleme
raise XmlSchemaDocValidationError(validation_error_msg) from exc
-def write_xml_doc(xml_doc: lxml.etree.ElementBase, output: IO[bytes]) -> None:
+def write_xml_doc(xml_doc: XmlElement, output: IO[bytes]) -> None:
"""
Write ``xml_doc`` to bytes stream ``output``.
@@ -264,7 +307,7 @@ def write_xml_doc(xml_doc: lxml.etree.ElementBase, output: IO[bytes]) -> None:
# note: use `IO[X]` for arguments and `TextIO`/`BinaryIO` for return types (says GVR).
# https://github.com/python/typing/issues/518#issuecomment-350903120
- xml_etree: lxml.etree.ElementTree = xml_doc.getroottree()
+ xml_etree: XmlElementTree = xml_doc.getroottree()
# See:
# https://lxml.de/api/lxml.etree._ElementTree-class.html#write
diff --git a/requirements/base.txt b/requirements/base.txt
index 0cb7cfe3..ac889be3 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -2,10 +2,33 @@
# note: it is mandatory to register all dependencies of the required packages.
# Required packages:
+cryptography==2.6.1
defusedxml==0.5.0
-lxml==4.2.5
+lxml==4.2.6
marshmallow==2.16.3
-pytz==2018.9
+pyOpenSSL==18.0.0
+pytz==2019.1
+signxml==2.6.0
# Packages dependencies:
-#none
+# - cryptography:
+# - asn1crypto
+# - cffi:
+# - pycparser
+# - six
+# - signxml:
+# - certifi
+# - cryptography
+# - defusedxml
+# - eight
+# - future
+# - lxml
+# - pyOpenSSL
+# - six
+asn1crypto==0.24.0
+certifi==2019.3.9
+cffi==1.12.3
+eight==0.4.2
+future==0.16.0
+pycparser==2.19
+six==1.12.0
diff --git a/requirements/extras.txt b/requirements/extras.txt
index d0e3ad4f..a59af561 100644
--- a/requirements/extras.txt
+++ b/requirements/extras.txt
@@ -2,4 +2,4 @@
# Required packages:
Django<2.2
-djangorestframework<3.9
+djangorestframework<3.10
diff --git a/requirements/release.txt b/requirements/release.txt
index 49ca0a96..db36ef42 100644
--- a/requirements/release.txt
+++ b/requirements/release.txt
@@ -4,7 +4,7 @@
# Required packages:
bumpversion==0.5.3
-setuptools==40.8.0
+setuptools==41.0.1
twine==1.13.0
wheel==0.33.1
diff --git a/requirements/test.txt b/requirements/test.txt
index 5fb90db5..daefd728 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -13,6 +13,7 @@ tox==3.7.0
# - coverage
# - requests
# - flake8:
+# - entrypoints
# - mccabe
# - pycodestyle
# - pyflakes
@@ -25,13 +26,14 @@ tox==3.7.0
# - py
# - toml
# - virtualenv
+entrypoints==0.3
filelock==3.0.10
mccabe==0.6.1
mypy-extensions==0.4.1
pluggy==0.9.0
py==1.8.0
pycodestyle==2.5.0
-pyflakes==2.1.0
+pyflakes==2.1.1
toml==0.10.0
-typed-ast==1.3.1
-virtualenv==16.4.3
+typed-ast==1.3.4
+virtualenv==16.5.0
diff --git a/setup.cfg b/setup.cfg
index 79258364..c6360984 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,6 +27,9 @@ disallow_untyped_defs = True
check_untyped_defs = True
warn_return_any = True
+[mypy-cryptography.*]
+ignore_missing_imports = True
+
[mypy-defusedxml.*]
ignore_missing_imports = True
@@ -39,6 +42,12 @@ ignore_missing_imports = True
[mypy-marshmallow.*]
ignore_missing_imports = True
+[mypy-OpenSSL.*]
+ignore_missing_imports = True
+
+[mypy-signxml.*]
+ignore_missing_imports = True
+
[mypy-rest_framework.*]
ignore_missing_imports = True
diff --git a/setup.py b/setup.py
index d8cd39da..5e9d2663 100755
--- a/setup.py
+++ b/setup.py
@@ -23,10 +23,13 @@ def get_version(*file_paths: Sequence[str]) -> str:
# TODO: add reasonable upper-bound for some of these packages?
requirements = [
+ 'cryptography>=2.6.1',
'defusedxml>=0.5.0',
- 'lxml>=4.2.5',
+ 'lxml>=4.2.6',
'marshmallow>=2.16.3',
+ 'pyOpenSSL>=18.0.0',
'pytz>=2018.7',
+ 'signxml>=2.6.0',
]
extras_requirements = {
diff --git a/tests/test_data/crypto/wildcard-google-com-cert.pem b/tests/test_data/crypto/wildcard-google-com-cert.pem
new file mode 100644
index 00000000..7fa3f3cf
--- /dev/null
+++ b/tests/test_data/crypto/wildcard-google-com-cert.pem
@@ -0,0 +1,46 @@
+-----BEGIN CERTIFICATE-----
+MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU
+MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw
+IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz
+NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh
+bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds
+ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49
+AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ
+gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF
+BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu
+Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs
+b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv
+gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz
+LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO
+Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC
+DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u
+Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j
+b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou
+Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs
+ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu
+Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n
+b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n
+c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp
+Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq
+LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl
+ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt
+Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb
+ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu
+Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0
+aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t
+ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK
+d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0
+aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow
+LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp
+BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O
+BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw
+FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD
+MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n
+L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y
+ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ
+IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb
++DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl
+g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP
+MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA
+ow==
+-----END CERTIFICATE-----
diff --git a/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem
new file mode 100644
index 00000000..1271a9f7
--- /dev/null
+++ b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx
+HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE
+ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD
+EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW
+GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa
+MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh
+MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE
+CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM
+b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI
+hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx
+O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC
+/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3
+FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV
+HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S
+ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu
+Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6
+Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1
+MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC
+AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu
+Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp
+AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A
+IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk
+AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA
+cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA
+mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD
+qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un
++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud
+B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY
++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==
+-----END CERTIFICATE-----
diff --git a/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt
new file mode 100644
index 00000000..4453bbd4
--- /dev/null
+++ b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt
@@ -0,0 +1 @@
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnkddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKiwqSxDcYjTT6vXsLPrZk=
diff --git a/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem
new file mode 100644
index 00000000..92083cd6
--- /dev/null
+++ b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM
+MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g
+QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg
+LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5
+NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ
+BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ
+QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu
+YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld
+pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo
+ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q
+f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh
+/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a
+h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA
+AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud
+DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw
+FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE
+bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw
+dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw
+NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv
+QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05
+oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h
+cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw
+AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/
+BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny
+bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD
+CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb
+9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J
+gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr
+2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA
+NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf
+zmrw
+-----END CERTIFICATE-----
diff --git a/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt
new file mode 100644
index 00000000..9f5c23a8
--- /dev/null
+++ b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt
@@ -0,0 +1 @@
+wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFDMrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==
diff --git a/tests/test_data/sii-crypto/prueba-sii-cert.pem b/tests/test_data/sii-crypto/prueba-sii-cert.pem
new file mode 100644
index 00000000..3ba15fa7
--- /dev/null
+++ b/tests/test_data/sii-crypto/prueba-sii-cert.pem
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIEPjCCA6mgAwIBAgIDAgGKMAsGCSqGSIb3DQEBBDCBsTEdMBsGA1UECBQUUmVn
+aW9uIE1ldHJvcG9saXRhbmExETAPBgNVBAcUCFNhbnRpYWdvMSIwIAYDVQQDFBlF
+LUNlcnRjaGlsZSBDQSBJbnRlcm1lZGlhMTYwNAYDVQQLFC1FbXByZXNhIE5hY2lv
+bmFsIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExFDASBgNVBAoUC0UtQ0VS
+VENISUxFMQswCQYDVQQGEwJDTDAeFw0wMjEwMDIxOTExNTlaFw0wMzEwMDIwMDAw
+MDBaMIHXMR0wGwYDVQQIFBRSZWdpb24gTWV0cm9wb2xpdGFuYTEnMCUGA1UECxQe
+U2VydmljaW8gZGUgSW1wdWVzdG9zIEludGVybm9zMScwJQYDVQQKFB5TZXJ2aWNp
+byBkZSBJbXB1ZXN0b3MgSW50ZXJub3MxETAPBgNVBAcUCFNhbnRpYWdvMR8wHQYJ
+KoZIhvcNAQkBFhB3Z29uemFsZXpAc2lpLmNsMSMwIQYDVQQDFBpXaWxpYmFsZG8g
+R29uemFsZXogQ2FicmVyYTELMAkGA1UEBhMCQ0wwXDANBgkqhkiG9w0BAQEFAANL
+ADBIAkEAvNQyaLPd3cQlBr0fQWooAKXSFan/WbaFtD5P7QDzcE1pBIvKY2Uv6uid
+ur/mGVB9IS4Fq/1xRIXy13FFmxLwTQIDAQABo4IBgjCCAX4wIwYDVR0RBBwwGqAY
+BggrBgEEAcNSAaAMFgowNzg4MDQ0Mi00MDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6
+Ly9jcmwuZS1jZXJ0Y2hpbGUuY2wvRWNlcnRjaGlsZUNBSS5jcmwwIwYDVR0SBBww
+GqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIHmBgNVHSAEgd4wgdswgdgGCCsG
+AQQBw1IAMIHLMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmUtY2VydGNoaWxlLmNs
+L3BvbGl0aWNhL2Nwcy5odG0wgZAGCCsGAQUFBwICMIGDGoGARWwgdGl0dWxhciBo
+YSBzaWRvIHZhbGlkYWRvIGVuIGZvcm1hIHByZXNlbmNpYWwsIHF1ZWRhbmRvIGhh
+YmlsaXRhZG8gZWwgQ2VydGlmaWNhZG8gcGFyYSB1c28gdHJpYnV0YXJpbywgcGFn
+b3MsIGNvbWVyY2lvIHUgb3Ryb3MwCwYDVR0PBAQDAgTwMAsGCSqGSIb3DQEBBAOB
+gQB2V4cTj7jo1RawmsRQUSnnvJjMCrZstcHY+Ss3IghVPO9eGoYzu5Q63vzt0Pi8
+CS91SBc7xo+LDoljaUyjOzj7zvU7TpWoFndiTQF3aCOtTkV+vjCMWW3sVHes4UCM
+DkF3VYK+rDTAadiaeDArTwsx4eNEpxFuA/TJwcXpLQRCDg==
+-----END CERTIFICATE-----
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml
new file mode 100644
index 00000000..6bf67797
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K| 33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
+
+
+
+
+
+
+
+
+
+ij2Qn6xOc2eRx3hwyO/GrzptoBk=
+
+
+
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
+ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
+wqSxDcYjTT6vXsLPrZk=
+
+
+
+
+
+pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl
+9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN
+Uavs/9J+gR9BBMs/eYE=
+
+AQAB
+
+
+
+
+abc
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml
new file mode 100644
index 00000000..1722e121
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
+
+
+
+
+
+
+
+
+
+ij2Qn6xOc2eRx3hwyO/GrzptoBk=
+
+
+
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
+ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
+wqSxDcYjTT6vXsLPrZk=
+
+
+
+
+
+pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl
+9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN
+Uavs/9J+gR9BBMs/eYE=
+
+AQAB
+
+
+
+
+MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx
++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml
new file mode 100644
index 00000000..2cd61095
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517000
+ 19.00
+ 478230
+ 2995230
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258500.00
+ 2517000
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2995230Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
+
+
+
+
+
+
+
+
+
+ij2Qn6xOc2eRx3hwyO/GrzptoBk=
+
+
+
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
+ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
+wqSxDcYjTT6vXsLPrZk=
+
+
+
+
+
+pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl
+9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN
+Uavs/9J+gR9BBMs/eYE=
+
+AQAB
+
+
+
+
+MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx
+HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE
+ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD
+EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW
+GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa
+MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh
+MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE
+CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM
+b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI
+hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx
+O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC
+/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3
+FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV
+HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S
+ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu
+Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6
+Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1
+MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC
+AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu
+Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp
+AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A
+IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk
+AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA
+cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA
+mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD
+qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un
++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud
+B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY
++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml
new file mode 100644
index 00000000..8a7b6b49
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
+
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml
new file mode 100644
index 00000000..ded4f5c2
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
+
+
+
+
+
+
+
+
+
+ij2Qn6xOc2eRx3hwyO/GrzptoBk=
+
+
+
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
+ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
+wqSxDcYjTT6vXsLPrZk=
+
+
+
+
+
+pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl
+9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN
+Uavs/9J+gR9BBMs/eYE=
+
+AQAB
+
+
+
+
+MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU
+MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw
+IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz
+NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh
+bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds
+ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49
+AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ
+gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF
+BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu
+Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs
+b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv
+gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz
+LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO
+Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC
+DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u
+Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j
+b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou
+Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs
+ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu
+Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n
+b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n
+c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp
+Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq
+LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl
+ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt
+Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb
+ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu
+Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0
+aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t
+ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK
+d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0
+aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow
+LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp
+BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O
+BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw
+FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD
+MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n
+L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y
+ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ
+IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb
++DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl
+g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP
+MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA
+ow==
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml
new file mode 100644
index 00000000..59b2e818
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+ij2Qn6xOc2eRx3hwyO/GrzptoBk=
+
+
+
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
+ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
+wqSxDcYjTT6vXsLPrZk=
+
+
+
+
+
+pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl
+9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN
+Uavs/9J+gR9BBMs/eYE=
+
+AQAB
+
+
+
+
+MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx
+HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE
+ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD
+EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW
+GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa
+MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh
+MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE
+CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM
+b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI
+hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx
+O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC
+/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3
+FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV
+HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S
+ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu
+Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6
+Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1
+MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC
+AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu
+Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp
+AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A
+IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk
+AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA
+cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA
+mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD
+qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un
++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud
+B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY
++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml
new file mode 100644
index 00000000..e2071898
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml
@@ -0,0 +1,56 @@
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml
new file mode 100644
index 00000000..0cb88a90
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml
@@ -0,0 +1,57 @@
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml
new file mode 100644
index 00000000..263ab7c2
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+ 33
+ 170
+ 2019-04-01
+ 1
+ 1
+ 2
+
+
+ 76354771-K
+ INGENIERIA ENACON SPA
+ Ingenieria y Construccion
+ ENACONLTDA@GMAIL.COM
+ 421000
+ 078525666
+ MERCED 753 16 ARBOLEDA DE QUIILOTA
+ QUILLOTA
+ QUILLOTA
+
+
+ 96790240-3
+ MINERA LOS PELAMBRES
+ EXTRACCION Y PROCESAMIENTO DE COBRE
+ Felipe Barria
+ Av. Apoquindo 4001 1802
+ LAS CONDES
+ SANTIAGO
+
+
+ 2517900
+ 19.00
+ 478401
+ 2996301
+
+
+
+ 1
+ Tableros electricos 3 tom
+ as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.-
+ 2.00
+ Unid
+ 1258950.00
+ 2517900
+
+
+ 1
+ 801
+ 4510083633
+ 2019-03-22
+
+76354771-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40
+
+
+
+
+
+
+
+
+
+
+
+ij2Qn6xOc2eRx3hwyO/GrzptoBk=
+
+
+
+fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk
+ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi
+wqSxDcYjTT6vXsLPrZk=
+
+
+
+
+
+pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl
+9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN
+Uavs/9J+gR9BBMs/eYE=
+
+AQAB
+
+
+
+
+MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx
+HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE
+ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD
+EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW
+GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa
+MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh
+MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE
+CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM
+b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI
+hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx
+O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC
+/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3
+FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV
+HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S
+ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu
+Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6
+Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1
+MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC
+AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu
+Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp
+AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A
+IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk
+AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA
+cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA
+mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD
+qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un
++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud
+B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY
++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml b/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml
new file mode 100644
index 00000000..e4914e04
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+ 33
+ 25568
+ 2019-03-29
+ 1
+ 1
+ 2
+
+
+ 76399752-9
+ COMERCIALIZADORA INNOVA MOBEL SPA
+ COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR
+ 87 472133
+ ANGEL.PEZO@APCASESORIAS.CL
+ 310001
+ 078904860
+ LOS CIPRESES 2834
+ LA PINTANA
+ SANTIAGO
+
+
+ 96874030-K
+ EMPRESAS LA POLAR S.A.
+ VENTA AL POR MENOR EN COMERCIOS DE VESTU
+ N Lote Despacho: 20921554 / N Sello: 660620
+ AVDA. SANTA CLARA 207 62 CIUDAD EMPRESARIAL
+ HUECHURABA
+ SANTIAGO
+
+
+ 194111
+ 19.00
+ 36881
+ 230992
+
+
+
+ 1
+
+ SKU
+ 19586316
+
+ JUEGO_LIVI - CHOCOLATE
+ ROMA 3.1.1
+ 1.00
+ UN
+ 194111.00
+ 194111
+
+
+ 1
+ 801
+ 638370
+ 2019-03-28
+
+76399752-933 | 255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA33 | 25568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+pZgE/rr79kgVqirnIRM+HUpB3Yt4fbyMaARGqtA==
+ 2019-03-28T13:59:52
+
+
+
+
+
+
+
+
+
+
+
+tk/D3mfO/KtdWyFXYZHe7dtYijg=
+
+
+
+wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD
+MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4
+FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1
+GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp
+0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==
+
+
+
+
+
+0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hjuEe9P78f6G5w6
+U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PPDK3JQg8dNy0w0nfaYhD98ZTw
+f5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr
+9N/SU4b8Ib9ue8t25tpxz2jsHlBLokXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/
+SobU35wgtdXK6wYYIIQNN+Zdv8AjisQpom3Rcw==
+
+AQAB
+
+
+
+
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK
+Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj
+YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh
+Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0
+MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT
+GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu
+YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1
+g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP
+HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh
+qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS
+EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G
+A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L
+WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG
++EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v
+YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr
+BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R
+BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt
+OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw
+OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H
+NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz
+ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV
+WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/
+w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB
+B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg
+jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml b/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml
new file mode 100644
index 00000000..bdecf2ff
--- /dev/null
+++ b/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+ 33
+ 25568
+ 2019-03-29
+ 1
+ 1
+ 2
+
+
+ 76399752-9
+ COMERCIALIZADORA INNOVA MOBEL SPA
+ COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR
+ 87 472133
+ ANGEL.PEZO@APCASESORIAS.CL
+ 310001
+ 078904860
+ LOS CIPRESES 2834
+ LA PINTANA
+ SANTIAGO
+
+
+ 96874030-K
+ EMPRESAS LA POLAR S.A.
+ VENTA AL POR MENOR EN COMERCIOS DE VESTU
+ N Lote Despacho: 20921554 / N Sello: 660620
+ AVDA. SANTA CLARA 207 62 CIUDAD EMPRESARIAL
+ HUECHURABA
+ SANTIAGO
+
+
+ 194111
+ 19.00
+ 36881
+ 230992
+
+
+
+ 1
+
+ SKU
+ 19586316
+
+ JUEGO_LIVI - CHOCOLATE
+ ROMA 3.1.1
+ 1.00
+ UN
+ 194111.00
+ 194111
+
+
+ 1
+ 801
+ 638370
+ 2019-03-28
+
+76399752-933 | 255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA33 | 25568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+pZgE/rr79kgVqirnIRM+HUpB3Yt4fbyMaARGqtA==
+ 2019-03-28T13:59:52
+
+
+
+
+
+
+
+
+
+
+
+tk/D3mfO/KtdWyFXYZHe7dtYijg=
+
+
+
+wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD
+MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4
+FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1
+GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp
+0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==
+
+
+
+
+
+0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hjuEe9P78f6G5w6
+U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PPDK3JQg8dNy0w0nfaYhD98ZTw
+f5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr
+9N/SU4b8Ib9ue8t25tpxz2jsHlBLokXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/
+SobU35wgtdXK6wYYIIQNN+Zdv8AjisQpom3Rcw==
+
+AQAB
+
+
+
+
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK
+Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj
+YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh
+Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0
+MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT
+GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu
+YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1
+g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP
+HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh
+qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS
+EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G
+A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L
+WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG
++EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v
+YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr
+BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R
+BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt
+OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw
+OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H
+NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz
+ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV
+WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/
+w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB
+B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg
+jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw
+
+
+
+
diff --git a/tests/test_data/sii-rtc/AEC--76354771-K--33---170--SEQ-2.xml b/tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml
similarity index 100%
rename from tests/test_data/sii-rtc/AEC--76354771-K--33---170--SEQ-2.xml
rename to tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml
diff --git a/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml b/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml
new file mode 100644
index 00000000..4c8c579d
--- /dev/null
+++ b/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml
@@ -0,0 +1,322 @@
+
+
+
+
+ 76399752-9
+ 76389992-6
+ fynpal-app-notif-st-capital@fynpal.com
+ 2019-04-04T09:09:52
+
+
+
+
+
+
+
+
+
+
+ 33
+ 25568
+ 2019-03-29
+ 1
+ 1
+ 2
+
+
+ 76399752-9
+ COMERCIALIZADORA INNOVA MOBEL SPA
+ COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR
+ 87 472133
+ ANGEL.PEZO@APCASESORIAS.CL
+ 310001
+ 078904860
+ LOS CIPRESES 2834
+ LA PINTANA
+ SANTIAGO
+
+
+ 96874030-K
+ EMPRESAS LA POLAR S.A.
+ VENTA AL POR MENOR EN COMERCIOS DE VESTU
+ N Lote Despacho: 20921554 / N Sello: 660620
+ AVDA. SANTA CLARA 207 62 CIUDAD EMPRESARIAL
+ HUECHURABA
+ SANTIAGO
+
+
+ 194111
+ 19.00
+ 36881
+ 230992
+
+
+
+ 1
+
+ SKU
+ 19586316
+
+ JUEGO_LIVI - CHOCOLATE
+ ROMA 3.1.1
+ 1.00
+ UN
+ 194111.00
+ 194111
+
+
+ 1
+ 801
+ 638370
+ 2019-03-28
+
+76399752-933 | 255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA33 | 25568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+pZgE/rr79kgVqirnIRM+HUpB3Yt4fbyMaARGqtA==
+ 2019-03-28T13:59:52
+
+
+
+
+
+
+
+
+
+
+
+tk/D3mfO/KtdWyFXYZHe7dtYijg=
+
+
+
+wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD
+MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4
+FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1
+GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp
+0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==
+
+
+
+
+
+0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hjuEe9P78f6G5w6
+U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PPDK3JQg8dNy0w0nfaYhD98ZTw
+f5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr
+9N/SU4b8Ib9ue8t25tpxz2jsHlBLokXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/
+SobU35wgtdXK6wYYIIQNN+Zdv8AjisQpom3Rcw==
+
+AQAB
+
+
+
+
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK
+Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj
+YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh
+Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0
+MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT
+GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu
+YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1
+g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP
+HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh
+qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS
+EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G
+A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L
+WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG
++EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v
+YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr
+BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R
+BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt
+OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw
+OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H
+NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz
+ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV
+WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/
+w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB
+B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg
+jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw
+
+
+
+2019-04-04T09:09:52f4zqbr0mGWngOkRb16XxngcE96o=
+vzMWkBYcBj9ziyTa0Aqkw9a1fePGC4AfOmt/v36OEajaKszI5Fz/E53deCZU+EQS
+h4puWlbiMxq6OiTi3ebrb/Kwlc1ZxmXkKzjIveDmeZhiVI9bR7o4zg3BGXdov+0L
+n+d4nvtLrxA1M78WhSp2IK9KOyxOhazXsTu4iYqoJH+zh3VTwKUncpJTnA6Aytqx
+YC+zIxAty2bfs7DsqP9y7DTjAsS//nNjRJGZ80yjVr8t5l/ystveobLCvPVYZe+f
+ezH8wN+M4lQ9EQvPJG5gAhVkudttJCysXTJkFGgV1sywgH9DxUHOcvwUpfDN6o0x
+OxXwwfwLQRgyPZFBpCjleg==
+
+0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hju
+Ee9P78f6G5w6U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PP
+DK3JQg8dNy0w0nfaYhD98ZTwf5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO
+2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr9N/SU4b8Ib9ue8t25tpxz2jsHlBL
+okXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/SobU35wgtdXK6wYY
+IIQNN+Zdv8AjisQpom3Rcw==
+AQAB
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM
+MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g
+QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg
+LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5
+NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ
+BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ
+QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu
+YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld
+pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo
+ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q
+f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh
+/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a
+h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA
+AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud
+DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw
+FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE
+bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw
+dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw
+NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv
+QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05
+oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h
+cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw
+AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/
+BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny
+bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD
+CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb
+9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J
+gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr
+2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA
+NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf
+zmrw
+
+
+
+
+ 1
+
+ 33
+ 76399752-9
+ 96874030-K
+ 25568
+ 2019-03-29
+ 230992
+
+
+ 76399752-9
+ COMERCIALIZADORA INNOVA MOBEL SPA
+ LOS CIPRESES 2834
+ camilo.perez@innovamobel.cl
+
+ 76399752-9
+ COMERCIALIZADORA INNOVA MOBEL SPA
+
+ Se declara bajo juramento que COMERCIALIZADORA INNOVA MOBEL SPA, RUT 76399752-9 ha puesto a disposici髇 del cesionario ST CAPITAL S.A., RUT 76389992-6, el o los documentos donde constan los recibos de las mercader韆s entregadas o servicios prestados, entregados por parte del deudor de la factura EMPRESAS LA POLAR S.A., RUT 96874030-K, deacuerdo a lo establecido en la Ley N19.983.
+
+
+ 76389992-6
+ ST CAPITAL S.A.
+ Isidora Goyenechea 2939 Oficina 602
+ fynpal-app-notif-st-capital@fynpal.com
+
+ 230992
+ 2019-04-28
+ 2019-04-04T09:09:52
+ qvEd+eB0/sEOf1o/7FpHckvpcLg=
+V+K6hraGHgDnk9LJoEC/A2jOPgfnb7akYxZ8n6TlKMxf+1hHb9njiaiJOfyp1owV
+/zQHd74CXlFy9v52pPpwVA9BFbda0J/AZCt8lmT21/WBqDwjrByhsfZ/+kmRxdxg
+ZnfFV/41zHtlo4ifUkyQLinLJHsM5KCjHiKMvQhc9QF/f9V0K9iPV0O8bJcPsYdZ
+H2JvmyWjJBB2Z1wEg7lQLlFmztM5tLGM8Iy58ENYiwfNbkVFx0Kiu/vUSqtNKDlu
+eb+1D4npn9KletXlgp7xFUH8lGJhk0QfjC691z51E0Pf1klcOz0tJChteiBDWSFZ
+SMCiVGE1L4Pv8cUN+CaRSg==
+
+0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hju
+Ee9P78f6G5w6U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PP
+DK3JQg8dNy0w0nfaYhD98ZTwf5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO
+2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr9N/SU4b8Ib9ue8t25tpxz2jsHlBL
+okXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/SobU35wgtdXK6wYY
+IIQNN+Zdv8AjisQpom3Rcw==
+AQAB
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM
+MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g
+QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg
+LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5
+NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ
+BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ
+QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu
+YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld
+pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo
+ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q
+f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh
+/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a
+h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA
+AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud
+DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw
+FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE
+bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw
+dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw
+NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv
+QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05
+oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h
+cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw
+AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/
+BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny
+bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD
+CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb
+9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J
+gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr
+2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA
+NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf
+zmrw
+
+
+
+ KUMkp+Ku3epZ2QCjeZ75tiEncMQ=
+hBUBX/XDhmNokXXfZ7R3drK78N5SX8xLn6sYyAaBTut4wILA4kHB9BW45oV0wS/A
+53l7EX5yg42KHRXQ+vVzc5R+zYpGvgAPnv8eM2lCQKmyEdhR0YoQ1YnRL/7vchJ2
+8TnrTxSMMePj589rOAUD8IeTr1vKyfdih+r6maTA6C+O2dzVf3zl/GtTstoZdX2B
+ZEf6/yzX9T7kFQ27zZ3WKGLFFjQKaQa2Nh/dIPEcfci1KgCZhozGPw9++xPG3P9I
+ewG3h95UvHjL1jOag3grvrEG+yCYlUpMq4vnUTuGfbwcW7nYq+HSU0IKDPccmzlh
+PCUn28yVEm+JlH0/P8QL3w==
+
+0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hju
+Ee9P78f6G5w6U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PP
+DK3JQg8dNy0w0nfaYhD98ZTwf5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO
+2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr9N/SU4b8Ib9ue8t25tpxz2jsHlBL
+okXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/SobU35wgtdXK6wYY
+IIQNN+Zdv8AjisQpom3Rcw==
+AQAB
+MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM
+MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g
+QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg
+LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5
+NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ
+BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ
+QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu
+YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld
+pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo
+ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q
+f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh
+/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a
+h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA
+AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud
+DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw
+FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE
+bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw
+dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t
+IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw
+NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv
+QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05
+oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h
+cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw
+AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/
+BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny
+bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD
+CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb
+9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J
+gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr
+2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA
+NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf
+zmrw
+
+
\ No newline at end of file
diff --git a/tests/test_dte_constants.py b/tests/test_dte_constants.py
new file mode 100644
index 00000000..b680fb48
--- /dev/null
+++ b/tests/test_dte_constants.py
@@ -0,0 +1,128 @@
+import unittest
+
+from cl_sii.dte import constants # noqa: F401
+from cl_sii.dte.constants import TipoDteEnum
+
+
+class TipoDteEnumTest(unittest.TestCase):
+
+ def test_members(self):
+ self.assertSetEqual(
+ {x for x in TipoDteEnum},
+ {
+ TipoDteEnum.FACTURA_ELECTRONICA,
+ TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA,
+ TipoDteEnum.FACTURA_COMPRA_ELECTRONICA,
+ TipoDteEnum.GUIA_DESPACHO_ELECTRONICA,
+ TipoDteEnum.NOTA_DEBITO_ELECTRONICA,
+ TipoDteEnum.NOTA_CREDITO_ELECTRONICA,
+ }
+ )
+
+ def test_FACTURA_ELECTRONICA(self):
+ value = TipoDteEnum.FACTURA_ELECTRONICA
+
+ self.assertEqual(value.name, 'FACTURA_ELECTRONICA')
+ self.assertEqual(value.value, 33)
+
+ assertions = [
+ (value.is_factura, True),
+ (value.is_factura_venta, True),
+ (value.is_factura_compra, False),
+ (value.is_nota, False),
+ (value.emisor_is_vendedor, True),
+ (value.receptor_is_vendedor, False),
+ ]
+
+ for (result, expected) in assertions:
+ self.assertEqual(result, expected)
+
+ def test_FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA(self):
+ value = TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA
+
+ self.assertEqual(value.name, 'FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA')
+ self.assertEqual(value.value, 34)
+
+ assertions = [
+ (value.is_factura, True),
+ (value.is_factura_venta, True),
+ (value.is_factura_compra, False),
+ (value.is_nota, False),
+ (value.emisor_is_vendedor, True),
+ (value.receptor_is_vendedor, False),
+ ]
+
+ for (result, expected) in assertions:
+ self.assertTrue(result is expected)
+
+ def test_FACTURA_COMPRA_ELECTRONICA(self):
+ value = TipoDteEnum.FACTURA_COMPRA_ELECTRONICA
+
+ self.assertEqual(value.name, 'FACTURA_COMPRA_ELECTRONICA')
+ self.assertEqual(value.value, 46)
+
+ assertions = [
+ (value.is_factura, True),
+ (value.is_factura_venta, False),
+ (value.is_factura_compra, True),
+ (value.is_nota, False),
+ (value.emisor_is_vendedor, False),
+ (value.receptor_is_vendedor, True),
+ ]
+
+ for (result, expected) in assertions:
+ self.assertTrue(result is expected)
+
+ def test_GUIA_DESPACHO_ELECTRONICA(self):
+ value = TipoDteEnum.GUIA_DESPACHO_ELECTRONICA
+
+ self.assertEqual(value.name, 'GUIA_DESPACHO_ELECTRONICA')
+ self.assertEqual(value.value, 52)
+
+ assertions = [
+ (value.is_factura, False),
+ (value.is_factura_venta, False),
+ (value.is_factura_compra, False),
+ (value.is_nota, False),
+ (value.emisor_is_vendedor, False),
+ (value.receptor_is_vendedor, False),
+ ]
+
+ for (result, expected) in assertions:
+ self.assertTrue(result is expected)
+
+ def test_NOTA_DEBITO_ELECTRONICA(self):
+ value = TipoDteEnum.NOTA_DEBITO_ELECTRONICA
+
+ self.assertEqual(value.name, 'NOTA_DEBITO_ELECTRONICA')
+ self.assertEqual(value.value, 56)
+
+ assertions = [
+ (value.is_factura, False),
+ (value.is_factura_venta, False),
+ (value.is_factura_compra, False),
+ (value.is_nota, True),
+ (value.emisor_is_vendedor, False),
+ (value.receptor_is_vendedor, False),
+ ]
+
+ for (result, expected) in assertions:
+ self.assertTrue(result is expected)
+
+ def test_NOTA_CREDITO_ELECTRONICA(self):
+ value = TipoDteEnum.NOTA_CREDITO_ELECTRONICA
+
+ self.assertEqual(value.name, 'NOTA_CREDITO_ELECTRONICA')
+ self.assertEqual(value.value, 61)
+
+ assertions = [
+ (value.is_factura, False),
+ (value.is_factura_venta, False),
+ (value.is_factura_compra, False),
+ (value.is_nota, True),
+ (value.emisor_is_vendedor, False),
+ (value.receptor_is_vendedor, False),
+ ]
+
+ for (result, expected) in assertions:
+ self.assertTrue(result is expected)
diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py
index 648c7251..d0d4acad 100644
--- a/tests/test_dte_data_models.py
+++ b/tests/test_dte_data_models.py
@@ -1,4 +1,6 @@
+import dataclasses
import unittest
+from datetime import date, datetime
from cl_sii.rut import Rut # noqa: F401
@@ -11,38 +13,187 @@
class DteNaturalKeyTest(unittest.TestCase):
- # TODO: implement!
- pass
+ def setUp(self) -> None:
+ super().setUp()
+
+ self.dte_nk_1 = DteNaturalKey(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ )
+
+ def test_init_fail(self) -> None:
+ # TODO: implement for 'DteNaturalKey()'
+ pass
+
+ def test_as_dict(self) -> None:
+ self.assertDictEqual(
+ self.dte_nk_1.as_dict(),
+ dict(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ )
+ )
+
+ def test_slug(self) -> None:
+ self.assertEqual(self.dte_nk_1.slug, '76354771-K--33--170')
class DteDataL0Test(unittest.TestCase):
- # TODO: implement!
- pass
+ def setUp(self) -> None:
+ super().setUp()
+
+ self.dte_l0_1 = DteDataL0(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ )
+
+ def test_init_fail(self) -> None:
+ # TODO: implement for 'DteDataL0()'
+ pass
+
+ def test_as_dict(self) -> None:
+ self.assertDictEqual(
+ self.dte_l0_1.as_dict(),
+ dict(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ ))
+
+ def test_natural_key(self) -> None:
+ self.assertEqual(
+ self.dte_l0_1.natural_key,
+ DteNaturalKey(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ ))
class DteDataL1Test(unittest.TestCase):
- # TODO: implement!
- pass
+ def setUp(self) -> None:
+ super().setUp()
+
+ self.dte_l1_1 = DteDataL1(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ fecha_emision_date=date(2019, 4, 1),
+ receptor_rut=Rut('96790240-3'),
+ monto_total=2996301,
+ )
+
+ def test_init_fail(self) -> None:
+ # TODO: implement for 'DteDataL1()'
+ pass
+
+ def test_as_dict(self) -> None:
+ self.assertDictEqual(
+ self.dte_l1_1.as_dict(),
+ dict(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ fecha_emision_date=date(2019, 4, 1),
+ receptor_rut=Rut('96790240-3'),
+ monto_total=2996301,
+ ))
+
+ def test_vendedor_rut_deudor_rut(self) -> None:
+ emisor_rut = self.dte_l1_1.emisor_rut
+ receptor_rut = self.dte_l1_1.receptor_rut
+ dte_factura_venta = dataclasses.replace(
+ self.dte_l1_1, tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA)
+ dte_factura_venta_exenta = dataclasses.replace(
+ self.dte_l1_1, tipo_dte=TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA)
+ dte_factura_compra = dataclasses.replace(
+ self.dte_l1_1, tipo_dte=TipoDteEnum.FACTURA_COMPRA_ELECTRONICA)
+ dte_nota_credito = dataclasses.replace(
+ self.dte_l1_1, tipo_dte=TipoDteEnum.NOTA_CREDITO_ELECTRONICA)
+
+ self.assertEqual(dte_factura_venta.vendedor_rut, emisor_rut)
+ self.assertEqual(dte_factura_venta_exenta.vendedor_rut, emisor_rut)
+ self.assertEqual(dte_factura_compra.vendedor_rut, receptor_rut)
+ with self.assertRaises(ValueError) as cm:
+ self.assertIsNone(dte_nota_credito.vendedor_rut)
+ self.assertEqual(
+ cm.exception.args,
+ ("Concept \"vendedor\" does not apply for this 'tipo_dte'.", dte_nota_credito.tipo_dte))
+
+ self.assertEqual(dte_factura_venta.deudor_rut, receptor_rut)
+ self.assertEqual(dte_factura_venta_exenta.deudor_rut, receptor_rut)
+ self.assertEqual(dte_factura_compra.deudor_rut, emisor_rut)
+ with self.assertRaises(ValueError) as cm:
+ self.assertIsNone(dte_nota_credito.deudor_rut)
+ self.assertEqual(
+ cm.exception.args,
+ ("Concept \"deudor\" does not apply for this 'tipo_dte'.", dte_nota_credito.tipo_dte))
class DteDataL2Test(unittest.TestCase):
- # TODO: implement!
- pass
+ def setUp(self) -> None:
+ super().setUp()
+
+ self.dte_l2_1 = DteDataL2(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ fecha_emision_date=date(2019, 4, 1),
+ receptor_rut=Rut('96790240-3'),
+ monto_total=2996301,
+ emisor_razon_social='INGENIERIA ENACON SPA',
+ receptor_razon_social='MINERA LOS PELAMBRES',
+ fecha_vencimiento_date=None,
+ firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40),
+ signature_value=None,
+ signature_x509_cert_pem=None,
+ emisor_giro='Ingenieria y Construccion',
+ emisor_email='hello@example.com',
+ receptor_email=None,
+ )
+
+ def test_init_fail(self) -> None:
+ # TODO: implement for 'DteDataL2()'
+ pass
+
+ def test_as_dict(self) -> None:
+ self.assertDictEqual(
+ self.dte_l2_1.as_dict(),
+ dict(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ fecha_emision_date=date(2019, 4, 1),
+ receptor_rut=Rut('96790240-3'),
+ monto_total=2996301,
+ emisor_razon_social='INGENIERIA ENACON SPA',
+ receptor_razon_social='MINERA LOS PELAMBRES',
+ fecha_vencimiento_date=None,
+ firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40),
+ signature_value=None,
+ signature_x509_cert_pem=None,
+ emisor_giro='Ingenieria y Construccion',
+ emisor_email='hello@example.com',
+ receptor_email=None,
+ ))
class FunctionsTest(unittest.TestCase):
def test_validate_contribuyente_razon_social(self) -> None:
- # TODO: implement!
+ # TODO: implement for 'validate_contribuyente_razon_social'
pass
def test_validate_dte_folio(self) -> None:
- # TODO: implement!
+ # TODO: implement for 'validate_dte_folio'
pass
def test_validate_dte_monto_total(self) -> None:
- # TODO: implement!
+ # TODO: implement for 'validate_dte_monto_total'
pass
diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py
index fedb651b..1944f17d 100644
--- a/tests/test_dte_parse.py
+++ b/tests/test_dte_parse.py
@@ -1,9 +1,11 @@
import difflib
import io
import unittest
-from datetime import date
+from datetime import date, datetime
import cl_sii.dte.constants
+from cl_sii.libs import crypto_utils
+from cl_sii.libs import encoding_utils
from cl_sii.libs import xml_utils
from cl_sii.rut import Rut
@@ -16,21 +18,95 @@
from .utils import read_test_file_bytes
-_TEST_DTE_NEEDS_CLEAN_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml'
-
-
class OthersTest(unittest.TestCase):
def test_DTE_XML_SCHEMA_OBJ(self) -> None:
# TODO: implement
pass
- def test_integration_ok(self) -> None:
- # TODO: split in separate tests, with more coverage.
- dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_FILE_PATH
+class FunctionValidateDteXmlTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+
+ cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76354771-K--33--170.xml')
+ cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76399752-9--33--25568.xml')
+
+ cls.dte_clean_xml_1_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml')
+ cls.dte_clean_xml_2_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml')
+
+ def test_validate_dte_xml_ok_dte_1(self) -> None:
+ xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes)
+ validate_dte_xml(xml_doc)
+
+ self.assertEqual(
+ xml_doc.getroottree().getroot().tag,
+ '{%s}DTE' % DTE_XMLNS)
+
+ def test_validate_dte_xml_ok_dte_2(self) -> None:
+ xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes)
+ validate_dte_xml(xml_doc)
+
+ self.assertEqual(
+ xml_doc.getroottree().getroot().tag,
+ '{%s}DTE' % DTE_XMLNS)
+
+ def test_validate_dte_xml_fail_x(self) -> None:
+ # TODO: implement more cases
+ pass
+
+ def test_validate_dte_xml_fail_dte_1(self) -> None:
+ file_bytes = self.dte_bad_xml_1_xml_bytes
+ xml_doc = xml_utils.parse_untrusted_xml(file_bytes)
+
+ self.assertEqual(
+ xml_doc.getroottree().getroot().tag,
+ 'DTE')
+
+ with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm:
+ validate_dte_xml(xml_doc)
+ self.assertSequenceEqual(
+ cm.exception.args,
+ ("Element 'DTE': No matching global declaration available for the validation root., "
+ "line 2", )
+ )
+
+ def test_validate_dte_xml_fail_dte_2(self) -> None:
+ file_bytes = self.dte_bad_xml_2_xml_bytes
+ xml_doc = xml_utils.parse_untrusted_xml(file_bytes)
+
+ self.assertEqual(
+ xml_doc.getroottree().getroot().tag,
+ 'DTE')
+
+ with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm:
+ validate_dte_xml(xml_doc)
+ self.assertSequenceEqual(
+ cm.exception.args,
+ ("Element 'DTE': No matching global declaration available for the validation root., "
+ "line 2", )
+ )
+
+
+class FunctionCleanDteXmlTest(unittest.TestCase):
- file_bytes = read_test_file_bytes(dte_bad_xml_file_path)
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+
+ cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76354771-K--33--170.xml')
+ cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76399752-9--33--25568.xml')
+
+ def test_clean_dte_xml_ok_1(self) -> None:
+ file_bytes = self.dte_bad_xml_1_xml_bytes
xml_doc = xml_utils.parse_untrusted_xml(file_bytes)
self.assertEqual(
@@ -44,8 +120,6 @@ def test_integration_ok(self) -> None:
("Element 'DTE': No matching global declaration available for the validation root., "
"line 2", )
)
- # This would raise:
- # parse_dte_xml(xml_doc)
xml_doc_cleaned, modified = clean_dte_xml(
xml_doc,
@@ -57,10 +131,6 @@ def test_integration_ok(self) -> None:
# This will not raise.
validate_dte_xml(xml_doc_cleaned)
- self.assertEqual(
- xml_doc_cleaned.getroottree().getroot().tag,
- '{%s}DTE' % DTE_XMLNS)
-
f = io.BytesIO()
xml_utils.write_xml_doc(xml_doc_cleaned, f)
file_bytes_rewritten = f.getvalue()
@@ -68,21 +138,6 @@ def test_integration_ok(self) -> None:
xml_doc_rewritten = xml_utils.parse_untrusted_xml(file_bytes_rewritten)
validate_dte_xml(xml_doc_rewritten)
- parsed_dte_rewritten = parse_dte_xml(xml_doc_cleaned)
-
- self.assertDictEqual(
- dict(parsed_dte_rewritten.as_dict()),
- dict(
- emisor_rut=Rut('76354771-K'),
- tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA,
- folio=170,
- fecha_emision_date=date(2019, 4, 1),
- receptor_rut=Rut('96790240-3'),
- monto_total=2996301,
- emisor_razon_social='INGENIERIA ENACON SPA',
- receptor_razon_social='MINERA LOS PELAMBRES',
- fecha_vencimiento_date=None,
- ))
expected_file_bytes_diff = (
b'--- \n',
@@ -124,41 +179,220 @@ def test_integration_ok(self) -> None:
expected_file_bytes_diff
)
+ def test_clean_dte_xml_ok_2(self) -> None:
+ file_bytes = self.dte_bad_xml_2_xml_bytes
+ xml_doc = xml_utils.parse_untrusted_xml(file_bytes)
-class FunctionCleanDteXmlTest(unittest.TestCase):
+ self.assertEqual(
+ xml_doc.getroottree().getroot().tag,
+ 'DTE')
- def test_clean_dte_xml_ok(self) -> None:
- # TODO: implement
- pass
+ with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm:
+ validate_dte_xml(xml_doc)
+ self.assertSequenceEqual(
+ cm.exception.args,
+ ("Element 'DTE': No matching global declaration available for the validation root., "
+ "line 2", )
+ )
+
+ xml_doc_cleaned, modified = clean_dte_xml(
+ xml_doc,
+ set_missing_xmlns=True,
+ remove_doc_personalizado=True,
+ )
+ self.assertTrue(modified)
+
+ # This will not raise.
+ validate_dte_xml(xml_doc_cleaned)
+
+ f = io.BytesIO()
+ xml_utils.write_xml_doc(xml_doc_cleaned, f)
+ file_bytes_rewritten = f.getvalue()
+ del f
+
+ xml_doc_rewritten = xml_utils.parse_untrusted_xml(file_bytes_rewritten)
+ validate_dte_xml(xml_doc_rewritten)
+
+ expected_file_bytes_diff = (
+ b'--- \n',
+ b'+++ \n',
+ b'@@ -1,5 +1,5 @@\n',
+ b'-',
+ b'-',
+ b"+",
+ b'+',
+ b' ',
+ b' ',
+ b' ',
+ b'@@ -64,13 +64,13 @@\n',
+ b' ',
+ b' ',
+ b' ',
+ b'-', # noqa: E501
+ b'-',
+ b'+', # noqa: E501
+ b'+',
+ b' ',
+ b' ',
+ b'-',
+ b'+',
+ b' ',
+ b'-',
+ b'+',
+ b' tk/D3mfO/KtdWyFXYZHe7dtYijg=',
+ b' ',
+ b' ',
+ )
+
+ file_bytes_diff_gen = difflib.diff_bytes(
+ dfunc=difflib.unified_diff,
+ a=file_bytes.splitlines(),
+ b=file_bytes_rewritten.splitlines())
+ self.assertSequenceEqual(
+ [diff_line for diff_line in file_bytes_diff_gen],
+ expected_file_bytes_diff
+ )
def test_clean_dte_xml_fail(self) -> None:
- # TODO: implement
+ # TODO: implement for 'clean_dte_xml', for many cases.
pass
def test__set_dte_xml_missing_xmlns_ok(self) -> None:
- # TODO: implement
+ # TODO: implement for '_set_dte_xml_missing_xmlns'.
pass
def test__set_dte_xml_missing_xmlns_fail(self) -> None:
- # TODO: implement
+ # TODO: implement for '_set_dte_xml_missing_xmlns'.
pass
def test__remove_dte_xml_doc_personalizado_ok(self) -> None:
- # TODO: implement
+ # TODO: implement for '_remove_dte_xml_doc_personalizado'.
pass
def test__remove_dte_xml_doc_personalizado_fail(self) -> None:
- # TODO: implement
+ # TODO: implement for '_remove_dte_xml_doc_personalizado'.
pass
class FunctionParseDteXmlTest(unittest.TestCase):
- # TODO: implement
- pass
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+
+ cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76354771-K--33--170.xml')
+ cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76399752-9--33--25568.xml')
+
+ cls.dte_clean_xml_1_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml')
+ cls.dte_clean_xml_2_xml_bytes = read_test_file_bytes(
+ 'test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml')
+
+ cls.dte_clean_xml_1_cert_pem_bytes = encoding_utils.clean_base64(
+ crypto_utils.remove_pem_cert_header_footer(
+ read_test_file_bytes('test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem')))
+ cls.dte_clean_xml_2_cert_pem_bytes = encoding_utils.clean_base64(
+ crypto_utils.remove_pem_cert_header_footer(
+ read_test_file_bytes('test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem')))
+
+ cls._TEST_DTE_1_SIGNATURE_VALUE = encoding_utils.decode_base64_strict(
+ read_test_file_bytes(
+ 'test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt'))
+ cls._TEST_DTE_2_SIGNATURE_VALUE = encoding_utils.decode_base64_strict(
+ read_test_file_bytes(
+ 'test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt'))
+
+ def test_data(self):
+ self.assertEqual(
+ self._TEST_DTE_1_SIGNATURE_VALUE,
+ b'~\xc6\x0f\xe6\x9f\xe55\xfa\x1f\x03?\x0f9(k&:\x97t\x14\xcd6\xdb\xef\xe3\xf4\xd6'
+ b'\x0b\x16\xef\xc12\x00^\xbe\xc1.\xb9o_p\xbf\x1e\x97\xe8\xe2\xf8\xaak\x14\xae\xd4'
+ b'\xe1\x85\x80\xf9\xe4u\xd0\xc8\x17\x08\xfff\xc5]m\xd0~2\x1aJ\x93(Z\xf3tq\x84\x9a'
+ b'X\x05PX\xdd\xcf\xb2\xf4\x9e\xa81\xf7Ht\xc0\x18^\x11$\x17\x0f0\xebr\x87\xca\x17_'
+ b'\xd8O]\x9d\xb2\xa2\xc2\xa4\xb1\r\xc6#M>\xaf^\xc2\xcf\xad\x99')
+ self.assertEqual(
+ self._TEST_DTE_2_SIGNATURE_VALUE,
+ b"\xc3\x03\x8cB\xe1jk\xa79\x836\x12'\x93\xd6~\x8d\x0e\x88\x07\xfe\xc8\xd7\t+\xac1"
+ b"\xe3\x12wT\xd7\xa1\x94\x07\x0c\xad@v\xab\xea\xca\xfc0\xe1\n\xf8\xf1\x02\x85\xf87"
+ b"\x10@\n!C2\xb4\xcc!\xfb\xf1\xaf\x05\xe3-\xddh_~\xe5HS\xad\xb1e\xfa'\x12\xf4I_"
+ b"\xad,^\xea\xbd`\xae\xd5\x8c\xf0\xf3*T\x7f\xb1\x83\x91\xbc\xa6&J\x13`1}$Q|\x91"
+ b"\x858\x15\xd56\xc2\xdbb\xc2w\xbd\\\x7f\x10\x14x\x17\xb2\x87\n?\xf6\xb2\x83\x08"
+ b"\x8exj\xa25\x7fh\xfc\xeeWKa\x96\xa6\xdf\x8a3m#\xae\xfbX\x9f\xb5\x050@\xff\xdd"
+ b"\xa3\xdap&\xf5\x18\x00b\xce\xbf&\x95Q\x19\x82\x06`&\xe9\xcc\x0c(i\x15\x0c\x84"
+ b"\x8a\x04\x970\xaeH\xef~\xc0s\xc0\xf6o\x0e\xd6\x07\x8e\xd6\x8fU\x81/{\x02\x15\x10"
+ b"\xe5]E\xed\x9c\xcb\xc2\x84\x15i\xd0tT]\x8b\x8a\x1f'\xe9\x0b:\x88\x05|\xa0b\xb2"
+ b"\x19{\x1cW\x80\xe4\xa7*\xef\xf2\x1a")
+
+ def test_parse_dte_xml_ok_1(self) -> None:
+ xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes)
+
+ parsed_dte = parse_dte_xml(xml_doc)
+ self.assertDictEqual(
+ dict(parsed_dte.as_dict()),
+ dict(
+ emisor_rut=Rut('76354771-K'),
+ tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=170,
+ fecha_emision_date=date(2019, 4, 1),
+ receptor_rut=Rut('96790240-3'),
+ monto_total=2996301,
+ emisor_razon_social='INGENIERIA ENACON SPA',
+ receptor_razon_social='MINERA LOS PELAMBRES',
+ fecha_vencimiento_date=None,
+ firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40),
+ signature_value=self._TEST_DTE_1_SIGNATURE_VALUE,
+ signature_x509_cert_pem=self.dte_clean_xml_1_cert_pem_bytes,
+ emisor_giro='Ingenieria y Construccion',
+ emisor_email='ENACONLTDA@GMAIL.COM',
+ receptor_email=None,
+ ))
+
+ def test_parse_dte_xml_ok_2(self) -> None:
+ xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes)
+ parsed_dte = parse_dte_xml(xml_doc)
+ self.assertDictEqual(
+ dict(parsed_dte.as_dict()),
+ dict(
+ emisor_rut=Rut('76399752-9'),
+ tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA,
+ folio=25568,
+ fecha_emision_date=date(2019, 3, 29),
+ receptor_rut=Rut('96874030-K'),
+ monto_total=230992,
+ emisor_razon_social='COMERCIALIZADORA INNOVA MOBEL SPA',
+ receptor_razon_social='EMPRESAS LA POLAR S.A.',
+ fecha_vencimiento_date=None,
+ firma_documento_dt_naive=datetime(2019, 3, 28, 13, 59, 52),
+ signature_value=self._TEST_DTE_2_SIGNATURE_VALUE,
+ signature_x509_cert_pem=self.dte_clean_xml_2_cert_pem_bytes,
+ emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR',
+ emisor_email='ANGEL.PEZO@APCASESORIAS.CL',
+ receptor_email=None,
+ ))
-class FunctionValidateDteXmlTest(unittest.TestCase):
+ def test_parse_dte_xml_fail_x(self) -> None:
+ # TODO: implement more cases
+ pass
+
+ def test_parse_dte_xml_fail_1(self) -> None:
+ xml_doc = xml_utils.parse_untrusted_xml(self.dte_bad_xml_1_xml_bytes)
+
+ with self.assertRaises(ValueError) as cm:
+ parse_dte_xml(xml_doc)
+ self.assertSequenceEqual(
+ cm.exception.args,
+ ("Top level XML element 'Document' is required.", )
+ )
+
+ def test_parse_dte_xml_fail_2(self) -> None:
+ xml_doc = xml_utils.parse_untrusted_xml(self.dte_bad_xml_2_xml_bytes)
- # TODO: implement
- pass
+ with self.assertRaises(ValueError) as cm:
+ parse_dte_xml(xml_doc)
+ self.assertSequenceEqual(
+ cm.exception.args,
+ ("Top level XML element 'Document' is required.", )
+ )
diff --git a/tests/test_libs_crypto_utils.py b/tests/test_libs_crypto_utils.py
new file mode 100644
index 00000000..4fe317f7
--- /dev/null
+++ b/tests/test_libs_crypto_utils.py
@@ -0,0 +1,644 @@
+import unittest
+from datetime import datetime
+
+import cryptography.hazmat.primitives.hashes
+import cryptography.x509
+from cryptography.x509 import oid
+
+from cl_sii.libs.crypto_utils import ( # noqa: F401
+ X509Cert, add_pem_cert_header_footer, load_pem_x509_cert, remove_pem_cert_header_footer,
+)
+
+from . import utils
+
+# TODO: get fake certificates, keys, and all the variations from
+# https://github.com/urllib3/urllib3/tree/1.24.2/dummyserver/certs
+
+# TODO: move me into 'cl_sii/crypto/constants.py'
+# - Organismo: MINISTERIO DE ECONOM脥A / SUBSECRETARIA DE ECONOMIA
+# - Decreto 181 (Julio-Agosto 2002)
+# "APRUEBA REGLAMENTO DE LA LEY 19.799 SOBRE DOCUMENTOS ELECTRONICOS, FIRMA ELECTRONICA
+# Y LA CERTIFICACION DE DICHA FIRMA"
+# - ref: https://www.leychile.cl/Consulta/m/norma_plana?org=&idNorma=201668
+# dice:
+# > RUT del titular del certificado : 1.3.6.1.4.1.8321.1
+# > RUT de la certificadora emisora : 1.3.6.1.4.1.8321.2
+_SII_CERT_CERTIFICADORA_EMISORA_RUT_OID = oid.ObjectIdentifier("1.3.6.1.4.1.8321.2")
+_SII_CERT_TITULAR_RUT_OID = oid.ObjectIdentifier("1.3.6.1.4.1.8321.1")
+
+
+class FunctionsTest(unittest.TestCase):
+
+ def test_add_pem_cert_header_footer(self) -> None:
+ # TODO: implement for function 'add_pem_cert_header_footer'.
+ pass
+
+ def test_remove_pem_cert_header_footer(self) -> None:
+ # TODO: implement for function 'remove_pem_cert_header_footer'.
+ pass
+
+
+class LoadPemX509CertTest(unittest.TestCase):
+
+ def test_load_pem_x509_cert_ok(self) -> None:
+ cert_pem_bytes = utils.read_test_file_bytes(
+ 'test_data/crypto/wildcard-google-com-cert.pem')
+
+ x509_cert = load_pem_x509_cert(cert_pem_bytes)
+
+ self.assertIsInstance(x509_cert, X509Cert)
+
+ #######################################################################
+ # main properties
+ #######################################################################
+
+ self.assertEqual(
+ x509_cert.version,
+ cryptography.x509.Version.v3)
+ self.assertIsInstance(
+ x509_cert.signature_hash_algorithm,
+ cryptography.hazmat.primitives.hashes.SHA256)
+ self.assertEqual(
+ x509_cert.signature_algorithm_oid,
+ oid.SignatureAlgorithmOID.RSA_WITH_SHA256)
+
+ self.assertEqual(
+ x509_cert.serial_number,
+ 122617997729991213273569581938043448870)
+ self.assertEqual(
+ x509_cert.not_valid_after,
+ datetime(2019, 6, 18, 13, 24))
+ self.assertEqual(
+ x509_cert.not_valid_before,
+ datetime(2019, 3, 26, 13, 40, 40))
+
+ #######################################################################
+ # issuer
+ #######################################################################
+
+ self.assertEqual(len(x509_cert.issuer.rdns), 3)
+ self.assertEqual(
+ x509_cert.issuer.rfc4514_string(),
+ 'C=US,'
+ 'O=Google Trust Services,'
+ 'CN=Google Internet Authority G3')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value,
+ 'US')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value,
+ 'Google Trust Services')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value,
+ 'Google Internet Authority G3')
+
+ #######################################################################
+ # subject
+ #######################################################################
+
+ self.assertEqual(len(x509_cert.subject.rdns), 5)
+ self.assertEqual(
+ x509_cert.subject.rfc4514_string(),
+ 'C=US,'
+ 'ST=California,'
+ 'L=Mountain View,'
+ 'O=Google LLC,'
+ 'CN=*.google.com')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value,
+ 'US')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value,
+ 'California')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value,
+ 'Mountain View')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value,
+ 'Google LLC')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value,
+ '*.google.com')
+
+ #######################################################################
+ # extensions
+ #######################################################################
+
+ cert_extensions = x509_cert.extensions
+ self.assertEqual(len(cert_extensions._extensions), 9)
+
+ # BASIC_CONSTRAINTS
+ basic_constraints_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.BasicConstraints)
+ self.assertEqual(basic_constraints_ext.critical, True)
+ self.assertEqual(basic_constraints_ext.value.ca, False)
+ self.assertIs(basic_constraints_ext.value.path_length, None)
+
+ # KEY_USAGE
+ key_usage_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.KeyUsage)
+ self.assertEqual(key_usage_ext.critical, True)
+ self.assertEqual(key_usage_ext.value.content_commitment, False)
+ self.assertEqual(key_usage_ext.value.crl_sign, False)
+ self.assertEqual(key_usage_ext.value.data_encipherment, False)
+ self.assertEqual(key_usage_ext.value.digital_signature, True)
+ self.assertEqual(key_usage_ext.value.key_agreement, False)
+ self.assertEqual(key_usage_ext.value.key_cert_sign, False)
+ self.assertEqual(key_usage_ext.value.key_encipherment, False)
+
+ # EXTENDED_KEY_USAGE
+ extended_key_usage_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.ExtendedKeyUsage)
+ self.assertEqual(extended_key_usage_ext.critical, False)
+ self.assertEqual(
+ extended_key_usage_ext.value._usages,
+ [oid.ExtendedKeyUsageOID.SERVER_AUTH])
+
+ # SUBJECT_ALTERNATIVE_NAME
+ subject_alt_name_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.SubjectAlternativeName)
+ self.assertEqual(subject_alt_name_ext.critical, False)
+ self.assertEqual(len(subject_alt_name_ext.value._general_names._general_names), 67)
+ self.assertEqual(
+ subject_alt_name_ext.value._general_names._general_names[0].value,
+ '*.google.com')
+
+ # AUTHORITY_INFORMATION_ACCESS
+ authority_information_access_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.AuthorityInformationAccess)
+ self.assertEqual(authority_information_access_ext.critical, False)
+ self.assertEqual(len(authority_information_access_ext.value._descriptions), 2)
+
+ # SUBJECT_KEY_IDENTIFIER
+ subject_key_identifier_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.SubjectKeyIdentifier)
+ self.assertEqual(subject_key_identifier_ext.critical, False)
+ self.assertEqual(
+ subject_key_identifier_ext.value.digest,
+ b'\xcf\x02\xda\x1aM\x80\x92\xff\x04E\xff\xcb7\x81\xe3O\x1d\x85\xb6\xb6')
+
+ # AUTHORITY_KEY_IDENTIFIER
+ authority_key_identifier_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.AuthorityKeyIdentifier)
+ self.assertEqual(authority_key_identifier_ext.critical, False)
+ self.assertIs(authority_key_identifier_ext.value.authority_cert_issuer, None)
+ self.assertIs(authority_key_identifier_ext.value.authority_cert_serial_number, None)
+ self.assertEqual(
+ authority_key_identifier_ext.value.key_identifier,
+ b'w\xc2\xb8P\x9agvv\xb1-\xc2\x86\xd0\x83\xa0~\xa6~\xbaK'
+ )
+
+ # CERTIFICATE_POLICIES
+ certificate_policies_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.CertificatePolicies)
+ self.assertEqual(certificate_policies_ext.critical, False)
+ self.assertSetEqual(
+ {policy_info.policy_identifier.dotted_string for policy_info in
+ certificate_policies_ext.value._policies},
+ {
+ # 'Google Trust Services'
+ # https://github.com/zmap/constants/blob/0816f6f/x509/certificate_policies.csv#L34
+ '1.3.6.1.4.1.11129.2.5.3',
+ # 'CA/B Forum Organization Validated'
+ # https://github.com/zmap/constants/blob/0816f6f/x509/certificate_policies.csv#L193
+ '2.23.140.1.2.2',
+ }
+ )
+
+ # CRL_DISTRIBUTION_POINTS
+ crl_distribution_points_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.CRLDistributionPoints)
+ self.assertEqual(crl_distribution_points_ext.critical, False)
+ self.assertEqual(len(crl_distribution_points_ext.value._distribution_points), 1)
+ self.assertEqual(
+ crl_distribution_points_ext.value._distribution_points[0].full_name[0].value,
+ 'http://crl.pki.goog/GTSGIAG3.crl')
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].crl_issuer, None)
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None)
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None)
+
+ def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None:
+ cert_pem_bytes = utils.read_test_file_bytes(
+ 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem')
+
+ x509_cert = load_pem_x509_cert(cert_pem_bytes)
+
+ self.assertIsInstance(x509_cert, X509Cert)
+
+ #######################################################################
+ # main properties
+ #######################################################################
+
+ self.assertEqual(
+ x509_cert.version,
+ cryptography.x509.Version.v3)
+ self.assertIsInstance(
+ x509_cert.signature_hash_algorithm,
+ cryptography.hazmat.primitives.hashes.SHA1)
+ self.assertEqual(
+ x509_cert.signature_algorithm_oid,
+ oid.SignatureAlgorithmOID.RSA_WITH_SHA1)
+
+ self.assertEqual(
+ x509_cert.serial_number,
+ 232680798042554446173213)
+ self.assertEqual(
+ x509_cert.not_valid_after,
+ datetime(2020, 9, 3, 21, 11, 12))
+ self.assertEqual(
+ x509_cert.not_valid_before,
+ datetime(2017, 9, 4, 21, 11, 12))
+
+ #######################################################################
+ # issuer
+ #######################################################################
+
+ self.assertEqual(len(x509_cert.issuer.rdns), 7)
+ self.assertEqual(
+ x509_cert.issuer.rfc4514_string(),
+ 'C=CL,ST=Region Metropolitana,'
+ 'L=Santiago,'
+ 'O=E-CERTCHILE,'
+ 'OU=Autoridad Certificadora,'
+ 'CN=E-CERTCHILE CA FIRMA ELECTRONICA SIMPLE,'
+ '1.2.840.113549.1.9.1=sclientes@e-certchile.cl')
+
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value,
+ 'CL')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value,
+ 'Region Metropolitana')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value,
+ 'Santiago')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value,
+ 'E-CERTCHILE')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value,
+ 'Autoridad Certificadora')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value,
+ 'E-CERTCHILE CA FIRMA ELECTRONICA SIMPLE')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.EMAIL_ADDRESS)[0].value,
+ 'sclientes@e-certchile.cl')
+
+ #######################################################################
+ # subject
+ #######################################################################
+
+ self.assertEqual(len(x509_cert.subject.rdns), 7)
+ self.assertEqual(
+ x509_cert.subject.rfc4514_string(),
+ 'C=CL,'
+ 'ST=VALPARAISO\\ ,'
+ 'L=Quillota,'
+ 'O=Servicios Bonilla y Lopez y Cia. Ltda.,'
+ 'OU=Ingenier铆a y Construcci贸n,'
+ 'CN=Ramon humberto Lopez Jara,'
+ '1.2.840.113549.1.9.1=enaconltda@gmail.com')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value,
+ 'CL')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value,
+ 'VALPARAISO ')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value,
+ 'Quillota')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value,
+ 'Servicios Bonilla y Lopez y Cia. Ltda.')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value,
+ 'Ingenier铆a y Construcci贸n')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value,
+ 'Ramon humberto Lopez Jara')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.EMAIL_ADDRESS)[0].value,
+ 'enaconltda@gmail.com')
+
+ #######################################################################
+ # extensions
+ #######################################################################
+
+ cert_extensions = x509_cert.extensions
+ self.assertEqual(len(cert_extensions._extensions), 9)
+
+ # KEY_USAGE
+ key_usage_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.KeyUsage)
+ self.assertEqual(key_usage_ext.critical, False)
+ self.assertEqual(key_usage_ext.value.content_commitment, True)
+ self.assertEqual(key_usage_ext.value.crl_sign, False)
+ self.assertEqual(key_usage_ext.value.data_encipherment, True)
+ self.assertEqual(key_usage_ext.value.digital_signature, True)
+ self.assertEqual(key_usage_ext.value.key_agreement, False)
+ self.assertEqual(key_usage_ext.value.key_cert_sign, False)
+ self.assertEqual(key_usage_ext.value.key_encipherment, True)
+
+ # ISSUER_ALTERNATIVE_NAME
+ issuer_alt_name_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.IssuerAlternativeName)
+ self.assertEqual(issuer_alt_name_ext.critical, False)
+ self.assertEqual(len(issuer_alt_name_ext.value._general_names._general_names), 1)
+ self.assertEqual(
+ issuer_alt_name_ext.value._general_names._general_names[0].type_id,
+ _SII_CERT_CERTIFICADORA_EMISORA_RUT_OID)
+ self.assertEqual(
+ issuer_alt_name_ext.value._general_names._general_names[0].value,
+ b'\x16\n96928180-5')
+
+ # SUBJECT_ALTERNATIVE_NAME
+ subject_alt_name_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.SubjectAlternativeName)
+ self.assertEqual(subject_alt_name_ext.critical, False)
+ self.assertEqual(len(subject_alt_name_ext.value._general_names._general_names), 1)
+ self.assertEqual(
+ subject_alt_name_ext.value._general_names._general_names[0].type_id,
+ _SII_CERT_TITULAR_RUT_OID)
+ self.assertEqual(
+ subject_alt_name_ext.value._general_names._general_names[0].value,
+ b'\x16\n13185095-6')
+
+ # AUTHORITY_INFORMATION_ACCESS
+ authority_information_access_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.AuthorityInformationAccess)
+ self.assertEqual(authority_information_access_ext.critical, False)
+ self.assertEqual(len(authority_information_access_ext.value._descriptions), 1)
+ self.assertEqual(
+ authority_information_access_ext.value._descriptions[0].access_location.value,
+ 'http://ocsp.ecertchile.cl/ocsp')
+ self.assertEqual(
+ authority_information_access_ext.value._descriptions[0].access_method,
+ oid.AuthorityInformationAccessOID.OCSP)
+
+ # SUBJECT_KEY_IDENTIFIER
+ subject_key_identifier_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.SubjectKeyIdentifier)
+ self.assertEqual(subject_key_identifier_ext.critical, False)
+ self.assertEqual(
+ subject_key_identifier_ext.value.digest,
+ b'\xd5\xd5G\x84]\x14U\xee\xd1\\\x8c\xf8r9w\xfdW\xb0\xfa\xaa')
+
+ # AUTHORITY_KEY_IDENTIFIER
+ authority_key_identifier_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.AuthorityKeyIdentifier)
+ self.assertEqual(authority_key_identifier_ext.critical, False)
+ self.assertIs(authority_key_identifier_ext.value.authority_cert_issuer, None)
+ self.assertIs(authority_key_identifier_ext.value.authority_cert_serial_number, None)
+ self.assertEqual(
+ authority_key_identifier_ext.value.key_identifier,
+ b'x\xe1>\x9f\xd2\x12\xb3z<\x8d\xcd0\x0eS\xb3C)\x07\xb3U')
+
+ # CERTIFICATE_POLICIES
+ certificate_policies_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.CertificatePolicies)
+ self.assertEqual(certificate_policies_ext.critical, False)
+ self.assertEqual(len(certificate_policies_ext.value._policies), 1)
+ # TODO: find out where did OID '1.3.6.1.4.1.8658.5' come from.
+ # Perhaps it was '1.3.6.1.4.1.8658'?
+ # https://oidref.com/1.3.6.1.4.1.8658
+ self.assertEqual(
+ certificate_policies_ext.value._policies[0].policy_identifier,
+ oid.ObjectIdentifier("1.3.6.1.4.1.8658.5"))
+ self.assertEqual(len(certificate_policies_ext.value._policies[0].policy_qualifiers), 2)
+ self.assertEqual(
+ certificate_policies_ext.value._policies[0].policy_qualifiers[0],
+ "http://www.e-certchile.cl/CPS.htm")
+ self.assertEqual(
+ certificate_policies_ext.value._policies[0].policy_qualifiers[1].explicit_text,
+ "Certificado Firma Simple. Ha sido validado en forma presencial, quedando habilitado "
+ "el Certificado para uso tributario")
+
+ # CRL_DISTRIBUTION_POINTS
+ crl_distribution_points_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.CRLDistributionPoints)
+ self.assertEqual(crl_distribution_points_ext.critical, False)
+ self.assertEqual(len(crl_distribution_points_ext.value._distribution_points), 1)
+ self.assertEqual(
+ crl_distribution_points_ext.value._distribution_points[0].full_name[0].value,
+ 'http://crl.e-certchile.cl/ecertchilecaFES.crl')
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].crl_issuer, None)
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None)
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None)
+
+ #######################################################################
+ # extra extensions
+ #######################################################################
+
+ # "Microsoft" / "Microsoft CertSrv Infrastructure" / "szOID_CERTIFICATE_TEMPLATE"
+ # See:
+ # http://oidref.com/1.3.6.1.4.1.311.21.7
+ # https://support.microsoft.com/en-ae/help/287547/object-ids-associated-with-microsoft-cryptography
+ some_microsoft_extension_oid = oid.ObjectIdentifier("1.3.6.1.4.1.311.21.7")
+ some_microsoft_ext = cert_extensions.get_extension_for_oid(some_microsoft_extension_oid)
+ self.assertEqual(some_microsoft_ext.critical, False)
+ self.assertTrue(isinstance(some_microsoft_ext.value.value, bytes))
+
+ def test_load_pem_x509_cert_ok_prueba_sii(self) -> None:
+ cert_pem_bytes = utils.read_test_file_bytes('test_data/sii-crypto/prueba-sii-cert.pem')
+
+ x509_cert = load_pem_x509_cert(cert_pem_bytes)
+
+ self.assertIsInstance(x509_cert, X509Cert)
+
+ #######################################################################
+ # main properties
+ #######################################################################
+
+ self.assertEqual(
+ x509_cert.version,
+ cryptography.x509.Version.v3)
+ self.assertIsInstance(
+ x509_cert.signature_hash_algorithm,
+ cryptography.hazmat.primitives.hashes.MD5)
+ self.assertEqual(
+ x509_cert.signature_algorithm_oid,
+ oid.SignatureAlgorithmOID.RSA_WITH_MD5)
+
+ self.assertEqual(
+ x509_cert.serial_number,
+ 131466)
+ self.assertEqual(
+ x509_cert.not_valid_after,
+ datetime(2003, 10, 2, 0, 0))
+ self.assertEqual(
+ x509_cert.not_valid_before,
+ datetime(2002, 10, 2, 19, 11, 59))
+
+ #######################################################################
+ # issuer
+ #######################################################################
+
+ self.assertEqual(len(x509_cert.issuer.rdns), 6)
+ self.assertEqual(
+ x509_cert.issuer.rfc4514_string(),
+ 'ST=Region Metropolitana,'
+ 'L=Santiago,'
+ 'CN=E-Certchile CA Intermedia,'
+ 'OU=Empresa Nacional de Certificacion Electronica,'
+ 'O=E-CERTCHILE,'
+ 'C=CL')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value,
+ 'CL')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value,
+ 'Region Metropolitana')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value,
+ 'Santiago')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value,
+ 'E-CERTCHILE')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value,
+ 'Empresa Nacional de Certificacion Electronica')
+ self.assertEqual(
+ x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value,
+ 'E-Certchile CA Intermedia')
+
+ #######################################################################
+ # subject
+ #######################################################################
+
+ self.assertEqual(len(x509_cert.subject.rdns), 7)
+ self.assertEqual(
+ x509_cert.subject.rfc4514_string(),
+ 'ST=Region Metropolitana,'
+ 'OU=Servicio de Impuestos Internos,'
+ 'O=Servicio de Impuestos Internos,'
+ 'L=Santiago,'
+ '1.2.840.113549.1.9.1=wgonzalez@sii.cl,'
+ 'CN=Wilibaldo Gonzalez Cabrera,'
+ 'C=CL')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value,
+ 'CL')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value,
+ 'Region Metropolitana')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value,
+ 'Santiago')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value,
+ 'Servicio de Impuestos Internos')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value,
+ 'Servicio de Impuestos Internos')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value,
+ 'Wilibaldo Gonzalez Cabrera')
+ self.assertEqual(
+ x509_cert.subject.get_attributes_for_oid(oid.NameOID.EMAIL_ADDRESS)[0].value,
+ 'wgonzalez@sii.cl')
+
+ #######################################################################
+ # extensions
+ #######################################################################
+
+ cert_extensions = x509_cert.extensions
+ self.assertEqual(len(cert_extensions._extensions), 5)
+
+ # KEY_USAGE
+ key_usage_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.KeyUsage)
+ self.assertEqual(key_usage_ext.critical, False)
+ self.assertEqual(key_usage_ext.value.content_commitment, True)
+ self.assertEqual(key_usage_ext.value.crl_sign, False)
+ self.assertEqual(key_usage_ext.value.data_encipherment, True)
+ self.assertEqual(key_usage_ext.value.digital_signature, True)
+ self.assertEqual(key_usage_ext.value.key_agreement, False)
+ self.assertEqual(key_usage_ext.value.key_cert_sign, False)
+ self.assertEqual(key_usage_ext.value.key_encipherment, True)
+
+ # ISSUER_ALTERNATIVE_NAME
+ issuer_alt_name_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.IssuerAlternativeName)
+ self.assertEqual(issuer_alt_name_ext.critical, False)
+ self.assertEqual(len(issuer_alt_name_ext.value._general_names._general_names), 1)
+ self.assertEqual(
+ issuer_alt_name_ext.value._general_names._general_names[0].type_id,
+ _SII_CERT_CERTIFICADORA_EMISORA_RUT_OID)
+ self.assertEqual(
+ issuer_alt_name_ext.value._general_names._general_names[0].value,
+ b'\x16\n96928180-5')
+
+ # SUBJECT_ALTERNATIVE_NAME
+ subject_alt_name_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.SubjectAlternativeName)
+ self.assertEqual(subject_alt_name_ext.critical, False)
+ self.assertEqual(len(subject_alt_name_ext.value._general_names._general_names), 1)
+ # TODO: find out where did OID '1.3.6.1.4.1.8658.1' come from.
+ # Shouldn't it have been equal to '_SII_CERT_TITULAR_RUT_OID'?
+ self.assertEqual(
+ subject_alt_name_ext.value._general_names._general_names[0].type_id,
+ oid.ObjectIdentifier("1.3.6.1.4.1.8658.1"))
+ self.assertEqual(
+ subject_alt_name_ext.value._general_names._general_names[0].value,
+ b'\x16\n07880442-4')
+
+ # CERTIFICATE_POLICIES
+ certificate_policies_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.CertificatePolicies)
+ self.assertEqual(certificate_policies_ext.critical, False)
+ self.assertEqual(len(certificate_policies_ext.value._policies), 1)
+ # TODO: find out where did OID '1.3.6.1.4.1.8658.0' come from.
+ # Perhaps it was '1.3.6.1.4.1.8658'?
+ # https://oidref.com/1.3.6.1.4.1.8658
+ self.assertEqual(
+ certificate_policies_ext.value._policies[0].policy_identifier,
+ oid.ObjectIdentifier("1.3.6.1.4.1.8658.0"))
+ self.assertEqual(len(certificate_policies_ext.value._policies[0].policy_qualifiers), 2)
+ self.assertEqual(
+ certificate_policies_ext.value._policies[0].policy_qualifiers[0],
+ "http://www.e-certchile.cl/politica/cps.htm")
+ self.assertEqual(
+ certificate_policies_ext.value._policies[0].policy_qualifiers[1].explicit_text,
+ "El titular ha sido validado en forma presencial, quedando habilitado el Certificado "
+ "para uso tributario, pagos, comercio u otros")
+
+ # CRL_DISTRIBUTION_POINTS
+ crl_distribution_points_ext = cert_extensions.get_extension_for_class(
+ cryptography.x509.extensions.CRLDistributionPoints)
+ self.assertEqual(crl_distribution_points_ext.critical, False)
+ self.assertEqual(len(crl_distribution_points_ext.value._distribution_points), 1)
+ self.assertEqual(
+ crl_distribution_points_ext.value._distribution_points[0].full_name[0].value,
+ 'http://crl.e-certchile.cl/EcertchileCAI.crl')
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].crl_issuer, None)
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None)
+ self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None)
+
+ def test_load_pem_x509_cert_ok_str_ascii(self) -> None:
+ cert_pem_str_ascii = utils.read_test_file_str_ascii(
+ 'test_data/crypto/wildcard-google-com-cert.pem')
+
+ x509_cert = load_pem_x509_cert(cert_pem_str_ascii)
+ self.assertIsInstance(x509_cert, X509Cert)
+
+ def test_load_pem_x509_cert_ok_str_utf8(self) -> None:
+ cert_pem_str_utf8 = utils.read_test_file_str_utf8(
+ 'test_data/crypto/wildcard-google-com-cert.pem')
+
+ x509_cert = load_pem_x509_cert(cert_pem_str_utf8)
+ self.assertIsInstance(x509_cert, X509Cert)
+
+ def test_load_pem_x509_cert_fail_type_error(self) -> None:
+ with self.assertRaises(TypeError) as cm:
+ load_pem_x509_cert(1)
+ self.assertEqual(cm.exception.args, ("Value must be str or bytes.", ))
+
+ def test_load_pem_x509_cert_fail_value_error(self) -> None:
+ with self.assertRaises(ValueError) as cm:
+ load_pem_x509_cert('hello')
+ self.assertEqual(
+ cm.exception.args,
+ ("Unable to load certificate. See "
+ "https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file "
+ "for more details.", ))
diff --git a/tests/test_libs_encoding_utils.py b/tests/test_libs_encoding_utils.py
new file mode 100644
index 00000000..0065d6f6
--- /dev/null
+++ b/tests/test_libs_encoding_utils.py
@@ -0,0 +1,18 @@
+import unittest
+
+from cl_sii.libs.encoding_utils import clean_base64, decode_base64_strict, validate_base64 # noqa: F401,E501
+
+
+class FunctionsTest(unittest.TestCase):
+
+ def test_clean_base64(self):
+ # TODO: implement for function 'clean_base64'.
+ pass
+
+ def test_decode_base64_strict(self):
+ # TODO: implement for function 'decode_base64_strict'.
+ pass
+
+ def test_validate_base64(self):
+ # TODO: implement for function 'validate_base64'.
+ pass
diff --git a/tests/test_libs_xml_utils.py b/tests/test_libs_xml_utils.py
index a8a15723..33ec3037 100644
--- a/tests/test_libs_xml_utils.py
+++ b/tests/test_libs_xml_utils.py
@@ -2,6 +2,7 @@
import lxml.etree
+from cl_sii.libs.xml_utils import XmlElement
from cl_sii.libs.xml_utils import ( # noqa: F401
XmlSyntaxError, XmlFeatureForbidden,
parse_untrusted_xml, read_xml_schema, validate_xml_doc, write_xml_doc,
@@ -20,7 +21,7 @@ def test_parse_untrusted_xml_valid(self) -> None:
b' \n'
b'')
xml = parse_untrusted_xml(value)
- self.assertIsInstance(xml, lxml.etree.ElementBase)
+ self.assertIsInstance(xml, XmlElement)
# print(xml)
self.assertEqual(
lxml.etree.tostring(xml, pretty_print=False),
diff --git a/tests/utils.py b/tests/utils.py
index ae424d5d..5d27943d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -13,3 +13,25 @@ def read_test_file_bytes(path: str) -> bytes:
content = file.read()
return content
+
+
+def read_test_file_str_ascii(path: str) -> str:
+ filepath = os.path.join(
+ _TESTS_DIR_PATH,
+ path,
+ )
+ with open(filepath, mode='rt', encoding='ascii') as file:
+ content = file.read()
+
+ return content
+
+
+def read_test_file_str_utf8(path: str) -> str:
+ filepath = os.path.join(
+ _TESTS_DIR_PATH,
+ path,
+ )
+ with open(filepath, mode='rt', encoding='utf8') as file:
+ content = file.read()
+
+ return content