From c7adc5a2cbf9049c6143661547c8e19aa436e220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 16 Apr 2019 16:23:09 -0400 Subject: [PATCH 01/38] data.ref: add XML schemas for "Cesion" (RTC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Formato XML del Archivo Electrónico de Cesión" of the "Registro electrónico de cesión de créditos". Source (2019-04-16): http://www.sii.cl/factura_electronica/schema_cesion.zip Some files were ignored: - 'DTE_v10.xsd': there is a newer version in the repo. - 'SiiTypes_v10.xsd': there is a newer version in the repo. - 'xmldsignature_v10.xsd': identical to the version in the repo. The included files are up to date with respect to those in repository/project "LibreDTE" (39156263) https://github.com/LibreDTE/libredte-lib/tree/39156263/schemas MD5 checksums: - 82d426fc3bd5f3a29e61a1d07ed4d6dd schema_cesion.zip - 8e80522d91bcf99077c12e15354eeae4 schema_cesion/AEC_v10.xsd - 8db534d686bc84fc7960199dd5fdacd8 schema_cesion/Cesion_v10.xsd - b2fd1907af733ae9401604c694837a85 schema_cesion/DTECedido_v10.xsd - e72ee34d798224f31a5127edda712bbf schema_cesion/DTE_v10.xsd - 620cf4867d8ba9c16dba74ab05830963 schema_cesion/Recibos_v10.xsd - b6a63aa427e3d528e46a7d94ccbdcb32 schema_cesion/SiiTypes_v10.xsd - 8b83aaae477a57d829b075230237102c schema_cesion/xmldsignature_v10.xsd --- .../schemas-xml/AEC_v10.xsd | 91 +++++++ .../schemas-xml/Cesion_v10.xsd | 230 ++++++++++++++++++ .../schemas-xml/DTECedido_v10.xsd | 59 +++++ .../factura_electronica/schemas-xml/README.md | 95 ++++++++ .../schemas-xml/Recibos_v10.xsd | 95 ++++++++ 5 files changed, 570 insertions(+) create mode 100644 cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd create mode 100644 cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd create mode 100644 cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd create mode 100644 cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd 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 recepcin conforme + + + + + + + + + + RUT de quien Firma el Recibo + + + + + Texto Ley 19.983, acredita la recepcion mercaderas o servicio. + + + + + + + + + + Fecha y Hora de la Firma del Recibo + + + + + + + + + Firma Digital sobre Documento + + + + + + From 44ac84c26ade1ea6c8420606eaf77d5522904e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 22 Apr 2019 15:40:39 -0400 Subject: [PATCH 02/38] test_data: fix a filename --- ...K--33---170--SEQ-2.xml => AEC--76354771-K--33--170--SEQ-2.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/test_data/sii-rtc/{AEC--76354771-K--33---170--SEQ-2.xml => AEC--76354771-K--33--170--SEQ-2.xml} (100%) 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 From 5814c828d877f37a170d2015ae0ee813008a514d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 17 Apr 2019 11:07:23 -0400 Subject: [PATCH 03/38] test_data: add real AEC and DTE XML files With permission from the "cesionario" of the "cesion" (ST Capital S.A.). The DTE XML file was extracted from the AEC XML file. --- .../sii-dte/DTE--76399752-9--33--25568.xml | 129 +++++++ .../AEC--76399752-9--33--25568--SEQ-1.xml | 322 ++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml create mode 100644 tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml 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-933255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA3325568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52
viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+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--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-933255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA3325568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52
viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+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:52
f4zqbr0mGWngOkRb16XxngcE96o= +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 disposicin del cesionario ST CAPITAL S.A., RUT 76389992-6, el o los documentos donde constan los recibos de las mercaderas 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 From 998f69f688cd93d9b5b502ae6ea4ff6ff7897af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 16 Apr 2019 19:19:23 -0400 Subject: [PATCH 04/38] libs.xml_utils: add class aliases For commonly used/references XML-related classes. --- cl_sii/libs/xml_utils.py | 17 ++++++++++------- tests/test_libs_xml_utils.py | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cl_sii/libs/xml_utils.py b/cl_sii/libs/xml_utils.py index 83a9d69e..9d10458d 100644 --- a/cl_sii/libs/xml_utils.py +++ b/cl_sii/libs/xml_utils.py @@ -7,6 +7,9 @@ import lxml.etree import xml.parsers.expat import xml.parsers.expat.errors +from lxml.etree import ElementBase as XmlElement # noqa: F401 +from lxml.etree import ElementTree as XmlElementTree # noqa: F401 +from lxml.etree import XMLSchema as XmlSchema # noqa: F401 logger = logging.getLogger(__name__) @@ -72,7 +75,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 +118,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 +195,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 +203,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 +243,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 +267,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/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), From f006a7392cbef1be13c0449295c7a216f52f9ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 16 Apr 2019 19:52:57 -0400 Subject: [PATCH 05/38] libs.xml_utils: add `XML_DSIG_NS_MAP` --- cl_sii/libs/xml_utils.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/cl_sii/libs/xml_utils.py b/cl_sii/libs/xml_utils.py index 9d10458d..caa7a625 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 @@ -15,6 +36,24 @@ 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 ############################################################################### From fb87ffab2f64ebe977267b382c5a2f263be62a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 18 Apr 2019 16:22:56 -0400 Subject: [PATCH 06/38] dte.constants: add features to `TipoDteEnum` Add helper properties. Also: - Improve docstrings and comments a little. - Implement tests. --- cl_sii/dte/constants.py | 71 ++++++++++++++++++-- tests/test_dte_constants.py | 128 ++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 tests/test_dte_constants.py 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/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) From 0bde60b18c95394612952aa1546ea925c67fbb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 22 Apr 2019 15:44:43 -0400 Subject: [PATCH 07/38] dte.parse: fix poor usage of 'lxml' --- cl_sii/dte/parse.py | 75 +++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index aac22134..3f4176c6 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -24,9 +24,8 @@ from datetime import date from typing import Optional, Tuple, Union -import lxml.etree - from cl_sii.libs import xml_utils +from cl_sii.libs.xml_utils import XmlElement 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,7 +113,7 @@ 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: +def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: """ Parse and deserialize DTE data from ``xml_doc``. @@ -123,18 +122,18 @@ def parse_dte_xml(xml_doc: lxml.etree.ElementBase) -> data_models.DteDataL2: # 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() + xml_em = xml_doc 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), + emisor_rut=_get_emisor_rut(xml_em), + tipo_dte=_get_tipo_dte(xml_em), + folio=_get_folio(xml_em), + fecha_emision_date=_get_fecha_emision(xml_em), + receptor_rut=_get_receptor_rut(xml_em), + monto_total=_get_monto_total(xml_em), + emisor_razon_social=_get_emisor_razon_social(xml_em), + receptor_razon_social=_get_receptor_razon_social(xml_em), + fecha_vencimiento_date=_get_fecha_vencimiento(xml_em, default=None), ) return obj_struct @@ -144,9 +143,7 @@ def parse_dte_xml(xml_doc: lxml.etree.ElementBase) -> data_models.DteDataL2: # 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 +177,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 @@ -196,41 +191,41 @@ def _remove_dte_xml_doc_personalizado( return xml_doc, modified -def _get_tipo_dte(xml_etree: lxml.etree.ElementTree) -> constants.TipoDteEnum: +def _get_tipo_dte(xml_em: XmlElement) -> 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) + value_str = xml_em.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: +def _get_folio(xml_em: XmlElement) -> 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) + value_str = xml_em.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: +def _get_fecha_emision(xml_em: XmlElement) -> 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) + value_str = xml_em.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, + xml_em: XmlElement, 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) + value_str = xml_em.findtext(em_path, namespaces=DTE_XMLNS_MAP) if value_str is None: if default is None or isinstance(default, date): value = default @@ -244,46 +239,46 @@ def _get_fecha_vencimiento( return value -def _get_emisor_rut(xml_etree: lxml.etree.ElementTree) -> Rut: +def _get_emisor_rut(xml_em: XmlElement) -> 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) + value_str = xml_em.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: +def _get_emisor_razon_social(xml_em: XmlElement) -> 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) + value_str: str = xml_em.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: +def _get_receptor_rut(xml_em: XmlElement) -> 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) + value_str = xml_em.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: +def _get_receptor_razon_social(xml_em: XmlElement) -> 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) + value_str: str = xml_em.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: +def _get_monto_total(xml_em: XmlElement) -> 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) + value_str = xml_em.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) From 95b1f3e6256a97ebd26105a4fa3c74a143546c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sun, 21 Apr 2019 17:26:52 -0400 Subject: [PATCH 08/38] dte.data_models: remove redundant property --- cl_sii/dte/data_models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index fa7ac32d..3e23482b 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -229,10 +229,6 @@ 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) - @dataclasses.dataclass(frozen=True) class DteDataL2(DteDataL1): From 3509a340db61beabbefd9dfd4d0081a79c2bb34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sun, 21 Apr 2019 17:28:04 -0400 Subject: [PATCH 09/38] dte.data_models: implement tests And add some test stubs. --- tests/test_dte_data_models.py | 130 +++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 11 deletions(-) diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 648c7251..d0e14ab6 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -1,4 +1,5 @@ import unittest +from datetime import date from cl_sii.rut import Rut # noqa: F401 @@ -11,38 +12,145 @@ 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, + )) 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, + ) + + 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, + )) 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 From 1a3d5e4d30c0efb6983a695de17bfaa7e209dd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sun, 21 Apr 2019 17:29:00 -0400 Subject: [PATCH 10/38] dte.data_models: add properties to `DteDataL1` - `vendedor_rut` - `deudor_rut` --- cl_sii/dte/data_models.py | 51 +++++++++++++++++++++++++++++++++++ tests/test_dte_data_models.py | 31 +++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 3e23482b..33390277 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -1,3 +1,20 @@ +""" +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 @@ -229,6 +246,40 @@ def __post_init__(self) -> None: validate_dte_monto_total(self.monto_total) + @property + 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) class DteDataL2(DteDataL1): diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index d0e14ab6..e72b3d6b 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -1,3 +1,4 @@ +import dataclasses import unittest from datetime import date @@ -103,6 +104,36 @@ def test_as_dict(self) -> None: 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): From 24c0411bbc26987d658eab979e9157d81df67c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 22 Apr 2019 17:52:09 -0400 Subject: [PATCH 11/38] dte.parse: improve tests --- tests/test_dte_parse.py | 106 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index fedb651b..5dd2968c 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -16,7 +16,8 @@ from .utils import read_test_file_bytes -_TEST_DTE_NEEDS_CLEAN_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml' +_TEST_DTE_NEEDS_CLEAN_1_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml' +_TEST_DTE_NEEDS_CLEAN_2_FILE_PATH = 'test_data/sii-dte/DTE--76399752-9--33--25568.xml' class OthersTest(unittest.TestCase): @@ -25,10 +26,10 @@ def test_DTE_XML_SCHEMA_OBJ(self) -> None: # TODO: implement pass - def test_integration_ok(self) -> None: + def test_integration_ok_1(self) -> None: # TODO: split in separate tests, with more coverage. - dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_FILE_PATH + dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_1_FILE_PATH file_bytes = read_test_file_bytes(dte_bad_xml_file_path) xml_doc = xml_utils.parse_untrusted_xml(file_bytes) @@ -124,6 +125,105 @@ def test_integration_ok(self) -> None: expected_file_bytes_diff ) + def test_integration_ok_2(self) -> None: + # TODO: split in separate tests, with more coverage. + + dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH + + file_bytes = read_test_file_bytes(dte_bad_xml_file_path) + 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", ) + ) + # This would raise: + # parse_dte_xml(xml_doc) + + 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) + + 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() + del f + + 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('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, + )) + + 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 + ) + class FunctionCleanDteXmlTest(unittest.TestCase): From 55b9acec8d1fbd230b468679841a2c983b6c805e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sun, 21 Apr 2019 23:21:56 -0400 Subject: [PATCH 12/38] dte.parse: refactor `parse_dte_xml` --- cl_sii/dte/parse.py | 204 +++++++++++++++++++++----------------------- 1 file changed, 96 insertions(+), 108 deletions(-) diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index 3f4176c6..05be0bd3 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -20,9 +20,8 @@ import io import logging import os -from dataclasses import MISSING, _MISSING_TYPE from datetime import date -from typing import Optional, Tuple, Union +from typing import Tuple from cl_sii.libs import xml_utils from cl_sii.libs.xml_utils import XmlElement @@ -115,7 +114,11 @@ def validate_dte_xml(xml_doc: XmlElement) -> None: 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. """ # TODO: separate the XML parsing stage from the deserialization stage, which could be @@ -124,20 +127,98 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: xml_em = xml_doc - obj_struct = data_models.DteDataL2( - emisor_rut=_get_emisor_rut(xml_em), - tipo_dte=_get_tipo_dte(xml_em), - folio=_get_folio(xml_em), - fecha_emision_date=_get_fecha_emision(xml_em), - receptor_rut=_get_receptor_rut(xml_em), - monto_total=_get_monto_total(xml_em), - emisor_razon_social=_get_emisor_razon_social(xml_em), - receptor_razon_social=_get_receptor_razon_social(xml_em), - fecha_vencimiento_date=_get_fecha_vencimiento(xml_em, default=None), + ########################################################################### + # XML elements finding + ########################################################################### + + documento_em = xml_em.find( + 'sii-dte:Documento', # "Informacion Tributaria del DTE" + namespaces=DTE_XMLNS_MAP) + + if documento_em is None: + raise ValueError("Top level XML element 'Document' is required.") + + encabezado_em = documento_em.find( + 'sii-dte:Encabezado', # "Identificacion y Totales del Documento" + namespaces=DTE_XMLNS_MAP) + + 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) + + # (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) + + 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) + + 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) + + monto_total_em = totales_em.find( + 'sii-dte:MntTotal', # "Monto Total del DTE" + namespaces=DTE_XMLNS_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() + + receptor_rut_value = Rut(receptor_rut_em.text.strip()) + receptor_razon_social_value = receptor_razon_social_em.text.strip() + + monto_total_value = int(monto_total_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, ) - return obj_struct - ############################################################################### # helpers @@ -189,96 +270,3 @@ def _remove_dte_xml_doc_personalizado(xml_doc: XmlElement) -> Tuple[XmlElement, xml_doc.remove(xml_em) return xml_doc, modified - - -def _get_tipo_dte(xml_em: XmlElement) -> constants.TipoDteEnum: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:TipoDTE' - - value_str = xml_em.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_em: XmlElement) -> int: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:Folio' - - value_str = xml_em.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_em: XmlElement) -> date: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:FchEmis' - - value_str = xml_em.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_em: XmlElement, - 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_em.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_em: XmlElement) -> Rut: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Emisor/sii-dte:RUTEmisor' - - value_str = xml_em.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_em: XmlElement) -> str: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Emisor/sii-dte:RznSoc' - - value_str: str = xml_em.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_em: XmlElement) -> Rut: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Receptor/sii-dte:RUTRecep' - - value_str = xml_em.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_em: XmlElement) -> str: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Receptor/sii-dte:RznSocRecep' - - value_str: str = xml_em.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_em: XmlElement) -> int: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Totales/sii-dte:MntTotal' - - value_str = xml_em.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) From 1445d013e810068119fdf586f48042dc9fce026c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 22 Apr 2019 15:31:22 -0400 Subject: [PATCH 13/38] dte.parse: augment `parse_dte_xml` --- cl_sii/dte/parse.py | 200 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 2 deletions(-) diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index 05be0bd3..5c275432 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -112,6 +112,7 @@ def validate_dte_xml(xml_doc: XmlElement) -> None: xml_utils.validate_xml_doc(DTE_XML_SCHEMA_OBJ, xml_doc) +# TODO: rename to 'parse_dte_xml_data' def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: """ Parse data from a DTE XML doc. @@ -121,6 +122,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: ``{http://www.sii.cl/SiiDte}/DTE`` XML element. """ + # 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`. @@ -131,17 +133,70 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: # 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) + # note: excluded because currently it is not useful. + # 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) + # note: excluded because currently it is not useful. + # 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) @@ -155,6 +210,55 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: '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" @@ -170,13 +274,68 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: '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): + # - 'CorreoEmisor': + # "Correo Elect. de contacto en empresa del receptor" (wrong!) + # - '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" 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) + + # '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): + # - 'CorreoRecep': + # "Correo Elect. de contacto en empresa del receptor" + # - '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" receptor_rut_em = receptor_em.find( 'sii-dte:RUTRecep', # "RUT del Receptor del DTE" namespaces=DTE_XMLNS_MAP) @@ -184,6 +343,43 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: 'sii-dte:RznSocRecep', # "Nombre o Razon Social 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) From 89196d3c289dbdf1152efa3036f4f055890e03fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 22 Apr 2019 15:31:51 -0400 Subject: [PATCH 14/38] dte.data_models: add fields to `DteDataL2` - `firma_documento_dt_naive` - `signature_value_base64` - `signature_x509_cert_base64` - `emisor_giro` - `emisor_email` - `receptor_email` --- cl_sii/dte/data_models.py | 80 ++++++++++++++++++++++++++++++++- cl_sii/dte/parse.py | 79 +++++++++++++++++++++++++------- tests/test_dte_data_models.py | 14 +++++- tests/test_dte_parse.py | 84 ++++++++++++++++++++++++++++++++++- 4 files changed, 238 insertions(+), 19 deletions(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 33390277..d7e43653 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -17,7 +17,7 @@ """ 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 @@ -74,6 +74,16 @@ 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_non_empty_str(value: str) -> None: + if len(value.strip()) == 0: + raise ValueError("String (stripped) length is 0.") + + @dataclasses.dataclass(frozen=True) class DteNaturalKey: @@ -312,6 +322,36 @@ 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_base64: Optional[str] = dc_field(default=None) + """ + DTE's digital signature's value (as base64 str). + """ + + signature_x509_cert_base64: Optional[str] = dc_field(default=None) + """ + DTE's digital signature's X509 certificate (as base64 str). + """ + + 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. @@ -332,3 +372,41 @@ 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_base64 is not None: + if not isinstance(self.signature_value_base64, str): + raise TypeError("Inappropriate type of 'signature_value_base64'.") + # TODO: validate that it is base64 + # TODO: bytes? + validate_clean_str(self.signature_value_base64) + validate_non_empty_str(self.signature_value_base64) + + if self.signature_x509_cert_base64 is not None: + if not isinstance(self.signature_x509_cert_base64, str): + raise TypeError("Inappropriate type of 'signature_x509_cert_base64'.") + # TODO: validate that it is base64 + # TODO: bytes? + validate_clean_str(self.signature_x509_cert_base64) + validate_non_empty_str(self.signature_x509_cert_base64) + + 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 5c275432..c8956ba4 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -20,7 +20,7 @@ import io import logging import os -from datetime import date +from datetime import date, datetime from typing import Tuple from cl_sii.libs import xml_utils @@ -146,10 +146,9 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: exportaciones_em = xml_em.find( 'sii-dte:Exportaciones', # "Informacion Tributaria de exportaciones" namespaces=DTE_XMLNS_MAP) - # note: excluded because currently it is not useful. - # signature_em = xml_em.find( - # 'ds:Signature', # "Firma Digital sobre Documento" - # namespaces=xml_utils.XML_DSIG_NS_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.") @@ -181,10 +180,9 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: # ted_em = documento_em.find( # 'sii-dte:TED', # "Timbre Electronico de DTE" # namespaces=DTE_XMLNS_MAP) - # note: excluded because currently it is not useful. - # tmst_firma_em = documento_em.find( - # 'sii-dte:TmstFirma', # "Fecha y Hora en que se Firmo Digitalmente el Documento" - # 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 @@ -277,8 +275,6 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: # '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): - # - 'CorreoEmisor': - # "Correo Elect. de contacto en empresa del receptor" (wrong!) # - 'Telefono': (occurrences: 0..2) # "Telefono Emisor" # - 'Acteco': (occurrences: 0..4) @@ -299,21 +295,24 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: # "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) + 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): - # - 'CorreoRecep': - # "Correo Elect. de contacto en empresa del receptor" # - 'CdgIntRecep': # "Codigo Interno del Receptor" # - 'Extranjero': @@ -336,12 +335,17 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: # "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 @@ -384,6 +388,35 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: '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 ########################################################################### @@ -397,12 +430,20 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: 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_base64 = signature_signature_value_em.text.strip() + signature_key_info_x509_cert_base64 = signature_key_info_x509_cert_em.text.strip() + return data_models.DteDataL2( emisor_rut=emisor_rut_value, tipo_dte=tipo_dte_value, @@ -413,6 +454,12 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: 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_base64=signature_signature_value_base64, + signature_x509_cert_base64=signature_key_info_x509_cert_base64, + emisor_giro=emisor_giro_value, + emisor_email=emisor_email_value, + receptor_email=receptor_email_value, ) diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index e72b3d6b..45040d98 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -1,6 +1,6 @@ import dataclasses import unittest -from datetime import date +from datetime import date, datetime from cl_sii.rut import Rut # noqa: F401 @@ -150,6 +150,12 @@ def setUp(self) -> None: 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_base64=None, + signature_x509_cert_base64=None, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, ) def test_init_fail(self) -> None: @@ -169,6 +175,12 @@ def test_as_dict(self) -> None: 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_base64=None, + signature_x509_cert_base64=None, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, )) diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index 5dd2968c..85c4de83 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -1,7 +1,7 @@ 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 xml_utils @@ -18,6 +18,76 @@ _TEST_DTE_NEEDS_CLEAN_1_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml' _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH = 'test_data/sii-dte/DTE--76399752-9--33--25568.xml' +_TEST_DTE_1_FILE_PATH = _TEST_DTE_NEEDS_CLEAN_1_FILE_PATH +_TEST_DTE_2_FILE_PATH = _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH +_TEST_DTE_1_SIGNATURE_VALUE = ( + 'fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk\n' + 'ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi\n' + 'wqSxDcYjTT6vXsLPrZk=') +_TEST_DTE_2_SIGNATURE_VALUE = ( + 'wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD\n' + 'MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4\n' + 'FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1\n' + 'GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp\n' + '0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==') +_TEST_DTE_1_X509_CERT = ( + 'MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx\n' + 'HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE\n' + 'ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD\n' + 'EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW\n' + 'GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa\n' + 'MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh\n' + 'MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE\n' + 'CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM\n' + 'b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI\n' + 'hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx\n' + 'O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC\n' + '/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3\n' + 'FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV\n' + 'HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S\n' + 'ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu\n' + 'Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6\n' + 'Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1\n' + 'MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC\n' + 'AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu\n' + 'Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp\n' + 'AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A\n' + 'IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk\n' + 'AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA\n' + 'cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA\n' + 'mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD\n' + 'qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un\n' + '+/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud\n' + 'B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY\n' + '+kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==') +_TEST_DTE_2_X509_CERT = ( + 'MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK\n' + 'Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj\n' + 'YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh\n' + 'Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0\n' + 'MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT\n' + 'GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu\n' + 'YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD\n' + 'ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1\n' + 'g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP\n' + 'HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh\n' + 'qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS\n' + 'EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G\n' + 'A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L\n' + 'WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG\n' + '+EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v\n' + 'YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t\n' + 'IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr\n' + 'BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R\n' + 'BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt\n' + 'OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw\n' + 'OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H\n' + 'NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz\n' + 'ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV\n' + 'WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/\n' + 'w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB\n' + 'B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg\n' + 'jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw') class OthersTest(unittest.TestCase): @@ -83,6 +153,12 @@ def test_integration_ok_1(self) -> None: 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_base64=_TEST_DTE_1_SIGNATURE_VALUE, + signature_x509_cert_base64=_TEST_DTE_1_X509_CERT, + emisor_giro='Ingenieria y Construccion', + emisor_email='ENACONLTDA@GMAIL.COM', + receptor_email=None, )) expected_file_bytes_diff = ( @@ -182,6 +258,12 @@ def test_integration_ok_2(self) -> None: 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_base64=_TEST_DTE_2_SIGNATURE_VALUE, + signature_x509_cert_base64=_TEST_DTE_2_X509_CERT, + emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', + emisor_email='ANGEL.PEZO@APCASESORIAS.CL', + receptor_email=None, )) expected_file_bytes_diff = ( From 6fa6e847048c12ba705036ff667d7bcfdaa09086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 23 Apr 2019 12:45:07 -0400 Subject: [PATCH 15/38] test_data.crypto: add real X.509 cert for '*.google.com' Retrieved on 2019-04-23. Details ------- *.google.com Identity: *.google.com Verified by: Google Internet Authority G3 Expires: 18/06/19 Subject Name C (Country): US ST (State): California L (Locality): Mountain View O (Organization): Google LLC CN (Common Name): *.google.com Issuer Name C (Country): US O (Organization): Google Trust Services CN (Common Name): Google Internet Authority G3 Issued Certificate Version: 3 Serial Number: 5C 3F 5E 0A F8 7F E3 83 F5 11 3E 51 23 52 EE 26 Not Valid Before: 2019-03-26 Not Valid After: 2019-06-18 Certificate Fingerprints SHA1: F5 19 59 D6 6D 50 BB DF 5E EC C0 9D E7 21 10 F6 C6 03 45 B5 MD5: 5F C6 5C 5E 61 F0 66 74 77 B9 4E 6C FC B2 2F 2C Public Key Info Key Algorithm: Elliptic Curve Key Parameters: 06 08 2A 86 48 CE 3D 03 01 07 Key Size: 256 Key SHA1 Fingerprint: 6F 37 DE 91 C0 D8 E1 AD 64 8E 30 F7 EF 1C 00 22 15 E8 92 6D Public Key: 04 03 69 59 22 D7 2D B2 66 E5 E4 73 73 51 09 9A F4 88 6F 3D 27 00 D4 FE 6F 5D 6B F9 78 52 50 20 1B 7D 08 52 FB 83 3F F7 56 09 81 51 3A 52 D6 49 AC B3 BB 18 6C 6F A7 5B 55 FC 32 FC E7 B3 5A 2C 45 Extended Key Usage Allowed Purposes: Server Authentication Critical: No Key Usage Usages: Digital signature Critical: Yes --- .../crypto/wildcard-google-com-cert.pem | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/test_data/crypto/wildcard-google-com-cert.pem 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----- From c325d0b2a6578bfe2c9b422affd599bf1c56687f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 23 Apr 2019 20:11:59 -0400 Subject: [PATCH 16/38] tests: add functions to `utils` --- tests/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) 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 From 9a3de55c26be9f12982324c8537131b02dfbaec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 23 Apr 2019 13:01:26 -0400 Subject: [PATCH 17/38] test_data.sii: add "cleaned" version of a real DTE The file is the output of command: ./scripts/clean_dte_xml_file.py file \ 'tests/test_data/sii-dte/DTE--76354771-K--33--170.xml' \ 'tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml' --- .../DTE--76354771-K--33--170--cleaned.xml | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml 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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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 From dcadff91d89a40d322aff83319ac74f25a19c357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 23 Apr 2019 12:31:26 -0400 Subject: [PATCH 18/38] test_data.sii: add modified versions of a real DTE --- ...3--170--cleaned-mod-bad-cert-no-base64.xml | 94 ++++++++++++ ...54771-K--33--170--cleaned-mod-bad-cert.xml | 95 ++++++++++++ ...-K--33--170--cleaned-mod-changed-monto.xml | 122 ++++++++++++++++ ...33--170--cleaned-mod-removed-signature.xml | 60 ++++++++ ...-K--33--170--cleaned-mod-replaced-cert.xml | 137 ++++++++++++++++++ 5 files changed, 508 insertions(+) create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml 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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2995230Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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 From aac61f68051dd34a579a4f7572b33fc5fc53085e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 24 Apr 2019 13:31:34 -0400 Subject: [PATCH 19/38] test_data.sii: add example cert by SII Although it is quite old (2002), it is helpful to test some cases. Source: from a sample DTE in http://www.sii.cl/factura_electronica/factura_mercado/manual_certificacion.pdf --- .../test_data/sii-crypto/prueba-sii-cert.pem | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/test_data/sii-crypto/prueba-sii-cert.pem 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----- From a2ef9f4b77d7a2c845a1f38a9a30fbca6ac4168f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 24 Apr 2019 13:33:11 -0400 Subject: [PATCH 20/38] test_data.sii: add a cert extracted from a real DTE Extract PEM-encoded data of the X.509 certificate inside XML document 'test_data/sii-dte/DTE--76354771-K--33--170.xml' --- .../DTE--76354771-K--33--170-cert.pem | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem 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----- From 86b0bff2fa9b0680c45894da6bf27ce39ad172a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 24 Apr 2019 16:13:05 -0400 Subject: [PATCH 21/38] test_data.sii: add signature-related files for real DTE --- ...4771-K--33--170--cleaned-signature_xml.xml | 64 +++++++++++++++++++ ...354771-K--33--170--cleaned-signed_data.xml | 56 ++++++++++++++++ ...6354771-K--33--170--cleaned-signed_xml.xml | 57 +++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml create mode 100644 tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml 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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+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-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
\ No newline at end of file From 370417d6d3767929619eee9bcbe6a1318f6afbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 12:31:05 -0400 Subject: [PATCH 22/38] requirements: update 'pytz' Changelog: - 2019.1 (2019-04-09) https://github.com/stub42/pytz/blob/release_2019.1/tz/NEWS Code diff: https://github.com/stub42/pytz/compare/release_2018.9...release_2019.1 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 0cb7cfe3..2406ba81 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -5,7 +5,7 @@ defusedxml==0.5.0 lxml==4.2.5 marshmallow==2.16.3 -pytz==2018.9 +pytz==2019.1 # Packages dependencies: #none From e6ad9ab12830f888a078aec66541e4106651d36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 12:38:33 -0400 Subject: [PATCH 23/38] requirements: update 'djangorestframework' (extras) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog: - 3.9.2 (2019-03-03)   https://www.django-rest-framework.org/community/release-notes/#392 - 3.9.1 (2019-01-16)   https://www.django-rest-framework.org/community/release-notes/#391 - 3.9.0 (2018-10-18)   https://www.django-rest-framework.org/community/release-notes/#390 Code diff: https://github.com/encode/django-rest-framework/compare/3.8.2...3.9.2 --- requirements/extras.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 893c13a1c8339523f585472a06cb842bb5e50f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 12:42:18 -0400 Subject: [PATCH 24/38] requirements: update 'lxml' To the latest version of the 4.2.x series. Changelog: - 4.2.6 (2019-01-02) https://github.com/lxml/lxml/blob/lxml-4.2.6/CHANGES.txt#L5 https://github.com/lxml/lxml/compare/lxml-4.2.5...lxml-4.2.6 --- requirements/base.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 2406ba81..4ecc05a3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,7 +3,7 @@ # Required packages: defusedxml==0.5.0 -lxml==4.2.5 +lxml==4.2.6 marshmallow==2.16.3 pytz==2019.1 diff --git a/setup.py b/setup.py index d8cd39da..32c3f25c 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def get_version(*file_paths: Sequence[str]) -> str: # TODO: add reasonable upper-bound for some of these packages? requirements = [ 'defusedxml>=0.5.0', - 'lxml>=4.2.5', + 'lxml>=4.2.6', 'marshmallow>=2.16.3', 'pytz>=2018.7', ] From 47f3fae6d76f1356645b19ed42468d6848958d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 13:00:26 -0400 Subject: [PATCH 25/38] requirements: add 'cryptography' > [..] a package which provides cryptographic recipes and primitives > to Python developers. Our goal is for it to be your "cryptographic > standard library". https://cryptography.io https://github.com/pyca/cryptography --- requirements/base.txt | 11 ++++++++++- setup.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 4ecc05a3..fda9977a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,10 +2,19 @@ # 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.6 marshmallow==2.16.3 pytz==2019.1 # Packages dependencies: -#none +# - cryptography: +# - asn1crypto +# - cffi: +# - pycparser +# - six +asn1crypto==0.24.0 +cffi==1.12.3 +pycparser==2.19 +six==1.12.0 diff --git a/setup.py b/setup.py index 32c3f25c..4b8db8b4 100755 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ 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.6', 'marshmallow>=2.16.3', From acbfbba2f5340cb1a152522ea249ae588e94ff4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 13:05:28 -0400 Subject: [PATCH 26/38] requirements: add 'pyOpenSSL' (not the latest version because of a compatibility issue with package 'signxml' v2.6.0) > A Python wrapper around the OpenSSL library. https://pyopenssl.org https://github.com/pyca/pyopenssl --- requirements/base.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index fda9977a..f71993b7 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,6 +6,7 @@ cryptography==2.6.1 defusedxml==0.5.0 lxml==4.2.6 marshmallow==2.16.3 +pyOpenSSL==18.0.0 pytz==2019.1 # Packages dependencies: diff --git a/setup.py b/setup.py index 4b8db8b4..8cdaf7f4 100755 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ def get_version(*file_paths: Sequence[str]) -> str: 'defusedxml>=0.5.0', 'lxml>=4.2.6', 'marshmallow>=2.16.3', + 'pyOpenSSL>=18.0.0', 'pytz>=2018.7', ] From 4bcdfbf52327236daa220ef2100d5352d84aad6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 13:14:19 -0400 Subject: [PATCH 27/38] requirements: add 'signxml' > XML Signature in Python. > SignXML is an implementation of the W3C XML Signature standard > in Python. This standard (also known as XMLDSig and RFC 3275) is > used to provide payload security in SAML 2.0 and WS-Security, > among other uses. https://signxml.readthedocs.io https://github.com/XML-Security/signxml --- requirements/base.txt | 13 +++++++++++++ setup.py | 1 + 2 files changed, 14 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index f71993b7..ac889be3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,6 +8,7 @@ lxml==4.2.6 marshmallow==2.16.3 pyOpenSSL==18.0.0 pytz==2019.1 +signxml==2.6.0 # Packages dependencies: # - cryptography: @@ -15,7 +16,19 @@ pytz==2019.1 # - 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/setup.py b/setup.py index 8cdaf7f4..5e9d2663 100755 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ def get_version(*file_paths: Sequence[str]) -> str: 'marshmallow>=2.16.3', 'pyOpenSSL>=18.0.0', 'pytz>=2018.7', + 'signxml>=2.6.0', ] extras_requirements = { From 39e5295943fa4ab1c2104df2e8041a820820a2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 13:20:50 -0400 Subject: [PATCH 28/38] requirements: update 'setuptools' (release) (no explanation necessary) --- requirements/release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 3ceb81fb65883ba21ff22a1c84c1d7f6efdd61e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 13:35:37 -0400 Subject: [PATCH 29/38] requirements: update dependencies of 'test' requirements --- requirements/test.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 From cde9fe6e09a4e97bee55134c7419eb62fecd5111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 10:22:28 -0400 Subject: [PATCH 30/38] libs.xml_utils: fix class alias `XmlElementTree` --- cl_sii/libs/xml_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cl_sii/libs/xml_utils.py b/cl_sii/libs/xml_utils.py index caa7a625..6ed10205 100644 --- a/cl_sii/libs/xml_utils.py +++ b/cl_sii/libs/xml_utils.py @@ -29,7 +29,8 @@ import xml.parsers.expat import xml.parsers.expat.errors from lxml.etree import ElementBase as XmlElement # noqa: F401 -from lxml.etree import ElementTree as XmlElementTree # 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 From 1ca2152d18c21859a395360ce1ba37be6b7f0ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 10:59:04 -0400 Subject: [PATCH 31/38] test_data.sii: add "cleaned" version of a real DTE The file is the output of command: ./scripts/clean_dte_xml_file.py file \ 'tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml' \ 'tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml' --- .../DTE--76399752-9--33--25568--cleaned.xml | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml 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-933255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA3325568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52
viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+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 From 2dd91a94c702fb645c2af0619422418d440327ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 12:03:17 -0400 Subject: [PATCH 32/38] test_data.sii: add a cert extracted from a real DTE Extract PEM-encoded data of the X.509 certificate inside XML document 'test_data/sii-dte/DTE--76399752-9--33--25568.xml' --- .../DTE--76399752-9--33--25568-cert.pem | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem 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----- From e9eb28f916bc977334d96f90db0ff76f9c78d20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 11:50:38 -0400 Subject: [PATCH 33/38] test_data.sii: add XML signature values extracted from real DTEs Extract XML signature value (base64-encoded binary data) from these XML documents: - 'test_data/sii-dte/DTE--76354771-K--33--170.xml' - 'test_data/sii-dte/DTE--76399752-9--33--25568.xml' --- .../DTE--76354771-K--33--170-signature-value-base64.txt | 1 + .../DTE--76399752-9--33--25568-signature-value-base64.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt create mode 100644 tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt 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-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== From 99e378286865c1eb7e3105641b8f91ab33729d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 24 Apr 2019 18:53:01 -0400 Subject: [PATCH 34/38] libs: add module `encoding_utils` --- cl_sii/libs/encoding_utils.py | 55 +++++++++++++++++++++++++++++++ tests/test_libs_encoding_utils.py | 18 ++++++++++ 2 files changed, 73 insertions(+) create mode 100644 cl_sii/libs/encoding_utils.py create mode 100644 tests/test_libs_encoding_utils.py 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/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 From 4e672189ed54e8cd1855e084c7277323fbc8d193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 10 Apr 2019 19:58:43 -0400 Subject: [PATCH 35/38] libs: add module `crypto_utils` Among others, it includes functions: - `load_pem_x509_cert` - `add_pem_cert_header_footer` - `remove_pem_cert_header_footer` Some tests have not been implemented yet. --- cl_sii/libs/crypto_utils.py | 61 +++ setup.cfg | 9 + tests/test_libs_crypto_utils.py | 644 ++++++++++++++++++++++++++++++++ 3 files changed, 714 insertions(+) create mode 100644 cl_sii/libs/crypto_utils.py create mode 100644 tests/test_libs_crypto_utils.py 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/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/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.", )) From 7d8e32ba06ba78965fcda909672637f0a9b33afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 10:56:34 -0400 Subject: [PATCH 36/38] dte.data_models: modify new fields of `DteDataL2` Modify 2 fields added to `DteDataL2` in 89196d3c (PR #21). That introduces some changes to function `parse_dte_xml`. Also includes a major reorganization of tests for module `parse`, improving separate coverage. --- cl_sii/dte/data_models.py | 50 +++-- cl_sii/dte/parse.py | 20 +- tests/test_dte_data_models.py | 8 +- tests/test_dte_parse.py | 362 +++++++++++++++++++--------------- 4 files changed, 256 insertions(+), 184 deletions(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index d7e43653..4591cda5 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -22,6 +22,7 @@ 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 @@ -79,9 +80,19 @@ def validate_clean_str(value: str) -> None: 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 (stripped) length is 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) @@ -327,14 +338,16 @@ class DteDataL2(DteDataL1): Datetime on which the "documento" was digitally signed. """ - signature_value_base64: Optional[str] = dc_field(default=None) + signature_value: Optional[bytes] = dc_field(default=None) """ - DTE's digital signature's value (as base64 str). + DTE's digital signature's value (raw bytes, without base64 encoding). """ - signature_x509_cert_base64: Optional[str] = dc_field(default=None) + signature_x509_cert_pem: Optional[bytes] = dc_field(default=None) """ - DTE's digital signature's X509 certificate (as base64 str). + DTE's digital signature's PEM-encoded X.509 cert. + + PEM-encoded implies base64-encoded. """ emisor_giro: Optional[str] = dc_field(default=None) @@ -377,21 +390,18 @@ def __post_init__(self) -> None: if not isinstance(self.firma_documento_dt_naive, datetime): raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.") - if self.signature_value_base64 is not None: - if not isinstance(self.signature_value_base64, str): - raise TypeError("Inappropriate type of 'signature_value_base64'.") - # TODO: validate that it is base64 - # TODO: bytes? - validate_clean_str(self.signature_value_base64) - validate_non_empty_str(self.signature_value_base64) - - if self.signature_x509_cert_base64 is not None: - if not isinstance(self.signature_x509_cert_base64, str): - raise TypeError("Inappropriate type of 'signature_x509_cert_base64'.") - # TODO: validate that it is base64 - # TODO: bytes? - validate_clean_str(self.signature_x509_cert_base64) - validate_non_empty_str(self.signature_x509_cert_base64) + 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): diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index c8956ba4..5cd5831f 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -23,8 +23,9 @@ 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 +from cl_sii.libs.xml_utils import XmlElement, XmlElementTree from cl_sii.rut import Rut from . import constants from . import data_models @@ -121,12 +122,19 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: 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`. + if not isinstance(xml_doc, (XmlElement, XmlElementTree)): + raise TypeError("'xml_doc' must be an 'XmlElement'.") + xml_em = xml_doc ########################################################################### @@ -441,8 +449,10 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text) - signature_signature_value_base64 = signature_signature_value_em.text.strip() - signature_key_info_x509_cert_base64 = signature_key_info_x509_cert_em.text.strip() + 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, @@ -455,8 +465,8 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: receptor_razon_social=receptor_razon_social_value, fecha_vencimiento_date=fecha_vencimiento_value, firma_documento_dt_naive=tmst_firma_value, - signature_value_base64=signature_signature_value_base64, - signature_x509_cert_base64=signature_key_info_x509_cert_base64, + 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, diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 45040d98..d0d4acad 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -151,8 +151,8 @@ def setUp(self) -> None: receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), - signature_value_base64=None, - signature_x509_cert_base64=None, + signature_value=None, + signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', emisor_email='hello@example.com', receptor_email=None, @@ -176,8 +176,8 @@ def test_as_dict(self) -> None: receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), - signature_value_base64=None, - signature_x509_cert_base64=None, + signature_value=None, + signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', emisor_email='hello@example.com', receptor_email=None, diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index 85c4de83..1944f17d 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -4,6 +4,8 @@ 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,92 +18,95 @@ from .utils import read_test_file_bytes -_TEST_DTE_NEEDS_CLEAN_1_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml' -_TEST_DTE_NEEDS_CLEAN_2_FILE_PATH = 'test_data/sii-dte/DTE--76399752-9--33--25568.xml' -_TEST_DTE_1_FILE_PATH = _TEST_DTE_NEEDS_CLEAN_1_FILE_PATH -_TEST_DTE_2_FILE_PATH = _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH -_TEST_DTE_1_SIGNATURE_VALUE = ( - 'fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk\n' - 'ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi\n' - 'wqSxDcYjTT6vXsLPrZk=') -_TEST_DTE_2_SIGNATURE_VALUE = ( - 'wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD\n' - 'MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4\n' - 'FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1\n' - 'GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp\n' - '0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==') -_TEST_DTE_1_X509_CERT = ( - 'MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx\n' - 'HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE\n' - 'ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD\n' - 'EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW\n' - 'GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa\n' - 'MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh\n' - 'MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE\n' - 'CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM\n' - 'b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI\n' - 'hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx\n' - 'O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC\n' - '/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3\n' - 'FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV\n' - 'HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S\n' - 'ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu\n' - 'Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6\n' - 'Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1\n' - 'MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC\n' - 'AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu\n' - 'Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp\n' - 'AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A\n' - 'IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk\n' - 'AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA\n' - 'cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA\n' - 'mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD\n' - 'qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un\n' - '+/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud\n' - 'B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY\n' - '+kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==') -_TEST_DTE_2_X509_CERT = ( - 'MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK\n' - 'Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj\n' - 'YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh\n' - 'Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0\n' - 'MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT\n' - 'GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu\n' - 'YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD\n' - 'ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1\n' - 'g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP\n' - 'HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh\n' - 'qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS\n' - 'EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G\n' - 'A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L\n' - 'WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG\n' - '+EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v\n' - 'YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t\n' - 'IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr\n' - 'BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R\n' - 'BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt\n' - 'OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw\n' - 'OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H\n' - 'NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz\n' - 'ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV\n' - 'WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/\n' - 'w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB\n' - 'B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg\n' - 'jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw') - - class OthersTest(unittest.TestCase): def test_DTE_XML_SCHEMA_OBJ(self) -> None: # TODO: implement pass - def test_integration_ok_1(self) -> None: - # TODO: split in separate tests, with more coverage. - dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_1_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') - file_bytes = read_test_file_bytes(dte_bad_xml_file_path) + 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): + + @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( @@ -115,8 +120,6 @@ def test_integration_ok_1(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, @@ -128,10 +131,6 @@ def test_integration_ok_1(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() @@ -139,27 +138,6 @@ def test_integration_ok_1(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, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), - signature_value_base64=_TEST_DTE_1_SIGNATURE_VALUE, - signature_x509_cert_base64=_TEST_DTE_1_X509_CERT, - emisor_giro='Ingenieria y Construccion', - emisor_email='ENACONLTDA@GMAIL.COM', - receptor_email=None, - )) expected_file_bytes_diff = ( b'--- \n', @@ -201,12 +179,8 @@ def test_integration_ok_1(self) -> None: expected_file_bytes_diff ) - def test_integration_ok_2(self) -> None: - # TODO: split in separate tests, with more coverage. - - dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH - - file_bytes = read_test_file_bytes(dte_bad_xml_file_path) + 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) self.assertEqual( @@ -220,8 +194,6 @@ def test_integration_ok_2(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, @@ -233,10 +205,6 @@ def test_integration_ok_2(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() @@ -244,27 +212,6 @@ def test_integration_ok_2(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('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_base64=_TEST_DTE_2_SIGNATURE_VALUE, - signature_x509_cert_base64=_TEST_DTE_2_X509_CERT, - emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', - emisor_email='ANGEL.PEZO@APCASESORIAS.CL', - receptor_email=None, - )) expected_file_bytes_diff = ( b'--- \n', @@ -306,41 +253,146 @@ def test_integration_ok_2(self) -> None: expected_file_bytes_diff ) - -class FunctionCleanDteXmlTest(unittest.TestCase): - - def test_clean_dte_xml_ok(self) -> None: - # TODO: implement - pass - 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) -class FunctionValidateDteXmlTest(unittest.TestCase): + 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, + )) + + def test_parse_dte_xml_fail_x(self) -> None: + # TODO: implement more cases + pass - # TODO: implement - 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) + + with self.assertRaises(ValueError) as cm: + parse_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Top level XML element 'Document' is required.", ) + ) From 9df0300b108eb1ff63bb61f90f4fa1271b5f61a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 15:13:50 -0400 Subject: [PATCH 37/38] HISTORY: update for new version --- HISTORY.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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) +++++++++++++++++++++++ From 6eebdb80d7320f3d755aa78225e4b068f39ae0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 15:14:01 -0400 Subject: [PATCH 38/38] =?UTF-8?q?Bump=20version:=200.4.0=20=E2=86=92=200.5?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cl_sii/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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'