diff --git a/pyproject.toml b/pyproject.toml index 99ca352..b993f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,14 @@ classifiers = [ dependencies = [ "beautifulsoup4", + "click", + "lxml", "pyperclip", ] +[project.scripts] +xinvoice = "xinvoice:main" + [project.urls] Source = "https://git.cpi.imtek.uni-freiburg.de/CPI/xinvoice.git" diff --git a/tests/carl-roth-1.xml b/tests/carl-roth-1.xml new file mode 100644 index 0000000..1cdf753 --- /dev/null +++ b/tests/carl-roth-1.xml @@ -0,0 +1,152 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0 + XRechnung 2.0 + 44334826 + 2022-07-13 + 380 + EUR + EUR + 9930: 08311000-DE142116817-24 + + 4100107978 + 24347050 + + + 36383746 + + + + + Carl Roth GmbH + Co.KG + + + Schoemperlenstraße 1-5 + Karlsruhe + 76185 + DE + + DE + + + + DE143621073 + + VAT + + + + Carl Roth GmbH + Co.KG + CarlRoth + + + Nadine Tänzer + +49/721/5606-136 + n.taenzer@carlroth.de + + + + + + + ALBEFRE3 + + + Postfach + Freiburg + 79085 + + DE + + + + Albert-Ludwigs-Universität Freiburg Finanzbuchhaltung u. Universitätskasse + ALBEFRE3 + + + + + 2022-07-01 + + + Stefan-Meier-Str. 31 + Freiburg im Breisgau + 79104 + + DE + + + + + + Albert-Ludwigs-Universität Freiburg Institut für Makromolekulare Chemie Leitung Magazin / Calvino, Stock:-02 Raum: 018 + + + + + 30 + 44334826 + + DE52660100750000180751 + Carl Roth GmbH + Co.KG + + PBNKDEFF660 + + + + + 14 Tage 2%, 30 Tage netto + + + 4.51 + + 23.72 + 4.51 + + S + 19 + + VAT + + + + + + 23.72 + 23.72 + 28.23 + 0 + 0 + 0 + 0 + 28.23 + + + 50 + 1 + 23.72 + + Schwefelsäure 96 % rein + Schwefelsäure 96 % + + 9316.1 + + + S + 19 + + VAT + + + + + 23.72 + 1 + + false + 4.18 + 27.90 + + + + diff --git a/tests/fisher-1.xml b/tests/fisher-1.xml new file mode 100644 index 0000000..098ca86 --- /dev/null +++ b/tests/fisher-1.xml @@ -0,0 +1,124 @@ + + + 2.1 + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + 4023255950 + 2022-07-11 + 2022-08-10 + 380 + 21 Tage 3%, 30 Tage netto + EUR + + 4100108427 + 1022710610 + + + + DE166515700 + + Fisher Scientific GmbH + + + Im Heiligen Feld 17 + Schwerte + 58239 + NRW + + DE + + + + DE166515700 + + VAT + + + + Fisher Scientific GmbH + DE166515700 + + + Debitorenbuchhaltung + +492304932899 + debitoren.desch@thermofisher.com + + + + + + DE142116817 + + Albert-Ludwigs-Universität Freiburg, Finanzbuchhaltung und Universitätskasse + + + Postfach + Freiburg + 79085 + + DE + + + + DE142116817 + + VAT + + + + Albert-Ludwigs-Universität Freiburg, Finanzbuchhaltung und Universitätskasse + DE142116817 + + + XRechnung@zv.uni-freiburg.de + + + + + 30 + + DE61506700090030390900 + + DEUTDEFF506 + + + + + 34.61 + + 182.16 + 34.61 + + S + 19.00 + + VAT + + + + + + 182.16 + 182.16 + 216.77 + 216.77 + + + 1 + 1.0 + 182.16 + + MTT, 1g, (3(4,5dimethylthiazol2yl) 2, + + S + 19.00 + + VAT + + + + + 182.16 + + + diff --git a/tests/test_xinvoice.py b/tests/test_xinvoice.py index 72ce5b2..2b2ac85 100644 --- a/tests/test_xinvoice.py +++ b/tests/test_xinvoice.py @@ -24,18 +24,92 @@ mistakes. import pytest -def test_example_unittest(): - """ example unittest +@pytest.fixture +def root_dir(request): + import pathlib - will be run by 'make test' and 'make testall' but not 'make coverage' - """ - assert True + return pathlib.Path(request.path).parent -@pytest.mark.functional -def test_example_functional_test(): - """ example unittest +def test_open_file(root_dir): + from bs4 import BeautifulSoup - will be by 'make coverage' and 'make testall' but not 'make test' - """ - assert True + from xinvoice import open_invoice + + result = open_invoice(root_dir / "carl-roth-1.xml") + + assert isinstance(result, BeautifulSoup) + + +@pytest.mark.parametrize( + "file_name, recipient", [("carl-roth-1.xml", "Calvino")] +) +def test_get_recipient_with_name(root_dir, file_name, recipient): + from xinvoice import open_invoice, get_recipient + + soup = open_invoice(root_dir / file_name) + + result = get_recipient(soup) + + assert recipient in result + + +@pytest.mark.parametrize("file_name", ["fisher-1.xml"]) +def test_get_recipient_without_name(root_dir, file_name): + from xinvoice import open_invoice, get_recipient + + soup = open_invoice(root_dir / file_name) + + result = get_recipient(soup) + + assert result is None + + +@pytest.mark.parametrize( + "file_name, expected_items", + [ + ("carl-roth-1.xml", ["Schwefelsäure 96 %"]), + ("fisher-1.xml", ["MTT, 1g, (3(4,5dimethylthiazol2yl) 2,"]), + ], +) +def test_get_items(root_dir, file_name, expected_items): + from xinvoice import get_items, open_invoice + + soup = open_invoice(root_dir / file_name) + + result = get_items(soup) + + assert result == expected_items + + +@pytest.mark.parametrize( + "file_name, expected_name", + [ + ("carl-roth-1.xml", "Céline"), + ("fisher-1.xml", "+++ UNKNOWN +++"), + ], +) +def test_get_recipient_short_name(root_dir, file_name, expected_name): + from xinvoice import open_invoice, get_recipient_short_name + + soup = open_invoice(root_dir / file_name) + + result = get_recipient_short_name(soup) + + assert result == expected_name + + +@pytest.mark.parametrize( + "file_name, expected_content", + [ + ("carl-roth-1.xml", ["Céline", "Schwefelsäure"]), + ("fisher-1.xml", ["UNKNOWN", "MTT"]), + ], +) +def test_parse(root_dir, file_name, expected_content): + from xinvoice import parse + + result = parse(root_dir / file_name) + + for part in expected_content: + assert part in result diff --git a/xinvoice/__init__.py b/xinvoice/__init__.py index 628bba7..67b77b8 100644 --- a/xinvoice/__init__.py +++ b/xinvoice/__init__.py @@ -4,3 +4,68 @@ Parsing X-Invoice files """ __version__ = "0.0.1" + +# from bs4 import BeautifulSoup + +import click +import pyperclip +from bs4 import BeautifulSoup + + +def open_invoice(file_path): + with open(file_path, "r") as handle: + return BeautifulSoup(handle, "xml") + + +def drill_down(soup, tags): + current_tag, *rest_tags = tags + findings = soup.find_all(current_tag) + if not findings: + yield None + elif not rest_tags: + yield from (f.string for f in findings) + else: + for child in findings: + yield from drill_down(child, rest_tags) + + +def get_recipient(soup): + results = drill_down( + soup, ["cac:DeliveryParty", "cac:PartyName", "cbc:Name"] + ) + return next(results) + + +def get_recipient_short_name(soup): + full_text = get_recipient(soup) + if full_text is None: + return "+++ UNKNOWN +++" + full_text = full_text.lower() + if "calvino" in full_text: + return "Céline" + if "pappas" in full_text: + return "Babis" + if "slesarenko" in full_text: + return "Slava" + return "CPI" + + +def get_items(soup): + result = drill_down(soup, ["cac:InvoiceLine", "cac:Item", "cbc:Name"]) + return list(result) + + +def parse(file_path): + soup = open_invoice(file_path) + recipient = get_recipient_short_name(soup) + items = get_items(soup) + lines = [f"for {recipient}:"] + items + text = "\n".join(lines) + pyperclip.copy(text) + return text + + +@click.command() +@click.argument("file_path", type=click.Path(exists=True)) +def main(file_path): + print(parse(file_path))