From 011bedd1e2c546e4a1f52bdfddf4744d7cd233a3 Mon Sep 17 00:00:00 2001 From: Alessio Gottardo Date: Sat, 9 Apr 2022 14:42:48 +0200 Subject: [PATCH 1/4] VIES inspection via an HTTP request and HTML response parsing --- a38/vies.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ requirements-lib.txt | 1 + tests/test_vies.py | 31 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 a38/vies.py create mode 100644 tests/test_vies.py diff --git a/a38/vies.py b/a38/vies.py new file mode 100644 index 0000000..a2e6e5c --- /dev/null +++ b/a38/vies.py @@ -0,0 +1,47 @@ +import logging +from io import StringIO +from typing import List, Tuple + +import requests +from lxml import etree + + +def inspect_vat(vat_state_code: str, vat_number: str) -> Tuple[str, int]: + params = { + "memberStateCode": vat_state_code, + "number": vat_number, + } + res = requests.post( + "https://ec.europa.eu/taxation_customs/vies/vatResponse.html", + data=params, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "a38", + }, + timeout=5, + ) + html_content = res.text + return html_content, res.status_code + + +def get_vat_details(vat_state_code: str, vat_number: str) -> List: + html_content, _ = inspect_vat(vat_state_code, vat_number) + parser = etree.HTMLParser() + html_doc = etree.parse(StringIO(html_content), parser).getroot() + vat_details = [] + keys = html_doc.xpath('//div[@class="static-field"]/label') + values = html_doc.xpath('//div[@class="static-field"]/div') + if len(keys) != len(values): + logging.warning( + "There's a mismatch between the VIES categories and their content" + ) + + for idx, _ in enumerate(keys): + vat_details.append( + { + "detail": "".join(keys[idx].itertext()).strip(), + "content": "".join(values[idx].itertext()).strip(), + } + ) + + return vat_details diff --git a/requirements-lib.txt b/requirements-lib.txt index a414286..cd9b821 100644 --- a/requirements-lib.txt +++ b/requirements-lib.txt @@ -2,3 +2,4 @@ python-dateutil pytz asn1crypto defusedxml +requests diff --git a/tests/test_vies.py b/tests/test_vies.py new file mode 100644 index 0000000..1a9a027 --- /dev/null +++ b/tests/test_vies.py @@ -0,0 +1,31 @@ +from unittest import TestCase + +from a38.vies import get_vat_details, inspect_vat + +# curl -X POST https://ec.europa.eu/taxation_customs/vies/vatResponse.html \ +# --silent --show-error \ +# --header "Content-Type: application/x-www-form-urlencoded" \ +# --data "memberStateCode=IT&number=00934460049&traderName=&traderStreet=&traderPostalCode=&traderCity=&requesterMemberStateCode=&requesterNumber=&check=Verify&action=check" | \ +# grep -A2 --color 'static-field' + + +class TestVIESRetrieval(TestCase): + + sample_country_code_it = "IT" + sample_vat_num_ferrero = "00934460049" + + # PYTHONPATH=. nose2-3 -s tests test_vies + def test_api_call(self): + res_html, status = inspect_vat( + self.sample_country_code_it, self.sample_vat_num_ferrero + ) + assert status == 200 + assert len(res_html) > 0 + + # PYTHONPATH=. nose2-3 -s tests test_vies + def test_get_vat_details(self): + vat_details = get_vat_details( + self.sample_country_code_it, self.sample_vat_num_ferrero + ) + # print(vat_details) + assert len(vat_details) == 6 From edfad43f9c04e61fe7889d8fa0e58f5334d1ef95 Mon Sep 17 00:00:00 2001 From: Alessio Gottardo Date: Sat, 9 Apr 2022 15:26:42 +0200 Subject: [PATCH 2/4] VIES as CLI + PyPI badge --- README.md | 13 ++++++++----- a38tool | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8fae0b3..eb15c55 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ![full workflow](https://github.com/Truelite/python-a38/actions/workflows/py.yml/badge.svg) +PyPI + Library to generate Italian Fattura Elettronica from Python. This library implements a declarative data model similar to Django models, that @@ -35,23 +37,24 @@ A simple command line wrapper to the library functions is available as `a38tool` ```text $ a38tool --help -usage: a38tool [-h] [--verbose] [--debug] - {json,xml,python,diff,validate,html,pdf,update_capath} ... +usage: a38tool [-h] [--verbose] [--debug] {json,yaml,xml,python,edit,diff,validate,html,pdf,update_capath,vies} ... Handle fattura elettronica files positional arguments: - {json,xml,python,diff,validate,html,pdf,update_capath} + {json,yaml,xml,python,edit,diff,validate,html,pdf,update_capath,vies} actions json output a fattura in JSON + yaml output a fattura in JSON xml output a fattura in XML python output a fattura as Python code + edit Open a fattura for modification in a text editor diff show the difference between two fatture validate validate the contents of a fattura html render a Fattura as HTML using a .xslt stylesheet pdf render a Fattura as PDF using a .xslt stylesheet - update_capath create/update an openssl CApath with CA certificates - that can be used to validate digital signatures + update_capath create/update an openssl CApath with CA certificates that can be used to validate digital signatures + vies inspect the VIES (VAT Information Exchange System) details for a given VAT number optional arguments: -h, --help show this help message and exit diff --git a/a38tool b/a38tool index 785c9cb..59e1bf8 100755 --- a/a38tool +++ b/a38tool @@ -359,6 +359,30 @@ class UpdateCAPath(App): from a38 import trustedlist as tl tl.update_capath(self.destdir, remove_old=self.remove_old) +class VIES(App): + """ + inspect the VIES (VAT Information Exchange System) + details for a given VAT number + """ + NAME = "vies" + + def __init__(self, args): + super().__init__(args) + self.country_code = args.country_code + self.vat_number = args.vat_number + + @classmethod + def add_subparser(cls, subparsers): + parser = super().add_subparser(subparsers) + parser.add_argument("--country-code", action="store_true", help="VAT Country Code e.g. IT") + parser.add_argument("--vat-number", action="store_true", help="VAT Number e.g. 01234567890") + return parser + + def run(self): + from a38 import vies + vat_details = vies.get_vat_details(self.country_code, self.vat_number) + print(vat_details) + def main(): parser = argparse.ArgumentParser(description="Handle fattura elettronica files") @@ -378,6 +402,7 @@ def main(): RenderHTML.add_subparser(subparsers) RenderPDF.add_subparser(subparsers) UpdateCAPath.add_subparser(subparsers) + VIES.add_subparser(subparsers) args = parser.parse_args() From 25ebd7ce0176e4ecd0eb85bf6a35a19547ef6b62 Mon Sep 17 00:00:00 2001 From: Alessio Gottardo Date: Sat, 9 Apr 2022 15:36:42 +0200 Subject: [PATCH 3/4] Render VAT details as information in stdout --- a38/vies.py | 6 ++++++ a38tool | 2 +- tests/test_vies.py | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/a38/vies.py b/a38/vies.py index a2e6e5c..19e0d60 100644 --- a/a38/vies.py +++ b/a38/vies.py @@ -45,3 +45,9 @@ def get_vat_details(vat_state_code: str, vat_number: str) -> List: ) return vat_details + + +def render_vat_details(vat_details: dict): + print() + for vat_detail in vat_details: + print(f"\t{vat_detail['detail']}: {vat_detail['content']}") diff --git a/a38tool b/a38tool index 59e1bf8..931754d 100755 --- a/a38tool +++ b/a38tool @@ -381,7 +381,7 @@ class VIES(App): def run(self): from a38 import vies vat_details = vies.get_vat_details(self.country_code, self.vat_number) - print(vat_details) + vies.render_vat_details(vat_details) def main(): diff --git a/tests/test_vies.py b/tests/test_vies.py index 1a9a027..552feeb 100644 --- a/tests/test_vies.py +++ b/tests/test_vies.py @@ -29,3 +29,6 @@ def test_get_vat_details(self): ) # print(vat_details) assert len(vat_details) == 6 + + # from a38.vies import render_vat_details + # render_vat_details(vat_details) From 3fa2fe796ff6b9d9c87ca74f6ea075bcc4e5a52a Mon Sep 17 00:00:00 2001 From: Alessio Gottardo Date: Sat, 9 Apr 2022 15:42:31 +0200 Subject: [PATCH 4/4] Make lxml mandatory (because of the test coverage) --- README.md | 3 +-- requirements-lib.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb15c55..81d9ff6 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,10 @@ parse all the example XML files distributed by ## Dependencies -Required: dateutil, pytz, asn1crypto, and the python3 standard library. +Required: dateutil, pytz, asn1crypto, defusedxml, lxml, requests and the python3 standard library. Optional: * yapf for formatting `a38tool python` output - * lxml for rendering to HTML * the wkhtmltopdf command for rendering to PDF * requests for downloading CA certificates for signature verification diff --git a/requirements-lib.txt b/requirements-lib.txt index cd9b821..42db4f4 100644 --- a/requirements-lib.txt +++ b/requirements-lib.txt @@ -2,4 +2,5 @@ python-dateutil pytz asn1crypto defusedxml +lxml requests