feat(msp): Beleg-Titel pro Profil per Template einstellbar

Bisher wurde der Titel hart als '{title_prefix} {period}' gebaut.
Jetzt zwei optionale Felder im Supplier Import Profile:

- title_template            (für Rechnungen / Lieferscheine)
- title_template_credit_note (für Gutschriften)

Verfügbare Platzhalter:
  {prefix}        — bisheriges title_prefix / title_prefix_credit_note
  {period}        — z. B. "09.2025" oder "09.2025 – 03.2026"
  {customer}      — Customer-Ref (CUST-…)
  {customer_name} — Anzeigename des Kunden
  {vendor}        — Vendor-Feld des Profils (z. B. "Microsoft")
  {posting_date}  — DD.MM.YYYY
  {document_type} — "Sales Invoice" / "Delivery Note"

Backwards-compatible: leeres Template fällt auf das alte Verhalten
zurück. Unbekannte Platzhalter werden geloggt und der Builder fällt
ebenfalls auf {prefix} {period} zurück, statt einen Run zu kippen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Malinowski
2026-04-15 09:04:21 +02:00
co-authored by Claude Opus 4.6
parent a1ebe0481c
commit db6b4e07fa
3 changed files with 56 additions and 5 deletions
@@ -22,6 +22,8 @@
"supplier": "SUPP-00887", "supplier": "SUPP-00887",
"title_prefix": "Abrechnung Microsoft Abo ", "title_prefix": "Abrechnung Microsoft Abo ",
"title_prefix_credit_note": "Gutschrift Microsoft Abo ", "title_prefix_credit_note": "Gutschrift Microsoft Abo ",
"title_template": "{prefix} {period}",
"title_template_credit_note": "{prefix} {period}",
"vendor": "Microsoft" "vendor": "Microsoft"
} }
] ]
+40 -5
View File
@@ -113,16 +113,15 @@ class DocumentBuilder:
"""Erzeugt das Grundgerüst (ohne Zeilen) der Sales Invoice / Delivery Note.""" """Erzeugt das Grundgerüst (ohne Zeilen) der Sales Invoice / Delivery Note."""
profile = self.profile profile = self.profile
period = self._format_period_label(first) period = self._format_period_label(first)
title_prefix = profile.title_prefix_credit_note if is_return else profile.title_prefix posting_date = self._coerce_german_date(first.raw.get("DATUM") or frappe.utils.today())
title = f"{(title_prefix or '').strip()} {period}".strip() title = self._render_title(customer, period, posting_date, is_return=is_return)
if self.output_mode == "Sales Invoice": if self.output_mode == "Sales Invoice":
doc = frappe.new_doc("Sales Invoice") doc = frappe.new_doc("Sales Invoice")
doc.customer = customer doc.customer = customer
doc.company = profile.default_company doc.company = profile.default_company
doc.currency = frappe.db.get_value("Company", profile.default_company, "default_currency") or "EUR" doc.currency = frappe.db.get_value("Company", profile.default_company, "default_currency") or "EUR"
doc.posting_date = first.raw.get("DATUM") or frappe.utils.today() doc.posting_date = posting_date
doc.posting_date = self._coerce_german_date(doc.posting_date)
doc.set_posting_time = 1 doc.set_posting_time = 1
if profile.default_cost_center: if profile.default_cost_center:
doc.cost_center = profile.default_cost_center doc.cost_center = profile.default_cost_center
@@ -147,7 +146,7 @@ class DocumentBuilder:
doc = frappe.new_doc("Delivery Note") doc = frappe.new_doc("Delivery Note")
doc.customer = customer doc.customer = customer
doc.company = profile.default_company doc.company = profile.default_company
doc.posting_date = self._coerce_german_date(first.raw.get("DATUM") or frappe.utils.today()) doc.posting_date = posting_date
doc.set_posting_time = 1 doc.set_posting_time = 1
if profile.default_cost_center: if profile.default_cost_center:
doc.cost_center = profile.default_cost_center doc.cost_center = profile.default_cost_center
@@ -312,6 +311,42 @@ class DocumentBuilder:
return invdate.strftime("%m.%Y") return invdate.strftime("%m.%Y")
return _d.today().strftime("%m.%Y") return _d.today().strftime("%m.%Y")
def _render_title(self, customer: str, period: str, posting_date, *, is_return: bool) -> str:
"""Rendert den Beleg-Titel aus dem konfigurierten Template (mit Fallback).
Platzhalter: {prefix}, {period}, {customer}, {customer_name},
{vendor}, {posting_date}, {document_type}.
"""
profile = self.profile
template = (profile.title_template_credit_note if is_return else profile.title_template) or ""
prefix = (profile.title_prefix_credit_note if is_return else profile.title_prefix) or ""
template = template.strip()
if not template:
# Backwards-compat: bisheriges Verhalten "{prefix} {period}".
return f"{prefix.strip()} {period}".strip()
customer_name = frappe.db.get_value("Customer", customer, "customer_name") or customer
try:
rendered = template.format(
prefix=prefix.strip(),
period=period,
customer=customer,
customer_name=customer_name,
vendor=profile.vendor or "",
posting_date=posting_date.strftime("%d.%m.%Y") if hasattr(posting_date, "strftime") else str(posting_date),
document_type=self.output_mode or "",
)
except (KeyError, IndexError) as e:
# Unbekannter Platzhalter — robust auf Fallback zurückfallen, aber dem Run
# trotzdem einen Hinweis mitgeben, indem wir den Profilfehler in die Logs schreiben.
frappe.log_error(
f"Title-Template enthält ungültigen Platzhalter ({e}); fällt auf '{{prefix}} {{period}}' zurück.",
"Supplier Import Builder",
)
rendered = f"{prefix.strip()} {period}".strip()
# Mehrfach-Whitespace zusammenfassen
return " ".join(rendered.split())
@staticmethod @staticmethod
def _copy_tax_template_rows(doc, template_name: str) -> None: def _copy_tax_template_rows(doc, template_name: str) -> None:
"""Kopiert die Zeilen des Sales-Taxes-and-Charges-Templates explizit in doc.taxes. """Kopiert die Zeilen des Sales-Taxes-and-Charges-Templates explizit in doc.taxes.
@@ -29,6 +29,8 @@
"text_section", "text_section",
"title_prefix", "title_prefix",
"title_prefix_credit_note", "title_prefix_credit_note",
"title_template",
"title_template_credit_note",
"intro_text_invoice", "intro_text_invoice",
"intro_text_credit_note", "intro_text_credit_note",
"fallback_section", "fallback_section",
@@ -170,6 +172,18 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Title Prefix (Credit Note)" "label": "Title Prefix (Credit Note)"
}, },
{
"fieldname": "title_template",
"fieldtype": "Data",
"label": "Title Template (Invoice)",
"description": "Optionales Format-String für den Beleg-Titel. Platzhalter: {prefix}, {period}, {customer}, {customer_name}, {vendor}, {posting_date}, {document_type}. Leer = '{prefix} {period}'."
},
{
"fieldname": "title_template_credit_note",
"fieldtype": "Data",
"label": "Title Template (Credit Note)",
"description": "Wie 'Title Template (Invoice)', nur für Gutschriften. Leer = '{prefix} {period}'."
},
{ {
"fieldname": "intro_text_invoice", "fieldname": "intro_text_invoice",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",