feat(msp): Supply Subscription data model + Supplier Import scaffolding

Führt ein lieferantenagnostisches Lizenzmodell ein, das den Weg für den
künftigen Ersatz der adnconnect-App ebnet:

- Supply Subscription: Golden Record pro Subscription-Lifecycle, unique
  über die externe Vendor-Subscription-ID (z. B. Microsoft Subscription-ID).
  Denormalisierte Felder current_period_end, qty_current, last_event_date
  werden aus den Events gepflegt.
- Supply Subscription Event: Eine Episode pro Abrechnungszeitraum, mit
  Rück-Links auf Sales Invoice, Delivery Note, Purchase Invoice und
  Supplier Import Line.
- Supplier Import Profile / Run / Line: Modulares Import-Framework, in dem
  pro Lieferant (ADN, künftig ALSO/Ingram/…) ein Profil mit parser_key +
  Defaults konfigurierbar ist. Ein Run bündelt einen konkreten Import
  inklusive Kanonik-Zeilen und Statistiken.
- Custom Field „supply_subscription" auf Sales Invoice Item und Delivery
  Note Item — das ist das einzige Standard-ERPNext-Feld, das unabhängig
  vom Lieferanten hinzukommt. Alle weiteren Attribute werden über den
  Link erreichbar.

Die Importer-Logik liegt separat (nächster Commit).
This commit is contained in:
David Malinowski
2026-04-14 12:41:14 +02:00
parent b014ed596b
commit b399b6f37d
16 changed files with 1346 additions and 15 deletions
+274 -15
View File
@@ -1,38 +1,297 @@
[ [
{ {
"name": "Contact-teams_user_id", "alignment": "",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"button_color": "",
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Sales Invoice Item",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "supply_subscription",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 1,
"insert_after": "description",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Supply Subscription",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-04-14 09:07:51.038936",
"module": "MSP",
"name": "Sales Invoice Item-supply_subscription",
"no_copy": 0,
"non_negative": 0,
"options": "Supply Subscription",
"permlevel": 0,
"placeholder": null,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"alignment": "",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"button_color": "",
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Delivery Note Item",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "supply_subscription",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 1,
"insert_after": "description",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Supply Subscription",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-04-14 09:07:51.230497",
"module": "MSP",
"name": "Delivery Note Item-supply_subscription",
"no_copy": 0,
"non_negative": 0,
"options": "Supply Subscription",
"permlevel": 0,
"placeholder": null,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"alignment": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"button_color": null,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": "MS Graph User-ID fuer Chat-Zuordnung",
"docstatus": 0,
"doctype": "Custom Field", "doctype": "Custom Field",
"dt": "Contact", "dt": "Contact",
"module": "MSP", "fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "teams_user_id", "fieldname": "teams_user_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "department",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Teams User ID", "label": "Teams User ID",
"description": "MS Graph User-ID fuer Chat-Zuordnung", "length": 0,
"insert_after": "department" "link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-04-14 09:07:08.458841",
"module": "MSP",
"name": "Contact-teams_user_id",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
}, },
{ {
"name": "Contact-is_external_contractor", "alignment": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"button_color": null,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": "0",
"depends_on": null,
"description": "Dienstleister (z.B. Solutions Factory)",
"docstatus": 0,
"doctype": "Custom Field", "doctype": "Custom Field",
"dt": "Contact", "dt": "Contact",
"module": "MSP", "fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "is_external_contractor", "fieldname": "is_external_contractor",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "teams_user_id",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Is External Contractor", "label": "Is External Contractor",
"description": "Dienstleister (z.B. Solutions Factory)", "length": 0,
"default": "0", "link_filters": null,
"insert_after": "teams_user_id" "mandatory_depends_on": null,
"modified": "2026-04-14 09:07:08.493826",
"module": "MSP",
"name": "Contact-is_external_contractor",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
}, },
{ {
"name": "Contact-contractor_for", "alignment": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"button_color": null,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": "eval:doc.is_external_contractor",
"description": "Deren Arbeit zaehlt als Kundenzeit",
"docstatus": 0,
"doctype": "Custom Field", "doctype": "Custom Field",
"dt": "Contact", "dt": "Contact",
"module": "MSP", "fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "contractor_for", "fieldname": "contractor_for",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "is_external_contractor",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Contractor For", "label": "Contractor For",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-04-14 09:07:08.528954",
"module": "MSP",
"name": "Contact-contractor_for",
"no_copy": 0,
"non_negative": 0,
"options": "Customer", "options": "Customer",
"description": "Deren Arbeit zaehlt als Kundenzeit", "permlevel": 0,
"depends_on": "eval:doc.is_external_contractor", "placeholder": null,
"insert_after": "is_external_contractor" "precision": null,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
} }
] ]
@@ -0,0 +1,180 @@
{
"actions": [],
"allow_rename": 0,
"creation": "2026-04-14 09:12:00.000000",
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"istable": 1,
"field_order": [
"source_row",
"customer",
"customer_external_ref",
"item",
"vendor_product_id",
"qty",
"rate",
"amount",
"period_start",
"period_end",
"subscription_external_id",
"subscription_start",
"billing_plan",
"term_duration",
"booking_type",
"marketplace_ref",
"order_ref",
"target_doc_type",
"target_doc_name",
"target_doc_row",
"line_status",
"error_message",
"raw_payload"
],
"fields": [
{
"fieldname": "source_row",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Source Row",
"description": "Zeilennummer in der Quelldatei"
},
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer"
},
{
"fieldname": "customer_external_ref",
"fieldtype": "Data",
"label": "Customer External Ref"
},
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{
"fieldname": "vendor_product_id",
"fieldtype": "Data",
"label": "Vendor Product ID"
},
{
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty"
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount"
},
{
"fieldname": "period_start",
"fieldtype": "Date",
"label": "Period Start"
},
{
"fieldname": "period_end",
"fieldtype": "Date",
"label": "Period End"
},
{
"fieldname": "subscription_external_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Subscription ID"
},
{
"fieldname": "subscription_start",
"fieldtype": "Date",
"label": "Subscription Start"
},
{
"fieldname": "billing_plan",
"fieldtype": "Select",
"label": "Billing Plan",
"options": "\nMonthly\nAnnual\nTriennial"
},
{
"fieldname": "term_duration",
"fieldtype": "Select",
"label": "Term Duration",
"options": "\nP1M\nP1Y\nP3Y"
},
{
"fieldname": "booking_type",
"fieldtype": "Select",
"label": "Booking Type",
"options": "\nNew\nRenewal\nBilling\nUpgrade\nCancel"
},
{
"fieldname": "marketplace_ref",
"fieldtype": "Data",
"label": "Marketplace Ref"
},
{
"fieldname": "order_ref",
"fieldtype": "Data",
"label": "Order Ref"
},
{
"fieldname": "target_doc_type",
"fieldtype": "Select",
"label": "Target DocType",
"options": "\nSales Invoice\nDelivery Note"
},
{
"fieldname": "target_doc_name",
"fieldtype": "Dynamic Link",
"options": "target_doc_type",
"label": "Target Document",
"in_list_view": 1
},
{
"fieldname": "target_doc_row",
"fieldtype": "Data",
"label": "Target Row"
},
{
"fieldname": "line_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nCreated\nSkipped\nError",
"default": "Pending"
},
{
"fieldname": "error_message",
"fieldtype": "Small Text",
"label": "Error Message"
},
{
"fieldname": "raw_payload",
"fieldtype": "Code",
"label": "Raw Payload",
"options": "JSON"
}
],
"index_web_pages_for_search": 0,
"modified": "2026-04-14 09:12:00.000000",
"modified_by": "Administrator",
"module": "MSP",
"name": "Supplier Import Line",
"owner": "Administrator",
"permissions": [],
"sort_field": "idx",
"sort_order": "ASC",
"states": [],
"track_changes": 0
}
@@ -0,0 +1,5 @@
from frappe.model.document import Document
class SupplierImportLine(Document):
pass
@@ -0,0 +1,221 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "field:profile_name",
"creation": "2026-04-14 09:10:00.000000",
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"profile_name",
"enabled",
"column_break_header",
"supplier",
"vendor",
"parser_section",
"parser_key",
"file_type",
"column_break_parser",
"default_output_mode",
"list_price_discount_pct",
"defaults_section",
"default_company",
"default_cost_center",
"default_warehouse",
"column_break_defaults",
"default_taxes_and_charges",
"default_payment_terms_template",
"default_tc_name",
"text_section",
"title_prefix",
"title_prefix_credit_note",
"intro_text_invoice",
"intro_text_credit_note",
"fallback_section",
"fallback_customer"
],
"fields": [
{
"fieldname": "profile_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Profile Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enabled",
"default": "1"
},
{
"fieldname": "column_break_header",
"fieldtype": "Column Break"
},
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier",
"reqd": 1
},
{
"fieldname": "vendor",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Vendor",
"options": "\nMicrosoft\nG DATA\nHornetsecurity\nCodeTwo\nOther"
},
{
"fieldname": "parser_section",
"fieldtype": "Section Break",
"label": "Parser"
},
{
"fieldname": "parser_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Parser Key",
"reqd": 1,
"description": "Technischer Schlüssel, der auf die Python-Parser-Klasse verweist (z. B. adn_monthly_csv_v1)"
},
{
"fieldname": "file_type",
"fieldtype": "Select",
"label": "File Type",
"options": "CSV\nExcel\nJSON\nAPI",
"default": "CSV"
},
{
"fieldname": "column_break_parser",
"fieldtype": "Column Break"
},
{
"fieldname": "default_output_mode",
"fieldtype": "Select",
"label": "Default Output Mode",
"options": "Sales Invoice\nDelivery Note",
"default": "Sales Invoice",
"reqd": 1
},
{
"fieldname": "list_price_discount_pct",
"fieldtype": "Float",
"label": "Listen­preis-Rabatt (%)",
"description": "Standardrabatt auf Listenpreise für diesen Lieferanten (ersetzt Magic-Number, z. B. 17,5)"
},
{
"fieldname": "defaults_section",
"fieldtype": "Section Break",
"label": "Dokument-Defaults"
},
{
"fieldname": "default_company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "default_cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "default_warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"description": "Für Delivery Notes erforderlich"
},
{
"fieldname": "column_break_defaults",
"fieldtype": "Column Break"
},
{
"fieldname": "default_taxes_and_charges",
"fieldtype": "Link",
"label": "Taxes and Charges",
"options": "Sales Taxes and Charges Template"
},
{
"fieldname": "default_payment_terms_template",
"fieldtype": "Link",
"label": "Payment Terms Template",
"options": "Payment Terms Template"
},
{
"fieldname": "default_tc_name",
"fieldtype": "Link",
"label": "Terms and Conditions",
"options": "Terms and Conditions"
},
{
"fieldname": "text_section",
"fieldtype": "Section Break",
"label": "Titel & Texte"
},
{
"fieldname": "title_prefix",
"fieldtype": "Data",
"label": "Title Prefix (Invoice)",
"description": "z. B. \"Abrechnung Microsoft Abo \" — Monat/Jahr wird beim Import angehängt"
},
{
"fieldname": "title_prefix_credit_note",
"fieldtype": "Data",
"label": "Title Prefix (Credit Note)"
},
{
"fieldname": "intro_text_invoice",
"fieldtype": "Text Editor",
"label": "Intro Text (Invoice)"
},
{
"fieldname": "intro_text_credit_note",
"fieldtype": "Text Editor",
"label": "Intro Text (Credit Note)"
},
{
"fieldname": "fallback_section",
"fieldtype": "Section Break",
"label": "Fallback"
},
{
"fieldname": "fallback_customer",
"fieldtype": "Link",
"label": "Fallback Customer",
"options": "Customer",
"description": "Wird verwendet, wenn der Endkunde in der Datei nicht im ERP gefunden wird"
}
],
"index_web_pages_for_search": 0,
"modified": "2026-04-14 09:10:00.000000",
"modified_by": "Administrator",
"module": "MSP",
"name": "Supplier Import Profile",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,8 @@
from frappe.model.document import Document
class SupplierImportProfile(Document):
def validate(self):
if self.default_output_mode == "Delivery Note" and not self.default_warehouse:
from frappe import throw, _
throw(_('Für Output Mode „Delivery Note“ muss ein Default Warehouse gesetzt sein.'))
@@ -0,0 +1,184 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "naming_series:",
"creation": "2026-04-14 09:15:00.000000",
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"naming_series",
"profile",
"status",
"column_break_head",
"file",
"output_mode",
"acknowledge_format_drift",
"start_import",
"timing_section",
"started_at",
"finished_at",
"column_break_timing",
"line_count",
"document_count",
"error_count",
"log_section",
"log",
"lines_section",
"lines"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "IMPRUN-.YYYY.-",
"reqd": 1
},
{
"fieldname": "profile",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Profile",
"options": "Supplier Import Profile",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Pending\nRunning\nSucceeded\nPartial\nFailed",
"default": "Pending",
"read_only": 1
},
{
"fieldname": "column_break_head",
"fieldtype": "Column Break"
},
{
"fieldname": "file",
"fieldtype": "Attach",
"label": "Source File",
"reqd": 1
},
{
"fieldname": "output_mode",
"fieldtype": "Select",
"label": "Output Mode (Override)",
"options": "\nSales Invoice\nDelivery Note",
"description": "Leer = Profil-Default verwenden. Je Kunde kann Customer.billing_mode weiterhin auf Delivery Note zwingen."
},
{
"fieldname": "acknowledge_format_drift",
"fieldtype": "Check",
"label": "Abweichendes Format akzeptieren",
"description": "Wenn gesetzt, werden Format-Warnungen vom Parser nur geloggt und nicht als Fehler behandelt"
},
{
"fieldname": "start_import",
"fieldtype": "Button",
"label": "Start Import",
"options": "do_import"
},
{
"fieldname": "timing_section",
"fieldtype": "Section Break",
"label": "Timing & Statistik"
},
{
"fieldname": "started_at",
"fieldtype": "Datetime",
"label": "Started At",
"read_only": 1
},
{
"fieldname": "finished_at",
"fieldtype": "Datetime",
"label": "Finished At",
"read_only": 1
},
{
"fieldname": "column_break_timing",
"fieldtype": "Column Break"
},
{
"fieldname": "line_count",
"fieldtype": "Int",
"label": "Lines",
"read_only": 1
},
{
"fieldname": "document_count",
"fieldtype": "Int",
"label": "Documents Created",
"read_only": 1
},
{
"fieldname": "error_count",
"fieldtype": "Int",
"label": "Errors",
"read_only": 1
},
{
"fieldname": "log_section",
"fieldtype": "Section Break",
"label": "Log",
"collapsible": 1
},
{
"fieldname": "log",
"fieldtype": "Long Text",
"label": "Log",
"read_only": 1
},
{
"fieldname": "lines_section",
"fieldtype": "Section Break",
"label": "Lines",
"collapsible": 1
},
{
"fieldname": "lines",
"fieldtype": "Table",
"label": "Lines",
"options": "Supplier Import Line"
}
],
"index_web_pages_for_search": 0,
"modified": "2026-04-14 09:15:00.000000",
"modified_by": "Administrator",
"module": "MSP",
"name": "Supplier Import Run",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,11 @@
import frappe
from frappe.model.document import Document
class SupplierImportRun(Document):
@frappe.whitelist()
def do_import(self):
"""Delegiert den eigentlichen Import an msp.importers.run_orchestrator.
Die Implementation folgt in Phase 1; hier nur die Einstiegsstelle."""
from msp.importers.run_orchestrator import execute_run
return execute_run(self)
@@ -0,0 +1,213 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "format:SUB-{external_id}",
"creation": "2026-04-14 09:00:00.000000",
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"title",
"external_id",
"status",
"column_break_basics",
"customer",
"supplier",
"vendor",
"product_section",
"item",
"item_name",
"vendor_product_id",
"column_break_product",
"billing_plan",
"term_duration",
"qty_current",
"timeline_section",
"start_date",
"current_period_end",
"column_break_timeline",
"last_event_date",
"last_booking_type"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"read_only": 1
},
{
"fieldname": "external_id",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "External Subscription ID",
"reqd": 1,
"unique": 1,
"search_index": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Active\nLapsed\nCancelled",
"default": "Active"
},
{
"fieldname": "column_break_basics",
"fieldtype": "Column Break"
},
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Supplier",
"options": "Supplier"
},
{
"fieldname": "vendor",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Vendor",
"options": "\nMicrosoft\nG DATA\nHornetsecurity\nCodeTwo\nOther"
},
{
"fieldname": "product_section",
"fieldtype": "Section Break",
"label": "Product"
},
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{
"fetch_from": "item.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "vendor_product_id",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Vendor Product ID",
"search_index": 1,
"description": "z. B. Microsoft Commercial Product ID (CFQ7TTC0...)"
},
{
"fieldname": "column_break_product",
"fieldtype": "Column Break"
},
{
"fieldname": "billing_plan",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Billing Plan",
"options": "\nMonthly\nAnnual\nTriennial"
},
{
"fieldname": "term_duration",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Term Duration",
"options": "\nP1M\nP1Y\nP3Y"
},
{
"fieldname": "qty_current",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Qty (current)"
},
{
"fieldname": "timeline_section",
"fieldtype": "Section Break",
"label": "Laufzeit"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Subscription Start",
"description": "Ursprüngliches Anlagedatum beim Lieferanten"
},
{
"fieldname": "current_period_end",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Aktuelles Periodenende",
"description": "Ende der zuletzt abgerechneten Periode"
},
{
"fieldname": "column_break_timeline",
"fieldtype": "Column Break"
},
{
"fieldname": "last_event_date",
"fieldtype": "Date",
"label": "Letzte Buchung",
"read_only": 1
},
{
"fieldname": "last_booking_type",
"fieldtype": "Data",
"label": "Letzter Buchungstyp",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"links": [
{
"group": "Events",
"link_doctype": "Supply Subscription Event",
"link_fieldname": "supply_subscription"
}
],
"modified": "2026-04-14 09:00:00.000000",
"modified_by": "Administrator",
"module": "MSP",
"name": "Supply Subscription",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,13 @@
import frappe
from frappe.model.document import Document
class SupplySubscription(Document):
def before_save(self):
self.title = self._compose_title()
def _compose_title(self) -> str:
parts = [self.customer or "?", self.item_name or self.item or self.vendor_product_id or "?"]
if self.vendor:
parts.append(self.vendor)
return " · ".join(p for p in parts if p)
@@ -0,0 +1,201 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "naming_series:",
"creation": "2026-04-14 09:00:00.000000",
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"naming_series",
"supply_subscription",
"booking_type",
"column_break_head",
"customer",
"supplier",
"period_section",
"period_start",
"period_end",
"column_break_period",
"qty",
"rate",
"amount",
"link_section",
"sales_invoice",
"sales_invoice_item_row",
"column_break_link",
"delivery_note",
"delivery_note_item_row",
"purchase_invoice",
"supplier_import_line"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "SUBE-.YYYY.-",
"reqd": 1
},
{
"fieldname": "supply_subscription",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Supply Subscription",
"options": "Supply Subscription",
"reqd": 1
},
{
"fieldname": "booking_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Booking Type",
"options": "\nNew\nRenewal\nBilling\nUpgrade\nCancel"
},
{
"fieldname": "column_break_head",
"fieldtype": "Column Break"
},
{
"fetch_from": "supply_subscription.customer",
"fieldname": "customer",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Customer",
"options": "Customer",
"read_only": 1
},
{
"fetch_from": "supply_subscription.supplier",
"fieldname": "supplier",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Supplier",
"options": "Supplier",
"read_only": 1
},
{
"fieldname": "period_section",
"fieldtype": "Section Break",
"label": "Zeitraum & Mengen"
},
{
"fieldname": "period_start",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Period Start",
"reqd": 1
},
{
"fieldname": "period_end",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Period End",
"reqd": 1
},
{
"fieldname": "column_break_period",
"fieldtype": "Column Break"
},
{
"fieldname": "qty",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Qty"
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount"
},
{
"fieldname": "link_section",
"fieldtype": "Section Break",
"label": "Verknüpfungen"
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Sales Invoice",
"options": "Sales Invoice"
},
{
"fieldname": "sales_invoice_item_row",
"fieldtype": "Data",
"label": "Sales Invoice Item (row)",
"description": "name der Zeile in tabSales Invoice Item"
},
{
"fieldname": "column_break_link",
"fieldtype": "Column Break"
},
{
"fieldname": "delivery_note",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Delivery Note",
"options": "Delivery Note"
},
{
"fieldname": "delivery_note_item_row",
"fieldtype": "Data",
"label": "Delivery Note Item (row)"
},
{
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Purchase Invoice",
"options": "Purchase Invoice"
},
{
"fieldname": "supplier_import_line",
"fieldtype": "Data",
"label": "Supplier Import Line (row)",
"description": "Rückverweis auf die Zeile im Supplier Import Run"
}
],
"index_web_pages_for_search": 0,
"modified": "2026-04-14 09:00:00.000000",
"modified_by": "Administrator",
"module": "MSP",
"name": "Supply Subscription Event",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
}
],
"sort_field": "period_end",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,36 @@
import frappe
from frappe.model.document import Document
class SupplySubscriptionEvent(Document):
def on_update(self):
self._refresh_subscription_denormals()
def on_trash(self):
self._refresh_subscription_denormals()
def _refresh_subscription_denormals(self):
"""Aktualisiert last_event_date / last_booking_type / current_period_end
auf der übergeordneten Supply Subscription auf Basis des letzten Events."""
if not self.supply_subscription:
return
latest = frappe.db.get_all(
"Supply Subscription Event",
filters={"supply_subscription": self.supply_subscription},
fields=["period_end", "booking_type", "qty"],
order_by="period_end desc",
limit=1,
)
if not latest:
frappe.db.set_value("Supply Subscription", self.supply_subscription, {
"last_event_date": None,
"last_booking_type": None,
})
return
row = latest[0]
frappe.db.set_value("Supply Subscription", self.supply_subscription, {
"last_event_date": row["period_end"],
"last_booking_type": row["booking_type"],
"current_period_end": row["period_end"],
"qty_current": row.get("qty") or 0,
})