Bestellungen aus Shopware als Sales Order in ERPNext importieren #27

Closed
opened 2026-05-10 16:46:10 +00:00 by csaeum · 2 comments
csaeum commented 2026-05-10 16:46:10 +00:00 (Migrated from gitlab.localdomain)

Shopware-Bestellungen über die Admin API abrufen und als ERPNext Sales Orders anlegen:

  • Endpoint: GET /api/order (nur neue, noch nicht importierte Bestellungen)
  • Felder: Bestellnummer, Positionen, Preise, Versandkosten, Status
  • Shopware Order ID in Custom Field der Sales Order speichern
  • Bereits importierte Bestellungen nicht doppelt anlegen
  • Implementierung in shopware6/api/order.py
Shopware-Bestellungen über die Admin API abrufen und als ERPNext Sales Orders anlegen: - Endpoint: `GET /api/order` (nur neue, noch nicht importierte Bestellungen) - Felder: Bestellnummer, Positionen, Preise, Versandkosten, Status - Shopware Order ID in Custom Field der Sales Order speichern - Bereits importierte Bestellungen nicht doppelt anlegen - Implementierung in `shopware6/api/order.py`
Owner

Beim Recherchieren für #8 (Sales Channel Mapping) ist mir Julians (TubaApollo) separates Plugin aufgefallen: shopware6-erpnext-webhook — ein eigenständiges Shopware-6-PHP-Plugin (Composer-Paket, MIT-lizenziert, kein Teil dieses Repos), das bei Bestell-Events (order.placed, order.state.changed, order_transaction.state.changed, customer.written) signierte (HMAC-SHA256) Webhooks verschickt.

Relevant für dieses Issue: Das Plugin bringt bereits ein fertiges ERPNext-Preset mit, das exakt folgenden Endpoint erwartet:

https://your-erpnext.com/api/method/ecommerce_integrations.shopware6.connection.webhook_handler

D.h. Julian hat die Gegenstelle auf unserer Seite schon mitgedacht (Payload-Format {event, data, timestamp, source}, Signaturprüfung per HMAC-SHA256).

Aktuell ist dieses Issue auf Polling (GET /api/order) gescoped. Bevor wir #27 umsetzen, sollten wir bewusst entscheiden: Polling beibehalten, oder auf dieses Webhook-Plugin als Trigger umsteigen (ggf. auch kombiniert — Webhook für Echtzeit-Import, Polling als Fallback/Backfill). Details dazu auch in Projekt.md unter "Reference Code → Julian's Webhook Plugin".

Keine Entscheidung an dieser Stelle, nur Referenz für später.

Beim Recherchieren für #8 (Sales Channel Mapping) ist mir Julians (TubaApollo) separates Plugin aufgefallen: [`shopware6-erpnext-webhook`](https://github.com/TubaApollo/shopware6-erpnext-webhook) — ein eigenständiges Shopware-6-PHP-Plugin (Composer-Paket, MIT-lizenziert, kein Teil dieses Repos), das bei Bestell-Events (`order.placed`, `order.state.changed`, `order_transaction.state.changed`, `customer.written`) signierte (HMAC-SHA256) Webhooks verschickt. Relevant für dieses Issue: Das Plugin bringt bereits ein fertiges ERPNext-Preset mit, das exakt folgenden Endpoint erwartet: ``` https://your-erpnext.com/api/method/ecommerce_integrations.shopware6.connection.webhook_handler ``` D.h. Julian hat die Gegenstelle auf unserer Seite schon mitgedacht (Payload-Format `{event, data, timestamp, source}`, Signaturprüfung per HMAC-SHA256). Aktuell ist dieses Issue auf Polling (`GET /api/order`) gescoped. Bevor wir #27 umsetzen, sollten wir bewusst entscheiden: Polling beibehalten, oder auf dieses Webhook-Plugin als Trigger umsteigen (ggf. auch kombiniert — Webhook für Echtzeit-Import, Polling als Fallback/Backfill). Details dazu auch in `Projekt.md` unter "Reference Code → Julian's Webhook Plugin". Keine Entscheidung an dieser Stelle, nur Referenz für später.
Owner

Umgesetzt in shopware6/api/order.py (import_orders()) + shopware6/sync/order.py (sync_orders_hourly()).

Architektur-Entscheidung (mit Nutzer abgestimmt): reines Polling, kein Webhook. Abgewogen gegen Marcels bereits validiertes Hybrid-Muster (Webhook + Scheduler-Fallback, siehe sein order/scheduled_sync.py) und gegen diesen Repos eigenen, rein webhook-getriebenen Shopify-Connector (shopify/order.py) — Shopify nutzt Webhooks, weil Shopifys eigenes App-Modell das verlangt, das ist keine Frappe/ERPNext-Designentscheidung. Polling bleibt näher am Frappe/ERPNext-Standardmuster (scheduler_events, wie schon #21/#22 für den Bestandssync), braucht keine zusätzliche Komponente (Julians separates shopware6-erpnext-webhook-Plugin, HMAC-Signaturprüfung, öffentlicher Endpoint) und ist von den drei Optionen am robustesten: idempotent, selbstheilend bei einem verpassten Lauf.

Kernspannung: Sales Order.customer/.company sind ERPNext-Pflichtfelder, aber Kundenanlage ist explizit #28s und Storefront-Härtung #29s Scope. Gelöst über das etablierte "fail loud, nie raten"-Prinzip (#18/#20/#25): #27 importiert nur Bestellungen mit eindeutig auflösbarer Storefront (über die bereits bestehende Shopware Storefront, #7) und eindeutig auflösbarem, bereits existierendem Kunden (E-Mail-Abgleich, keine Neuanlage — bleibt #28). Alles andere wird übersprungen und im Error Log dokumentiert, nicht geraten.

Design-Details:

  • Zeitfenster statt Cursor: jeder Lauf prüft die letzten 3 Tage erneut, Dedup läuft über das neue shopware_order_id Custom Field auf Sales Order — kein eigenes "last sync"-Tracking nötig.
  • Keine Sales-Taxes-and-Charges-Zeilen — bleibt #30s Scope. Item-Rates werden je nach Shopwares taxStatus netto übernommen oder über dieselbe unrundende Formel aus #18 (gross / (1+tax_rate/100)) umgerechnet.
  • Neues Shopware Account.auto_submit_orders-Feld (Default aus, Nutzerentscheidung): steuert pro Account, ob importierte Bestellungen automatisch eingereicht (docstatus=1) oder als Entwurf angelegt werden.
  • Multi-Currency explizit behandelt: conversion_rate=1 bei gleicher Währung, sonst über ERPNext eigene get_exchange_rate() — nicht auflösbar heißt failed, nicht stillschweigend 1.
  • Versandkosten als Zeile auf einem einzigen, automatisch angelegten, nicht lagergeführten "Shipping"-Item.

Live-Fund während der Verifikation: eine echte Testbestellung besteht komplett aus Shopwares Musterbestellungs-Feature — Positionen mit type: "product" (nicht vom Typ unterscheidbar!), aber payload.isSample: true und Preis 0, referencedId zeigt auf das echte Produkt. #27s bestehende Item-Auflösung verarbeitet das bereits korrekt (Preis 0, kein Crash), aber ob solche Positionen so importiert oder gesondert markiert werden sollen, ist eine offene Frage — als neues, noch nicht eingeplantes Issue #33 festgehalten statt hier inline entschieden.

Live verifiziert: Erfolgspfad (korrekte Company/Preisliste/Währung/Kunde/Items/Rates/Versand/Custom Fields) gegen eine konstruierte Test-Storefront + Test-Kunde; zweimaliger Lauf ohne Duplikat (Dedup); alle vier Skip-Pfade (unbekannte Storefront, unbekannter Kunde, storniert, nicht auflösbare Position) mit korrektem Zähler + Error-Log-Eintrag; auto_submit_orders=0/1 beide getestet; taxStatus="net"-Pfad bestätigt. Alle Testdaten gelöscht und Löschung unabhängig bestätigt.

Details: docs/architecture.md → "Issue 27: Import Shopware orders as Sales Orders — pure polling".

Umgesetzt in `shopware6/api/order.py` (`import_orders()`) + `shopware6/sync/order.py` (`sync_orders_hourly()`). **Architektur-Entscheidung (mit Nutzer abgestimmt): reines Polling, kein Webhook.** Abgewogen gegen Marcels bereits validiertes Hybrid-Muster (Webhook + Scheduler-Fallback, siehe sein `order/scheduled_sync.py`) und gegen diesen Repos eigenen, rein webhook-getriebenen Shopify-Connector (`shopify/order.py`) — Shopify nutzt Webhooks, weil Shopifys eigenes App-Modell das verlangt, das ist keine Frappe/ERPNext-Designentscheidung. Polling bleibt näher am Frappe/ERPNext-Standardmuster (`scheduler_events`, wie schon #21/#22 für den Bestandssync), braucht keine zusätzliche Komponente (Julians separates `shopware6-erpnext-webhook`-Plugin, HMAC-Signaturprüfung, öffentlicher Endpoint) und ist von den drei Optionen am robustesten: idempotent, selbstheilend bei einem verpassten Lauf. **Kernspannung**: `Sales Order.customer`/`.company` sind ERPNext-Pflichtfelder, aber Kundenanlage ist explizit #28s und Storefront-Härtung #29s Scope. Gelöst über das etablierte "fail loud, nie raten"-Prinzip (#18/#20/#25): #27 importiert nur Bestellungen mit eindeutig auflösbarer Storefront (über die bereits bestehende `Shopware Storefront`, #7) und eindeutig auflösbarem, **bereits existierendem** Kunden (E-Mail-Abgleich, keine Neuanlage — bleibt #28). Alles andere wird übersprungen und im Error Log dokumentiert, nicht geraten. **Design-Details**: - Zeitfenster statt Cursor: jeder Lauf prüft die letzten 3 Tage erneut, Dedup läuft über das neue `shopware_order_id` Custom Field auf `Sales Order` — kein eigenes "last sync"-Tracking nötig. - Keine Sales-Taxes-and-Charges-Zeilen — bleibt #30s Scope. Item-Rates werden je nach Shopwares `taxStatus` netto übernommen oder über dieselbe unrundende Formel aus #18 (`gross / (1+tax_rate/100)`) umgerechnet. - Neues `Shopware Account.auto_submit_orders`-Feld (Default **aus**, Nutzerentscheidung): steuert pro Account, ob importierte Bestellungen automatisch eingereicht (`docstatus=1`) oder als Entwurf angelegt werden. - Multi-Currency explizit behandelt: `conversion_rate=1` bei gleicher Währung, sonst über ERPNext eigene `get_exchange_rate()` — nicht auflösbar heißt `failed`, nicht stillschweigend `1`. - Versandkosten als Zeile auf einem einzigen, automatisch angelegten, nicht lagergeführten `"Shipping"`-Item. **Live-Fund während der Verifikation**: eine echte Testbestellung besteht komplett aus Shopwares Musterbestellungs-Feature — Positionen mit `type: "product"` (nicht vom Typ unterscheidbar!), aber `payload.isSample: true` und Preis 0, `referencedId` zeigt auf das echte Produkt. #27s bestehende Item-Auflösung verarbeitet das bereits korrekt (Preis 0, kein Crash), aber ob solche Positionen so importiert oder gesondert markiert werden sollen, ist eine offene Frage — als neues, noch nicht eingeplantes Issue #33 festgehalten statt hier inline entschieden. **Live verifiziert**: Erfolgspfad (korrekte Company/Preisliste/Währung/Kunde/Items/Rates/Versand/Custom Fields) gegen eine konstruierte Test-Storefront + Test-Kunde; zweimaliger Lauf ohne Duplikat (Dedup); alle vier Skip-Pfade (unbekannte Storefront, unbekannter Kunde, storniert, nicht auflösbare Position) mit korrektem Zähler + Error-Log-Eintrag; `auto_submit_orders=0`/`1` beide getestet; `taxStatus="net"`-Pfad bestätigt. Alle Testdaten gelöscht und Löschung unabhängig bestätigt. Details: `docs/architecture.md` → "Issue 27: Import Shopware orders as Sales Orders — pure polling".
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Frappe-Projekte/ecommerce_integrations#27
No description provided.