From 82b52cff8ac8b153a8629ef7d0628c69814b36fb Mon Sep 17 00:00:00 2001 From: "Daniel F. Rose" Date: Tue, 12 May 2026 20:12:11 +0200 Subject: [PATCH 1/2] feat: implement XML attachment file naming functionality (#253) * feat(eu_einvoice): configurable base name for auto-attached XRechnung XML Add optional pattern on E Invoice Settings (auto_name_format_for_xml_file) for the file base name when auto-attaching XML on Sales Invoice submit. Empty pattern keeps using the document name; otherwise use naming-series-style segments (dot-separated, parse_naming_series, {field} placeholders, date tokens), strip leading # per segment so counter-only parts do not consume Series, sanitize with get_safe_file_name, and fall back to the document name on error. Colocate get_xml_attachment_file_base_name with Sales Invoice custom logic and cover naming edge cases in test_sales_invoice.py. Refresh E Invoice Settings controller and de / template catalogs for the new strings. * refactor(eu_einvoice): enhance download_xrechnung to use configurable base name Update the download_xrechnung function to retrieve the Sales Invoice document and check permissions before generating the XML file. The filename now utilizes a configurable base name from E Invoice Settings, improving flexibility for file naming. This change ensures that the generated XML file adheres to the specified naming format. * chore(eu_einvoice): update E Invoice Settings and localization files Rearrange fields in e_invoice_settings.json for better organization, adding a label for the XML Settings section. Update localization files (de.po and main.pot) to reflect changes in field names and add new translations for XML Settings. Adjust POT creation dates for consistency. * refactor(eu_einvoice): centralize sales invoice xml stem naming Resolve the downloadable XML filename stem in get_xml_attachment_file_base_name: read *Auto name format for XML file* from **E Invoice Settings** when the pattern argument is omitted, accept an optional keyword-only pattern override, and build the stem with parse_naming_series plus a no-op number generator so naming has no series counter DB side effects. Sanitize with get_safe_file_name and fall back to doc.name when the pattern is empty or fails. Call sites use the helper without duplicating settings reads. Tests pass pattern= for empty or whitespace patterns, and assert the settings-backed path by patching frappe.get_single_value only for **E Invoice Settings** / auto_name_format_for_xml_file. --------- Co-authored-by: Daniel Rose <26166128+dafrose@users.noreply.github.com> (cherry picked from commit d45a1f065e02fae62d3d0e2186510d0e106f6ccc) Co-authored-by: Cursor --- .../custom/sales_invoice.py | 45 ++++- .../custom/test_sales_invoice.py | 58 ++++++ .../e_invoice_settings.json | 16 +- .../e_invoice_settings/e_invoice_settings.py | 1 + eu_einvoice/locale/de.po | 179 ++++++++++++++++-- eu_einvoice/locale/main.pot | 179 ++++++++++++++++-- 6 files changed, 449 insertions(+), 29 deletions(-) create mode 100644 eu_einvoice/european_e_invoice/custom/test_sales_invoice.py diff --git a/eu_einvoice/european_e_invoice/custom/sales_invoice.py b/eu_einvoice/european_e_invoice/custom/sales_invoice.py index da4ba878..2a8a45d1 100644 --- a/eu_einvoice/european_e_invoice/custom/sales_invoice.py +++ b/eu_einvoice/european_e_invoice/custom/sales_invoice.py @@ -15,8 +15,10 @@ from drafthorse.models.trade import LogisticsServiceCharge from drafthorse.models.tradelines import LineItem from frappe import _ -from frappe.core.doctype.file.utils import find_file_by_url +from frappe.core.doctype.file.utils import find_file_by_url, get_safe_file_name from frappe.core.utils import html2text +from frappe.model.naming import parse_naming_series +from frappe.utils import cstr from frappe.utils.data import date_diff, flt, getdate, to_markdown from eu_einvoice.common_codes import CommonCodeRetriever @@ -45,8 +47,10 @@ @frappe.whitelist() def download_xrechnung(invoice_id: str): - frappe.local.response.filename = f"{invoice_id}.xml" - frappe.local.response.filecontent = get_einvoice(invoice_id) + invoice = frappe.get_doc("Sales Invoice", invoice_id) + base_name = get_xml_attachment_file_base_name(invoice) + frappe.local.response.filecontent = get_einvoice(invoice) + frappe.local.response.filename = f"{base_name}.xml" frappe.local.response.type = "download" @@ -906,7 +910,8 @@ def _attach_xml_file(doc: SalesInvoice, xml_content: bytes, field_name: str | No ) return - file_name = f"{doc.name}.xml".replace("/", "-") + base_name = get_xml_attachment_file_base_name(doc) + file_name = f"{base_name}.xml" # Create new File document file_doc = frappe.new_doc("File") @@ -1129,3 +1134,35 @@ def attach_xml_to_pdf(invoice_id: str, pdf_data: bytes) -> bytes: xml_bytes = get_einvoice(invoice_id) return attach_xml(pdf_data, xml_bytes, level) + + +def get_xml_attachment_file_base_name(doc, *, pattern: str | None = None) -> str: + """Filename stem (no `.xml`) for the downloadable XML. + + Uses *pattern* when given, otherwise reads *Auto name format for XML file* + from **E Invoice Settings**. Falls back to `doc.name` when the pattern is + empty or fails to resolve. Result is sanitized via `get_safe_file_name` + (same rules as **File** attachments). + """ + if pattern is None: + pattern = frappe.get_single_value("E Invoice Settings", "auto_name_format_for_xml_file") + pattern = cstr(pattern).strip() + if pattern: + try: + base = parse_naming_series(pattern, doc=doc, number_generator=_no_series_counter).strip() + except Exception: + frappe.log_error( + title=_("E Invoice XML file name pattern failed"), + message=frappe.get_traceback(), + reference_doctype=doc.doctype, + reference_name=doc.name, + ) + base = "" + if base: + return get_safe_file_name(base) + return get_safe_file_name(doc.name) + + +def _no_series_counter(_key: str, _digits: int) -> str: + """Disable the series counter so XML naming has no DB side effects.""" + return "" diff --git a/eu_einvoice/european_e_invoice/custom/test_sales_invoice.py b/eu_einvoice/european_e_invoice/custom/test_sales_invoice.py new file mode 100644 index 00000000..6e36ff10 --- /dev/null +++ b/eu_einvoice/european_e_invoice/custom/test_sales_invoice.py @@ -0,0 +1,58 @@ +from unittest.mock import patch + +import frappe +from frappe.tests.utils import FrappeTestCase + +from eu_einvoice.european_e_invoice.custom.sales_invoice import ( + get_xml_attachment_file_base_name, +) + + +class TestXmlAttachmentNaming(FrappeTestCase): + def test_auto_name_format_from_e_invoice_settings(self): + doc = frappe._dict(name="SINV-00001", po_no="PO-42", doctype="Sales Invoice") + field = "auto_name_format_for_xml_file" + pattern = "XMLSTEM.-.{po_no}" + real_gsv = frappe.get_single_value + + def get_single_value(doctype, fname, cache=True): + if doctype == "E Invoice Settings" and fname == field: + return pattern + return real_gsv(doctype, fname, cache=cache) + + with patch.object(frappe, "get_single_value", side_effect=get_single_value): + self.assertEqual(get_xml_attachment_file_base_name(doc), "XMLSTEM-PO-42") + + def test_empty_or_whitespace_uses_doc_name(self): + doc = frappe._dict(name="SINV/00001", doctype="Sales Invoice") + self.assertEqual(get_xml_attachment_file_base_name(doc, pattern=""), "SINV_00001") + self.assertEqual(get_xml_attachment_file_base_name(doc, pattern=" "), "SINV_00001") + + def test_dot_separated_pattern_with_field(self): + doc = frappe._dict(name="SINV-00001", po_no="PO-1", doctype="Sales Invoice") + base = get_xml_attachment_file_base_name(doc, pattern="EXAMPLE.-.{po_no}") + self.assertEqual(base, "EXAMPLE-PO-1") + + def test_dot_separated_inv_field_end(self): + doc = frappe._dict(name="SINV-00001", po_no="X-9", doctype="Sales Invoice") + base = get_xml_attachment_file_base_name(doc, pattern="INV.-.{po_no}.-.END") + self.assertEqual(base, "INV-X-9-END") + + def test_leading_hashes_stripped_per_part_avoids_series_counter(self): + doc = frappe._dict(name="SINV-00001", po_no="PO-1", doctype="Sales Invoice") + base = get_xml_attachment_file_base_name(doc, pattern="EXAMPLE.-.{po_no}.#####") + self.assertEqual(base, "EXAMPLE-PO-1") + + def test_hash_in_literal_sanitized_like_file_utils(self): + doc = frappe._dict(name="SINV-00001", doctype="Sales Invoice") + base = get_xml_attachment_file_base_name(doc, pattern="INV#X") + self.assertEqual(base, "INV_X") + + def test_only_hash_segments_falls_back_to_doc_name(self): + doc = frappe._dict(name="SINV-00001", doctype="Sales Invoice") + self.assertEqual(get_xml_attachment_file_base_name(doc, pattern="#####"), "SINV-00001") + + def test_slash_in_resolved_base_is_sanitized(self): + doc = frappe._dict(name="SINV-00001", po_no="A/B", doctype="Sales Invoice") + base = get_xml_attachment_file_base_name(doc, pattern="{po_no}") + self.assertEqual(base, "A_B") diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json b/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json index 9b5f467f..996517ac 100644 --- a/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json @@ -13,6 +13,8 @@ "error_action_on_submit", "sales_invoice_number_field", "section_break_yldr", + "auto_name_format_for_xml_file", + "column_break_vlzj", "auto_attach_xml", "attach_field_for_xml_file" ], @@ -74,13 +76,23 @@ "fieldname": "sales_invoice_number_field", "fieldtype": "Autocomplete", "label": "Sales Invoice Number Field" + }, + { + "fieldname": "column_break_vlzj", + "fieldtype": "Column Break" + }, + { + "description": "Pattern for the file name of the XML. .xml is appended automatically. Leave empty to use the document name (Sales Invoice ID). In case of an error, the document name is used as fallback.\n

Segments use a dot (.) between literal text and dynamic parts. Example:
EXAMPLE-.MM.-.{po_no}.-.YYYY", + "fieldname": "auto_name_format_for_xml_file", + "fieldtype": "Data", + "label": "Auto Name Format for XML File" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-02-06 17:52:09.990716", + "modified": "2026-05-15 12:00:00.000000", "modified_by": "Administrator", "module": "European e-Invoice", "name": "E Invoice Settings", @@ -101,4 +113,4 @@ "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py b/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py index f61bdfcf..8f7cc181 100644 --- a/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py @@ -18,6 +18,7 @@ class EInvoiceSettings(Document): attach_field_for_xml_file: DF.Autocomplete | None auto_attach_xml: DF.Check + auto_name_format_for_xml_file: DF.Data | None error_action_on_save: DF.Literal["", "Warning Message", "Error Message"] error_action_on_submit: DF.Literal["", "Warning Message", "Error Message"] sales_invoice_number_field: DF.Autocomplete | None diff --git a/eu_einvoice/locale/de.po b/eu_einvoice/locale/de.po index b7c75151..8440538a 100644 --- a/eu_einvoice/locale/de.po +++ b/eu_einvoice/locale/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: European e-Invoice VERSION\n" "Report-Msgid-Bugs-To: hallo@alyf.de\n" -"POT-Creation-Date: 2026-05-12 20:40+0053\n" +"POT-Creation-Date: 2026-05-15 13:02+0053\n" "PO-Revision-Date: 2026-02-05 11:13+0100\n" "Last-Translator: hallo@alyf.de\n" "Language: de\n" @@ -18,10 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:790 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:794 msgid "A document level discount is currently not supported in the e-invoice." msgstr "Ein Rabatt auf der Dokumentebene wird in der E-Rechnung derzeit nicht unterstützt." +#. Label of a Data field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Account Name" +msgstr "" + #. Label of a Select field in DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json msgid "Action on Validation Error during Save" @@ -60,6 +65,11 @@ msgstr "Eine E-Rechnung mit der gleichen Rechnungs-ID und Lieferanten existiert msgid "Attach Field for XML File" msgstr "Anhangsfeld für XML-Datei" +#. Label of a Data field in DocType 'E Invoice Settings' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +msgid "Auto Name Format for XML File" +msgstr "Namensmuster für die XML-Datei" + #. Label of a Check field in DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json msgid "Auto-attach XML File" @@ -160,11 +170,11 @@ msgstr "Berechneter Betrag" msgid "Calculation Percent" msgstr "Berechnung Prozentsatz" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:821 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:825 msgid "Cannot create E Invoice." msgstr "E-Rechnung konnte nicht erstellt werden." -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:835 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:839 msgid "Cannot validate E Invoice schematron." msgstr "E-Rechnung konnte nicht validiert werden." @@ -173,11 +183,21 @@ msgstr "E-Rechnung konnte nicht validiert werden." msgid "Charge Total" msgstr "Zuschläge gesamt" +#. Label of a Link in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "Code List" +msgstr "" + #. Description of a Card Break in the E-Invoicing Workspace #: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json msgid "Code Lists are typically public lists of common codes used in data interchange between third parties" msgstr "Code-Listen sind in der Regel öffentliche Listen von häufig verwendeten Codes, die in der Datenübertragung zwischen Drittanbietern verwendet werden." +#. Label of a Link in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "Common Code" +msgstr "" + #. Description of the 'Sales Invoice Number Field' (Autocomplete) field in #. DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json @@ -222,6 +242,23 @@ msgstr "Kundennummer beim Lieferanten" msgid "Customer numbers assigned by the supplier" msgstr "Vom Lieferanten zugewiesene Kundennummern" +#. Label of a Tab Break field in DocType 'E Invoice Import' +#. Label of a Section Break field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Delivery" +msgstr "" + +#. Label of a Section Break field in DocType 'E Invoice Payment Term' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_payment_term/e_invoice_payment_term.json +msgid "Discount" +msgstr "" + +#. Label of a shortcut in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "Documentation" +msgstr "" + #: eu_einvoice/european_e_invoice/custom/sales_invoice.js:16 msgid "Download eInvoice" msgstr "E-Rechnung herunterladen" @@ -279,7 +316,11 @@ msgstr "E-Rechnungs-Einstellungen" msgid "E Invoice Trade Tax" msgstr "E-Rechnungs-Steuer" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:803 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:1155 +msgid "E Invoice XML file name pattern failed" +msgstr "Namensmuster für E-Rechnungs-XML-Datei fehlgeschlagen" + +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:807 msgid "E Invoice is not correct" msgstr "E-Rechnung ist nicht korrekt" @@ -312,11 +353,11 @@ msgstr "Schema der elektronischen Adresse" msgid "Embedded Document" msgstr "Eingebettetes Dokument" -#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:50 +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:51 msgid "Field '{0}' does not exist on Sales Invoice doctype" msgstr "Feld '{0}' existiert nicht im DocType 'Sales Invoice'" -#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:58 +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:59 msgid "Field '{0}' must be of type 'Attach'. Current type: {1}" msgstr "Feld '{0}' muss vom Typ 'Anhängen' sein. Aktueller Typ: {1}" @@ -325,6 +366,37 @@ msgstr "Feld '{0}' muss vom Typ 'Anhängen' sein. Aktueller Typ: {1}" msgid "File Section" msgstr "Datei-Abschnitt" +#. Label of a Currency field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Grand Total" +msgstr "" + +#. Label of a Data field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "IBAN" +msgstr "" + +#. Label of a Data field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Invoice ID" +msgstr "" + +#. Label of a Date field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Issue Date" +msgstr "" + +#. Label of a Link field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Item" +msgstr "" + +#. Label of a Table field in DocType 'E Invoice Import' +#. Label of a Tab Break field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Items" +msgstr "" + #. Label of a Currency field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Line Total" @@ -339,11 +411,21 @@ msgstr "Mit {0} verknüpfen" msgid "Monetary Summation" msgstr "Gesamtbeträge" +#. Label of a Currency field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Net Rate" +msgstr "" + #. Label of a shortcut in the E-Invoicing Workspace #: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json msgid "New E Invoice Import" msgstr "Neuer E-Rechnungs-Import" +#. Label of a shortcut in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "New Sales Invoice" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:443 msgid "No machine-readable data was found in the PDF file. You can create a regular Purchase Invoice manually instead." msgstr "In der PDF-Datei wurden keine maschinenlesbaren Daten gefunden. Sie können stattdessen eine reguläre Eingangsrechnung manuell erstellen." @@ -363,11 +445,26 @@ msgstr "Nur bei E-Rechnungs-Profil \"XRECHNUNG\" beim Buchen" msgid "Partial Amount" msgstr "Teilbetrag" +#. Description of the 'Auto Name Format for XML File' (Data) field in DocType +#. 'E Invoice Settings' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +msgid "" +"Pattern for the file name of the XML. .xml is appended automatically. Leave empty to use the document name (Sales Invoice ID). In case of an error, the document name is used as fallback.\n" +"

Segments use a dot (.) between literal text and dynamic parts. Example:
EXAMPLE-.MM.-.{po_no}.-.YYYY" +msgstr "" +"Muster für den Dateinamen der XML-Datei. .xml wird automatisch angehängt. Leer lassen, um den Dokumentnamen zu verwenden (ID der Ausgangsrechnung). Bei einem Fehler wird der Dokumentname als Fallback verwendet.\n" +"

Literaltext und dynamische Teile werden mit einem Punkt (.) getrennt. Beispiel:
BEISPIEL-.MM.-.{po_no}.-.YYYY" + #. Label of a Section Break field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Payment Means" msgstr "Zahlungsmittel" +#. Label of a Table field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Payment Terms" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:119 msgid "Please create or select a supplier before submitting" msgstr "Bitte vor dem Buchen einen Lieferanten auswählen oder erstellen." @@ -384,6 +481,11 @@ msgstr "Bitte beachten Sie die Validierungsfehler der E-Rechnung." msgid "Please select a company before submitting" msgstr "Bitte vor dem Buchen ein Unternehmen auswählen." +#. Label of a Section Break field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Product" +msgstr "" + #. Label of a Small Text field in DocType 'E Invoice Item' #: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json msgid "Product Description" @@ -394,10 +496,20 @@ msgstr "Produktbeschreibung" msgid "Product Name" msgstr "Produktname" +#. Linked DocType in E Invoice Import's connections +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Purchase Invoice" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:644 msgid "Purchase Invoice {0} is already linked to E Invoice Import {1}" msgstr "Eingangsrechnung {0} ist bereits mit E-Rechnungs-Import {1} verknüpft" +#. Label of a Link field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Purchase Order" +msgstr "" + #. Label of a Link field in DocType 'E Invoice Item' #: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json msgid "Purchase Order Row" @@ -408,6 +520,11 @@ msgstr "Bestellposition" msgid "Rate Applicable Percent" msgstr "Anwendbarer Prozentsatz" +#. Label of a Section Break field in DocType 'E Invoice Settings' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +msgid "Sales Invoice" +msgstr "" + #. Label of a Autocomplete field in DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json msgid "Sales Invoice Number Field" @@ -475,6 +592,16 @@ msgstr "Verkäufer-Steuer-ID" msgid "Settlement" msgstr "Ausgleich" +#. Label of a Link field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Supplier" +msgstr "" + +#. Label of a Link field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Supplier Address" +msgstr "" + #: eu_einvoice/custom_fields.py:28 msgid "Supplier Invoice File" msgstr "Lieferantenrechnungsdatei" @@ -497,11 +624,31 @@ msgstr "Lieferantennummern" msgid "Supplier numbers assigned by the customer" msgstr "Vom Kunden zugewiesene Lieferantennummern" +#. Label of a Link field in DocType 'E Invoice Trade Tax' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_trade_tax/e_invoice_trade_tax.json +msgid "Tax Account" +msgstr "" + #. Label of a Currency field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Tax Basis Total" msgstr "Steuerbasis gesamt" +#. Label of a Percent field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Tax Rate" +msgstr "" + +#. Label of a Currency field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Tax Total" +msgstr "" + +#. Label of a Table field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Taxes" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:453 msgid "The format of the uploaded file ({0}) is not supported for E-Invoices. Please upload a valid E-Invoice file or create a regular Purchase Invoice manually instead." msgstr "Das Format der hochgeladenen Datei ({0}) wird für E-Rechnungen nicht unterstützt. Bitte laden Sie eine gültige E-Rechnungsdatei hoch oder erstellen Sie stattdessen eine reguläre Eingangsrechnung manuell." @@ -510,11 +657,21 @@ msgstr "Das Format der hochgeladenen Datei ({0}) wird für E-Rechnungen nicht un msgid "The uploaded file does not contain valid XML data." msgstr "Die hochgeladene Datei enthält keine gültige XML-Daten." +#. Label of a Currency field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Total Amount" +msgstr "" + #. Label of a Currency field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Total Prepaid" msgstr "Vorausbezahlt" +#. Label of a Link field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "UOM" +msgstr "" + #. Label of a Data field in DocType 'E Invoice Item' #: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json msgid "Unit Code" @@ -563,19 +720,19 @@ msgstr "Validierungswarnungen" msgid "Warning Message" msgstr "Warnung" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:769 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:773 msgid "{0} row #{1}: Discount Date should be after Posting Date" msgstr "{0} Zeile {1}: Rabatt-Datum sollte nach Buchungsdatum liegen" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:758 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:762 msgid "{0} row #{1}: The charge type 'Actual' is only supported in the eInvoice profiles 'EXTENDED' and 'XRECHNUNG'." msgstr "Die Gebührenart 'Tatsächlich' wird nur in den E-Rechnungs-Profilen 'EXTENDED' und 'XRECHNUNG' unterstützt." -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:746 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:750 msgid "{0} row #{1}: Type '{2}' is not supported in e-invoice" msgstr "{0} Zeile #{1}: Typ '{2}' wird in der E-Rechnung nicht unterstützt" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:781 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:785 msgid "{0}: Only one mode of payment will be considered in the e-invoice." msgstr "{0}: Nur eine Zahlungsart wird in der E-Rechnung berücksichtigt." diff --git a/eu_einvoice/locale/main.pot b/eu_einvoice/locale/main.pot index 879bcab9..43238292 100644 --- a/eu_einvoice/locale/main.pot +++ b/eu_einvoice/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: European e-Invoice VERSION\n" "Report-Msgid-Bugs-To: hallo@alyf.de\n" -"POT-Creation-Date: 2026-05-12 20:40+0053\n" -"PO-Revision-Date: 2026-05-12 20:40+0053\n" +"POT-Creation-Date: 2026-05-15 13:02+0053\n" +"PO-Revision-Date: 2026-05-15 13:02+0053\n" "Last-Translator: hallo@alyf.de\n" "Language-Team: hallo@alyf.de\n" "MIME-Version: 1.0\n" @@ -16,10 +16,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:790 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:794 msgid "A document level discount is currently not supported in the e-invoice." msgstr "" +#. Label of a Data field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Account Name" +msgstr "" + #. Label of a Select field in DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json msgid "Action on Validation Error during Save" @@ -58,6 +63,11 @@ msgstr "" msgid "Attach Field for XML File" msgstr "" +#. Label of a Data field in DocType 'E Invoice Settings' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +msgid "Auto Name Format for XML File" +msgstr "" + #. Label of a Check field in DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json msgid "Auto-attach XML File" @@ -158,11 +168,11 @@ msgstr "" msgid "Calculation Percent" msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:821 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:825 msgid "Cannot create E Invoice." msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:835 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:839 msgid "Cannot validate E Invoice schematron." msgstr "" @@ -171,11 +181,21 @@ msgstr "" msgid "Charge Total" msgstr "" +#. Label of a Link in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "Code List" +msgstr "" + #. Description of a Card Break in the E-Invoicing Workspace #: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json msgid "Code Lists are typically public lists of common codes used in data interchange between third parties" msgstr "" +#. Label of a Link in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "Common Code" +msgstr "" + #. Description of the 'Sales Invoice Number Field' (Autocomplete) field in #. DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json @@ -220,6 +240,23 @@ msgstr "" msgid "Customer numbers assigned by the supplier" msgstr "" +#. Label of a Tab Break field in DocType 'E Invoice Import' +#. Label of a Section Break field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Delivery" +msgstr "" + +#. Label of a Section Break field in DocType 'E Invoice Payment Term' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_payment_term/e_invoice_payment_term.json +msgid "Discount" +msgstr "" + +#. Label of a shortcut in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "Documentation" +msgstr "" + #: eu_einvoice/european_e_invoice/custom/sales_invoice.js:16 msgid "Download eInvoice" msgstr "" @@ -277,7 +314,11 @@ msgstr "" msgid "E Invoice Trade Tax" msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:803 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:1155 +msgid "E Invoice XML file name pattern failed" +msgstr "" + +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:807 msgid "E Invoice is not correct" msgstr "" @@ -310,11 +351,11 @@ msgstr "" msgid "Embedded Document" msgstr "" -#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:50 +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:51 msgid "Field '{0}' does not exist on Sales Invoice doctype" msgstr "" -#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:58 +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.py:59 msgid "Field '{0}' must be of type 'Attach'. Current type: {1}" msgstr "" @@ -323,6 +364,37 @@ msgstr "" msgid "File Section" msgstr "" +#. Label of a Currency field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Grand Total" +msgstr "" + +#. Label of a Data field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "IBAN" +msgstr "" + +#. Label of a Data field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Invoice ID" +msgstr "" + +#. Label of a Date field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Issue Date" +msgstr "" + +#. Label of a Link field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Item" +msgstr "" + +#. Label of a Table field in DocType 'E Invoice Import' +#. Label of a Tab Break field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Items" +msgstr "" + #. Label of a Currency field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Line Total" @@ -337,11 +409,21 @@ msgstr "" msgid "Monetary Summation" msgstr "" +#. Label of a Currency field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Net Rate" +msgstr "" + #. Label of a shortcut in the E-Invoicing Workspace #: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json msgid "New E Invoice Import" msgstr "" +#. Label of a shortcut in the E-Invoicing Workspace +#: eu_einvoice/european_e_invoice/workspace/e_invoicing/e_invoicing.json +msgid "New Sales Invoice" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:443 msgid "No machine-readable data was found in the PDF file. You can create a regular Purchase Invoice manually instead." msgstr "" @@ -361,11 +443,24 @@ msgstr "" msgid "Partial Amount" msgstr "" +#. Description of the 'Auto Name Format for XML File' (Data) field in DocType +#. 'E Invoice Settings' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +msgid "" +"Pattern for the file name of the XML. .xml is appended automatically. Leave empty to use the document name (Sales Invoice ID). In case of an error, the document name is used as fallback.\n" +"

Segments use a dot (.) between literal text and dynamic parts. Example:
EXAMPLE-.MM.-.{po_no}.-.YYYY" +msgstr "" + #. Label of a Section Break field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Payment Means" msgstr "" +#. Label of a Table field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Payment Terms" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:119 msgid "Please create or select a supplier before submitting" msgstr "" @@ -382,6 +477,11 @@ msgstr "" msgid "Please select a company before submitting" msgstr "" +#. Label of a Section Break field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Product" +msgstr "" + #. Label of a Small Text field in DocType 'E Invoice Item' #: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json msgid "Product Description" @@ -392,10 +492,20 @@ msgstr "" msgid "Product Name" msgstr "" +#. Linked DocType in E Invoice Import's connections +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Purchase Invoice" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:644 msgid "Purchase Invoice {0} is already linked to E Invoice Import {1}" msgstr "" +#. Label of a Link field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Purchase Order" +msgstr "" + #. Label of a Link field in DocType 'E Invoice Item' #: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json msgid "Purchase Order Row" @@ -406,6 +516,11 @@ msgstr "" msgid "Rate Applicable Percent" msgstr "" +#. Label of a Section Break field in DocType 'E Invoice Settings' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json +msgid "Sales Invoice" +msgstr "" + #. Label of a Autocomplete field in DocType 'E Invoice Settings' #: eu_einvoice/european_e_invoice/doctype/e_invoice_settings/e_invoice_settings.json msgid "Sales Invoice Number Field" @@ -473,6 +588,16 @@ msgstr "" msgid "Settlement" msgstr "" +#. Label of a Link field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Supplier" +msgstr "" + +#. Label of a Link field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Supplier Address" +msgstr "" + #: eu_einvoice/custom_fields.py:28 msgid "Supplier Invoice File" msgstr "" @@ -495,11 +620,31 @@ msgstr "" msgid "Supplier numbers assigned by the customer" msgstr "" +#. Label of a Link field in DocType 'E Invoice Trade Tax' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_trade_tax/e_invoice_trade_tax.json +msgid "Tax Account" +msgstr "" + #. Label of a Currency field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Tax Basis Total" msgstr "" +#. Label of a Percent field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Tax Rate" +msgstr "" + +#. Label of a Currency field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Tax Total" +msgstr "" + +#. Label of a Table field in DocType 'E Invoice Import' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json +msgid "Taxes" +msgstr "" + #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py:453 msgid "The format of the uploaded file ({0}) is not supported for E-Invoices. Please upload a valid E-Invoice file or create a regular Purchase Invoice manually instead." msgstr "" @@ -508,11 +653,21 @@ msgstr "" msgid "The uploaded file does not contain valid XML data." msgstr "" +#. Label of a Currency field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "Total Amount" +msgstr "" + #. Label of a Currency field in DocType 'E Invoice Import' #: eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json msgid "Total Prepaid" msgstr "" +#. Label of a Link field in DocType 'E Invoice Item' +#: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json +msgid "UOM" +msgstr "" + #. Label of a Data field in DocType 'E Invoice Item' #: eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json msgid "Unit Code" @@ -562,19 +717,19 @@ msgstr "" msgid "Warning Message" msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:769 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:773 msgid "{0} row #{1}: Discount Date should be after Posting Date" msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:758 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:762 msgid "{0} row #{1}: The charge type 'Actual' is only supported in the eInvoice profiles 'EXTENDED' and 'XRECHNUNG'." msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:746 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:750 msgid "{0} row #{1}: Type '{2}' is not supported in e-invoice" msgstr "" -#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:781 +#: eu_einvoice/european_e_invoice/custom/sales_invoice.py:785 msgid "{0}: Only one mode of payment will be considered in the e-invoice." msgstr "" From 3ed4c2eddd8383cf2d62a864996736cd2a646047 Mon Sep 17 00:00:00 2001 From: Daniel Rose <26166128+dafrose@users.noreply.github.com> Date: Fri, 15 May 2026 14:03:05 +0200 Subject: [PATCH 2/2] fix: use local safe filename helper for xml attachment naming Frappe v15 does not expose get_safe_file_name on file utils; mirror the v16+ implementation beside get_xml_attachment_file_base_name as _get_safe_file_name. --- .../european_e_invoice/custom/sales_invoice.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/eu_einvoice/european_e_invoice/custom/sales_invoice.py b/eu_einvoice/european_e_invoice/custom/sales_invoice.py index 2a8a45d1..99028c70 100644 --- a/eu_einvoice/european_e_invoice/custom/sales_invoice.py +++ b/eu_einvoice/european_e_invoice/custom/sales_invoice.py @@ -15,7 +15,7 @@ from drafthorse.models.trade import LogisticsServiceCharge from drafthorse.models.tradelines import LineItem from frappe import _ -from frappe.core.doctype.file.utils import find_file_by_url, get_safe_file_name +from frappe.core.doctype.file.utils import find_file_by_url from frappe.core.utils import html2text from frappe.model.naming import parse_naming_series from frappe.utils import cstr @@ -1141,7 +1141,7 @@ def get_xml_attachment_file_base_name(doc, *, pattern: str | None = None) -> str Uses *pattern* when given, otherwise reads *Auto name format for XML file* from **E Invoice Settings**. Falls back to `doc.name` when the pattern is - empty or fails to resolve. Result is sanitized via `get_safe_file_name` + empty or fails to resolve. Result is sanitized via `_get_safe_file_name` (same rules as **File** attachments). """ if pattern is None: @@ -1159,8 +1159,13 @@ def get_xml_attachment_file_base_name(doc, *, pattern: str | None = None) -> str ) base = "" if base: - return get_safe_file_name(base) - return get_safe_file_name(doc.name) + return _get_safe_file_name(base) + return _get_safe_file_name(doc.name) + + +def _get_safe_file_name(file_name: str) -> str: + """Local-only; mirrors ``get_safe_file_name`` in Frappe v16+ file utils.""" + return re.sub(r"[/\\%?#]", "_", file_name) def _no_series_counter(_key: str, _digits: int) -> str: