chore(msp): Dev-/Test-Helfer für Supplier-Import (nur Entwicklungs-Hilfsmittel)

Nicht-produktive Helper-Module im importers/-Paket, mit Unterstrich-Präfix
markiert. Werden von keiner Produktiv-Codebasis aufgerufen, sondern nur
manuell über `bench execute`:

- _smoke_test: parst eine CSV und gibt Verteilungsstatistiken aus
  (distinct customers/products, enums).
- _runner: Convenience-Wrapper, um einen bestehenden Import-Run per
  Namen zu starten.
- _inspect: liefert kompakte JSON-Übersicht über einen Import-Run
  (Status, Statistik, erste Zeilen) zur Fehlersuche.
- _batchtest: importiert mehrere CSVs chronologisch und summiert
  Subscription-/Event-Zahlen.
- _cleanup: löscht alles, was vom neuen Importer erzeugt wurde. Nur für
  Dev-/Test-Instanzen gedacht; prüft vorab auf test-DB oder
  developer_mode.
This commit is contained in:
David Malinowski
2026-04-14 12:41:48 +02:00
parent 687a1faac0
commit 384378b605
5 changed files with 201 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
"""Batch-Test: mehrere ADN-CSVs chronologisch importieren."""
from __future__ import annotations
import json
import os
import frappe
from msp.importers._runner import run as run_one
CSV_SEQUENCE = [
"/private/files/433148_Rechnungen_17_10_2025.csv",
"/private/files/433148_Rechnungen_17_11_2025.csv",
"/private/files/433148_Rechnungen_18_12_2025.csv",
"/private/files/433148_Rechnungen_16_01_2026.csv",
"/private/files/433148_Rechnungen_16_02_2026.csv",
"/private/files/433148_Rechnungen_19_03_2026.csv",
]
def run(profile: str = "ADN Monthly CSV v1") -> str:
results = []
for path in CSV_SEQUENCE:
abs_check = os.path.join(frappe.get_site_path(), "private", "files", os.path.basename(path))
abs_check = os.path.abspath(abs_check)
if not os.path.exists(abs_check):
results.append({"file": path, "skipped": "file not found"})
continue
doc = frappe.get_doc({
"doctype": "Supplier Import Run",
"profile": profile,
"file": path,
"output_mode": "Sales Invoice",
})
doc.insert(ignore_permissions=True)
frappe.db.commit()
summary = run_one(doc.name)
results.append({"file": path, "run": doc.name, **summary})
# Supply-Subscription-Bestand ermitteln
subs_total = frappe.db.count("Supply Subscription")
events_total = frappe.db.count("Supply Subscription Event")
return json.dumps({
"runs": results,
"subscriptions_total": subs_total,
"events_total": events_total,
}, default=str, indent=2, ensure_ascii=False)
+50
View File
@@ -0,0 +1,50 @@
"""Cleanup-Helper: löscht alles, was der neue Importer erzeugt hat (nur Test!)."""
from __future__ import annotations
import frappe
def cleanup_all():
"""Entfernt sämtliche vom neuen Importer angelegten Daten (Test-System only!)."""
if "test" not in frappe.local.conf.get("db_name", "") and not frappe.local.conf.get("developer_mode"):
raise Exception("Cleanup nur in Entwicklungs-/Test-Umgebungen erlaubt.")
# 1. Supply Subscription Events → 2. Supply Subscriptions
frappe.db.sql("DELETE FROM `tabSupply Subscription Event`")
frappe.db.sql("DELETE FROM `tabSupply Subscription`")
# 3. Supplier Import Lines (child) → 4. Runs
frappe.db.sql("DELETE FROM `tabSupplier Import Line`")
frappe.db.sql("DELETE FROM `tabSupplier Import Run`")
# 5. Draft Sales Invoices, die über den neuen Importer erzeugt wurden
# (Heuristik: aus Supplier Import Line.target_doc_name wären sie referenziert
# gewesen — da wir die Lines gerade gelöscht haben, nehmen wir stattdessen
# alle Draft-SIs mit unserem supply_subscription-Link als Anzeichen.)
draft_sis = frappe.db.sql_list("""
SELECT DISTINCT parent FROM `tabSales Invoice Item`
WHERE supply_subscription IS NOT NULL AND supply_subscription != ''
""")
for name in draft_sis:
doc = frappe.get_doc("Sales Invoice", name)
if doc.docstatus == 0:
doc.delete()
# 6. Draft Delivery Notes entsprechend
draft_dns = frappe.db.sql_list("""
SELECT DISTINCT parent FROM `tabDelivery Note Item`
WHERE supply_subscription IS NOT NULL AND supply_subscription != ''
""")
for name in draft_dns:
doc = frappe.get_doc("Delivery Note", name)
if doc.docstatus == 0:
doc.delete()
frappe.db.commit()
return {
"supply_subscriptions_deleted": True,
"import_runs_deleted": True,
"sales_invoices_cleaned": len(draft_sis),
"delivery_notes_cleaned": len(draft_dns),
}
+32
View File
@@ -0,0 +1,32 @@
"""Inspect-Helpers für die Entwicklung."""
from __future__ import annotations
import json
import frappe
def inspect_run(name: str, show_lines: int = 5) -> str:
run = frappe.get_doc("Supplier Import Run", name)
lines = run.lines[:show_lines]
return json.dumps({
"status": run.status,
"file": run.file,
"line_count": run.line_count,
"document_count": run.document_count,
"error_count": run.error_count,
"log_tail": (run.log or "")[-2000:],
"first_lines": [
{
"customer_external_ref": l.customer_external_ref,
"customer": l.customer,
"vendor_product_id": l.vendor_product_id,
"qty": l.qty,
"amount": l.amount,
"line_status": l.line_status,
"error_message": l.error_message,
"target": f"{l.target_doc_type or ''} {l.target_doc_name or ''}",
}
for l in lines
],
}, default=str, ensure_ascii=False, indent=2)
+13
View File
@@ -0,0 +1,13 @@
"""Convenience-Helper für Bench-Tests."""
from __future__ import annotations
import frappe
from msp.importers.run_orchestrator import execute_run
def run(name: str) -> dict:
"""Startet einen bestehenden Supplier Import Run anhand seines Namens."""
doc = frappe.get_doc("Supplier Import Run", name)
return execute_run(doc)
+59
View File
@@ -0,0 +1,59 @@
"""Temporärer Smoke-Test für den ADN-Parser.
Aufruf:
bench --site <site> execute msp.importers._smoke_test.run --kwargs '{"path":"..."}'
"""
from __future__ import annotations
from collections import Counter
import json
from msp.importers.registry import resolve
def run(path: str) -> str:
parser = resolve("adn_monthly_csv_v1")
result = parser.parse(path)
counters = {
"rows": len(result.rows),
"format_drift": result.format_drift,
"warnings": result.warnings,
"invoice_date": str(result.invoice_date) if result.invoice_date else None,
}
by_customer = Counter(r.customer_external_ref for r in result.rows)
by_product = Counter(r.vendor_product_id for r in result.rows)
by_billing_plan = Counter(r.billing_plan for r in result.rows)
by_term = Counter(r.term_duration for r in result.rows)
by_booking = Counter(r.booking_type for r in result.rows)
doc_types = Counter(r.document_type for r in result.rows)
sample = result.rows[0] if result.rows else None
sample_dump = None
if sample:
sample_dump = {
"customer_external_ref": sample.customer_external_ref,
"vendor_product_id": sample.vendor_product_id,
"qty": sample.qty,
"amount": sample.amount,
"period_start": str(sample.period_start) if sample.period_start else None,
"period_end": str(sample.period_end) if sample.period_end else None,
"subscription_external_id": sample.subscription_external_id,
"billing_plan": sample.billing_plan,
"term_duration": sample.term_duration,
"booking_type": sample.booking_type,
}
return json.dumps({
"counters": counters,
"by_customer_top": by_customer.most_common(10),
"by_product_top": by_product.most_common(10),
"by_billing_plan": by_billing_plan,
"by_term_duration": by_term,
"by_booking_type": by_booking,
"document_types": doc_types,
"distinct_customers": len(by_customer),
"distinct_products": len(by_product),
"sample_first_row": sample_dump,
}, default=str, ensure_ascii=False, indent=2)