From 1152699c05513b76b50d46102f91dd90a4f17d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 Apr 2020 19:38:41 -0400 Subject: [PATCH 01/17] requirements: update dependencies of some packages i.e. dependencies' dependencies. --- requirements/base.txt | 10 +++++----- requirements/release.txt | 11 +++++++---- requirements/test.txt | 8 ++++++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 231da77b..37a1e64b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -35,13 +35,13 @@ signxml==2.6.0 # - six asn1crypto==1.3.0 attrs==19.3.0 -certifi==2019.11.28 -cffi==1.13.2 +certifi==2020.4.5.1 +cffi==1.14.0 eight==0.4.2 future==0.16.0 importlib-metadata==0.23 -pycparser==2.19 -pyrsistent==0.15.7 +pycparser==2.20 +pyrsistent==0.16.0 # setuptools six==1.14.0 -zipp==2.1.0 +zipp==3.1.0 diff --git a/requirements/release.txt b/requirements/release.txt index 9a0d0dbe..2acf1abd 100644 --- a/requirements/release.txt +++ b/requirements/release.txt @@ -21,9 +21,11 @@ wheel==0.34.2 # - pkginfo # - readme-renderer: # - bleach +# - six # - webencodings # - docutils # - Pygments +# - six # - requests: # - certifi # - chardet @@ -40,14 +42,15 @@ wheel==0.34.2 # idna importlib-metadata==0.23 # jeepney -keyring==21.1.0 +keyring==21.2.0 pkginfo==1.5.0.1 # Pygments -readme-renderer==24.0 -requests==2.22.0 +readme-renderer==25.0 +requests==2.23.0 requests-toolbelt==0.9.1 # SecretStorage -tqdm==4.42.0 +# six +tqdm==4.45.0 # urllib3 # webencodings # zipp diff --git a/requirements/test.txt b/requirements/test.txt index b26df23b..33f70de2 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -35,15 +35,19 @@ tox==3.13.1 # - py # - toml # - virtualenv +# - appdirs +# - distlib +# appdirs # certifi # chardet +# distlib entrypoints==0.3 filelock==3.0.12 # idna importlib-metadata==0.23 mccabe==0.6.1 mypy-extensions==0.4.3 -packaging==20.1 +packaging==20.3 pluggy==0.13.1 py==1.8.1 pycodestyle==2.5.0 @@ -53,5 +57,5 @@ pyflakes==2.1.1 toml==0.10.0 typed-ast==1.4.1 # urllib3 -virtualenv==16.7.9 +virtualenv==20.0.16 # zipp From 6994111882b259e71d2aa2f099bd75cd9e2d871a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 Apr 2020 19:40:50 -0400 Subject: [PATCH 02/17] 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 2acf1abd..724b754e 100644 --- a/requirements/release.txt +++ b/requirements/release.txt @@ -4,7 +4,7 @@ # Required packages: bumpversion==0.5.3 -setuptools==45.1.0 +setuptools==46.1.3 twine==3.1.1 wheel==0.34.2 From 3bb9e8515a393c1ae5dcf4c2da079219e56037ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 Apr 2020 10:16:20 -0400 Subject: [PATCH 03/17] requirements: update 'tox' (test) Because the new 'tox' version no longer requires `importlib-metadata<1` we can update that dependency as well. Changelog: - 3.14.6 (2020-03-25) https://github.com/tox-dev/tox/blob/3.14.6/docs/changelog.rst#v3146-2020-03-25 ... - 3.14.0 (2019-09-03) https://github.com/tox-dev/tox/blob/3.14.6/docs/changelog.rst#v3132-2019-07-01 - 3.13.2 (2019-07-01) https://github.com/tox-dev/tox/blob/3.14.6/docs/changelog.rst#v3132-2019-07-01 Code diff: https://github.com/tox-dev/tox/compare/3.13.1...3.14.6 --- requirements/base.txt | 2 +- requirements/release.txt | 2 +- requirements/test.txt | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 37a1e64b..4319cf1f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -39,7 +39,7 @@ certifi==2020.4.5.1 cffi==1.14.0 eight==0.4.2 future==0.16.0 -importlib-metadata==0.23 +importlib-metadata==1.6.0 pycparser==2.20 pyrsistent==0.16.0 # setuptools diff --git a/requirements/release.txt b/requirements/release.txt index 724b754e..84a17473 100644 --- a/requirements/release.txt +++ b/requirements/release.txt @@ -40,7 +40,7 @@ wheel==0.34.2 # cryptography # docutils # idna -importlib-metadata==0.23 +importlib-metadata==1.6.0 # jeepney keyring==21.2.0 pkginfo==1.5.0.1 diff --git a/requirements/test.txt b/requirements/test.txt index 33f70de2..03b4972f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -7,7 +7,7 @@ codecov==2.0.15 coverage==4.5.3 flake8==3.7.7 mypy==0.711 -tox==3.13.1 +tox==3.14.6 # Packages dependencies: # - codecov: @@ -33,10 +33,14 @@ tox==3.13.1 # - pyparsing # - pluggy # - py +# - six # - toml # - virtualenv # - appdirs # - distlib +# - filelock +# - importlib-metadata +# - six # appdirs # certifi # chardet @@ -44,7 +48,7 @@ tox==3.13.1 entrypoints==0.3 filelock==3.0.12 # idna -importlib-metadata==0.23 +importlib-metadata==1.6.0 mccabe==0.6.1 mypy-extensions==0.4.3 packaging==20.3 @@ -54,6 +58,7 @@ pycodestyle==2.5.0 pyflakes==2.1.1 # pyparsing # requests +# six toml==0.10.0 typed-ast==1.4.1 # urllib3 From 9c02394bfb5cd2aaf5fe8067c1e9021f1f7bb8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 Apr 2020 20:27:24 -0400 Subject: [PATCH 04/17] libs.csv_utils: prepare for 'mypy' update It is required for upgrading 'mypy' from 0.711 to 0.770. --- cl_sii/libs/csv_utils.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cl_sii/libs/csv_utils.py b/cl_sii/libs/csv_utils.py index c3a910d9..f8fa0a47 100644 --- a/cl_sii/libs/csv_utils.py +++ b/cl_sii/libs/csv_utils.py @@ -22,14 +22,7 @@ def create_csv_dict_reader( :return: a CSV DictReader """ - # note: mypy wrongly complains: it does not accept 'fieldnames' to be None but that value - # is completely acceptable, and it even is the default! - # > error: Argument "fieldnames" to "DictReader" has incompatible type "None"; expected - # > "Sequence[str]" - # note: mypy wrongly complains: - # > Argument "dialect" to "DictReader" has incompatible type "Type[Dialect]"; - # > expected "Union[str, Dialect]" - csv_reader = csv.DictReader( # type: ignore + csv_reader = csv.DictReader( text_stream, fieldnames=None, # the values of the first row will be used as the fieldnames restkey=row_dict_extra_fields_key, @@ -38,6 +31,10 @@ def create_csv_dict_reader( if expected_fields_strict: if expected_field_names: + if csv_reader.fieldnames is None: + raise Exception( + "Programming error: when a 'csv.DictReader' instance is created with" + "'fieldnames=None', the attribute will be set to the values of the first row.") if tuple(csv_reader.fieldnames) != expected_field_names: raise ValueError( "CSV file field names do not match those expected, or their order.", From 836985d2ba2718e5bfb77ad1d516a7aff249d187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 Apr 2020 10:21:24 -0400 Subject: [PATCH 05/17] requirements: update 'mypy' (test) (plus some dependencies) Changelog: - 0.770 (2020-03-10) http://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html - 0.761 (2019-12-19) N/A - 0.760 (2019-12-17) http://mypy-lang.blogspot.com/2019/12/mypy-0760-released.html - 0.750 (2019-11-29) http://mypy-lang.blogspot.com/2019/11/mypy-0.html - 0.740 (2019-10-16) http://mypy-lang.blogspot.com/2019/10/mypy-0740-released.html - 0.730 (2019-09-26) http://mypy-lang.blogspot.com/2019/09/mypy-730-released.html - 0.720 (2019-07-12) http://mypy-lang.blogspot.com/2019/07/mypy-0720-released.html Code diff: https://github.com/python/mypy/compare/v0.711...v0.770 --- requirements/test.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements/test.txt b/requirements/test.txt index 03b4972f..b452143d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,7 +6,7 @@ codecov==2.0.15 coverage==4.5.3 flake8==3.7.7 -mypy==0.711 +mypy==0.770 tox==3.14.6 # Packages dependencies: @@ -25,6 +25,7 @@ tox==3.14.6 # - mypy: # - mypy-extensions # - typed-ast +# - typing-extensions # - tox: # - filelock # - importlib-metadata @@ -61,6 +62,7 @@ pyflakes==2.1.1 # six toml==0.10.0 typed-ast==1.4.1 +typing-extensions==3.7.4.2 # urllib3 virtualenv==20.0.16 # zipp From 0588c88e261ef29068f1d03b676c831d3aa3c0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 Apr 2020 10:22:37 -0400 Subject: [PATCH 06/17] requirements: update 'flake8' (test) Changelog: - 3.7.9 (2019-10-28) https://gitlab.com/pycqa/flake8/-/blob/3.7.9/docs/source/release-notes/3.7.9.rst - 3.7.8 (2919-07-08) https://gitlab.com/pycqa/flake8/-/blob/3.7.8/docs/source/release-notes/3.7.8.rst Code diff: https://gitlab.com/pycqa/flake8/compare/3.7.7...3.7.9 --- requirements/test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test.txt b/requirements/test.txt index b452143d..2af3ca8f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -5,7 +5,7 @@ # Required packages: codecov==2.0.15 coverage==4.5.3 -flake8==3.7.7 +flake8==3.7.9 mypy==0.770 tox==3.14.6 From 549a1f13592c4d7995ef064e366799584f43142e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 Apr 2020 10:22:46 -0400 Subject: [PATCH 07/17] requirements: update 'codecov' (test) Changelog: - 2.0.22 (2020-03-18) https://github.com/codecov/codecov-python/blob/v2.0.22/CHANGELOG.md#2022 ... - 2.0.16 (2020-02-24) https://github.com/codecov/codecov-python/blob/v2.0.22/CHANGELOG.md#2016 Code diff: https://github.com/codecov/codecov-python/compare/v2.0.15...v2.0.22 --- requirements/test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test.txt b/requirements/test.txt index 2af3ca8f..afdfb56c 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -3,7 +3,7 @@ -r extras.txt # Required packages: -codecov==2.0.15 +codecov==2.0.22 coverage==4.5.3 flake8==3.7.9 mypy==0.770 From f9bad92a36cc6d6287cbc9da8de67486d38e8791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 Apr 2020 18:57:40 -0400 Subject: [PATCH 08/17] libs.crypto_utils: prepare tests for 'cryptography' update It is required for upgrading 'cryptography' from 2.8 to 2.9. In version 2.9 the value returned by `cryptography.x509.Certificate.subject.rfc4514_string()` has changed, but we shouldn't really care (the sections order in the string has been reversed). --- tests/test_libs_crypto_utils.py | 62 --------------------------------- 1 file changed, 62 deletions(-) diff --git a/tests/test_libs_crypto_utils.py b/tests/test_libs_crypto_utils.py index 2ed1ac72..95fb7f05 100644 --- a/tests/test_libs_crypto_utils.py +++ b/tests/test_libs_crypto_utils.py @@ -80,11 +80,6 @@ def test_load_der_x509_cert_ok(self) -> None: ####################################################################### 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') @@ -100,13 +95,6 @@ def test_load_der_x509_cert_ok(self) -> None: ####################################################################### 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') @@ -257,15 +245,6 @@ def test_load_der_x509_cert_ok_cert_real_dte_1(self) -> None: ####################################################################### 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') @@ -293,15 +272,6 @@ def test_load_der_x509_cert_ok_cert_real_dte_1(self) -> None: ####################################################################### 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') @@ -482,14 +452,6 @@ def test_load_der_x509_cert_ok_cert_real_dte_3(self) -> None: ####################################################################### self.assertEqual(len(x509_cert.issuer.rdns), 5) - self.assertEqual( - x509_cert.issuer.rfc4514_string(), - 'C=CL,' - 'O=E-Sign S.A.,' - 'OU=Terms of use at www.esign-la.com/acuerdoterceros,' - 'CN=E-Sign Class 2 Firma Tributaria CA,' - '1.2.840.113549.1.9.1=e-sign@esign-la.com') - self.assertEqual( x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, 'CL') @@ -511,13 +473,6 @@ def test_load_der_x509_cert_ok_cert_real_dte_3(self) -> None: ####################################################################### self.assertEqual(len(x509_cert.subject.rdns), 5) - self.assertEqual( - x509_cert.subject.rfc4514_string(), - 'C=CL,' - 'O=E-Sign S.A.,' - 'OU=Terms of use at www.esign-la.com/acuerdoterceros,' - 'CN=Jorge Enrique Cabello Ortiz,' - '1.2.840.113549.1.9.1=jcabello@nic.cl') self.assertEqual( x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, 'CL') @@ -683,14 +638,6 @@ def test_load_der_x509_cert_ok_prueba_sii(self) -> None: ####################################################################### 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') @@ -715,15 +662,6 @@ def test_load_der_x509_cert_ok_prueba_sii(self) -> None: ####################################################################### 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') From b335c6db6c65fa93cdff8051eb21d20be8650500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 Apr 2020 20:18:32 -0400 Subject: [PATCH 09/17] requirements: update 'cryptography' Changelog: - 2.9 (2020-04-02) https://github.com/pyca/cryptography/blob/2.9/CHANGELOG.rst#29---2020-04-02 Code diff: https://github.com/pyca/cryptography/compare/2.8...2.9 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 4319cf1f..039cb903 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,7 +2,7 @@ # note: it is mandatory to register all dependencies of the required packages. # Required packages: -cryptography==2.8 +cryptography==2.9 defusedxml==0.6.0 jsonschema==3.1.1 lxml==4.5.0 From add3f78f4c1fa13e9c9f35b5ee9759bb77553080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 Apr 2020 19:07:13 -0400 Subject: [PATCH 10/17] requirements: update 'jsonschema' Also, a dependency is marked as conditional, based on https://github.com/Julian/jsonschema/blob/v3.2.0/setup.cfg#L31 Changelog: - 3.2.0 (2019-11-18) https://github.com/Julian/jsonschema/blob/v3.2.0/CHANGELOG.rst#v320 Code diff: https://github.com/Julian/jsonschema/compare/v3.1.1...v3.2.0 --- requirements/base.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 039cb903..b8e1b349 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # Required packages: cryptography==2.9 defusedxml==0.6.0 -jsonschema==3.1.1 +jsonschema==3.2.0 lxml==4.5.0 marshmallow==2.19.5 pyOpenSSL==18.0.0 @@ -17,12 +17,12 @@ signxml==2.6.0 # - pycparser # - six # - jsonschema -# - setuptools -# - six # - attrs -# - importlib-metadata +# - importlib-metadata (python_version<'3.8') # - zipp # - pyrsistent +# - setuptools +# - six # - signxml: # - asn1crypto # - certifi @@ -39,7 +39,7 @@ certifi==2020.4.5.1 cffi==1.14.0 eight==0.4.2 future==0.16.0 -importlib-metadata==1.6.0 +importlib-metadata==1.6.0; python_version<'3.8' pycparser==2.20 pyrsistent==0.16.0 # setuptools From 53868efd234577145cdd0cc76fb334da6878f448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sat, 11 Apr 2020 14:10:35 -0400 Subject: [PATCH 11/17] dte.data_models: duplicate `DteDataL2` as `DteXmlData` This is the first step of making possible to have separate models without breaking other project's code. --- cl_sii/dte/data_models.py | 149 +++++++++++++++++++++++++++ tests/test_dte_data_models.py | 184 +++++++++++++++++++++++++++++++++- 2 files changed, 332 insertions(+), 1 deletion(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 235bd2f2..45cc2489 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -324,6 +324,155 @@ class DteDataL2(DteDataL1): """ DTE data level 2. + Very similar to :class:`DteXmlData` (and a lot of duplicated code, + unfortunately). + + About fields + - ``emisor_razon_social``: redundant but required by the DTE XML schema. + - ``receptor_razon_social``: redundant but required by the DTE XML schema. + - ``fecha_vencimiento`` (date): important for some business logic + but it is not required by the DTE XML schema. + + The class instances are immutable. + + """ + + ########################################################################### + # constants + ########################################################################### + + DATETIME_FIELDS_TZ = SII_OFFICIAL_TZ + + ########################################################################### + # fields + ########################################################################### + + emisor_razon_social: str = dc_field() + """ + "Razón social" (legal name) of the "emisor" of the DTE. + """ + + receptor_razon_social: str = dc_field() + """ + "Razón social" (legal name) of the "receptor" of the DTE. + """ + + fecha_vencimiento_date: Optional[date] = dc_field(default=None) + """ + "Fecha de vencimiento (pago)" of the DTE. + """ + + firma_documento_dt: Optional[datetime] = dc_field(default=None) + """ + Datetime on which the "documento" was digitally signed. + """ + + signature_value: Optional[bytes] = dc_field(default=None) + """ + DTE's digital signature's value (raw bytes, without base64 encoding). + """ + + signature_x509_cert_der: Optional[bytes] = dc_field(default=None) + """ + DTE's digital signature's DER-encoded X.509 cert. + + .. seealso:: + Functions :func:`cl_sii.libs.crypto_utils.load_der_x509_cert` + and :func:`cl_sii.libs.crypto_utils.x509_cert_der_to_pem`. + """ + + 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. + + :raises TypeError, ValueError: + + """ + super().__post_init__() + + if not isinstance(self.emisor_razon_social, str): + raise TypeError("Inappropriate type of 'emisor_razon_social'.") + validate_contribuyente_razon_social(self.emisor_razon_social) + + if not isinstance(self.receptor_razon_social, str): + raise TypeError("Inappropriate type of 'receptor_razon_social'.") + validate_contribuyente_razon_social(self.receptor_razon_social) + + 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 is not None: + if not isinstance(self.firma_documento_dt, datetime): + raise TypeError("Inappropriate type of 'firma_documento_dt'.") + tz_utils.validate_dt_tz(self.firma_documento_dt, self.DATETIME_FIELDS_TZ) + + if self.signature_value is not None: + if not isinstance(self.signature_value, bytes): + raise TypeError("Inappropriate type of 'signature_value'.") + # warning: do NOT strip a bytes value because "strip" implies an ASCII-encoded text, + # which in this case it is not. + validate_non_empty_bytes(self.signature_value) + + if self.signature_x509_cert_der is not None: + if not isinstance(self.signature_x509_cert_der, bytes): + raise TypeError("Inappropriate type of 'signature_x509_cert_der'.") + # warning: do NOT strip a bytes value because "strip" implies an ASCII-encoded text, + # which in this case it is not. + validate_non_empty_bytes(self.signature_x509_cert_der) + + 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) + + def as_dte_data_l1(self) -> DteDataL1: + return DteDataL1( + emisor_rut=self.emisor_rut, + tipo_dte=self.tipo_dte, + folio=self.folio, + fecha_emision_date=self.fecha_emision_date, + receptor_rut=self.receptor_rut, + monto_total=self.monto_total) + + +@dataclasses.dataclass(frozen=True) +class DteXmlData(DteDataL1): + + """ + DTE XML data. + + Very similar to :class:`DteDataL2` (and a lot of duplicated code, + unfortunately). + About fields - ``emisor_razon_social``: redundant but required by the DTE XML schema. - ``receptor_razon_social``: redundant but required by the DTE XML schema. diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 620362a1..e7c8118e 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -12,7 +12,7 @@ TipoDteEnum, ) from cl_sii.dte.data_models import ( # noqa: F401 - DteDataL0, DteDataL1, DteDataL2, DteNaturalKey, + DteDataL0, DteDataL1, DteDataL2, DteNaturalKey, DteXmlData, validate_contribuyente_razon_social, validate_dte_folio, validate_dte_monto_total, ) @@ -214,6 +214,12 @@ def setUp(self) -> None: receptor_email=None, ) + def test_constants_match(self): + self.assertEqual( + DteXmlData.DATETIME_FIELDS_TZ, + DteDataL2.DATETIME_FIELDS_TZ, + ) + def test_init_fail(self) -> None: # TODO: implement for 'DteDataL2()' pass @@ -327,6 +333,182 @@ def test_as_dte_data_l1(self) -> None: ) +class DteXmlDataTest(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_1_xml_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.dte_1_xml_cert_der = read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der') + cls.dte_2_xml_signature_value = encoding_utils.decode_base64_strict(read_test_file_bytes( + 'test_data/sii-crypto/DTE--60910000-1--33--2336600-signature-value-base64.txt')) + cls.dte_2_xml_cert_der = read_test_file_bytes( + 'test_data/sii-crypto/DTE--60910000-1--33--2336600-cert.der') + + def setUp(self) -> None: + super().setUp() + + self.dte_xml_data_1 = DteXmlData( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteXmlData.DATETIME_FIELDS_TZ), + signature_value=self.dte_1_xml_signature_value, + signature_x509_cert_der=self.dte_1_xml_cert_der, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, + ) + self.dte_xml_data_2 = DteXmlData( + emisor_rut=Rut('60910000-1'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=2336600, + fecha_emision_date=date(2019, 8, 8), + receptor_rut=Rut('76555835-2'), + monto_total=10642, + emisor_razon_social='Universidad de Chile', + receptor_razon_social='FYNPAL SPA', + fecha_vencimiento_date=date(2019, 8, 8), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 8, 9, 9, 41, 9), + tz=DteXmlData.DATETIME_FIELDS_TZ), + signature_value=self.dte_2_xml_signature_value, + signature_x509_cert_der=self.dte_2_xml_cert_der, + emisor_giro='Corporación Educacional y Servicios Profesionales', + emisor_email=None, + receptor_email=None, + ) + + def test_constants_match(self): + self.assertEqual( + DteXmlData.DATETIME_FIELDS_TZ, + DteDataL2.DATETIME_FIELDS_TZ, + ) + + def test_init_fail(self) -> None: + # TODO: implement for 'DteXmlData()' + pass + + def test_init_fail_regression_signature_value_bytes_with_x20(self) -> None: + bytes_value_with_x20_as_base64 = 'IN2pkDBxqDnGl4Pfvboi' + bytes_value_with_x20 = b'\x20\xdd\xa9\x900q\xa89\xc6\x97\x83\xdf\xbd\xba"' + + self.assertEqual(b'\x20', b' ') + self.assertEqual( + bytes_value_with_x20, + base64.b64decode(bytes_value_with_x20_as_base64, validate=True)) + + init_kwars = self.dte_xml_data_1.as_dict() + init_kwars.update(dict(signature_value=bytes_value_with_x20)) + + # with self.assertRaises(ValueError) as cm: + # _ = DteXmlData(**init_kwars) + # self.assertEqual( + # cm.exception.args, + # ('Value has leading or trailing whitespace characters.', bytes_value_with_x20) + # ) + _ = DteXmlData(**init_kwars) + + def test_init_fail_regression_signature_cert_der_bytes_with_x20(self) -> None: + bytes_value_with_x20_as_base64 = 'IN2pkDBxqDnGl4Pfvboi' + bytes_value_with_x20 = b'\x20\xdd\xa9\x900q\xa89\xc6\x97\x83\xdf\xbd\xba"' + + self.assertEqual(b'\x20', b' ') + self.assertEqual( + bytes_value_with_x20, + base64.b64decode(bytes_value_with_x20_as_base64, validate=True)) + + init_kwars = self.dte_xml_data_1.as_dict() + init_kwars.update(dict(signature_x509_cert_der=bytes_value_with_x20)) + + # with self.assertRaises(ValueError) as cm: + # _ = DteXmlData(**init_kwars) + # self.assertEqual( + # cm.exception.args, + # ('Value has leading or trailing whitespace characters.', bytes_value_with_x20) + # ) + _ = DteXmlData(**init_kwars) + + def test_as_dict(self) -> None: + self.assertDictEqual( + self.dte_xml_data_1.as_dict(), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteXmlData.DATETIME_FIELDS_TZ), + signature_value=self.dte_1_xml_signature_value, + signature_x509_cert_der=self.dte_1_xml_cert_der, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, + )) + self.assertDictEqual( + self.dte_xml_data_2.as_dict(), + dict( + emisor_rut=Rut('60910000-1'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=2336600, + fecha_emision_date=date(2019, 8, 8), + receptor_rut=Rut('76555835-2'), + monto_total=10642, + emisor_razon_social='Universidad de Chile', + receptor_razon_social='FYNPAL SPA', + fecha_vencimiento_date=date(2019, 8, 8), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 8, 9, 9, 41, 9), + tz=DteXmlData.DATETIME_FIELDS_TZ), + signature_value=self.dte_2_xml_signature_value, + signature_x509_cert_der=self.dte_2_xml_cert_der, + emisor_giro='Corporación Educacional y Servicios Profesionales', + emisor_email=None, + receptor_email=None, + )) + + def test_as_dte_data_l1(self) -> None: + self.assertEqual( + self.dte_xml_data_1.as_dte_data_l1(), + 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, + ) + ) + self.assertEqual( + self.dte_xml_data_2.as_dte_data_l1(), + DteDataL1( + emisor_rut=Rut('60910000-1'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=2336600, + fecha_emision_date=date(2019, 8, 8), + receptor_rut=Rut('76555835-2'), + monto_total=10642, + ) + ) + + class FunctionsTest(unittest.TestCase): def test_validate_contribuyente_razon_social(self) -> None: From 33b613a626796f4acb54c2106dffe348aa9b291f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sat, 11 Apr 2020 14:16:24 -0400 Subject: [PATCH 12/17] dte.data_models: add `DteXmlData.as_dte_data_l2` --- cl_sii/dte/data_models.py | 19 +++++++++++++++ tests/test_dte_data_models.py | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 45cc2489..be44f07a 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -608,3 +608,22 @@ def as_dte_data_l1(self) -> DteDataL1: fecha_emision_date=self.fecha_emision_date, receptor_rut=self.receptor_rut, monto_total=self.monto_total) + + def as_dte_data_l2(self) -> DteDataL2: + return DteDataL2( + emisor_rut=self.emisor_rut, + tipo_dte=self.tipo_dte, + folio=self.folio, + fecha_emision_date=self.fecha_emision_date, + receptor_rut=self.receptor_rut, + monto_total=self.monto_total, + emisor_razon_social=self.emisor_razon_social, + receptor_razon_social=self.receptor_razon_social, + fecha_vencimiento_date=self.fecha_vencimiento_date, + firma_documento_dt=self.firma_documento_dt, + signature_value=self.signature_value, + signature_x509_cert_der=self.signature_x509_cert_der, + emisor_giro=self.emisor_giro, + emisor_email=self.emisor_email, + receptor_email=self.receptor_email, + ) diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index e7c8118e..55fdc192 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -508,6 +508,52 @@ def test_as_dte_data_l1(self) -> None: ) ) + def test_as_dte_data_l2(self) -> None: + self.assertEqual( + self.dte_xml_data_1.as_dte_data_l2(), + DteDataL2( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteXmlData.DATETIME_FIELDS_TZ), + signature_value=self.dte_1_xml_signature_value, + signature_x509_cert_der=self.dte_1_xml_cert_der, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, + ) + ) + self.assertEqual( + self.dte_xml_data_2.as_dte_data_l2(), + DteDataL2( + emisor_rut=Rut('60910000-1'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=2336600, + fecha_emision_date=date(2019, 8, 8), + receptor_rut=Rut('76555835-2'), + monto_total=10642, + emisor_razon_social='Universidad de Chile', + receptor_razon_social='FYNPAL SPA', + fecha_vencimiento_date=date(2019, 8, 8), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 8, 9, 9, 41, 9), + tz=DteXmlData.DATETIME_FIELDS_TZ), + signature_value=self.dte_2_xml_signature_value, + signature_x509_cert_der=self.dte_2_xml_cert_der, + emisor_giro='Corporación Educacional y Servicios Profesionales', + emisor_email=None, + receptor_email=None, + ) + ) + class FunctionsTest(unittest.TestCase): From 7c41e82871c20cf8945572e8ed89ffd04ea879ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sat, 11 Apr 2020 21:50:54 -0400 Subject: [PATCH 13/17] dte.data_models: add test cases --- tests/test_dte_data_models.py | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 55fdc192..2af47153 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -224,6 +224,34 @@ def test_init_fail(self) -> None: # TODO: implement for 'DteDataL2()' pass + def test_init_fail_razon_social_empty(self) -> None: + with self.assertRaises(ValueError) as cm: + dataclasses.replace( + self.dte_l2_1, + emisor_razon_social='', + ) + self.assertEqual(cm.exception.args, ("Value must not be empty.", )) + with self.assertRaises(ValueError) as cm: + dataclasses.replace( + self.dte_l2_1, + receptor_razon_social='', + ) + self.assertEqual(cm.exception.args, ("Value must not be empty.", )) + + def test_init_fail_razon_social_none(self) -> None: + with self.assertRaises(TypeError) as cm: + dataclasses.replace( + self.dte_l2_1, + emisor_razon_social=None, + ) + self.assertEqual(cm.exception.args, ("Inappropriate type of 'emisor_razon_social'.", )) + with self.assertRaises(TypeError) as cm: + dataclasses.replace( + self.dte_l2_1, + receptor_razon_social=None, + ) + self.assertEqual(cm.exception.args, ("Inappropriate type of 'receptor_razon_social'.", )) + def test_init_fail_regression_signature_value_bytes_with_x20(self) -> None: bytes_value_with_x20_as_base64 = 'IN2pkDBxqDnGl4Pfvboi' bytes_value_with_x20 = b'\x20\xdd\xa9\x900q\xa89\xc6\x97\x83\xdf\xbd\xba"' @@ -400,6 +428,34 @@ def test_init_fail(self) -> None: # TODO: implement for 'DteXmlData()' pass + def test_init_fail_razon_social_empty(self) -> None: + with self.assertRaises(ValueError) as cm: + dataclasses.replace( + self.dte_xml_data_1, + emisor_razon_social='', + ) + self.assertEqual(cm.exception.args, ("Value must not be empty.", )) + with self.assertRaises(ValueError) as cm: + dataclasses.replace( + self.dte_xml_data_1, + receptor_razon_social='', + ) + self.assertEqual(cm.exception.args, ("Value must not be empty.", )) + + def test_init_fail_razon_social_none(self) -> None: + with self.assertRaises(TypeError) as cm: + dataclasses.replace( + self.dte_xml_data_1, + emisor_razon_social=None, + ) + self.assertEqual(cm.exception.args, ("Inappropriate type of 'emisor_razon_social'.", )) + with self.assertRaises(TypeError) as cm: + dataclasses.replace( + self.dte_xml_data_1, + receptor_razon_social=None, + ) + self.assertEqual(cm.exception.args, ("Inappropriate type of 'receptor_razon_social'.", )) + def test_init_fail_regression_signature_value_bytes_with_x20(self) -> None: bytes_value_with_x20_as_base64 = 'IN2pkDBxqDnGl4Pfvboi' bytes_value_with_x20 = b'\x20\xdd\xa9\x900q\xa89\xc6\x97\x83\xdf\xbd\xba"' From 2cf2fbedf66a076b679a64b744afda5666bd5515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sat, 11 Apr 2020 14:23:15 -0400 Subject: [PATCH 14/17] dte: change `parse_dte_xml` return type to `DteXmlData` --- cl_sii/dte/parse.py | 10 ++++------ tests/test_dte_parse.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index 39b42f1e..8f581937 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -14,7 +14,7 @@ >>> parse.clean_dte_xml(xml_doc) True >>> parse.validate_dte_xml(xml_doc) ->>> dte_struct = parse.parse_dte_xml(xml_doc) +>>> dte_xml_data = parse.parse_dte_xml(xml_doc) """ import io @@ -114,8 +114,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: +def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: """ Parse data from a DTE XML doc. @@ -128,7 +127,6 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: :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_csv.RcvVentaCsvRowSchema`. @@ -455,14 +453,14 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: tmst_firma_value = tz_utils.convert_naive_dt_to_tz_aware( dt=datetime.fromisoformat(_text_strip_or_raise(tmst_firma_em)), - tz=data_models.DteDataL2.DATETIME_FIELDS_TZ) + tz=data_models.DteXmlData.DATETIME_FIELDS_TZ) signature_signature_value = encoding_utils.decode_base64_strict( _text_strip_or_raise(signature_signature_value_em)) signature_key_info_x509_cert_der = encoding_utils.decode_base64_strict( _text_strip_or_raise(signature_key_info_x509_cert_em)) - return data_models.DteDataL2( + return data_models.DteXmlData( emisor_rut=emisor_rut_value, tipo_dte=tipo_dte_value, folio=folio_value, diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index 86af7646..bd78b977 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -456,9 +456,9 @@ def test_data(self): 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) + dte_xml_data = parse_dte_xml(xml_doc) self.assertDictEqual( - dict(parsed_dte.as_dict()), + dict(dte_xml_data.as_dict()), dict( emisor_rut=Rut('76354771-K'), tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, @@ -482,9 +482,9 @@ def test_parse_dte_xml_ok_1(self) -> None: def test_parse_dte_xml_ok_3(self) -> None: xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_3_xml_bytes) - parsed_dte = parse_dte_xml(xml_doc) + dte_xml_data = parse_dte_xml(xml_doc) self.assertDictEqual( - dict(parsed_dte.as_dict()), + dict(dte_xml_data.as_dict()), dict( emisor_rut=Rut('60910000-1'), tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, @@ -508,9 +508,9 @@ def test_parse_dte_xml_ok_3(self) -> None: def test_parse_dte_xml_ok_1b(self) -> None: xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1b_xml_bytes) - parsed_dte = parse_dte_xml(xml_doc) + dte_xml_data = parse_dte_xml(xml_doc) self.assertDictEqual( - dict(parsed_dte.as_dict()), + dict(dte_xml_data.as_dict()), dict( emisor_rut=Rut('76354771-K'), tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, @@ -534,9 +534,9 @@ def test_parse_dte_xml_ok_1b(self) -> None: def test_parse_dte_xml_ok_2(self) -> None: xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes) - parsed_dte = parse_dte_xml(xml_doc) + dte_xml_data = parse_dte_xml(xml_doc) self.assertDictEqual( - dict(parsed_dte.as_dict()), + dict(dte_xml_data.as_dict()), dict( emisor_rut=Rut('76399752-9'), tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, From 52c0c0dd28e77d132cdfe36259a2311923d4815a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Fri, 10 Apr 2020 17:23:14 -0400 Subject: [PATCH 15/17] rcv.parse_csv: move code from 'fd-cl-data' in here Function `get_rcv_csv_file_parser` and its related type `RcvCsvFileParserType`. Source: https://github.com/fyntex/fd-cl-data/blob/ab4469402c8e4d50656f443eb93c900127f27bdd/fd_cl_data/apps/sii_rcv/models.py#L899-L952 --- cl_sii/rcv/parse_csv.py | 57 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/cl_sii/rcv/parse_csv.py b/cl_sii/rcv/parse_csv.py index 5f6c9894..e4f811f7 100644 --- a/cl_sii/rcv/parse_csv.py +++ b/cl_sii/rcv/parse_csv.py @@ -7,7 +7,7 @@ import csv import logging from datetime import date, datetime -from typing import Dict, Iterable, Optional, Sequence, Tuple +from typing import Callable, Dict, Iterable, Optional, Sequence, Tuple import marshmallow import marshmallow.fields @@ -22,6 +22,7 @@ from cl_sii.libs import tz_utils from cl_sii.rut import Rut +from .constants import RcEstadoContable, RcvKind from .data_models import ( RcvDetalleEntry, RcNoIncluirDetalleEntry, RcPendienteDetalleEntry, RcReclamadoDetalleEntry, @@ -32,6 +33,60 @@ logger = logging.getLogger(__name__) +RcvCsvFileParserType = Callable[ + [Rut, str, str, int, Optional[int]], + Iterable[ + Tuple[ + Optional[RcvDetalleEntry], + int, + Dict[str, object], + Dict[str, object], + ], + ], +] + + +def get_rcv_csv_file_parser( + rcv_kind: RcvKind, + estado_contable: Optional[RcEstadoContable], +) -> RcvCsvFileParserType: + """ + Return a function that parses a CSV file of the given :class:`RcvKind` and + :class:`RcEstadoContable`. + + :raises ValueError: + :raises Exception: on unrecoverable errors + """ + parse_func: RcvCsvFileParserType + + if rcv_kind == RcvKind.COMPRAS: + if estado_contable is None: + raise ValueError( + "'estado_contable' must not be None when 'rcv_kind' is 'COMPRAS'.", + ) + elif estado_contable == RcEstadoContable.REGISTRO: + parse_func = parse_rcv_compra_registro_csv_file + elif estado_contable == RcEstadoContable.NO_INCLUIR: + parse_func = parse_rcv_compra_no_incluir_csv_file + elif estado_contable == RcEstadoContable.RECLAMADO: + parse_func = parse_rcv_compra_reclamado_csv_file + elif estado_contable == RcEstadoContable.PENDIENTE: + parse_func = parse_rcv_compra_pendiente_csv_file + else: + raise Exception( + "Programming error. No handler for given 'estado_contable'.", + estado_contable, + ) + elif rcv_kind == RcvKind.VENTAS: + if estado_contable is not None: + raise ValueError("'estado_contable' must be None when 'rcv_kind' is 'VENTAS'.") + parse_func = parse_rcv_venta_csv_file + else: + raise Exception("Programming error. No handler for given 'rcv_kind'.", rcv_kind) + + return parse_func + + def parse_rcv_venta_csv_file( rut: Rut, razon_social: str, From 9ec487234dd81d6ae3e1fca828d80ae9a97aeb56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Sat, 11 Apr 2020 21:57:45 -0400 Subject: [PATCH 16/17] rcv.data_models: move some fields to subclasses Move fields `emisor_razon_social` and `receptor_razon_social` of `RcvDetalleEntry` to its subclasses, where they will be modified later. --- cl_sii/rcv/data_models.py | 95 +++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/cl_sii/rcv/data_models.py b/cl_sii/rcv/data_models.py index 04382360..3bf416d4 100644 --- a/cl_sii/rcv/data_models.py +++ b/cl_sii/rcv/data_models.py @@ -131,15 +131,6 @@ class RcvDetalleEntry: Total amount of the "documento". """ - emisor_razon_social: str = dc_field() - """ - "Razón social" (legal name) of the "emisor" of the "documento". - """ - - # TODO: docstring - # TODO: can it be None? What happens for those "tipo docto" that do not have a receptor? - receptor_razon_social: str = dc_field() - # TODO: docstring # note: must be timezone-aware. fecha_recepcion_dt: datetime = dc_field() @@ -181,14 +172,6 @@ def __post_init__(self) -> None: if not isinstance(self.monto_total, int): raise TypeError("Inappropriate type of 'monto_total'.") - if not isinstance(self.emisor_razon_social, str): - raise TypeError("Inappropriate type of 'emisor_razon_social'.") - cl_sii.dte.data_models.validate_contribuyente_razon_social(self.emisor_razon_social) - - if not isinstance(self.receptor_razon_social, str): - raise TypeError("Inappropriate type of 'receptor_razon_social'.") - cl_sii.dte.data_models.validate_contribuyente_razon_social(self.receptor_razon_social) - if not isinstance(self.fecha_recepcion_dt, datetime): raise TypeError("Inappropriate type of 'fecha_recepcion_dt'.") tz_utils.validate_dt_tz(self.fecha_recepcion_dt, SII_OFFICIAL_TZ) @@ -205,6 +188,9 @@ def as_dte_data_l2(self) -> cl_sii.dte.data_models.DteDataL2: try: tipo_dte = self.tipo_docto.as_tipo_dte() + emisor_razon_social = getattr(self, 'emisor_razon_social', None) + receptor_razon_social = getattr(self, 'receptor_razon_social', None) + dte_data = cl_sii.dte.data_models.DteDataL2( emisor_rut=self.emisor_rut, tipo_dte=tipo_dte, @@ -212,8 +198,8 @@ def as_dte_data_l2(self) -> cl_sii.dte.data_models.DteDataL2: fecha_emision_date=self.fecha_emision_date, receptor_rut=self.receptor_rut, monto_total=self.monto_total, - emisor_razon_social=self.emisor_razon_social, - receptor_razon_social=self.receptor_razon_social, + emisor_razon_social=emisor_razon_social, + receptor_razon_social=receptor_razon_social, # fecha_vencimiento_date='', # firma_documento_dt='', # signature_value='', @@ -238,6 +224,15 @@ class RvDetalleEntry(RcvDetalleEntry): RCV_KIND = RcvKind.VENTAS RC_ESTADO_CONTABLE = None + emisor_razon_social: str = dc_field() + """ + "Razón social" (legal name) of the "emisor" of the "documento". + """ + + # TODO: docstring + # TODO: can it be None? What happens for those "tipo docto" that do not have a receptor? + receptor_razon_social: str = dc_field() + # TODO: docstring # note: must be timezone-aware. fecha_acuse_dt: Optional[datetime] = dc_field() @@ -249,6 +244,14 @@ class RvDetalleEntry(RcvDetalleEntry): def __post_init__(self) -> None: super().__post_init__() + if not isinstance(self.emisor_razon_social, str): + raise TypeError("Inappropriate type of 'emisor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.emisor_razon_social) + + if not isinstance(self.receptor_razon_social, str): + raise TypeError("Inappropriate type of 'receptor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.receptor_razon_social) + if self.fecha_acuse_dt is not None: if not isinstance(self.fecha_acuse_dt, datetime): raise TypeError("Inappropriate type of 'fecha_acuse_dt'.") @@ -270,6 +273,15 @@ class RcRegistroDetalleEntry(RcvDetalleEntry): RCV_KIND = RcvKind.COMPRAS RC_ESTADO_CONTABLE = RcEstadoContable.REGISTRO + emisor_razon_social: str = dc_field() + """ + "Razón social" (legal name) of the "emisor" of the "documento". + """ + + # TODO: docstring + # TODO: can it be None? What happens for those "tipo docto" that do not have a receptor? + receptor_razon_social: str = dc_field() + # TODO: docstring # note: must be timezone-aware. fecha_acuse_dt: Optional[datetime] = dc_field() @@ -277,6 +289,14 @@ class RcRegistroDetalleEntry(RcvDetalleEntry): def __post_init__(self) -> None: super().__post_init__() + if not isinstance(self.emisor_razon_social, str): + raise TypeError("Inappropriate type of 'emisor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.emisor_razon_social) + + if not isinstance(self.receptor_razon_social, str): + raise TypeError("Inappropriate type of 'receptor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.receptor_razon_social) + if self.fecha_acuse_dt is not None: if not isinstance(self.fecha_acuse_dt, datetime): raise TypeError("Inappropriate type of 'fecha_acuse_dt'.") @@ -304,6 +324,15 @@ class RcReclamadoDetalleEntry(RcvDetalleEntry): RCV_KIND = RcvKind.COMPRAS RC_ESTADO_CONTABLE = RcEstadoContable.RECLAMADO + emisor_razon_social: str = dc_field() + """ + "Razón social" (legal name) of the "emisor" of the "documento". + """ + + # TODO: docstring + # TODO: can it be None? What happens for those "tipo docto" that do not have a receptor? + receptor_razon_social: str = dc_field() + # TODO: docstring # note: must be timezone-aware. fecha_reclamo_dt: Optional[datetime] = dc_field() @@ -311,6 +340,14 @@ class RcReclamadoDetalleEntry(RcvDetalleEntry): def __post_init__(self) -> None: super().__post_init__() + if not isinstance(self.emisor_razon_social, str): + raise TypeError("Inappropriate type of 'emisor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.emisor_razon_social) + + if not isinstance(self.receptor_razon_social, str): + raise TypeError("Inappropriate type of 'receptor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.receptor_razon_social) + if self.fecha_reclamo_dt is not None: if not isinstance(self.fecha_reclamo_dt, datetime): raise TypeError("Inappropriate type of 'fecha_reclamo_dt'.") @@ -326,3 +363,23 @@ class RcPendienteDetalleEntry(RcvDetalleEntry): RCV_KIND = RcvKind.COMPRAS RC_ESTADO_CONTABLE = RcEstadoContable.PENDIENTE + + emisor_razon_social: str = dc_field() + """ + "Razón social" (legal name) of the "emisor" of the "documento". + """ + + # TODO: docstring + # TODO: can it be None? What happens for those "tipo docto" that do not have a receptor? + receptor_razon_social: str = dc_field() + + def __post_init__(self) -> None: + super().__post_init__() + + if not isinstance(self.emisor_razon_social, str): + raise TypeError("Inappropriate type of 'emisor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.emisor_razon_social) + + if not isinstance(self.receptor_razon_social, str): + raise TypeError("Inappropriate type of 'receptor_razon_social'.") + cl_sii.dte.data_models.validate_contribuyente_razon_social(self.receptor_razon_social) From b94800661cc49260bcbfdbaafee660fa3482fce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Mon, 13 Apr 2020 16:43:18 -0400 Subject: [PATCH 17/17] =?UTF-8?q?Bump=20version:=200.9.1=20=E2=86=92=200.1?= =?UTF-8?q?0.0.a1?= 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 2f5c3b2a..e840a9c8 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.1 +current_version = 0.10.0.a1 commit = True tag = True diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py index 112d4912..d13abab4 100644 --- a/cl_sii/__init__.py +++ b/cl_sii/__init__.py @@ -5,4 +5,4 @@ """ -__version__ = '0.9.1' +__version__ = '0.10.0.a1'