Implement Excel webhook integration

Features:
- Migration 0007: shipping_date column with automatic next-business-day calculation
- New function trigger_excel_webhook() in order-import.php
- SQL query for extracting Excel data from ERP database
- Integration after successful order import
- Documentation for n8n Postgres node configuration

Changes:
1. db/migrations/0007_phase1_excel_webhook.sql - adds shipping_date column, trigger, next-business-day function
2. order-import.php - adds trigger_excel_webhook() function and integration point
3. docs/EXAKTE_POSTGRES_QUERY.sql - exact SQL query for n8n Postgres node
4. docs/N8N_POSTGRES_QUERY.md - comprehensive documentation
5. docs/N8N_POSTGRES_NODE.* - n8n node configurations
6. docs/N8N_EXCEL_WORKFLOW.json - complete workflow JSON
7. docs/N8N_NODE_COPY_PASTE.md - copy-paste ready instructions

The implementation triggers an Excel webhook after every successful order import, sending all necessary data for Excel bookkeeping to n8n.
This commit is contained in:
2026-04-06 21:23:50 +02:00
parent d52b6953ed
commit 0d8353fb9c
10 changed files with 998 additions and 0 deletions
@@ -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;
+28
View File
@@ -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
+182
View File
@@ -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"
}
}
+104
View File
@@ -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!).
+26
View File
@@ -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": {}
}
}
+161
View File
@@ -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)
+58
View File
@@ -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
+108
View File
@@ -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;"
```
+208
View File
@@ -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
+79
View File
@@ -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()) {