Browse Source

first working version

main
Holger Frey 2 years ago
parent
commit
0a50b18ee4
  1. 5
      pyproject.toml
  2. 152
      tests/carl-roth-1.xml
  3. 124
      tests/fisher-1.xml
  4. 96
      tests/test_xinvoice.py
  5. 65
      xinvoice/__init__.py

5
pyproject.toml

@ -29,9 +29,14 @@ classifiers = [ @@ -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"

152
tests/carl-roth-1.xml

@ -0,0 +1,152 @@ @@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:sac="urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:sbc="urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ProfileID>XRechnung 2.0</cbc:ProfileID>
<cbc:ID>44334826</cbc:ID>
<cbc:IssueDate>2022-07-13</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cbc:TaxCurrencyCode>EUR</cbc:TaxCurrencyCode>
<cbc:BuyerReference>9930: 08311000-DE142116817-24</cbc:BuyerReference>
<cac:OrderReference>
<cbc:ID>4100107978</cbc:ID>
<cbc:SalesOrderID>24347050</cbc:SalesOrderID>
</cac:OrderReference>
<cac:DespatchDocumentReference>
<cbc:ID>36383746</cbc:ID>
</cac:DespatchDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Carl Roth GmbH + Co.KG</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Schoemperlenstraße 1-5</cbc:StreetName>
<cbc:CityName>Karlsruhe</cbc:CityName>
<cbc:PostalZone>76185</cbc:PostalZone>
<cbc:CountrySubentity>DE</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE143621073</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Carl Roth GmbH + Co.KG</cbc:RegistrationName>
<cbc:CompanyID>CarlRoth</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>Nadine Tänzer</cbc:Name>
<cbc:Telephone>+49/721/5606-136</cbc:Telephone>
<cbc:ElectronicMail>n.taenzer@carlroth.de</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyIdentification>
<cbc:ID>ALBEFRE3</cbc:ID>
</cac:PartyIdentification>
<cac:PostalAddress>
<cbc:StreetName>Postfach</cbc:StreetName>
<cbc:CityName>Freiburg</cbc:CityName>
<cbc:PostalZone>79085</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Albert-Ludwigs-Universität Freiburg Finanzbuchhaltung u. Universitätskasse</cbc:RegistrationName>
<cbc:CompanyID>ALBEFRE3</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:Delivery>
<cbc:ActualDeliveryDate>2022-07-01</cbc:ActualDeliveryDate>
<cac:DeliveryLocation>
<cac:Address>
<cbc:StreetName>Stefan-Meier-Str. 31</cbc:StreetName>
<cbc:CityName>Freiburg im Breisgau</cbc:CityName>
<cbc:PostalZone>79104</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
<cac:DeliveryParty>
<cac:PartyName>
<cbc:Name>Albert-Ludwigs-Universität Freiburg Institut für Makromolekulare Chemie Leitung Magazin / Calvino, Stock:-02 Raum: 018</cbc:Name>
</cac:PartyName>
</cac:DeliveryParty>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="Credit transfer">30</cbc:PaymentMeansCode>
<cbc:PaymentID>44334826</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID schemeID="IBAN">DE52660100750000180751</cbc:ID>
<cbc:Name>Carl Roth GmbH + Co.KG</cbc:Name>
<cac:FinancialInstitutionBranch>
<cbc:ID>PBNKDEFF660</cbc:ID>
</cac:FinancialInstitutionBranch>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>14 Tage 2%, 30 Tage netto</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">4.51</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">23.72</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">4.51</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">23.72</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">23.72</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">28.23</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount currencyID="EUR">0</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount currencyID="EUR">0</cbc:ChargeTotalAmount>
<cbc:PrepaidAmount currencyID="EUR">0</cbc:PrepaidAmount>
<cbc:PayableRoundingAmount currencyID="EUR">0</cbc:PayableRoundingAmount>
<cbc:PayableAmount currencyID="EUR">28.23</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>50</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">23.72</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Schwefelsäure 96 % rein </cbc:Description>
<cbc:Name>Schwefelsäure 96 %</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>9316.1</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">23.72</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:Amount currencyID="EUR">4.18</cbc:Amount>
<cbc:BaseAmount currencyID="EUR">27.90</cbc:BaseAmount>
</cac:AllowanceCharge>
</cac:Price>
</cac:InvoiceLine>
</Invoice>

124
tests/fisher-1.xml

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:ccts="urn:un:unece:uncefact:documentation:2" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:qdt="urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2" xmlns:udt="urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>4023255950</cbc:ID>
<cbc:IssueDate>2022-07-11</cbc:IssueDate>
<cbc:DueDate>2022-08-10</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:Note>21 Tage 3%, 30 Tage netto</cbc:Note>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:OrderReference>
<cbc:ID>4100108427</cbc:ID>
<cbc:SalesOrderID>1022710610</cbc:SalesOrderID>
</cac:OrderReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="9930">DE166515700</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Fisher Scientific GmbH</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Im Heiligen Feld 17</cbc:StreetName>
<cbc:CityName>Schwerte</cbc:CityName>
<cbc:PostalZone>58239</cbc:PostalZone>
<cbc:CountrySubentity>NRW</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE166515700</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Fisher Scientific GmbH</cbc:RegistrationName>
<cbc:CompanyID>DE166515700</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>Debitorenbuchhaltung</cbc:Name>
<cbc:Telephone>+492304932899</cbc:Telephone>
<cbc:ElectronicMail>debitoren.desch@thermofisher.com</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="9930">DE142116817</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Albert-Ludwigs-Universität Freiburg, Finanzbuchhaltung und Universitätskasse</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Postfach</cbc:StreetName>
<cbc:CityName>Freiburg</cbc:CityName>
<cbc:PostalZone>79085</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE142116817</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Albert-Ludwigs-Universität Freiburg, Finanzbuchhaltung und Universitätskasse</cbc:RegistrationName>
<cbc:CompanyID>DE142116817</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:ElectronicMail>XRechnung@zv.uni-freiburg.de</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
<cac:PayeeFinancialAccount>
<cbc:ID>DE61506700090030390900</cbc:ID>
<cac:FinancialInstitutionBranch>
<cbc:ID>DEUTDEFF506</cbc:ID>
</cac:FinancialInstitutionBranch>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">34.61</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">182.16</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">34.61</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.00</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">182.16</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">182.16</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">216.77</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">216.77</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">182.16</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>MTT, 1g, (3(4,5dimethylthiazol2yl) 2,</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.00</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">182.16</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>

96
tests/test_xinvoice.py

@ -24,18 +24,92 @@ mistakes. @@ -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

65
xinvoice/__init__.py

@ -4,3 +4,68 @@ Parsing X-Invoice files @@ -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))

Loading…
Cancel
Save