diff --git a/README.md b/README.md index 8fae0b3..81d9ff6 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 @@ -20,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 @@ -35,23 +36,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/a38/vies.py b/a38/vies.py new file mode 100644 index 0000000..19e0d60 --- /dev/null +++ b/a38/vies.py @@ -0,0 +1,53 @@ +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 + + +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 785c9cb..931754d 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) + vies.render_vat_details(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() diff --git a/requirements-lib.txt b/requirements-lib.txt index a414286..42db4f4 100644 --- a/requirements-lib.txt +++ b/requirements-lib.txt @@ -2,3 +2,5 @@ python-dateutil pytz asn1crypto defusedxml +lxml +requests diff --git a/tests/test_vies.py b/tests/test_vies.py new file mode 100644 index 0000000..552feeb --- /dev/null +++ b/tests/test_vies.py @@ -0,0 +1,34 @@ +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 a38.vies import render_vat_details + # render_vat_details(vat_details)