Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

![full workflow](https://github.com/Truelite/python-a38/actions/workflows/py.yml/badge.svg)

<a href="https://pypi.org/project/a38/"><img alt="PyPI" src="https://img.shields.io/pypi/v/a38"></a>

Library to generate Italian Fattura Elettronica from Python.

This library implements a declarative data model similar to Django models, that
Expand All @@ -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

Expand All @@ -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
Expand Down
53 changes: 53 additions & 0 deletions a38/vies.py
Original file line number Diff line number Diff line change
@@ -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']}")
25 changes: 25 additions & 0 deletions a38tool
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions requirements-lib.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ python-dateutil
pytz
asn1crypto
defusedxml
lxml
requests
34 changes: 34 additions & 0 deletions tests/test_vies.py
Original file line number Diff line number Diff line change
@@ -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)