Just messing around

This commit is contained in:
shamoon 2024-12-20 15:54:42 -08:00
parent 452ea2ccf9
commit 01706dd391
14 changed files with 577 additions and 2 deletions

View File

@ -32,7 +32,7 @@ repos:
rev: v2.3.0
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)|(^src/paperless_einvoice/tests/samples/)|(^src/paperless_einvoice/templates/)"
exclude_types:
- pofile
- json

View File

@ -20,6 +20,7 @@ django-multiselectfield = "*"
django-soft-delete = "*"
djangorestframework = "~=3.15.2"
djangorestframework-guardian = "*"
drafthorse = "*"
drf-writable-nested = "*"
bleach = "*"
celery = {extras = ["redis"], version = "*"}

18
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "148cd379b8ceeb857ea817bea1432821d8ea20ffe8d0bfc04c89871a87b87b0f"
"sha256": "bf51fa211450fbf310e070739f225429fd7947eb72ba2a48b6bc92a1cd4cb51e"
},
"pipfile-spec": 6,
"requires": {},
@ -629,6 +629,14 @@
"index": "pypi",
"version": "==0.3.0"
},
"drafthorse": {
"hashes": [
"sha256:4d16a6dd60708676465e63ac7ff1b5f140e8c1c58be7d0eda66bc309814fc5c5",
"sha256:e67d9f21bbada2282e5f63257c01cc56f8cb2667000f67e68bbddf6956484375"
],
"index": "pypi",
"version": "==2.4.0"
},
"drf-writable-nested": {
"hashes": [
"sha256:d8ddc606dc349e56373810842965712a5789e6a5ca7704729d15429b95f8f2ee"
@ -1668,6 +1676,14 @@
"markers": "python_version >= '3.8'",
"version": "==2.9.0"
},
"pypdf": {
"hashes": [
"sha256:3bd4f503f4ebc58bae40d81e81a9176c400cbbac2ba2d877367595fb524dfdfc",
"sha256:425a129abb1614183fd1aca6982f650b47f8026867c0ce7c4b9f281c443d2740"
],
"markers": "python_version >= '3.8'",
"version": "==5.1.0"
},
"python-dateutil": {
"hashes": [
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",

View File

@ -1089,6 +1089,10 @@ TIKA_GOTENBERG_ENDPOINT = os.getenv(
if TIKA_ENABLED:
INSTALLED_APPS.append("paperless_tika.apps.PaperlessTikaConfig")
EINVOICE_PARSER_ENABLED = __get_boolean("PAPERLESS_EINVOICE_PARSER_ENABLED", "NO")
if EINVOICE_PARSER_ENABLED and TIKA_ENABLED:
INSTALLED_APPS.append("paperless_einvoice.apps.PaperlessEInvoiceConfig")
AUDIT_LOG_ENABLED = __get_boolean("PAPERLESS_AUDIT_LOG_ENABLED", "true")
if AUDIT_LOG_ENABLED:
INSTALLED_APPS.append("auditlog")

View File

View File

@ -0,0 +1,15 @@
from django.apps import AppConfig
from django.conf import settings
from paperless_einvoice.signals import einvoice_consumer_declaration
class PaperlessEInvoiceConfig(AppConfig):
name = "paperless_einvoice"
def ready(self):
from documents.signals import document_consumer_declaration
if settings.TIKA_ENABLED and settings.EINVOICE_PARSER_ENABLED:
document_consumer_declaration.connect(einvoice_consumer_declaration)
AppConfig.ready(self)

View File

@ -0,0 +1,86 @@
from pathlib import Path
from django.conf import settings
from drafthorse.models.document import Document
from gotenberg_client import GotenbergClient
from gotenberg_client.options import MarginType
from gotenberg_client.options import MarginUnitType
from gotenberg_client.options import PageMarginsType
from gotenberg_client.options import PageSize
from gotenberg_client.options import PdfAFormat
from jinja2 import Template
from documents.parsers import ParseError
from paperless.models import OutputTypeChoices
from paperless_tika.parsers import TikaDocumentParser
class EInvoiceDocumentParser(TikaDocumentParser):
"""
This parser parses e-invoices using Tika and Gotenberg
"""
logging_name = "paperless.parsing.einvoice"
def convert_to_pdf(self, document_path: Path, file_name):
pdf_path = Path(self.tempdir) / "convert.pdf"
self.log.info(f"Converting {document_path} to PDF as {pdf_path}")
with document_path.open("r") as f:
xml = f.read().encode("utf-8")
invoice = Document.parse(xml)
context = {
"id": invoice.trade.agreement.seller.name,
}
template = Template("templates/invoice.j2.html")
html_file = Path(self.tempdir) / "invoice_as_html.html"
html_file.write_text(
template.render(context),
)
with (
GotenbergClient(
host=settings.TIKA_GOTENBERG_ENDPOINT,
timeout=settings.CELERY_TASK_TIME_LIMIT,
) as client,
client.chromium.html_to_pdf() as route,
):
# Set the output format of the resulting PDF
if settings.OCR_OUTPUT_TYPE in {
OutputTypeChoices.PDF_A,
OutputTypeChoices.PDF_A2,
}:
route.pdf_format(PdfAFormat.A2b)
elif settings.OCR_OUTPUT_TYPE == OutputTypeChoices.PDF_A1:
self.log.warning(
"Gotenberg does not support PDF/A-1a, choosing PDF/A-2b instead",
)
route.pdf_format(PdfAFormat.A2b)
elif settings.OCR_OUTPUT_TYPE == OutputTypeChoices.PDF_A3:
route.pdf_format(PdfAFormat.A3b)
try:
response = (
route.index(html_file)
.resource(Path(__file__).parent / "templates" / "invoice.css")
.margins(
PageMarginsType(
top=MarginType(0.1, MarginUnitType.Inches),
bottom=MarginType(0.1, MarginUnitType.Inches),
left=MarginType(0.1, MarginUnitType.Inches),
right=MarginType(0.1, MarginUnitType.Inches),
),
)
.size(PageSize(height=11.7, width=8.27))
.scale(1.0)
.run()
)
pdf_path.write_bytes(response.content)
return pdf_path
except Exception as err:
raise ParseError(
f"Error while converting document to PDF: {err}",
) from err

View File

@ -0,0 +1,15 @@
def get_parser(*args, **kwargs):
from paperless_einvoice.parsers import EInvoiceDocumentParser
return EInvoiceDocumentParser(*args, **kwargs)
def einvoice_consumer_declaration(sender, **kwargs):
return {
"parser": get_parser,
"weight": 10,
"mime_types": {
"text/xml": ".xml",
"application/xml": ".xml",
},
}

View File

@ -0,0 +1,138 @@
@charset "UTF-8";
@page {
margin: 3cm;
@bottom-left {
color: #1ee494;
font-family: Pacifico;
}
@bottom-center {
content: string(title);
color: #a9a;
font-family: Pacifico;
font-size: 9pt;
}
}
footer {
width: 0;
height: 0;
visibility: hidden;
string-set: title content();
}
html {
color: #14213d;
font-family: Source Sans Pro;
font-size: 11pt;
line-height: 1.6;
}
html body {
margin: 0;
}
html h1 {
color: #1ee494;
font-family: Pacifico;
font-size: 40pt;
margin: 0;
}
html aside {
display: flex;
margin: 2em 0 4em;
}
html aside address {
font-style: normal;
white-space: pre-line;
}
html aside address#from {
color: #a9a;
flex: 1;
}
html aside address#to {
text-align: right;
}
html dl {
text-align: right;
position: absolute;
right: 0;
top: 0;
}
html dl dt,
html dl dd {
display: inline;
margin: 0;
}
html dl dt {
color: #a9a;
}
html dl dt::before {
content: '';
display: block;
}
html dl dt::after {
content: ':';
}
html table {
border-collapse: collapse;
width: 100%;
}
html table th {
border-bottom: .2mm solid #a9a;
color: #a9a;
font-size: 10pt;
font-weight: 400;
padding-bottom: .25cm;
text-transform: uppercase;
}
html table td {
padding-top: 7mm;
}
html table td:last-of-type {
color: #1ee494;
font-weight: bold;
text-align: right;
}
html table th,
html table td {
text-align: center;
}
html table th:first-of-type,
html table td:first-of-type {
text-align: left;
}
html table th:last-of-type,
html table td:last-of-type {
text-align: right;
}
html table#total {
background: #f6f6f6;
border-color: #f6f6f6;
border-style: solid;
border-width: 2cm 3cm;
bottom: 0;
font-size: 20pt;
margin: 0 -3cm;
position: absolute;
width: 18cm;
}

View File

@ -0,0 +1,80 @@
<html>
<head>
<title>
Rechnung {{ id }}
</title>
</head>
<body>
<h1>Rechnung</h1>
<p id="fromaddress">
<u>westnetz w.V., Karl-Heine-Str. 93, 04229 Leipzig</u>
</p>
<p id="toaddress">
{{ address | join("<br />") }}
</p>
<table class="infoheader">
<tr>
<th>Rechnungsnummer</th>
<th>Leistungszeitraum</th>
<th>Rechnungsdatum</th>
</tr>
<tr>
<td class="info">{{ id }}</td>
<td class="info">{{ period }}</td>
<td class="info">{{ date }}</td>
</tr>
</table>
<h2>Rechnungspositionen</h2>
<table class="positions">
<tr>
<th id="tablepos">Pos.</th>
<th id="tabledesc">Beschreibung</th>
<th id="tablesingle">Einzelpreis</th>
<th id="tablequantity">Anzahl</th>
<th id="tabletotal">Gesamtpreis</th>
</tr>
{% for item in items %}
<tr class="position">
<td class="positions">{{ item.item }}</td>
<td class="positions">{{ item.description }}</td>
<td class="positions">{{ item.price }}</td>
<td class="positions">{{ item.quantity }}</td>
<td class="positions">{{ item.subtotal }}</td>
</tr>
{% endfor %}
</table>
<table class="positions sum">
<tr>
<th>Nettobetrag</th>
<th>Mehrwertsteuer (19%)</th>
<th>Rechnungsbetrag</th>
</tr>
<tr>
<td class="total">{{ total_net }}</td>
<td class="total">{{ total_vat }}</td>
<td class="total strong">{{ total_gross }}</td>
</tr>
</table>
<h2>Hinweise</h2>
<p>
Alle Preise verstehen sich in Euro und sind innerhalb von 14 Tagen auf das nebenstehend angegebene Konto zu überweisen. Um eine möglichst fehlerfreie Verbuchung ihrer Einzahlungen vornehmen zu können, bitten wir um die Angabe der Rechnungsnummer {{ id }} im Verwendungszeck. Bei Daueraufträgen benötigen wir zumindest ihre Kundennummer {{ cid }}.
</p>
<p>
Diese Rechnung wurde maschinell erstellt und ist auch ohne Unterschrift gültig.
</p>
<embed src="file:///{{ logo_asset_file }}" id="logo" />
</body>
</html>

View File

View File

@ -0,0 +1,25 @@
from collections.abc import Generator
from pathlib import Path
import pytest
from paperless_einvoice.parsers import EInvoiceDocumentParser
@pytest.fixture()
def einvoice_parser() -> Generator[EInvoiceDocumentParser, None, None]:
try:
parser = EInvoiceDocumentParser(logging_group=None)
yield parser
finally:
parser.cleanup()
@pytest.fixture(scope="session")
def sample_dir() -> Path:
return (Path(__file__).parent / Path("samples")).resolve()
@pytest.fixture(scope="session")
def sample_xml_file(sample_dir: Path) -> Path:
return sample_dir / "zugferd_2p1_BASIC-WL_Einfach.xml"

View File

@ -0,0 +1,176 @@
<?xml version='1.0' encoding='UTF-8' ?>
<!-- English disclaimer below.-->
<!--Nutzungsrechte
ZUGFeRD Datenformat Version 2.2.0, 14.02.2022
Beispiel Version 14.02.2022
Zweck des Forums elektronisch Rechnung Deutschland, welches am 31. März 2010 under der Arbeitsgemeinschaft für
wirtschaftliche Verwaltung e. V. gegründet wurde, ist u. a. die Schaffung und Spezifizierung eines offenen Datenformats
für strukturierten elektronischen Datenaustausch auf der Grundlage oftener und nicht diskriminierender, standardisierter
Technologien („ZUGFeRD Datenformat“).
Das ZUGFeRD Datenformat wird nach Maßgabe des FeRD sowohl Unternehmen also auch der öffentlichen Verwaltung
frei zugänglich gemacht. Hierfür bietet FeRD allen Unternehmen und Organisationen der öffentlichen Verwaltung eine
Lizenz für die Nutzung des urheberrechtlich geschützten ZUGFeRD-Datenformats zu fairen, sachgerechten und nicht
diskriminierenden Bedingungen an.
Die Spezifikation des FeRD zur Implementierung des ZUGFeRD Datenformats ist in ihrer jeweils geltenden Fassung
abrufbar under www.ferd-net.de.
Im Einzelnen schließt die Nutzungsgewährung ein:
=====================================
FeRD räumt eine Lizenz für die Nutzung des urheberrechtlich geschützten ZUGFeRD Datenformats in der jeweils
geltenden und akzeptierten Fassung (www.ferd-net.de) ein.
Die Lizenz beinhaltet ein unwiderrufliches Nutzungsrecht einschließlich des Rechts der Weiterentwicklung,
Weiterbearbeitung und Verbindung mit anderen Produkten.
Die Lizenz gilt insbesondere für die Entwicklung, die Gestaltung, die Herstellung, den Verkauf, die Nutzung oder
anderweitige Verwendung des ZUGFeRD Datenformats für Hardware- und/oder Softwareprodukte sowie sonstige
Anwendungen und Dienste.
Diese Lizenz schließt nicht die wesentlichen Patente der Mitglieder von FeRD ein. Also wesentliche Patente sind Patente
und Patentanmeldungen weltweit zu verstehen, die einen oder mehrere Patentansprüche beinhalten, bei denen es sich um
notwendige Ansprüche handelt. Notwendige Ansprüche sind lediglich jene Ansprüche der Wesentlichen Patente, die durch
die Implementierung des ZUGFeRD Datenformats notwendigerweise verletzt würden.
Der Lizenznehmer ist berechtigt, seinen jeweiligen Konzerngesellschaften ein unbefristetes, weltweites, nicht übertragbares,
unwiderrufliches Nutzungsrecht einschließlich des Rechts der Weiterentwicklung, Weiterbearbeitung und Verbindung mit
anderen Produkten einzuräumen.
Die Lizenz wird kostenfrei zur Verfügung gestellt.
Außer im Falle vorsätzlichen Verschuldens oder grober Fahrlässigkeit haftet FeRD weder für Nutzungsausfall, entgangenen
Gewinn, Datenverlust, Kommunikationsverlust, Einnahmeausfall, Vertragseinbußen, Geschäftsausfall oder für Kosten,
Schäden, Verluste oder Haftpflichten im Zusammenhang mit einer Unterbrechung der Geschäftstätigkeit, noch für konkrete,
beiläufig entstandene, mittelbare Schäden, Straf- oder Folgeschäden und zwar auch dann nicht, wenn die Möglichkeit der
Kosten, Verluste bzw. Schäden hätte normalerweise vorhergesehen werden können.-->
<!--Right of use
ZUGFeRD Data format version 2.2.0, February 14th, 2022
The purpose of the Forum elektronische Rechnung Deutschland (FeRD), which was founded on March 31, 2010 under the
umbrella of Arbeitsgemeinschaft für wirtschaftliche Verwaltung e. V., is, among other things, to create and specify an
open data format for structured electronic data exchange on the basis of open and non discriminatory, standardised
technologies ("ZUGFeRD data format").
The ZUGFeRD data format is used by both companies and public administration according to the FeRD
made freely accessible. For this purpose FeRD offers all companies and organisations of the public administration a
License to use the copyrighted ZUGFeRD data format in a fair, appropriate and non
discriminatory conditions.
The specification of the FeRD for the implementation of the ZUGFeRD data format is, in its currently valid version
available at www.ferd-net.de.
In detail, the grant of use includes
=====================================
FeRD grants a license for the use of the copyrighted ZUGFeRD data format in the respective
valid and accepted version (www.ferd-net.de).
The license includes an irrevocable right of use including the right of further development,
Further processing and connection with other products.
The license applies in particular to the development, design, production, sale, use or
other use of the ZUGFeRD data format for hardware and/or software products and other
applications and services.
This license does not include the essential patents of the members of FeRD. The essential patents are patents
and patent applications worldwide which contain one or more claims that are
necessary claims. Necessary claims are only those claims of the essential patents which are
the implementation of the ZUGFeRD data format would necessarily be violated.
The Licensee is entitled to provide its respective group companies with an unlimited, worldwide, non-transferable,
irrevocable right of use including the right of further development, further processing and connection with
other products.
The license is provided free of charge.
Except in the case of intentional fault or gross negligence, FeRD is not liable for loss of use, loss of
Profit, loss of data, loss of communication, loss of revenue, loss of contracts, loss of business or for costs
damages, losses or liabilities in connection with an interruption of business, nor for concrete,
incidental, indirect, punitive or consequential damages, even if the possibility of
costs, losses or damages could normally have been foreseen.-->
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:factur-x.eu:1p0:basicwl</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>TX-471102</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20191030</udt:DateTimeString>
</ram:IssueDateTime>
<ram:IncludedNote>
<ram:Content>Rechnung gemäß Taxifahrt vom 29.10.2019</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Taxiunternehmen TX GmbH
Lieferantenstraße 20
10369 Berlin
Deutschland
Geschäftsführer: Hans Mustermann
Handelsregisternummer: H A 123
</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Unsere GLN: 4000001123452
Ihre GLN: 4000001987658
Ihre Kundennummer: GE2020211
</ram:Content>
</ram:IncludedNote>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Taxiunternehmen TX GmbH</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>10369</ram:PostcodeCode>
<ram:LineOne>Lieferantenstraße 20</ram:LineOne>
<ram:CityName>Berlin</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Taxi-Gast AG Mitte</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>13351</ram:PostcodeCode>
<ram:LineOne>Hans Mustermann</ram:LineOne>
<ram:LineTwo>Kundenstraße 15</ram:LineTwo>
<ram:CityName>Berlin</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
<ram:ActualDeliverySupplyChainEvent>
<ram:OccurrenceDateTime>
<udt:DateTimeString format="102">20191029</udt:DateTimeString>
</ram:OccurrenceDateTime>
</ram:ActualDeliverySupplyChainEvent>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>1.18</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>16.90</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>7</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:DueDateDateTime>
<udt:DateTimeString format="102">20191129</udt:DateTimeString>
</ram:DueDateDateTime>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>16.90</ram:LineTotalAmount>
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
<ram:TaxBasisTotalAmount>16.90</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">1.18</ram:TaxTotalAmount>
<ram:GrandTotalAmount>18.08</ram:GrandTotalAmount>
<ram:DuePayableAmount>18.08</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,19 @@
from pathlib import Path
import pytest
from pytest_django.fixtures import SettingsWrapper
from pytest_httpx import HTTPXMock
from paperless_einvoice.parsers import EInvoiceDocumentParser
@pytest.mark.django_db()
class TestEInvoiceParser:
def test_parse(
self,
httpx_mock: HTTPXMock,
settings: SettingsWrapper,
einvoice_parser: EInvoiceDocumentParser,
sample_xml_file: Path,
):
return None