Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70272e81d6 | |||
| 0d8353fb9c |
@@ -0,0 +1,44 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. Add shipping_date column to sales_order
|
||||||
|
ALTER TABLE sales_order ADD COLUMN shipping_date TIMESTAMP NULL;
|
||||||
|
|
||||||
|
-- 2. Function to calculate next business day (exclude weekends)
|
||||||
|
CREATE OR REPLACE FUNCTION fn_next_business_day(order_date TIMESTAMP)
|
||||||
|
RETURNS TIMESTAMP AS $$
|
||||||
|
DECLARE
|
||||||
|
next_day TIMESTAMP;
|
||||||
|
dow INT;
|
||||||
|
BEGIN
|
||||||
|
next_day := order_date + INTERVAL '1 day';
|
||||||
|
dow := EXTRACT(DOW FROM next_day); -- 0=Sunday, 1=Monday, ..., 6=Saturday
|
||||||
|
|
||||||
|
-- Skip weekends
|
||||||
|
IF dow = 0 THEN -- Sunday
|
||||||
|
next_day := next_day + INTERVAL '1 day';
|
||||||
|
ELSIF dow = 6 THEN -- Saturday
|
||||||
|
next_day := next_day + INTERVAL '2 days';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN next_day;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 3. Trigger for automatic shipping_date calculation
|
||||||
|
CREATE OR REPLACE FUNCTION fn_auto_shipping_date()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Only calculate for new orders or when shipping_date is empty
|
||||||
|
IF NEW.shipping_date IS NULL AND NEW.order_date IS NOT NULL THEN
|
||||||
|
NEW.shipping_date := fn_next_business_day(NEW.order_date);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_auto_shipping_date
|
||||||
|
BEFORE INSERT OR UPDATE ON sales_order
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION fn_auto_shipping_date();
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
SELECT
|
||||||
|
so.external_ref AS "Bestellnummer",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS "Bestelldatum",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS "Versanddatum",
|
||||||
|
COALESCE(ad.first_name, '') AS "Vorname",
|
||||||
|
COALESCE(ad.last_name, '') AS "Nachname",
|
||||||
|
COALESCE(ad.street, '') AS "Strasse",
|
||||||
|
COALESCE(ad.house_number, '') AS "Hausnummer",
|
||||||
|
COALESCE(ad.zip, '') AS "PLZ",
|
||||||
|
COALESCE(ad.city, '') AS "Stadt",
|
||||||
|
COALESCE(ad.country_name, '') AS "Land",
|
||||||
|
COALESCE(pm.code, '') AS "Zahlungsart",
|
||||||
|
COALESCE(so.amount_net, 0) AS "Gesamtbetrag_netto",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS "Versandkosten",
|
||||||
|
COALESCE(so.total_amount, 0) AS "Gesamtbetrag_brutto",
|
||||||
|
COALESCE(so.amount_discount, 0) AS "Rabatt",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS "#_ChagaFlaschen",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS "#_ReishiFlaschen",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS "#_ShiitakeFlaschen",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS "#_LionsManeFlaschen"
|
||||||
|
FROM sales_order so
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
WHERE so.external_ref = $1
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"name": "Excel Buchhaltung Befüllung",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"path": "excel_befuellen",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [-288, -64],
|
||||||
|
"id": "webhook-node",
|
||||||
|
"name": "Webhook",
|
||||||
|
"webhookId": "excel-befuellen-webhook"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "SELECT \n -- Bestellinformationen\n so.external_ref AS \"Bestellnummer\",\n TO_CHAR(so.order_date, 'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"Bestelldatum\",\n TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS \"Versanddatum\",\n \n -- Kundenadresse (Lieferadresse)\n COALESCE(ad.first_name, '') AS \"Vorname\",\n COALESCE(ad.last_name, '') AS \"Nachname\",\n COALESCE(ad.street, '') AS \"Strasse\",\n COALESCE(ad.house_number, '') AS \"Hausnummer\",\n COALESCE(ad.zip, '') AS \"PLZ\",\n COALESCE(ad.city, '') AS \"Stadt\",\n COALESCE(ad.country_name, '') AS \"Land\",\n \n -- Zahlungs- und Betragsinformationen\n COALESCE(pm.code, '') AS \"Zahlungsart\",\n COALESCE(so.amount_net, 0) AS \"Gesamtbetrag_netto\",\n COALESCE(so.amount_shipping, 0) AS \"Versandkosten\",\n COALESCE(so.total_amount, 0) AS \"Gesamtbetrag_brutto\",\n COALESCE(so.amount_discount, 0) AS \"Rabatt\",\n \n -- Produktzählungen (nur aktive Produkte)\n COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS \"#_ChagaFlaschen\", -- CHAGA (ID 8)\n COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS \"#_ReishiFlaschen\", -- 003.01 (ID 5)\n COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS \"#_ShiitakeFlaschen\", -- SHIITAKE (ID 9)\n COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS \"#_LionsManeFlaschen\" -- 005.02 (ID 6)\n \nFROM sales_order so\n-- Lieferadresse\nLEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'\n-- Zahlungsart\nLEFT JOIN payment_method pm ON so.payment_method_id = pm.id\n-- Bestellpositionen und Allokationen\nLEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id\nLEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id\n-- Produkte (nur aktive)\nLEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'\n\nWHERE so.external_ref = $1\nGROUP BY so.id, ad.id, pm.id",
|
||||||
|
"queryValues": {
|
||||||
|
"values": {
|
||||||
|
"value": "={{ $json.Bestellnummer }}",
|
||||||
|
"string": "={{ $json.Bestellnummer }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"maxRows": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [-64, -64],
|
||||||
|
"id": "postgres-node",
|
||||||
|
"name": "Excel Daten aus ERP abfragen",
|
||||||
|
"credentials": {
|
||||||
|
"postgres": {
|
||||||
|
"id": "YOUR_POSTGRES_CREDENTIAL_ID",
|
||||||
|
"name": "ERP Naurua Database"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"resource": "worksheet",
|
||||||
|
"operation": "upsert",
|
||||||
|
"workbook": {
|
||||||
|
"__rl": true,
|
||||||
|
"value": "01CF7VVDAE6RTC2NMMTREI2C66KS7ZTNBK",
|
||||||
|
"mode": "list",
|
||||||
|
"cachedResultName": "Buchhaltung",
|
||||||
|
"cachedResultUrl": "https://beaufortdata-my.sharepoint.com/personal/mathias_glaeser_beaufort_ch/_layouts/15/Doc.aspx?sourcedoc=%7B2D66F404-8C35-489C-8D0B-DE54BF99B42A%7D&file=Buchhaltung.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1"
|
||||||
|
},
|
||||||
|
"worksheet": {
|
||||||
|
"__rl": true,
|
||||||
|
"value": "{1CBC09E2-CE51-0349-A736-1EA898A76FF2}",
|
||||||
|
"mode": "list",
|
||||||
|
"cachedResultName": "n8n",
|
||||||
|
"cachedResultUrl": "https://beaufortdata-my.sharepoint.com/personal/mathias_glaeser_beaufort_ch/_layouts/15/Doc.aspx?sourcedoc=%7B2D66F404-8C35-489C-8D0B-DE54BF99B42A%7D&file=Buchhaltung.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=n8n!A1"
|
||||||
|
},
|
||||||
|
"columnToMatchOn": "Bestelldatum",
|
||||||
|
"valueToMatchOn": "={{ $json.Bestelldatum }}",
|
||||||
|
"fieldsUi": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"column": "Versanddatum",
|
||||||
|
"fieldValue": "={{ $json.Versanddatum }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Bestellnummer",
|
||||||
|
"fieldValue": "={{ $json.Bestellnummer }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Vorname",
|
||||||
|
"fieldValue": "={{ $json.Vorname }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Nachname",
|
||||||
|
"fieldValue": "={{ $json.Nachname }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Strasse",
|
||||||
|
"fieldValue": "={{ $json.Strasse }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "PLZ",
|
||||||
|
"fieldValue": "={{ $json.PLZ }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Hausnummer",
|
||||||
|
"fieldValue": "={{ $json.Hausnummer }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Stadt",
|
||||||
|
"fieldValue": "={{ $json.Stadt }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Land",
|
||||||
|
"fieldValue": "={{ $json.Land }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Zahlungsart",
|
||||||
|
"fieldValue": "={{ $json.Zahlungsart }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Gesamtbetrag_netto",
|
||||||
|
"fieldValue": "={{ $json.Gesamtbetrag_netto }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Versandkosten",
|
||||||
|
"fieldValue": "={{ $json.Versandkosten }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Gesamtbetrag_brutto",
|
||||||
|
"fieldValue": "={{ $json.Gesamtbetrag_brutto }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "Rabatt",
|
||||||
|
"fieldValue": "={{ $json.Rabatt }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "#_ChagaFlaschen",
|
||||||
|
"fieldValue": "={{ $json['#_ChagaFlaschen'] }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "#_LionsManeFlaschen",
|
||||||
|
"fieldValue": "={{ $json['#_LionsManeFlaschen'] }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "#_ReishiFlaschen",
|
||||||
|
"fieldValue": "={{ $json['#_ReishiFlaschen'] }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "#_ShiitakeFlaschen",
|
||||||
|
"fieldValue": "={{ $json['#_ShiitakeFlaschen'] }}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.microsoftExcel",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [240, -64],
|
||||||
|
"id": "excel-node",
|
||||||
|
"name": "Bestellungen auf mathias onedrive Excel übertragen",
|
||||||
|
"retryOnFail": true,
|
||||||
|
"credentials": {
|
||||||
|
"microsoftExcelOAuth2Api": {
|
||||||
|
"id": "xrxWImZOTzNL3hTl",
|
||||||
|
"name": "Microsoft Excel account OneDrive MGL Naurua"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Excel Daten aus ERP abfragen",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Excel Daten aus ERP abfragen": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Bestellungen auf mathias onedrive Excel übertragen",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {},
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "excel-befuellen-workflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# n8n Postgres Node - Copy & Paste
|
||||||
|
|
||||||
|
## SQL Query für Postgres Node
|
||||||
|
Kopiere diesen SQL-Code in das Query-Feld der n8n Postgres Node:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
-- Bestellinformationen
|
||||||
|
so.external_ref AS "Bestellnummer",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS "Bestelldatum",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS "Versanddatum",
|
||||||
|
|
||||||
|
-- Kundenadresse (Lieferadresse)
|
||||||
|
COALESCE(ad.first_name, '') AS "Vorname",
|
||||||
|
COALESCE(ad.last_name, '') AS "Nachname",
|
||||||
|
COALESCE(ad.street, '') AS "Strasse",
|
||||||
|
COALESCE(ad.house_number, '') AS "Hausnummer",
|
||||||
|
COALESCE(ad.zip, '') AS "PLZ",
|
||||||
|
COALESCE(ad.city, '') AS "Stadt",
|
||||||
|
COALESCE(ad.country_name, '') AS "Land",
|
||||||
|
|
||||||
|
-- Zahlungs- und Betragsinformationen
|
||||||
|
COALESCE(pm.code, '') AS "Zahlungsart",
|
||||||
|
COALESCE(so.amount_net, 0) AS "Gesamtbetrag_netto",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS "Versandkosten",
|
||||||
|
COALESCE(so.total_amount, 0) AS "Gesamtbetrag_brutto",
|
||||||
|
COALESCE(so.amount_discount, 0) AS "Rabatt",
|
||||||
|
|
||||||
|
-- Produktzählungen (nur aktive Produkte)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS "#_ChagaFlaschen", -- CHAGA (ID 8)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS "#_ReishiFlaschen", -- 003.01 (ID 5)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS "#_ShiitakeFlaschen", -- SHIITAKE (ID 9)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS "#_LionsManeFlaschen" -- 005.02 (ID 6)
|
||||||
|
|
||||||
|
FROM sales_order so
|
||||||
|
-- Lieferadresse
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
-- Zahlungsart
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
-- Bestellpositionen und Allokationen
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
-- Produkte (nur aktive)
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
|
||||||
|
WHERE so.external_ref = $1
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Values (Parameter)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"values": {
|
||||||
|
"value": "={{ $json.Bestellnummer }}",
|
||||||
|
"string": "={{ $json.Bestellnummer }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Connection
|
||||||
|
```
|
||||||
|
Name: ERP Naurua Database
|
||||||
|
Host: 192.168.1.199
|
||||||
|
Port: 55432
|
||||||
|
Database: naurua_erp
|
||||||
|
User: codex_db_user
|
||||||
|
Password: Ze90re0KAry8gyJ6eAx0Gf4IelEGI
|
||||||
|
SSL: Disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"maxRows": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Fields
|
||||||
|
Die Query liefert diese Felder für die Excel-Node:
|
||||||
|
|
||||||
|
| Feldname | Typ | Beispiel |
|
||||||
|
|----------|-----|----------|
|
||||||
|
| Bestellnummer | String | "10477" |
|
||||||
|
| Bestelldatum | ISO DateTime | "2026-04-05T23:12:07" |
|
||||||
|
| Versanddatum | ISO Date | "2026-04-06" |
|
||||||
|
| Vorname | String | "Irendy" |
|
||||||
|
| Nachname | String | "Bucio" |
|
||||||
|
| Strasse | String | "Weingartenstrasse" |
|
||||||
|
| Hausnummer | String | "5" |
|
||||||
|
| PLZ | String | "4600" |
|
||||||
|
| Stadt | String | "Olten" |
|
||||||
|
| Land | String | "CH" |
|
||||||
|
| Zahlungsart | String | "" |
|
||||||
|
| Gesamtbetrag_netto | Number | 49.95 |
|
||||||
|
| Versandkosten | Number | 4.95 |
|
||||||
|
| Gesamtbetrag_brutto | Number | 44.91 |
|
||||||
|
| Rabatt | Number | 9.99 |
|
||||||
|
| #_ChagaFlaschen | Number | 0 |
|
||||||
|
| #_ReishiFlaschen | Number | 0 |
|
||||||
|
| #_ShiitakeFlaschen | Number | 0 |
|
||||||
|
| #_LionsManeFlaschen | Number | 1.0000 |
|
||||||
|
|
||||||
|
## Excel-Node Mapping
|
||||||
|
Die Excel-Node muss diese Feldnamen exakt übernehmen (Groß-/Kleinschreibung beachten!).
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Excel Daten aus ERP abfragen",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [0, 0],
|
||||||
|
"credentials": {
|
||||||
|
"postgres": {
|
||||||
|
"id": "YOUR_CREDENTIAL_ID_HERE",
|
||||||
|
"name": "ERP Naurua Database"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "SELECT \n -- Bestellinformationen\n so.external_ref AS \"Bestellnummer\",\n TO_CHAR(so.order_date, 'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"Bestelldatum\",\n TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS \"Versanddatum\",\n \n -- Kundenadresse (Lieferadresse)\n COALESCE(ad.first_name, '') AS \"Vorname\",\n COALESCE(ad.last_name, '') AS \"Nachname\",\n COALESCE(ad.street, '') AS \"Strasse\",\n COALESCE(ad.house_number, '') AS \"Hausnummer\",\n COALESCE(ad.zip, '') AS \"PLZ\",\n COALESCE(ad.city, '') AS \"Stadt\",\n COALESCE(ad.country_name, '') AS \"Land\",\n \n -- Zahlungs- und Betragsinformationen\n COALESCE(pm.code, '') AS \"Zahlungsart\",\n COALESCE(so.amount_net, 0) AS \"Gesamtbetrag_netto\",\n COALESCE(so.amount_shipping, 0) AS \"Versandkosten\",\n COALESCE(so.total_amount, 0) AS \"Gesamtbetrag_brutto\",\n COALESCE(so.amount_discount, 0) AS \"Rabatt\",\n \n -- Produktzählungen (nur aktive Produkte)\n COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS \"#_ChagaFlaschen\", -- CHAGA (ID 8)\n COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS \"#_ReishiFlaschen\", -- 003.01 (ID 5)\n COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS \"#_ShiitakeFlaschen\", -- SHIITAKE (ID 9)\n COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS \"#_LionsManeFlaschen\" -- 005.02 (ID 6)\n \nFROM sales_order so\n-- Lieferadresse\nLEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'\n-- Zahlungsart\nLEFT JOIN payment_method pm ON so.payment_method_id = pm.id\n-- Bestellpositionen und Allokationen\nLEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id\nLEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id\n-- Produkte (nur aktive)\nLEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'\n\nWHERE so.external_ref = $1\nGROUP BY so.id, ad.id, pm.id",
|
||||||
|
"queryValues": {
|
||||||
|
"values": {
|
||||||
|
"value": "={{ $json.Bestellnummer }}",
|
||||||
|
"string": "={{ $json.Bestellnummer }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"maxRows": 1
|
||||||
|
},
|
||||||
|
"additionalFields": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# n8n Postgres Node Konfiguration
|
||||||
|
|
||||||
|
## Database Connection
|
||||||
|
```
|
||||||
|
Name: ERP Naurua Database
|
||||||
|
Host: 192.168.1.199
|
||||||
|
Port: 55432
|
||||||
|
Database: naurua_erp
|
||||||
|
User: codex_db_user
|
||||||
|
Password: Ze90re0KAry8gyJ6eAx0Gf4IelEGI
|
||||||
|
SSL: Disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Parameter
|
||||||
|
|
||||||
|
### Operation
|
||||||
|
```
|
||||||
|
Execute Query
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
-- Bestellinformationen
|
||||||
|
so.external_ref AS "Bestellnummer",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS "Bestelldatum",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS "Versanddatum",
|
||||||
|
|
||||||
|
-- Kundenadresse (Lieferadresse)
|
||||||
|
COALESCE(ad.first_name, '') AS "Vorname",
|
||||||
|
COALESCE(ad.last_name, '') AS "Nachname",
|
||||||
|
COALESCE(ad.street, '') AS "Strasse",
|
||||||
|
COALESCE(ad.house_number, '') AS "Hausnummer",
|
||||||
|
COALESCE(ad.zip, '') AS "PLZ",
|
||||||
|
COALESCE(ad.city, '') AS "Stadt",
|
||||||
|
COALESCE(ad.country_name, '') AS "Land",
|
||||||
|
|
||||||
|
-- Zahlungs- und Betragsinformationen
|
||||||
|
COALESCE(pm.code, '') AS "Zahlungsart",
|
||||||
|
COALESCE(so.amount_net, 0) AS "Gesamtbetrag_netto",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS "Versandkosten",
|
||||||
|
COALESCE(so.total_amount, 0) AS "Gesamtbetrag_brutto",
|
||||||
|
COALESCE(so.amount_discount, 0) AS "Rabatt",
|
||||||
|
|
||||||
|
-- Produktzählungen (nur aktive Produkte)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS "#_ChagaFlaschen", -- CHAGA (ID 8)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS "#_ReishiFlaschen", -- 003.01 (ID 5)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS "#_ShiitakeFlaschen", -- SHIITAKE (ID 9)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS "#_LionsManeFlaschen" -- 005.02 (ID 6)
|
||||||
|
|
||||||
|
FROM sales_order so
|
||||||
|
-- Lieferadresse
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
-- Zahlungsart
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
-- Bestellpositionen und Allokationen
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
-- Produkte (nur aktive)
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
|
||||||
|
WHERE so.external_ref = $1
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Values
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"values": {
|
||||||
|
"value": "={{ $json.Bestellnummer }}",
|
||||||
|
"string": "={{ $json.Bestellnummer }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"maxRows": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Integration
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
```
|
||||||
|
Webhook → Postgres Node → Excel Node
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Input
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Bestellnummer": "10477"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Output
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Bestellnummer": "10477",
|
||||||
|
"Bestelldatum": "2026-04-05T23:12:07",
|
||||||
|
"Versanddatum": "2026-04-06",
|
||||||
|
"Vorname": "Irendy",
|
||||||
|
"Nachname": "Bucio",
|
||||||
|
"Strasse": "Weingartenstrasse",
|
||||||
|
"Hausnummer": "5",
|
||||||
|
"PLZ": "4600",
|
||||||
|
"Stadt": "Olten",
|
||||||
|
"Land": "CH",
|
||||||
|
"Zahlungsart": "",
|
||||||
|
"Gesamtbetrag_netto": 49.95,
|
||||||
|
"Versandkosten": 4.95,
|
||||||
|
"Gesamtbetrag_brutto": 44.91,
|
||||||
|
"Rabatt": 9.99,
|
||||||
|
"#_ChagaFlaschen": 0,
|
||||||
|
"#_ReishiFlaschen": 0,
|
||||||
|
"#_ShiitakeFlaschen": 0,
|
||||||
|
"#_LionsManeFlaschen": 1.0000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Produkt-Mapping
|
||||||
|
|
||||||
|
| Excel-Feld | Produkt-ID | SKU | Status |
|
||||||
|
|------------|------------|-----|--------|
|
||||||
|
| `#_ChagaFlaschen` | 8 | `CHAGA` | aktiv |
|
||||||
|
| `#_ReishiFlaschen` | 5 | `003.01` | aktiv |
|
||||||
|
| `#_ShiitakeFlaschen` | 9 | `SHIITAKE` | aktiv |
|
||||||
|
| `#_LionsManeFlaschen` | 6 | `005.02` | aktiv |
|
||||||
|
|
||||||
|
**Ignoriert:** ID 7 (`LIONSMANE`) - inaktiv
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
### Keine Bestellung gefunden
|
||||||
|
- Alle Felder werden als leere Strings oder 0 zurückgegeben
|
||||||
|
- Excel-Node kann trotzdem ausgeführt werden
|
||||||
|
|
||||||
|
### Keine Produkt-Allokationen
|
||||||
|
- Produktzählungen sind 0
|
||||||
|
- Andere Felder sind normal gefüllt
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Query direkt in DB
|
||||||
|
```bash
|
||||||
|
psql "postgresql://codex_db_user:Ze90re0KAry8gyJ6eAx0Gf4IelEGI@192.168.1.199:55432/naurua_erp" -c "
|
||||||
|
-- Test mit existierender Bestellung
|
||||||
|
SELECT external_ref FROM sales_order WHERE external_ref LIKE '10%' LIMIT 1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Response
|
||||||
|
Erwartet eine Zeile mit allen Feldern gefüllt oder als leere Strings/0.
|
||||||
|
|
||||||
|
## Wichtige Hinweise
|
||||||
|
|
||||||
|
1. **shipping_date** wird automatisch via Trigger berechnet (nächster Arbeitstag)
|
||||||
|
2. **Wochenend-Logik**: Freitag → Montag, Samstag → Montag, Sonntag → Montag
|
||||||
|
3. **Feldnamen** müssen exakt wie oben angegeben sein (Groß-/Kleinschreibung beachten)
|
||||||
|
4. **Produkt-IDs** sind fest in der Query hinterlegt
|
||||||
|
5. **Max Rows** = 1 (eine Bestellung pro Aufruf)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: Excel Daten aus ERP abfragen
|
||||||
|
type: n8n-nodes-base.postgres
|
||||||
|
typeVersion: 2.1
|
||||||
|
position: [0, 0]
|
||||||
|
credentials:
|
||||||
|
postgres:
|
||||||
|
id: YOUR_CREDENTIAL_ID_HERE
|
||||||
|
name: ERP Naurua Database
|
||||||
|
parameters:
|
||||||
|
operation: executeQuery
|
||||||
|
query: |
|
||||||
|
SELECT
|
||||||
|
-- Bestellinformationen
|
||||||
|
so.external_ref AS "Bestellnummer",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS "Bestelldatum",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS "Versanddatum",
|
||||||
|
|
||||||
|
-- Kundenadresse (Lieferadresse)
|
||||||
|
COALESCE(ad.first_name, '') AS "Vorname",
|
||||||
|
COALESCE(ad.last_name, '') AS "Nachname",
|
||||||
|
COALESCE(ad.street, '') AS "Strasse",
|
||||||
|
COALESCE(ad.house_number, '') AS "Hausnummer",
|
||||||
|
COALESCE(ad.zip, '') AS "PLZ",
|
||||||
|
COALESCE(ad.city, '') AS "Stadt",
|
||||||
|
COALESCE(ad.country_name, '') AS "Land",
|
||||||
|
|
||||||
|
-- Zahlungs- und Betragsinformationen
|
||||||
|
COALESCE(pm.code, '') AS "Zahlungsart",
|
||||||
|
COALESCE(so.amount_net, 0) AS "Gesamtbetrag_netto",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS "Versandkosten",
|
||||||
|
COALESCE(so.total_amount, 0) AS "Gesamtbetrag_brutto",
|
||||||
|
COALESCE(so.amount_discount, 0) AS "Rabatt",
|
||||||
|
|
||||||
|
-- Produktzählungen (nur aktive Produkte)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS "#_ChagaFlaschen", -- CHAGA (ID 8)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS "#_ReishiFlaschen", -- 003.01 (ID 5)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS "#_ShiitakeFlaschen", -- SHIITAKE (ID 9)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS "#_LionsManeFlaschen" -- 005.02 (ID 6)
|
||||||
|
|
||||||
|
FROM sales_order so
|
||||||
|
-- Lieferadresse
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
-- Zahlungsart
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
-- Bestellpositionen und Allokationen
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
-- Produkte (nur aktive)
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
|
||||||
|
WHERE so.external_ref = $1
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
queryValues:
|
||||||
|
values:
|
||||||
|
value: "={{ $json.Bestellnummer }}"
|
||||||
|
string: "={{ $json.Bestellnummer }}"
|
||||||
|
options:
|
||||||
|
maxRows: 1
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# n8n Postgres Node - Korrigierte Version
|
||||||
|
|
||||||
|
## SQL Query (ohne Kommentare, mit $1 Parameter)
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
so.external_ref AS "Bestellnummer",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS "Bestelldatum",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS "Versanddatum",
|
||||||
|
COALESCE(ad.first_name, '') AS "Vorname",
|
||||||
|
COALESCE(ad.last_name, '') AS "Nachname",
|
||||||
|
COALESCE(ad.street, '') AS "Strasse",
|
||||||
|
COALESCE(ad.house_number, '') AS "Hausnummer",
|
||||||
|
COALESCE(ad.zip, '') AS "PLZ",
|
||||||
|
COALESCE(ad.city, '') AS "Stadt",
|
||||||
|
COALESCE(ad.country_name, '') AS "Land",
|
||||||
|
COALESCE(pm.code, '') AS "Zahlungsart",
|
||||||
|
COALESCE(so.amount_net, 0) AS "Gesamtbetrag_netto",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS "Versandkosten",
|
||||||
|
COALESCE(so.total_amount, 0) AS "Gesamtbetrag_brutto",
|
||||||
|
COALESCE(so.amount_discount, 0) AS "Rabatt",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS "#_ChagaFlaschen",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS "#_ReishiFlaschen",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS "#_ShiitakeFlaschen",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS "#_LionsManeFlaschen"
|
||||||
|
FROM sales_order so
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
WHERE so.external_ref = $1
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative mit einfachen Feldnamen (keine Anführungszeichen):
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
so.external_ref AS Bestellnummer,
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS Bestelldatum,
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS Versanddatum,
|
||||||
|
COALESCE(ad.first_name, '') AS Vorname,
|
||||||
|
COALESCE(ad.last_name, '') AS Nachname,
|
||||||
|
COALESCE(ad.street, '') AS Strasse,
|
||||||
|
COALESCE(ad.house_number, '') AS Hausnummer,
|
||||||
|
COALESCE(ad.zip, '') AS PLZ,
|
||||||
|
COALESCE(ad.city, '') AS Stadt,
|
||||||
|
COALESCE(ad.country_name, '') AS Land,
|
||||||
|
COALESCE(pm.code, '') AS Zahlungsart,
|
||||||
|
COALESCE(so.amount_net, 0) AS Gesamtbetrag_netto,
|
||||||
|
COALESCE(so.amount_shipping, 0) AS Versandkosten,
|
||||||
|
COALESCE(so.total_amount, 0) AS Gesamtbetrag_brutto,
|
||||||
|
COALESCE(so.amount_discount, 0) AS Rabatt,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS _ChagaFlaschen,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS _ReishiFlaschen,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS _ShiitakeFlaschen,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS _LionsManeFlaschen
|
||||||
|
FROM sales_order so
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
WHERE so.external_ref = $1
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Values in n8n
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"values": {
|
||||||
|
"value": "={{ $json.Bestellnummer }}",
|
||||||
|
"string": "={{ $json.Bestellnummer }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Query direkt in DB
|
||||||
|
```bash
|
||||||
|
psql "postgresql://codex_db_user:Ze90re0KAry8gyJ6eAx0Gf4IelEGI@192.168.1.199:55432/naurua_erp" -c "
|
||||||
|
SELECT
|
||||||
|
so.external_ref AS Bestellnummer,
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD\"T\"HH24:MI:SS') AS Bestelldatum,
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS Versanddatum,
|
||||||
|
COALESCE(ad.first_name, '') AS Vorname,
|
||||||
|
COALESCE(ad.last_name, '') AS Nachname,
|
||||||
|
COALESCE(ad.street, '') AS Strasse,
|
||||||
|
COALESCE(ad.house_number, '') AS Hausnummer,
|
||||||
|
COALESCE(ad.zip, '') AS PLZ,
|
||||||
|
COALESCE(ad.city, '') AS Stadt,
|
||||||
|
COALESCE(ad.country_name, '') AS Land,
|
||||||
|
COALESCE(pm.code, '') AS Zahlungsart,
|
||||||
|
COALESCE(so.amount_net, 0) AS Gesamtbetrag_netto,
|
||||||
|
COALESCE(so.amount_shipping, 0) AS Versandkosten,
|
||||||
|
COALESCE(so.total_amount, 0) AS Gesamtbetrag_brutto,
|
||||||
|
COALESCE(so.amount_discount, 0) AS Rabatt,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS _ChagaFlaschen,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS _ReishiFlaschen,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS _ShiitakeFlaschen,
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS _LionsManeFlaschen
|
||||||
|
FROM sales_order so
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
WHERE so.external_ref = '10477'
|
||||||
|
GROUP BY so.id, ad.id, pm.id;"
|
||||||
|
```
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Postgres Query für n8n Excel Node
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Diese Query extrahiert alle benötigten Daten für die Excel-Buchhaltung basierend auf einer Bestellnummer.
|
||||||
|
|
||||||
|
## Postgres Node Konfiguration für n8n
|
||||||
|
|
||||||
|
### Connection Details
|
||||||
|
```
|
||||||
|
Host: 192.168.1.199
|
||||||
|
Port: 55432
|
||||||
|
Database: naurua_erp
|
||||||
|
User: codex_db_user
|
||||||
|
Password: Ze90re0KAry8gyJ6eAx0Gf4IelEGI
|
||||||
|
SSL: Disabled (lokales Netzwerk)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL Query für Excel-Daten
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
-- Bestellinformationen
|
||||||
|
so.external_ref AS "Bestellnummer",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD"T"HH24:MI:SS') AS "Bestelldatum",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS "Versanddatum",
|
||||||
|
|
||||||
|
-- Kundenadresse (Lieferadresse)
|
||||||
|
COALESCE(ad.first_name, '') AS "Vorname",
|
||||||
|
COALESCE(ad.last_name, '') AS "Nachname",
|
||||||
|
COALESCE(ad.street, '') AS "Strasse",
|
||||||
|
COALESCE(ad.house_number, '') AS "Hausnummer",
|
||||||
|
COALESCE(ad.zip, '') AS "PLZ",
|
||||||
|
COALESCE(ad.city, '') AS "Stadt",
|
||||||
|
COALESCE(ad.country_name, '') AS "Land",
|
||||||
|
|
||||||
|
-- Zahlungs- und Betragsinformationen
|
||||||
|
COALESCE(pm.code, '') AS "Zahlungsart",
|
||||||
|
COALESCE(so.amount_net, 0) AS "Gesamtbetrag_netto",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS "Versandkosten",
|
||||||
|
COALESCE(so.total_amount, 0) AS "Gesamtbetrag_brutto",
|
||||||
|
COALESCE(so.amount_discount, 0) AS "Rabatt",
|
||||||
|
|
||||||
|
-- Produktzählungen (nur aktive Produkte)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS "#_ChagaFlaschen", -- CHAGA (ID 8)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS "#_ReishiFlaschen", -- 003.01 (ID 5)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS "#_ShiitakeFlaschen", -- SHIITAKE (ID 9)
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS "#_LionsManeFlaschen" -- 005.02 (ID 6)
|
||||||
|
|
||||||
|
FROM sales_order so
|
||||||
|
-- Lieferadresse
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
-- Zahlungsart
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
-- Bestellpositionen und Allokationen
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
-- Produkte (nur aktive)
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
|
||||||
|
WHERE so.external_ref = :bestellnummer
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameter
|
||||||
|
- `:bestellnummer` - Die externe Bestellreferenz (z.B. `W123456` oder `DIR-20260329-00017`)
|
||||||
|
|
||||||
|
## Produkt-Mapping
|
||||||
|
|
||||||
|
| Excel-Feld | Produkt-ID | SKU | Name | Status |
|
||||||
|
|------------|------------|-----|------|--------|
|
||||||
|
| `#_ChagaFlaschen` | 8 | `CHAGA` | Chaga Extrakt Tinktur 50 ml | 100% rein | aktiv |
|
||||||
|
| `#_ReishiFlaschen` | 5 | `003.01` | Reishi Extrakt Tinktur 50 ml | 100% rein | aktiv |
|
||||||
|
| `#_ShiitakeFlaschen` | 9 | `SHIITAKE` | Shiitake Extrakt Tinktur 50 ml | 100% rein | aktiv |
|
||||||
|
| `#_LionsManeFlaschen` | 6 | `005.02` | Lion's Mane Extrakt Tinktur 50 ml | 100% rein | aktiv |
|
||||||
|
|
||||||
|
**Ignoriert:** Produkt-ID 7 (`LIONSMANE`) - Status: inaktiv
|
||||||
|
|
||||||
|
## Feld-Erklärungen
|
||||||
|
|
||||||
|
### Bestellinformationen
|
||||||
|
- `Bestellnummer` - Eindeutige Bestellreferenz (Wix: BestellungNr, Direktverkauf: DIR-...)
|
||||||
|
- `Bestelldatum` - ISO 8601 Format: `YYYY-MM-DDTHH:MM:SS`
|
||||||
|
- `Versanddatum` - ISO Date Format: `YYYY-MM-DD` (automatisch berechnet als nächster Arbeitstag)
|
||||||
|
|
||||||
|
### Kundenadresse
|
||||||
|
- Alle Felder aus der Lieferadresse (`type = 'shipping'`)
|
||||||
|
- Leere Werte werden als leere Strings zurückgegeben
|
||||||
|
|
||||||
|
### Zahlungsinformationen
|
||||||
|
- `Zahlungsart` - Interner Code aus `payment_method` (z.B. `twint`, `bank_transfer`, `card`, `pickup`)
|
||||||
|
- Beträge mit 2 Dezimalstellen, NULL wird zu 0
|
||||||
|
|
||||||
|
### Produktzählungen
|
||||||
|
- Summe der allokierten Mengen pro Produkt
|
||||||
|
- Nur aktive Produkte (`status = 'active'`)
|
||||||
|
- NULL wird zu 0
|
||||||
|
|
||||||
|
## Beispieldaten
|
||||||
|
|
||||||
|
### Input
|
||||||
|
```
|
||||||
|
Bestellnummer: "W123456"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Bestellnummer": "W123456",
|
||||||
|
"Bestelldatum": "2026-04-06T14:30:00",
|
||||||
|
"Versanddatum": "2026-04-07",
|
||||||
|
"Vorname": "Max",
|
||||||
|
"Nachname": "Mustermann",
|
||||||
|
"Strasse": "Musterstrasse",
|
||||||
|
"Hausnummer": "123",
|
||||||
|
"PLZ": "8000",
|
||||||
|
"Stadt": "Zürich",
|
||||||
|
"Land": "Schweiz",
|
||||||
|
"Zahlungsart": "twint",
|
||||||
|
"Gesamtbetrag_netto": 85.00,
|
||||||
|
"Versandkosten": 5.90,
|
||||||
|
"Gesamtbetrag_brutto": 100.00,
|
||||||
|
"Rabatt": 0.00,
|
||||||
|
"#_ChagaFlaschen": 2,
|
||||||
|
"#_ReishiFlaschen": 1,
|
||||||
|
"#_ShiitakeFlaschen": 0,
|
||||||
|
"#_LionsManeFlaschen": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
### Keine Bestellung gefunden
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Bestellnummer": "W123456",
|
||||||
|
"Bestelldatum": "",
|
||||||
|
"Versanddatum": "",
|
||||||
|
"Vorname": "",
|
||||||
|
"Nachname": "",
|
||||||
|
"Strasse": "",
|
||||||
|
"Hausnummer": "",
|
||||||
|
"PLZ": "",
|
||||||
|
"Stadt": "",
|
||||||
|
"Land": "",
|
||||||
|
"Zahlungsart": "",
|
||||||
|
"Gesamtbetrag_netto": 0,
|
||||||
|
"Versandkosten": 0,
|
||||||
|
"Gesamtbetrag_brutto": 0,
|
||||||
|
"Rabatt": 0,
|
||||||
|
"#_ChagaFlaschen": 0,
|
||||||
|
"#_ReishiFlaschen": 0,
|
||||||
|
"#_ShiitakeFlaschen": 0,
|
||||||
|
"#_LionsManeFlaschen": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keine Produkt-Allokationen
|
||||||
|
- Alle Produktzählungen sind 0
|
||||||
|
- Andere Felder sind normal gefüllt
|
||||||
|
|
||||||
|
## Integration in n8n Flow
|
||||||
|
|
||||||
|
### Flow-Ablauf
|
||||||
|
1. **Webhook Node**: Empfängt `Bestellnummer` vom ERP
|
||||||
|
2. **Postgres Node**: Führt diese Query mit `:bestellnummer = $json.Bestellnummer` aus
|
||||||
|
3. **Excel Node**: Schreibt das Ergebnis in die Excel-Datei
|
||||||
|
4. **Response Node**: Optional - Bestätigung senden
|
||||||
|
|
||||||
|
### Query Parameters in n8n
|
||||||
|
```
|
||||||
|
Query: (siehe oben)
|
||||||
|
Query Values:
|
||||||
|
bestellnummer: {{ $json.Bestellnummer }}
|
||||||
|
Operation: Select
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technische Hinweise
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Query verwendet JOINs auf indizierten Spalten
|
||||||
|
- Gruppierung ist effizient für einzelne Bestellungen
|
||||||
|
- `COALESCE` verhindert NULL-Werte für Excel-Kompatibilität
|
||||||
|
|
||||||
|
### Datum-Logik
|
||||||
|
- `shipping_date` wird automatisch via Trigger berechnet
|
||||||
|
- Wochenend-Logik: Freitag → Montag, Samstag → Montag, Sonntag → Montag
|
||||||
|
- Keine Feiertags-Logik in Phase 1
|
||||||
|
|
||||||
|
### Schema-Konsistenz
|
||||||
|
- Alle Tabellen existieren nach Migration 0007
|
||||||
|
- `shipping_date` kann NULL sein (für sehr alte Bestellungen)
|
||||||
|
- Produkt-IDs sind statisch in der aktuellen DB
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Query
|
||||||
|
```sql
|
||||||
|
-- Test mit existierender Bestellung
|
||||||
|
SELECT * FROM sales_order WHERE external_ref LIKE 'W%' OR external_ref LIKE 'DIR-%' LIMIT 5;
|
||||||
|
|
||||||
|
-- Test Query mit konkreter Bestellnummer
|
||||||
|
-- [Hier Query einfügen und :bestellnummer ersetzen]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Results
|
||||||
|
- Eine Zeile pro Bestellung
|
||||||
|
- Alle Felder gefüllt oder leere Strings/0
|
||||||
|
- Produktzählungen als Ganzzahlen
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
# Phase 1: DEV-Ist-Stand Tabellen- und Prozesskonzept
|
||||||
|
Stand: 2026-06-02
|
||||||
|
Status: Verbindlicher DEV-Ist-Stand fuer Phase 1
|
||||||
|
|
||||||
|
## 1. Uebersicht
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die technische Wahrheit des aktuellen DEV-Standes auf Basis von:
|
||||||
|
|
||||||
|
- `docs/CONCEPT.md`
|
||||||
|
- `docs/SCHEMA_PHASE1.sql`
|
||||||
|
- `docs/PROCESS_PHASE1.md`
|
||||||
|
- `db/migrations/0001_phase1_core.sql` bis `db/migrations/0006_phase1_otc_products.sql`
|
||||||
|
- `order-import.php`
|
||||||
|
- `n8n/exports/current/*.json`
|
||||||
|
|
||||||
|
Wichtige Einordnung:
|
||||||
|
|
||||||
|
- `SCHEMA_PHASE1.sql` bleibt ein Draft und ist nicht identisch mit der live DB.
|
||||||
|
- Die live DB hat 18 Base Tables und 1 View.
|
||||||
|
- Die Forecast-Erweiterung aus `0003_phase1_inventory_forecast.sql` ist im live Schema nicht aktiv.
|
||||||
|
- Die Doku unten trennt deshalb zwischen implementiertem Ist-Stand und nicht gefundenen Zielbild-Teilen.
|
||||||
|
|
||||||
|
Tabellen:
|
||||||
|
|
||||||
|
- `party`
|
||||||
|
- `address`
|
||||||
|
- `contact`
|
||||||
|
- `product`
|
||||||
|
- `sellable_item`
|
||||||
|
- `external_item_alias`
|
||||||
|
- `sellable_item_component`
|
||||||
|
- `warehouse`
|
||||||
|
- `location`
|
||||||
|
- `stock_lot`
|
||||||
|
- `payment_method`
|
||||||
|
- `shipping_method`
|
||||||
|
- `sales_order`
|
||||||
|
- `sales_order_line`
|
||||||
|
- `stock_move`
|
||||||
|
- `sales_order_line_lot_allocation`
|
||||||
|
- `audit_log`
|
||||||
|
- `outbound_webhook_event`
|
||||||
|
|
||||||
|
View:
|
||||||
|
|
||||||
|
- `v_stock_lot_balance`
|
||||||
|
|
||||||
|
Prozesse:
|
||||||
|
|
||||||
|
- `n8n.bestell-eingang-online-shop`
|
||||||
|
- `order-import.php`
|
||||||
|
- `db.trigger.sales_order`
|
||||||
|
- `db.trigger.sales_order_line`
|
||||||
|
- `db.trigger.stock_move`
|
||||||
|
- `db.trigger.product`
|
||||||
|
- `n8n.adressetikette-erstellen`
|
||||||
|
- Outbox-Ablage ueber `outbound_webhook_event`
|
||||||
|
|
||||||
|
## 2. Zweck
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die aktuelle DB- und Prozesswahrheit fuer Phase 1 auf dem DEV-Stand.
|
||||||
|
|
||||||
|
Es ist eine technische Uebersicht fuer Analyse, Umsetzung und Review.
|
||||||
|
|
||||||
|
Nicht Bestandteil dieses Dokuments sind:
|
||||||
|
|
||||||
|
- API-Details
|
||||||
|
- UI-Details
|
||||||
|
- Migrations-Rollout-Planung
|
||||||
|
- offene Zielbilddiskussionen ausserhalb des aktuellen DEV-Standes
|
||||||
|
|
||||||
|
## 3. Normatives Datenmodell
|
||||||
|
|
||||||
|
### 3.1 `party`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- gemeinsamer Kontaktstamm fuer Kunden und Lieferanten
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `type` ist `customer`, `supplier` oder `both`
|
||||||
|
- `status` ist `active` oder `inactive`
|
||||||
|
- `email` wird fuer Match/Upsert genutzt
|
||||||
|
|
||||||
|
### 3.2 `address`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Rechnungs- und Lieferadressen pro `party`
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `type` ist `billing` oder `shipping`
|
||||||
|
- `raw_payload` speichert die Rohdaten des Eingangsdokuments
|
||||||
|
|
||||||
|
### 3.3 `contact`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- zusaetzliche Ansprechpartnerdaten pro `party`
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- keine weitere Prozesslogik im Code gefunden
|
||||||
|
|
||||||
|
### 3.4 `product`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- lagergefuehrtes Produkt mit Bestand und Charge
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `sku` ist eindeutig
|
||||||
|
- beim Insert erzeugt der Trigger `trg_product_bootstrap_lots` sofort eine `current`- und eine `open`-Charge
|
||||||
|
|
||||||
|
### 3.5 `sellable_item`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- verkaufbarer Shop-Artikel
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- Bestellungen referenzieren zuerst `sellable_item`
|
||||||
|
- der Import kann neue `sellable_item`-Datensaetze automatisch erzeugen
|
||||||
|
|
||||||
|
### 3.6 `external_item_alias`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Mapping externer Shop-Daten auf `sellable_item`
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `source_system = 'wix'`
|
||||||
|
- Aufloesung ueber Artikelnummer, normalisierten Titel oder Originaltitel
|
||||||
|
|
||||||
|
### 3.7 `sellable_item_component`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Stueckliste eines `sellable_item` auf lagergefuehrte Produkte
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `qty_per_item > 0`
|
||||||
|
- wird vom Import und von Seed-Skripten gepflegt
|
||||||
|
|
||||||
|
### 3.8 `warehouse`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- oberer Lagerstandort
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- im Seed ist ein Hauptlager vorgesehen
|
||||||
|
|
||||||
|
### 3.9 `location`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- konkreter Lagerort innerhalb eines `warehouse`
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `type` ist `storage`, `receiving`, `dispatch` oder `adjustment`
|
||||||
|
|
||||||
|
### 3.10 `stock_lot`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Charge eines Produkts
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `status` ist `open`, `current` oder `closed`
|
||||||
|
- pro Produkt sind `current` und `open` per Unique Index abgesichert
|
||||||
|
- die live DB hat keine `sellout_date`- oder `warning_state`-Spalten
|
||||||
|
- die Sicht `v_stock_lot_balance` ist die Bestandswahrheit
|
||||||
|
|
||||||
|
### 3.11 `payment_method`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- normalisierte Zahlungsart
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- Seed-Werte im DEV: `card`, `twint`, `bank_transfer`, `cash`, `paypal`
|
||||||
|
|
||||||
|
### 3.12 `shipping_method`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- normalisierte Versandart
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- Seed-Werte im DEV: `post_standard`, `pickup`
|
||||||
|
|
||||||
|
### 3.13 `sales_order`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Bestellkopf fuer Online-Import und Direktverkauf
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `external_ref` ist eindeutig
|
||||||
|
- `order_source` ist `wix` oder `direct`
|
||||||
|
- `party_id` ist nullable
|
||||||
|
- `payment_status` ist faktisch auf `paid` beschraenkt
|
||||||
|
- `shipping_date` existiert im live Schema und wird per Trigger berechnet, falls leer
|
||||||
|
|
||||||
|
### 3.14 `sales_order_line`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Bestellposition
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `qty_cancelled` und `line_status` werden per Trigger synchronisiert
|
||||||
|
- `sellable_item_id` kann `NULL` sein
|
||||||
|
|
||||||
|
### 3.15 `stock_move`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Bewegungsjournal fuer Zu- und Abgaenge
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `move_type` ist `in`, `out`, `transfer` oder `adjustment`
|
||||||
|
- Bewegungen werden vor Insert/Update gegen Negativbestand validiert
|
||||||
|
|
||||||
|
### 3.16 `sales_order_line_lot_allocation`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- explizite Rueckverfolgung zwischen Bestellposition und Charge
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- `allocation_status` ist `reserved`, `allocated`, `released` oder `cancelled`
|
||||||
|
- im Import wird `allocated` verwendet
|
||||||
|
|
||||||
|
### 3.17 `audit_log`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- technische Historisierung
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- im Schema vorhanden
|
||||||
|
- im geprüften PHP- und n8n-Flow nicht als zentraler Pflichtpfad sichtbar
|
||||||
|
|
||||||
|
### 3.18 `outbound_webhook_event`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- Outbox fuer ERP-Events
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- Spalten: `event_type`, `event_key`, `aggregate_type`, `aggregate_id`, `payload`, `status`, `attempt_count`, `next_attempt_at`, `last_attempt_at`, `last_error`, `created_at`, `sent_at`
|
||||||
|
- `fn_enqueue_event(...)` schreibt idempotent in diese Tabelle
|
||||||
|
- ein Dispatcher/Worker fuer die Auslieferung wurde im geprüften DEV-Baum nicht gefunden
|
||||||
|
|
||||||
|
### 3.19 `v_stock_lot_balance`
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
|
||||||
|
- berechneter Chargensaldo
|
||||||
|
|
||||||
|
Ist-Stand:
|
||||||
|
|
||||||
|
- die Sicht ermittelt `qty_in`, `qty_out` und `qty_net`
|
||||||
|
- sie ist die Grundlage fuer Bestandspruefung und Auto-Switch
|
||||||
|
|
||||||
|
## 4. Normatives Prozessmodell
|
||||||
|
|
||||||
|
Alle Prozesse gehoeren fachlich zur Phase-1-Bestell-, Lager- und Integrationslogik.
|
||||||
|
|
||||||
|
### 4.1 `n8n.bestell-eingang-online-shop`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- E-Mail auf `Neue Bestellung` erkennen
|
||||||
|
- Payload in ERP-JSON umformen
|
||||||
|
- JSON per HTTP POST an `order-import.php` senden
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- IMAP-Eingang
|
||||||
|
- n8n-Extraktionslogik
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- keine DB direkt
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- n8n ist hier nur die Integrationshuelle fuer den ERP-Import
|
||||||
|
|
||||||
|
### 4.2 `order-import.php`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- eingehende Bestelldaten idempotent in die ERP-DB schreiben
|
||||||
|
- Kontakte, Adressen, Bestellkopf, Positionen und Chargenrueckverfolgung aufbauen
|
||||||
|
- bei Reimport vorhandene Allokationen zurueckbuchen
|
||||||
|
- nach Commit die Label- und Excel-Flows direkt anstossen
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- `.env`
|
||||||
|
- eingehendes Webhook-JSON
|
||||||
|
- `party`, `address`, `sales_order`, `sales_order_line`
|
||||||
|
- `external_item_alias`
|
||||||
|
- `sellable_item_component`
|
||||||
|
- `stock_lot`
|
||||||
|
- `v_stock_lot_balance`
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- `party`
|
||||||
|
- `address`
|
||||||
|
- `sales_order`
|
||||||
|
- `sales_order_line`
|
||||||
|
- `sellable_item`
|
||||||
|
- `external_item_alias`
|
||||||
|
- `sellable_item_component`
|
||||||
|
- `warehouse`
|
||||||
|
- `location`
|
||||||
|
- `stock_lot`
|
||||||
|
- `stock_move`
|
||||||
|
- `sales_order_line_lot_allocation`
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- die Bestellung wird im ERP gespeichert und mit Lagerbewegung verknuepft
|
||||||
|
- vorhandene Allokationen derselben `external_ref` werden vor dem Neuimport rueckwaerts gebucht
|
||||||
|
- nach erfolgreichem Commit werden Label- und Excel-N8N-Flows direkt per HTTP ausgelöst
|
||||||
|
|
||||||
|
### 4.3 `db.trigger.sales_order`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- `shipping_date` aus `order_date` ableiten
|
||||||
|
- `order.imported` und `order.cancelled.full` in die Outbox schreiben
|
||||||
|
- `direct`-Bestellungen automatisch mit `DIR-...` Nummer versehen
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- `sales_order`
|
||||||
|
- `fn_next_business_day(timestamp)`
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- `sales_order.shipping_date`
|
||||||
|
- `outbound_webhook_event`
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- fehlendes `shipping_date` wird auf den naechsten Werktag gesetzt
|
||||||
|
- neue Auftraege und Vollstornos werden als Event in die Outbox gelegt
|
||||||
|
- direkte Auftraege erhalten bei leerer Nummer automatisch einen `DIR-...`-Ref
|
||||||
|
|
||||||
|
### 4.4 `db.trigger.sales_order_line`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- `line_status` aus `qty_cancelled` ableiten
|
||||||
|
- Partial-Cancel-Event enqueuen
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- `sales_order_line`
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- `sales_order_line.line_status`
|
||||||
|
- `outbound_webhook_event`
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- `allocated`, `partially_cancelled` und `cancelled` werden deterministisch gesetzt
|
||||||
|
- ein Wechsel auf `partially_cancelled` erzeugt ein `order.cancelled.partial`-Event
|
||||||
|
|
||||||
|
### 4.5 `db.trigger.stock_move`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- Negativbestand verhindern
|
||||||
|
- Chargenwechsel bei leerer `current`-Charge ausloesen
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- `stock_move`
|
||||||
|
- `stock_lot`
|
||||||
|
- `v_stock_lot_balance`
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- `stock_lot`
|
||||||
|
- `outbound_webhook_event`
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- `out`-Bewegungen sind nur auf `current`-Chargen erlaubt
|
||||||
|
- bei `qty_net <= 0` wird die naechste `open`-Charge `current`
|
||||||
|
- danach wird wieder eine neue `open`-Charge angelegt
|
||||||
|
|
||||||
|
### 4.6 `db.trigger.product`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- neue Produkte sofort chargenfaehig machen
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- `product`
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- `stock_lot`
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- jedes neue Produkt bekommt direkt eine `current`- und eine `open`-Charge
|
||||||
|
|
||||||
|
### 4.7 `n8n.adressetikette-erstellen`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- Lieferadresse entgegennehmen
|
||||||
|
- Felder normalisieren
|
||||||
|
- HTML/CSS in PDF/PNG umsetzen
|
||||||
|
- Ergebnis per SFTP hochladen
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- Webhook-Input
|
||||||
|
- Gotenberg / pdf2png / SFTP-Ziele
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- keine ERP-DB
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- eigenstaendiger Ausgabekanal fuer Versandetiketten
|
||||||
|
|
||||||
|
### 4.8 Outbox-Ablage ueber `outbound_webhook_event`
|
||||||
|
|
||||||
|
Fachliche Aufgabe:
|
||||||
|
|
||||||
|
- Eventdaten fuer spaetere Auslieferung sammeln
|
||||||
|
|
||||||
|
Liest:
|
||||||
|
|
||||||
|
- `fn_enqueue_event`
|
||||||
|
|
||||||
|
Schreibt:
|
||||||
|
|
||||||
|
- `outbound_webhook_event`
|
||||||
|
|
||||||
|
Fachliche Wirkung:
|
||||||
|
|
||||||
|
- Queue wird im geprüften DEV-Stand befuellt
|
||||||
|
- ein dazugehoeriger Dispatcher/Worker wurde im Code nicht gefunden
|
||||||
|
|
||||||
|
## 5. Technische Einbettung
|
||||||
|
|
||||||
|
Die reale Phase-1-Logik bildet einen kleinen operativen Kern mit vier Schwerpunkten:
|
||||||
|
|
||||||
|
1. Kontakt- und Adressstamm
|
||||||
|
2. Bestellimport via n8n -> PHP -> DB
|
||||||
|
3. Chargen- und Lagerbewegung mit Trigger-Logik
|
||||||
|
4. Label-Generierung als separater n8n-Ausgang
|
||||||
|
|
||||||
|
Wichtige Abweichungen zum alten Zielbild:
|
||||||
|
|
||||||
|
- Sellout-Forecast ist im live Schema nicht aktiv.
|
||||||
|
- Ein Dispatcher fuer `outbound_webhook_event` wurde im geprüften DEV-Baum nicht gefunden.
|
||||||
|
- Eine separate Direct-Sale-Eingabestrecke wurde im geprüften DEV-Baum nicht gefunden; vorhanden ist nur die DB-/Trigger-Unterstuetzung fuer `order_source = direct`.
|
||||||
|
|
||||||
|
## 6. Kurzfazit
|
||||||
|
|
||||||
|
Die aktuelle technische Wahrheit von Phase 1 ist auf Nachvollziehbarkeit und direkte Integrationspfade ausgerichtet:
|
||||||
|
|
||||||
|
- Bestellungen werden idempotent gespeichert
|
||||||
|
- Lagerbestand wird ueber Chargen und Bewegungen abgebildet
|
||||||
|
- Rueckverfolgung erfolgt ueber explizite Allokationen
|
||||||
|
- n8n importiert und erzeugt Labels direkt
|
||||||
|
- Outbox-Events werden geschrieben, aber ein Dispatcher ist im geprüften DEV-Stand nicht vorhanden
|
||||||
@@ -348,6 +348,83 @@ function trigger_shipping_label_flow(array $order, array $localEnv): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trigger_excel_webhook(PDO $pdo, int $orderId, array $localEnv): array
|
||||||
|
{
|
||||||
|
$url = env_value('N8N_OUTBOUND_URL_ADRESSE', $localEnv);
|
||||||
|
if ($url === '' || strpos($url, 'excel_befuellen') === false) {
|
||||||
|
return [
|
||||||
|
'enabled' => false,
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Excel webhook URL not configured or incorrect (must contain "excel_befuellen")',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQL Query for all Excel data (without comments for n8n compatibility)
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
so.external_ref AS \"Bestellnummer\",
|
||||||
|
TO_CHAR(so.order_date, 'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"Bestelldatum\",
|
||||||
|
TO_CHAR(so.shipping_date, 'YYYY-MM-DD') AS \"Versanddatum\",
|
||||||
|
COALESCE(ad.first_name, '') AS \"Vorname\",
|
||||||
|
COALESCE(ad.last_name, '') AS \"Nachname\",
|
||||||
|
COALESCE(ad.street, '') AS \"Strasse\",
|
||||||
|
COALESCE(ad.house_number, '') AS \"Hausnummer\",
|
||||||
|
COALESCE(ad.zip, '') AS \"PLZ\",
|
||||||
|
COALESCE(ad.city, '') AS \"Stadt\",
|
||||||
|
COALESCE(ad.country_name, '') AS \"Land\",
|
||||||
|
COALESCE(pm.code, '') AS \"Zahlungsart\",
|
||||||
|
COALESCE(so.amount_net, 0) AS \"Gesamtbetrag_netto\",
|
||||||
|
COALESCE(so.amount_shipping, 0) AS \"Versandkosten\",
|
||||||
|
COALESCE(so.total_amount, 0) AS \"Gesamtbetrag_brutto\",
|
||||||
|
COALESCE(so.amount_discount, 0) AS \"Rabatt\",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 8 THEN a.qty ELSE 0 END), 0) AS \"#_ChagaFlaschen\",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 5 THEN a.qty ELSE 0 END), 0) AS \"#_ReishiFlaschen\",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 9 THEN a.qty ELSE 0 END), 0) AS \"#_ShiitakeFlaschen\",
|
||||||
|
COALESCE(SUM(CASE WHEN p.id = 6 THEN a.qty ELSE 0 END), 0) AS \"#_LionsManeFlaschen\"
|
||||||
|
FROM sales_order so
|
||||||
|
LEFT JOIN address ad ON so.party_id = ad.party_id AND ad.type = 'shipping'
|
||||||
|
LEFT JOIN payment_method pm ON so.payment_method_id = pm.id
|
||||||
|
LEFT JOIN sales_order_line sol ON so.id = sol.sales_order_id
|
||||||
|
LEFT JOIN sales_order_line_lot_allocation a ON sol.id = a.sales_order_line_id
|
||||||
|
LEFT JOIN product p ON a.product_id = p.id AND p.status = 'active'
|
||||||
|
WHERE so.id = :order_id
|
||||||
|
GROUP BY so.id, ad.id, pm.id
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([':order_id' => $orderId]);
|
||||||
|
$data = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
return [
|
||||||
|
'enabled' => true,
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Order data not found or no product allocations',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication headers (same mechanism as shipping_label_flow)
|
||||||
|
$headers = [];
|
||||||
|
$secret = env_value('N8N_WEBHOOK_SECRET', $localEnv);
|
||||||
|
if ($secret !== '') {
|
||||||
|
$headers['X-Webhook-Secret'] = $secret;
|
||||||
|
$headers['X-N8N-Secret'] = $secret;
|
||||||
|
$headers['X-API-Key'] = $secret;
|
||||||
|
$headers['Authorization'] = 'Bearer ' . $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = post_json($url, $data, $headers, 20);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => true,
|
||||||
|
'ok' => $result['ok'],
|
||||||
|
'status' => $result['status'],
|
||||||
|
'url' => $url,
|
||||||
|
'message' => $result['ok'] ? 'Excel webhook triggered' : ($result['error'] !== '' ? $result['error'] : 'Excel webhook returned non-2xx'),
|
||||||
|
'responseBody' => $result['body'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function find_or_create_party(PDO $pdo, array $data): int
|
function find_or_create_party(PDO $pdo, array $data): int
|
||||||
{
|
{
|
||||||
$email = trim((string) ($data['EmailKunde'] ?? ''));
|
$email = trim((string) ($data['EmailKunde'] ?? ''));
|
||||||
@@ -1520,6 +1597,7 @@ try {
|
|||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
$labelTrigger = trigger_shipping_label_flow($data, $env);
|
$labelTrigger = trigger_shipping_label_flow($data, $env);
|
||||||
|
$excelTrigger = trigger_excel_webhook($pdo, $orderId, $env);
|
||||||
|
|
||||||
json_response(200, [
|
json_response(200, [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
@@ -1529,6 +1607,7 @@ try {
|
|||||||
'inventory' => $inventory,
|
'inventory' => $inventory,
|
||||||
'inventoryRollback' => $inventoryRollback,
|
'inventoryRollback' => $inventoryRollback,
|
||||||
'labelTrigger' => $labelTrigger,
|
'labelTrigger' => $labelTrigger,
|
||||||
|
'excelTrigger' => $excelTrigger,
|
||||||
]);
|
]);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user