n8n-nodes-zugferd-reader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/USAGE.md +207 -0
- package/package.json +36 -0
- package/src/index.ts +1 -0
- package/src/nodes/ZugferdReader/ZugferdReader.node.ts +331 -0
- package/src/nodes/ZugferdReader/zugferd.svg +10 -0
- package/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# N8N ZUGFeRD Reader Node
|
|
2
|
+
|
|
3
|
+
Ein N8N Custom Node zum Extrahieren von ZUGFeRD/Factur-X XML-Daten aus PDF-Rechnungen.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Extrahiert ZUGFeRD und Factur-X XML aus PDF-Dateien
|
|
8
|
+
- ✅ Automatische Erkennung der XML-Anhänge
|
|
9
|
+
- ✅ Unterstützt Binary Data und Dateipfad als Input
|
|
10
|
+
- ✅ Ausgabe als JSON oder Raw XML
|
|
11
|
+
- ✅ Listet alle verfügbaren Anhänge auf
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### In N8N installieren
|
|
16
|
+
|
|
17
|
+
1. **Als Community Node:**
|
|
18
|
+
```bash
|
|
19
|
+
npm install n8n-nodes-zugferd-reader
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. **Manuell installieren:**
|
|
23
|
+
```bash
|
|
24
|
+
# Projekt bauen
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
|
|
28
|
+
# In N8N custom nodes Verzeichnis kopieren
|
|
29
|
+
cp -r dist ~/.n8n/custom/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **Entwicklungsmodus:**
|
|
33
|
+
```bash
|
|
34
|
+
npm install
|
|
35
|
+
npm run dev
|
|
36
|
+
|
|
37
|
+
# N8N mit custom nodes starten
|
|
38
|
+
n8n start
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Verwendung
|
|
42
|
+
|
|
43
|
+
### Input Modes
|
|
44
|
+
|
|
45
|
+
**Binary Data (Standard):**
|
|
46
|
+
- Verwendet die Binary Data aus einem vorherigen Node
|
|
47
|
+
- Ideal für Workflows mit HTTP Request, Read Binary File, etc.
|
|
48
|
+
|
|
49
|
+
**File Path:**
|
|
50
|
+
- Liest PDF direkt vom Dateisystem
|
|
51
|
+
- Nützlich für lokale Dateien
|
|
52
|
+
|
|
53
|
+
### Output Formats
|
|
54
|
+
|
|
55
|
+
**Parsed JSON (Standard):**
|
|
56
|
+
- Wandelt XML in JSON um
|
|
57
|
+
- Einfach zu verarbeiten in nachfolgenden Nodes
|
|
58
|
+
|
|
59
|
+
**Raw XML:**
|
|
60
|
+
- Gibt das originale XML zurück
|
|
61
|
+
- Nützlich wenn du das XML weiterverarbeiten möchtest
|
|
62
|
+
|
|
63
|
+
**Both:**
|
|
64
|
+
- Gibt sowohl JSON als auch XML zurück
|
|
65
|
+
|
|
66
|
+
### Beispiel Workflow
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
|
|
70
|
+
│ HTTP Request │───▶│ ZUGFeRD Reader │───▶│ Process │
|
|
71
|
+
│ (Get PDF) │ │ │ │ Invoice │
|
|
72
|
+
└─────────────────┘ └──────────────────┘ └─────────────┘
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Beispiel Output
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"attachmentName": "factur-x.xml",
|
|
80
|
+
"availableAttachments": ["factur-x.xml"],
|
|
81
|
+
"invoice": {
|
|
82
|
+
"rsm:CrossIndustryInvoice": {
|
|
83
|
+
"rsm:ExchangedDocumentContext": {
|
|
84
|
+
"ram:GuidelineSpecifiedDocumentContextParameter": {
|
|
85
|
+
"ram:ID": "urn:cen.eu:en16931:2017"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"rsm:ExchangedDocument": {
|
|
89
|
+
"ram:ID": "RE-2024-0001",
|
|
90
|
+
"ram:TypeCode": "380",
|
|
91
|
+
"ram:IssueDateTime": {
|
|
92
|
+
"@_format": "102",
|
|
93
|
+
"#text": "20240115"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"rsm:SupplyChainTradeTransaction": {
|
|
97
|
+
"ram:ApplicableHeaderTradeAgreement": {
|
|
98
|
+
"ram:SellerTradeParty": {
|
|
99
|
+
"ram:Name": "Muster GmbH"
|
|
100
|
+
},
|
|
101
|
+
"ram:BuyerTradeParty": {
|
|
102
|
+
"ram:Name": "Kunde AG"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"ram:ApplicableHeaderTradeSettlement": {
|
|
106
|
+
"ram:InvoiceCurrencyCode": "EUR",
|
|
107
|
+
"ram:SpecifiedTradeSettlementHeaderMonetarySummation": {
|
|
108
|
+
"ram:TaxBasisTotalAmount": "1000.00",
|
|
109
|
+
"ram:TaxTotalAmount": "190.00",
|
|
110
|
+
"ram:GrandTotalAmount": "1190.00"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Unterstützte Standards
|
|
120
|
+
|
|
121
|
+
- ZUGFeRD 1.0 / 2.x
|
|
122
|
+
- Factur-X
|
|
123
|
+
- XRechnung
|
|
124
|
+
- EN16931 (CII Format)
|
|
125
|
+
|
|
126
|
+
## Technische Details
|
|
127
|
+
|
|
128
|
+
### Dependencies
|
|
129
|
+
|
|
130
|
+
- `pdf-lib`: PDF-Manipulation und Anhang-Extraktion
|
|
131
|
+
- `fast-xml-parser`: Schnelles XML-zu-JSON Parsing
|
|
132
|
+
|
|
133
|
+
### Wie es funktioniert
|
|
134
|
+
|
|
135
|
+
1. PDF wird geladen (aus Binary Data oder Dateisystem)
|
|
136
|
+
2. Eingebettete Dateien werden aus dem PDF extrahiert
|
|
137
|
+
3. XML-Anhänge werden identifiziert (auto oder custom)
|
|
138
|
+
4. XML wird optional zu JSON geparst
|
|
139
|
+
5. Daten werden als Output zurückgegeben
|
|
140
|
+
|
|
141
|
+
### Error Handling
|
|
142
|
+
|
|
143
|
+
Der Node bietet detaillierte Fehlermeldungen:
|
|
144
|
+
- Keine eingebetteten Dateien gefunden
|
|
145
|
+
- Kein ZUGFeRD XML gefunden (mit Liste verfügbarer Anhänge)
|
|
146
|
+
- PDF-Lesefehler
|
|
147
|
+
- XML-Parsing-Fehler
|
|
148
|
+
|
|
149
|
+
## Entwicklung
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Dependencies installieren
|
|
153
|
+
npm install
|
|
154
|
+
|
|
155
|
+
# TypeScript kompilieren
|
|
156
|
+
npm run build
|
|
157
|
+
|
|
158
|
+
# Watch mode für Entwicklung
|
|
159
|
+
npm run dev
|
|
160
|
+
|
|
161
|
+
# In lokalem N8N testen
|
|
162
|
+
N8N_CUSTOM_EXTENSIONS=~/.n8n/custom n8n start
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Lizenz
|
|
166
|
+
|
|
167
|
+
MIT
|
|
168
|
+
|
|
169
|
+
## Support
|
|
170
|
+
|
|
171
|
+
Bei Problemen oder Fragen, bitte ein Issue erstellen.
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Verwendungsbeispiele
|
|
2
|
+
|
|
3
|
+
## Beispiel 1: PDF von URL laden und verarbeiten
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
HTTP Request (GET PDF) → ZUGFeRD Reader → Set Node (Rechnung verarbeiten)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**HTTP Request Node:**
|
|
10
|
+
- Method: GET
|
|
11
|
+
- URL: `https://example.com/invoice.pdf`
|
|
12
|
+
- Response Format: File
|
|
13
|
+
- Binary Property: `data`
|
|
14
|
+
|
|
15
|
+
**ZUGFeRD Reader Node:**
|
|
16
|
+
- Input Mode: Binary Data
|
|
17
|
+
- Binary Property: `data`
|
|
18
|
+
- Output Format: Parsed JSON
|
|
19
|
+
|
|
20
|
+
**Set Node (Rechnung verarbeiten):**
|
|
21
|
+
```javascript
|
|
22
|
+
// Zugriff auf Rechnungsdaten
|
|
23
|
+
const invoice = $json.invoice;
|
|
24
|
+
const rechnungsNr = invoice['rsm:CrossIndustryInvoice']?.['rsm:ExchangedDocument']?.['ram:ID'];
|
|
25
|
+
const gesamtBetrag = invoice['rsm:CrossIndustryInvoice']?.['rsm:SupplyChainTradeTransaction']?.['ram:ApplicableHeaderTradeSettlement']?.['ram:SpecifiedTradeSettlementHeaderMonetarySummation']?.['ram:GrandTotalAmount'];
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
rechnungsNummer: rechnungsNr,
|
|
29
|
+
betrag: parseFloat(gesamtBetrag),
|
|
30
|
+
waehrung: 'EUR'
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Beispiel 2: Lokale PDF-Datei lesen
|
|
35
|
+
|
|
36
|
+
**ZUGFeRD Reader Node:**
|
|
37
|
+
- Input Mode: File Path
|
|
38
|
+
- File Path: `/path/to/invoice.pdf`
|
|
39
|
+
- Output Format: Both (JSON + XML)
|
|
40
|
+
|
|
41
|
+
## Beispiel 3: E-Mail-Anhang verarbeiten
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Email Trigger → Extract Attachments → ZUGFeRD Reader → Database Insert
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Email Trigger:**
|
|
48
|
+
- Wartet auf neue E-Mails mit PDF-Anhängen
|
|
49
|
+
|
|
50
|
+
**Extract Attachments:**
|
|
51
|
+
- Extrahiert PDF aus E-Mail
|
|
52
|
+
|
|
53
|
+
**ZUGFeRD Reader:**
|
|
54
|
+
- Input Mode: Binary Data
|
|
55
|
+
- Binary Property: `data`
|
|
56
|
+
- Output Format: Parsed JSON
|
|
57
|
+
|
|
58
|
+
**Database Insert:**
|
|
59
|
+
- Speichert Rechnungsdaten in Datenbank
|
|
60
|
+
|
|
61
|
+
## Beispiel 4: Batch-Verarbeitung mehrerer PDFs
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Read Binary Files (*.pdf) → ZUGFeRD Reader → Function Node → Spreadsheet
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Function Node - Daten aufbereiten:**
|
|
68
|
+
```javascript
|
|
69
|
+
const items = [];
|
|
70
|
+
|
|
71
|
+
for (const item of $input.all()) {
|
|
72
|
+
const invoice = item.json.invoice?.['rsm:CrossIndustryInvoice'];
|
|
73
|
+
|
|
74
|
+
if (invoice) {
|
|
75
|
+
const doc = invoice['rsm:ExchangedDocument'];
|
|
76
|
+
const trade = invoice['rsm:SupplyChainTradeTransaction'];
|
|
77
|
+
const seller = trade?.['ram:ApplicableHeaderTradeAgreement']?.['ram:SellerTradeParty'];
|
|
78
|
+
const buyer = trade?.['ram:ApplicableHeaderTradeAgreement']?.['ram:BuyerTradeParty'];
|
|
79
|
+
const monetary = trade?.['ram:ApplicableHeaderTradeSettlement']?.['ram:SpecifiedTradeSettlementHeaderMonetarySummation'];
|
|
80
|
+
|
|
81
|
+
items.push({
|
|
82
|
+
json: {
|
|
83
|
+
rechnungsNummer: doc?.['ram:ID'],
|
|
84
|
+
datum: doc?.['ram:IssueDateTime']?.['#text'],
|
|
85
|
+
lieferant: seller?.['ram:Name'],
|
|
86
|
+
kunde: buyer?.['ram:Name'],
|
|
87
|
+
nettoBetrag: monetary?.['ram:TaxBasisTotalAmount'],
|
|
88
|
+
steuerBetrag: monetary?.['ram:TaxTotalAmount'],
|
|
89
|
+
bruttoBetrag: monetary?.['ram:GrandTotalAmount'],
|
|
90
|
+
waehrung: trade?.['ram:ApplicableHeaderTradeSettlement']?.['ram:InvoiceCurrencyCode']
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return items;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Beispiel 5: Fehlerbehandlung
|
|
100
|
+
|
|
101
|
+
**ZUGFeRD Reader Node:**
|
|
102
|
+
- Aktiviere "Continue On Fail" in den Node Settings
|
|
103
|
+
- Bei Fehler wird ein Error-Objekt zurückgegeben
|
|
104
|
+
|
|
105
|
+
**IF Node - Fehlerprüfung:**
|
|
106
|
+
```javascript
|
|
107
|
+
// Prüfe ob Fehler aufgetreten ist
|
|
108
|
+
return $json.error === undefined;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Bei Erfolg:** Weiter zur Verarbeitung
|
|
112
|
+
**Bei Fehler:** Log-Node oder Benachrichtigung
|
|
113
|
+
|
|
114
|
+
## Beispiel 6: Verschiedene ZUGFeRD-Versionen
|
|
115
|
+
|
|
116
|
+
Der Node erkennt automatisch:
|
|
117
|
+
- ZUGFeRD 1.0 (`ZUGFeRD-invoice.xml`)
|
|
118
|
+
- ZUGFeRD 2.x (`factur-x.xml`)
|
|
119
|
+
- XRechnung (`xrechnung.xml`)
|
|
120
|
+
|
|
121
|
+
Wenn Auto-Detect nicht funktioniert:
|
|
122
|
+
- XML Attachment Name: Custom Name
|
|
123
|
+
- Custom Attachment Name: `dein-custom-name.xml`
|
|
124
|
+
|
|
125
|
+
## Typische Datenstruktur (ZUGFeRD 2.x / Factur-X)
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"rsm:CrossIndustryInvoice": {
|
|
130
|
+
"@_xmlns:rsm": "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
|
|
131
|
+
"@_xmlns:ram": "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
|
|
132
|
+
"@_xmlns:qdt": "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
|
|
133
|
+
"@_xmlns:udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100",
|
|
134
|
+
|
|
135
|
+
"rsm:ExchangedDocumentContext": {
|
|
136
|
+
"ram:GuidelineSpecifiedDocumentContextParameter": {
|
|
137
|
+
"ram:ID": "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:extended"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
"rsm:ExchangedDocument": {
|
|
142
|
+
"ram:ID": "RE-2024-0001",
|
|
143
|
+
"ram:TypeCode": "380",
|
|
144
|
+
"ram:IssueDateTime": {
|
|
145
|
+
"@_format": "102",
|
|
146
|
+
"#text": "20240115"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
"rsm:SupplyChainTradeTransaction": {
|
|
151
|
+
"ram:ApplicableHeaderTradeAgreement": {
|
|
152
|
+
"ram:SellerTradeParty": {
|
|
153
|
+
"ram:Name": "Lieferant GmbH",
|
|
154
|
+
"ram:PostalTradeAddress": {
|
|
155
|
+
"ram:PostcodeCode": "12345",
|
|
156
|
+
"ram:LineOne": "Musterstraße 1",
|
|
157
|
+
"ram:CityName": "Berlin",
|
|
158
|
+
"ram:CountryID": "DE"
|
|
159
|
+
},
|
|
160
|
+
"ram:SpecifiedTaxRegistration": {
|
|
161
|
+
"ram:ID": {
|
|
162
|
+
"@_schemeID": "VA",
|
|
163
|
+
"#text": "DE123456789"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"ram:BuyerTradeParty": {
|
|
168
|
+
"ram:Name": "Kunde AG"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
"ram:ApplicableHeaderTradeSettlement": {
|
|
173
|
+
"ram:InvoiceCurrencyCode": "EUR",
|
|
174
|
+
"ram:SpecifiedTradeSettlementHeaderMonetarySummation": {
|
|
175
|
+
"ram:TaxBasisTotalAmount": "1000.00",
|
|
176
|
+
"ram:TaxTotalAmount": {
|
|
177
|
+
"@_currencyID": "EUR",
|
|
178
|
+
"#text": "190.00"
|
|
179
|
+
},
|
|
180
|
+
"ram:GrandTotalAmount": {
|
|
181
|
+
"@_currencyID": "EUR",
|
|
182
|
+
"#text": "1190.00"
|
|
183
|
+
},
|
|
184
|
+
"ram:DuePayableAmount": {
|
|
185
|
+
"@_currencyID": "EUR",
|
|
186
|
+
"#text": "1190.00"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Tipps
|
|
196
|
+
|
|
197
|
+
1. **Namespace Handling:** ZUGFeRD/Factur-X verwendet XML-Namespaces (`rsm:`, `ram:`, etc.). Diese werden im JSON beibehalten.
|
|
198
|
+
|
|
199
|
+
2. **Attribute vs. Text:** XML-Attribute werden mit `@_` prefix gespeichert, Text-Content als `#text`.
|
|
200
|
+
|
|
201
|
+
3. **Array vs. Objekt:** Einzelne Elemente werden als Objekt geparst, mehrere als Array. Prüfe immer mit `Array.isArray()`.
|
|
202
|
+
|
|
203
|
+
4. **Währungen:** Beträge haben oft ein `@_currencyID` Attribut.
|
|
204
|
+
|
|
205
|
+
5. **Datumsformat:** Datum ist oft im Format `YYYYMMDD` (format="102").
|
|
206
|
+
|
|
207
|
+
6. **Debugging:** Nutze "Both" als Output Format um das Original-XML zu sehen.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-zugferd-reader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "N8N node to extract ZUGFeRD/Factur-X XML from PDF invoices",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"dev": "tsc --watch"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"n8n-community-node-package",
|
|
12
|
+
"n8n",
|
|
13
|
+
"zugferd",
|
|
14
|
+
"factur-x",
|
|
15
|
+
"invoice",
|
|
16
|
+
"pdf",
|
|
17
|
+
"xml"
|
|
18
|
+
],
|
|
19
|
+
"n8n": {
|
|
20
|
+
"n8nNodesApiVersion": 1,
|
|
21
|
+
"nodes": [
|
|
22
|
+
"dist/nodes/ZugferdReader/ZugferdReader.node.js"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.11.0",
|
|
29
|
+
"n8n-workflow": "^1.68.0",
|
|
30
|
+
"typescript": "^5.3.3"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"pdf-lib": "^1.17.1",
|
|
34
|
+
"fast-xml-parser": "^4.3.4"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ZugferdReader } from './nodes/ZugferdReader/ZugferdReader.node';
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IExecuteFunctions,
|
|
3
|
+
INodeExecutionData,
|
|
4
|
+
INodeType,
|
|
5
|
+
INodeTypeDescription,
|
|
6
|
+
NodeOperationError,
|
|
7
|
+
} from 'n8n-workflow';
|
|
8
|
+
|
|
9
|
+
import { PDFDocument } from 'pdf-lib';
|
|
10
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
11
|
+
|
|
12
|
+
interface EmbeddedFile {
|
|
13
|
+
name: string;
|
|
14
|
+
data: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractNamesArray(obj: any): any[] {
|
|
18
|
+
if (obj.lookup) {
|
|
19
|
+
const kids = obj.lookup('Kids');
|
|
20
|
+
if (kids && Array.isArray(kids)) {
|
|
21
|
+
let result: any[] = [];
|
|
22
|
+
for (const kid of kids) {
|
|
23
|
+
result = result.concat(extractNamesArray(kid));
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const names = obj.lookup('Names');
|
|
29
|
+
if (names && Array.isArray(names)) {
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getEmbeddedFiles(pdfDoc: PDFDocument): EmbeddedFile[] {
|
|
38
|
+
const embeddedFiles: EmbeddedFile[] = [];
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const context = pdfDoc.context;
|
|
42
|
+
const catalog = context.lookup(context.trailerInfo.Root) as any;
|
|
43
|
+
|
|
44
|
+
if (!catalog || !catalog.get) {
|
|
45
|
+
return embeddedFiles;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const names = catalog.lookup('Names');
|
|
49
|
+
if (!names) {
|
|
50
|
+
return embeddedFiles;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const embeddedFilesRef = names.lookup('EmbeddedFiles');
|
|
54
|
+
if (!embeddedFilesRef) {
|
|
55
|
+
return embeddedFiles;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const namesArray = extractNamesArray(embeddedFilesRef);
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < namesArray.length; i += 2) {
|
|
61
|
+
const fileName = namesArray[i];
|
|
62
|
+
const fileSpec = namesArray[i + 1];
|
|
63
|
+
|
|
64
|
+
if (fileSpec && fileSpec.lookup) {
|
|
65
|
+
const efDict = fileSpec.lookup('EF');
|
|
66
|
+
if (efDict && efDict.lookup) {
|
|
67
|
+
const fileStream = efDict.lookup('F');
|
|
68
|
+
if (fileStream && fileStream.contents) {
|
|
69
|
+
const contents = fileStream.contents;
|
|
70
|
+
const decoder = new TextDecoder('utf-8');
|
|
71
|
+
const text = decoder.decode(contents);
|
|
72
|
+
embeddedFiles.push({
|
|
73
|
+
name: fileName,
|
|
74
|
+
data: text,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// If extraction fails, return empty array
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return embeddedFiles;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class ZugferdReader implements INodeType {
|
|
88
|
+
description: INodeTypeDescription = {
|
|
89
|
+
displayName: 'ZUGFeRD Reader',
|
|
90
|
+
name: 'zugferdReader',
|
|
91
|
+
icon: 'file:zugferd.svg',
|
|
92
|
+
group: ['transform'],
|
|
93
|
+
version: 1,
|
|
94
|
+
description: 'Extract ZUGFeRD/Factur-X XML data from PDF invoices',
|
|
95
|
+
defaults: {
|
|
96
|
+
name: 'ZUGFeRD Reader',
|
|
97
|
+
},
|
|
98
|
+
inputs: ['main'],
|
|
99
|
+
outputs: ['main'],
|
|
100
|
+
properties: [
|
|
101
|
+
{
|
|
102
|
+
displayName: 'Input Mode',
|
|
103
|
+
name: 'inputMode',
|
|
104
|
+
type: 'options',
|
|
105
|
+
options: [
|
|
106
|
+
{
|
|
107
|
+
name: 'Binary Data',
|
|
108
|
+
value: 'binary',
|
|
109
|
+
description: 'Read PDF from binary data property',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'File Path',
|
|
113
|
+
value: 'filepath',
|
|
114
|
+
description: 'Read PDF from file system path',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
default: 'binary',
|
|
118
|
+
description: 'How to provide the PDF file',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
displayName: 'Binary Property',
|
|
122
|
+
name: 'binaryProperty',
|
|
123
|
+
type: 'string',
|
|
124
|
+
default: 'data',
|
|
125
|
+
required: true,
|
|
126
|
+
displayOptions: {
|
|
127
|
+
show: {
|
|
128
|
+
inputMode: ['binary'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
description: 'Name of the binary property containing the PDF',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
displayName: 'File Path',
|
|
135
|
+
name: 'filePath',
|
|
136
|
+
type: 'string',
|
|
137
|
+
default: '',
|
|
138
|
+
required: true,
|
|
139
|
+
displayOptions: {
|
|
140
|
+
show: {
|
|
141
|
+
inputMode: ['filepath'],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
description: 'Path to the PDF file on the file system',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
displayName: 'Output Format',
|
|
148
|
+
name: 'outputFormat',
|
|
149
|
+
type: 'options',
|
|
150
|
+
options: [
|
|
151
|
+
{
|
|
152
|
+
name: 'Parsed JSON',
|
|
153
|
+
value: 'json',
|
|
154
|
+
description: 'Parse XML and return as JSON object',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'Raw XML',
|
|
158
|
+
value: 'xml',
|
|
159
|
+
description: 'Return raw XML string',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'Both',
|
|
163
|
+
value: 'both',
|
|
164
|
+
description: 'Return both parsed JSON and raw XML',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
default: 'json',
|
|
168
|
+
description: 'Format of the output data',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
displayName: 'XML Attachment Name',
|
|
172
|
+
name: 'xmlAttachmentName',
|
|
173
|
+
type: 'options',
|
|
174
|
+
options: [
|
|
175
|
+
{
|
|
176
|
+
name: 'Auto-Detect',
|
|
177
|
+
value: 'auto',
|
|
178
|
+
description: 'Automatically detect ZUGFeRD/Factur-X XML',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'Custom Name',
|
|
182
|
+
value: 'custom',
|
|
183
|
+
description: 'Specify custom attachment name',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
default: 'auto',
|
|
187
|
+
description: 'Name of the XML attachment in the PDF',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
displayName: 'Custom Attachment Name',
|
|
191
|
+
name: 'customAttachmentName',
|
|
192
|
+
type: 'string',
|
|
193
|
+
default: '',
|
|
194
|
+
displayOptions: {
|
|
195
|
+
show: {
|
|
196
|
+
xmlAttachmentName: ['custom'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
description: 'Custom name of the XML attachment to extract',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
205
|
+
const items = this.getInputData();
|
|
206
|
+
const returnData: INodeExecutionData[] = [];
|
|
207
|
+
|
|
208
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
209
|
+
try {
|
|
210
|
+
const inputMode = this.getNodeParameter('inputMode', itemIndex) as string;
|
|
211
|
+
const outputFormat = this.getNodeParameter('outputFormat', itemIndex) as string;
|
|
212
|
+
const xmlAttachmentName = this.getNodeParameter('xmlAttachmentName', itemIndex) as string;
|
|
213
|
+
|
|
214
|
+
let pdfBytes: Uint8Array;
|
|
215
|
+
|
|
216
|
+
// Get PDF data based on input mode
|
|
217
|
+
if (inputMode === 'binary') {
|
|
218
|
+
const binaryProperty = this.getNodeParameter('binaryProperty', itemIndex) as string;
|
|
219
|
+
const binaryData = this.helpers.assertBinaryData(itemIndex, binaryProperty);
|
|
220
|
+
pdfBytes = await this.helpers.getBinaryDataBuffer(itemIndex, binaryProperty);
|
|
221
|
+
} else {
|
|
222
|
+
const filePath = this.getNodeParameter('filePath', itemIndex) as string;
|
|
223
|
+
const fs = await import('fs/promises');
|
|
224
|
+
pdfBytes = await fs.readFile(filePath);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Load PDF document
|
|
228
|
+
const pdfDoc = await PDFDocument.load(pdfBytes);
|
|
229
|
+
|
|
230
|
+
// Get embedded files
|
|
231
|
+
const embeddedFiles = getEmbeddedFiles(pdfDoc);
|
|
232
|
+
|
|
233
|
+
if (embeddedFiles.length === 0) {
|
|
234
|
+
throw new NodeOperationError(
|
|
235
|
+
this.getNode(),
|
|
236
|
+
'No embedded files found in PDF',
|
|
237
|
+
{ itemIndex }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Find ZUGFeRD/Factur-X XML
|
|
242
|
+
let xmlData: string | null = null;
|
|
243
|
+
let foundAttachmentName: string | null = null;
|
|
244
|
+
|
|
245
|
+
if (xmlAttachmentName === 'auto') {
|
|
246
|
+
// Auto-detect common ZUGFeRD/Factur-X names
|
|
247
|
+
const commonNames = [
|
|
248
|
+
'factur-x.xml',
|
|
249
|
+
'FacturX.xml',
|
|
250
|
+
'zugferd-invoice.xml',
|
|
251
|
+
'ZUGFeRD-invoice.xml',
|
|
252
|
+
'xrechnung.xml',
|
|
253
|
+
'XRechnung.xml',
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
for (const file of embeddedFiles) {
|
|
257
|
+
const fileName = file.name.toLowerCase();
|
|
258
|
+
if (
|
|
259
|
+
commonNames.some(name => fileName.includes(name.toLowerCase())) ||
|
|
260
|
+
fileName.endsWith('.xml')
|
|
261
|
+
) {
|
|
262
|
+
xmlData = file.data;
|
|
263
|
+
foundAttachmentName = file.name;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const customName = this.getNodeParameter('customAttachmentName', itemIndex) as string;
|
|
269
|
+
const file = embeddedFiles.find((f: EmbeddedFile) => f.name === customName);
|
|
270
|
+
if (file) {
|
|
271
|
+
xmlData = file.data;
|
|
272
|
+
foundAttachmentName = file.name;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!xmlData) {
|
|
277
|
+
throw new NodeOperationError(
|
|
278
|
+
this.getNode(),
|
|
279
|
+
`No ZUGFeRD/Factur-X XML found. Available attachments: ${embeddedFiles.map((f: EmbeddedFile) => f.name).join(', ')}`,
|
|
280
|
+
{ itemIndex }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Prepare output based on format
|
|
285
|
+
let json: any = {};
|
|
286
|
+
|
|
287
|
+
if (outputFormat === 'json' || outputFormat === 'both') {
|
|
288
|
+
const parser = new XMLParser({
|
|
289
|
+
ignoreAttributes: false,
|
|
290
|
+
attributeNamePrefix: '@_',
|
|
291
|
+
textNodeName: '#text',
|
|
292
|
+
parseAttributeValue: true,
|
|
293
|
+
parseTagValue: true,
|
|
294
|
+
});
|
|
295
|
+
json = parser.parse(xmlData);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const outputData: any = {
|
|
299
|
+
attachmentName: foundAttachmentName,
|
|
300
|
+
availableAttachments: embeddedFiles.map((f: EmbeddedFile) => f.name),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (outputFormat === 'json') {
|
|
304
|
+
outputData.invoice = json;
|
|
305
|
+
} else if (outputFormat === 'xml') {
|
|
306
|
+
outputData.xml = xmlData;
|
|
307
|
+
} else {
|
|
308
|
+
outputData.invoice = json;
|
|
309
|
+
outputData.xml = xmlData;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
returnData.push({
|
|
313
|
+
json: outputData,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (this.continueOnFail()) {
|
|
318
|
+
returnData.push({
|
|
319
|
+
json: {
|
|
320
|
+
error: error instanceof Error ? error.message : String(error),
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return [returnData];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<!-- PDF Document -->
|
|
3
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
4
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
5
|
+
|
|
6
|
+
<!-- XML/Code Symbol -->
|
|
7
|
+
<path d="M9 15l-2 2 2 2"/>
|
|
8
|
+
<path d="M15 15l2 2-2 2"/>
|
|
9
|
+
<line x1="11" y1="19" x2="13" y2="13"/>
|
|
10
|
+
</svg>
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"moduleResolution": "node"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|