Skill: quantx-heiko/zugferd-invoice (version 1.0.0)

# zugferd-invoice Create ZUGFeRD 2.1 / Factur-X compliant e-invoices with **visible merged pages**. German B2B / Gov-ready. 🇩🇪 **Germany/EU only** - This skill implements the German ZUGFeRD standard for electronic invoicing. ## Features - ✅ **Visible pages**: Merge invoice + time report into a single multi-page PDF - ✅ **ZUGFeRD 2.1 compliant**: PDF/A-3b with embedded XML - ✅ **EN16931 valid**: Ready for B2B portals and government submission - ✅ **No paid APIs**: Uses MustangProject (open source) + GhostScript - ✅ **Two workflows**: Visible pages (recommended) or attachment-only fallback ## Requirements | Dependency | Install | Notes | |------------|---------|-------| | **Java 11+** | `brew install openjdk@21` | Required by MustangProject | | **GhostScript** | `brew install ghostscript` | PDF/A-3 conversion | | **mustang.jar** | Manual download | ZUGFeRD processor | ### Install mustang.jar ```bash mkdir -p ~/.openclaw/tools/mustang curl -L https://github.com/ZUGFeRD/mustangproject/releases/download/core-2.22.0/mustang.jar \ -o ~/.openclaw/tools/mustang/mustang.jar ``` ## Usage ### Recommended: GhostScript Workflow (Visible Pages) ```bash export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH" cd ~/.openclaw/workspace/skills/zugferd-invoice python3 scripts/zugferd_pages_workflow.py \ --invoice Rechnung.pdf \ --attachment Zeitnachweis.pdf \ --output Rechnung_komplett.pdf ``` **Output:** - Multi-page PDF (both documents visible) - PDF/A-3b compliant - ZUGFeRD 2.1 XML embedded - Valid for B2B/Gov portals ### Fallback: Attachment-Only Workflow ```bash python3 scripts/zugferd_workflow.py \ --invoice Rechnung.pdf \ --attachment Zeitnachweis.pdf \ --output Rechnung_komplett.pdf ``` **Output:** - Invoice visible, time report as file attachment - Use when GhostScript is unavailable ## Arguments | Flag | Description | Required | |------|-------------|----------| | `--invoice` | Original ZUGFeRD e-invoice PDF | ✅ Yes | | `--attachment` | Time report / additional document | ✅ Yes | | `--output` | Output path for merged PDF | ✅ Yes | | `--keep-temp` | Keep temporary files (debug) | ❌ No | ## Workflows Compared | Feature | GhostScript (Recommended) | Attachment-Only (Fallback) | |---------|---------------------------|----------------------------| | Visible pages | ✅ Both documents | ⚠️ Only invoice | | Time report visibility | ✅ Page 2+ | ❌ File attachment only | | PDF/A-3 compliance | ✅ Full | ✅ Full | | Requires GhostScript | ✅ Yes | ❌ No | | Complexity | Medium | Low | ## Technical Details ### GhostScript Workflow Steps 1. **Extract XML** from original e-invoice 2. **Merge PDFs** with GhostScript (visible pages) 3. **Convert to PDF/A-3** with GhostScript 4. **Embed XML** with MustangProject combine 5. **Validate** final PDF ### Why This Works Standard PDF merging breaks PDF/A-3 compliance. The trick: 1. Merge first (any PDFs allowed) 2. Then convert the **merged** result to PDF/A-3 3. Finally embed the ZUGFeRD XML This preserves visible pages while achieving full compliance. ### Why Attachment-Only? If your time report cannot be converted to PDF/A (e.g., Lexware exports with broken ICC profiles), use `--attachments` flag which embeds files without visible merging. ## Troubleshooting ### "gs not found" ```bash brew install ghostscript ``` ### "java not found" ```bash brew install openjdk@21 export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH" ``` ### "mustang.jar not found" Download from [MustangProject releases](https://github.com/ZUGFeRD/mustangproject/releases) ### Validation fails - Ensure input invoice is valid ZUGFeRD PDF/A-3 - Check GhostScript version: `gs --version` (should be 10+) ## Output Verification ```bash # Check PDF/A compliance java -jar ~/.openclaw/tools/mustang/mustang.jar --action validate \ --source Rechnung_komplett.pdf # Should output: <summary status="valid"/> ``` ## Notes - **ZUGFeRD = Germany only**: Electronic invoicing standard for German B2B - **Factur-X**: French equivalent (also supported) - **EN16931**: European standard for e-invoices - **Source PDFs**: Invoice must be ZUGFeRD PDF/A-3; attachment can be any PDF ## Dependencies - Python 3.8+ - Java 11+ (MustangProject) - GhostScript 10+ - MustangProject CLI 2.22.0+ ## See Also - [ZUGFeRD Standard](https://www.ferd-net.de/) - [MustangProject](https://github.com/ZUGFeRD/mustangproject) - [EN16931](https://ec.europa.eu/digital-building-blocks/wikis/display/DIGITAL/EN16931+compliance)

path: README.md
content: # ZUGFeRD Invoice Workflow

Erstellt valide ZUGFeRD 2.1 / Factur-X PDFs aus
- Rechnung (mit integriertem ZUGFeRD-XML)
- Zeitnachweis/-dokument (als Anhang)

## Zwei Workflows

| Workflow | Sichtbare Seiten | Anhang als Datei | Empfohlen |
|----------|-----------------|------------------|-----------|
| **zugferd_pages_workflow.py** | ✅ Ja | ❌ Nein | **Standard** |
| **zugferd_workflow.py** | ❌ Nein | ✅ Ja | Fallback |

---

## Empfohlen: Sichtbare Seiten (NEU)

**Durchbruch:** GhostScript merge + PDF/A-3 Konvertierung **vor** dem XML-Combine.

### Ablauf

```
Rechnung.pdf (PDF/A-3) ─┬─▶ [1. XML extrahieren]
                        │
Zeitnachweis.pdf        │
(nicht PDF/A)           │
                        ├─▶ [2. GhostScript merge] ──▶ Merged.pdf
                        │                                    │
                        │                              [3. GhostScript
                        │                               PDF/A-3 Konvertierung]
                        │                                    │
                        │                                    ▼
                        │                           MergedPDFA3.pdf (PDF/A-3)
                        │                                    │
                        └────────────────────────────────────┘
                                         │
                                    [4. XML einbetten]
                                         │
                                         ▼
                              ✅ Rechnung_komplett.pdf
                              ✅ SICHTBARE SEITEN
                              ✅ ZUGFeRD-konform
```

### Verwendung

```bash
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
cd ~/.openclaw/workspace/skills/zugferd-invoice

python3 scripts/zugferd_pages_workflow.py \
  --invoice Rechnung_RE0182.pdf \
  --attachment Zeitnachweis.pdf \
  --output Rechnung_komplett.pdf
```

### Output

- PDF/A-3b compliant (isCompliant=true)
- Enthält ZUGFeRD 2.1 XML (Factur-X)
- **SICHTBARE SEITEN** beider Dokumente
- Validiert für B2B/Gov-Portal Einreichung

---

## Fallback: Attachment-Only

Falls der GhostScript-Workflow nicht funktioniert.

```bash
python3 scripts/zugferd_workflow.py \
  --invoice Rechnung.pdf \
  --attachment Zeitnachweis.pdf \
  --output Rechnung_komplett.pdf
```

**Unterschied:** Der Zeitnachweis ist als **Datei-Attachment** (nicht als sichtbare Seite) im PDF eingebettet. Im PDF-Reader unter "Anhänge" zu finden.

### Warum funktioniert Page-Merge normalerweise nicht?

Merging eines **nicht**-PDF/A-konformen Zeitnachweise-PDFs (z.B. von Lexware exportiert) zerstört die PDF/A-3 Compliance. Diese PDFs haben typischerweise:
- Fehlende/ungültige ICC-Profile
- Keine ToUnicode-CMaps
- Transparenz-Probleme
- Fehlende OutputIntent

**Der Trick mit GhostScript:** Merge zuerst, dann konvertiere zu PDF/A-3 → funktioniert!

---

## Voraussetzungen

- Java 21 (Homebrew: `brew install openjdk@21`)
- GhostScript (`brew install ghostscript`)
- MustangProject: `~/.openclaw/tools/mustang/mustang.jar`
  ```bash
  mkdir -p ~/.openclaw/tools/mustang
  curl -L https://github.com/ZUGFeRD/mustangproject/releases/download/core-2.22.0/mustang.jar \
       -o ~/.openclaw/tools/mustang/mustang.jar
  ```

## Dateien

| Datei | Zweck |
|-------|-------|
| `scripts/zugferd_pages_workflow.py` | **👑 NEU: Sichtbare Seiten + ZUGFeRD** (GhostScript) |
| `scripts/zugferd_workflow.py` | Attachment-Only Workflow (Fallback) |
| `temp/` | Arbeitsverzeichnis (automatisch) |
| `output/` | Standard-Ausgabeverzeichnis |

---

## Abgelegte Ansätze (nicht benutzen)

- ❌ **Preview.app Merge** - Unzuverlässig (Berechtigungs-Hölle)
- ❌ **qpdf merge** - Zerstört PDF/A-3
- ❌ **pypdf merge** - Zerstört PDF/A-3 + XML
- ❌ **mustang a3only** - Funktioniert nur für PDF/A-1 zu A-3


path: requirements.txt
content: # ZUGFeRD Invoice Skill
# System requirements (not pip):
# - Java 11+ (brew install openjdk@21)
# - GhostScript (brew install ghostscript)
# - mustang.jar (manual download, see SKILL.md)

# No Python packages required - pure standard library


path: scripts/zugferd_pages_workflow.py
content: #!/usr/bin/env python3
"""
ZUGFeRD Invoice Workflow - Sichtbare Seiten + ZUGFeRD-konform

Workflow (basierend auf lokalem Macbook-Prozess von Heiko):
1. ZUGFeRD XML aus e-Rechnung extrahieren (mustangproject)
2. PDFs mit GhostScript mergen (sichtbare Seiten)
3. Merged PDF zu PDF/A-3 konvertieren (GhostScript)
4. XML wieder einbetten (mustangproject combine)
5. Validieren

Usage:
    python3 zugferd_pages_workflow.py \
        --invoice Rechnung.pdf \
        --attachment Zeitnachweis.pdf \
        --output Rechnung_mit_Anhang.pdf

Requirements:
    - Java 11+ (für MustangProject)
    - GhostScript (gs): brew install ghostscript
    - mustang.jar: ~/.openclaw/tools/mustang/mustang.jar
"""

import argparse
import subprocess
import sys
import os
import shutil
import tempfile
from pathlib import Path

MUSTANG_JAR = os.path.expanduser("~/.openclaw/tools/mustang/mustang.jar")
WORK_DIR = Path(__file__).parent.parent / "temp"

def run_mustang(args):
    """Execute mustang.jar with Java 21"""
    cmd = ["java", "-jar", MUSTANG_JAR] + args
    env = os.environ.copy()
    env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "")
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60)
    return result.stdout, result.stderr, result.returncode

def run_ghostscript(args):
    """Execute GhostScript (gs)"""
    cmd = ["gs"] + args
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    return result.stdout, result.stderr, result.returncode

def validate(pdf_path):
    """Validate PDF - returns dict with status"""
    stdout, stderr, _ = run_mustang(["--action", "validate", "--source", pdf_path, "--no-notices"])
    output = stdout + stderr
    
    return {
        "pdf_compliant": "isCompliant=true" in output,
        "pdf_flavour": "pdf-a/3" if "pdf-a/3" in output.lower() else ("pdf-a/1" if "pdf-a/1" in output.lower() else "unknown"),
        "has_xml": "XML:valid" in output or "attached" in output.lower(),
        "xml_valid": "<summary status=\"valid\"/>" in output,
        "raw": output
    }

def extract_xml(pdf_path, out_xml):
    """Extract ZUGFeRD XML from PDF"""
    _, stderr, rc = run_mustang([
        "--action", "extract",
        "--source", pdf_path,
        "--out", out_xml
    ])
    success = rc == 0 and os.path.exists(out_xml) and os.path.getsize(out_xml) > 100
    if not success and os.path.exists(out_xml):
        # Debug output
        print(f"    Debug: XML file exists but may be empty: {os.path.getsize(out_xml)} bytes")
        print(f"    stderr: {stderr[:500]}")
    return success

def merge_pdfs_ghostscript(pdf1, pdf2, output):
    """
    Step 2: Merge PDFs using GhostScript (sichtbare Seiten)
    """
    args = [
        "-dBATCH",
        "-dNOPAUSE",
        "-sDEVICE=pdfwrite",
        "-sOutputFile=" + output,
        "-dQUIET",
        pdf1,
        pdf2
    ]
    stdout, stderr, rc = run_ghostscript(args)
    
    if rc != 0:
        print(f"    GhostScript merge error: {stderr[:500]}")
        return False
    
    return os.path.exists(output) and os.path.getsize(output) > 1000

def convert_to_pdfa3(input_pdf, output_pdf):
    """
    Step 3: Convert merged PDF to PDF/A-3 using GhostScript
    """
    args = [
        "-dPDFA=3",
        "-dBATCH",
        "-dNOPAUSE",
        "-sDEVICE=pdfwrite",
        "-dPDFACompatibilityPolicy=1",
        "-sColorConversionStrategy=RGB",
        "-sOutputFile=" + output_pdf,
        "-dQUIET",
        input_pdf
    ]
    stdout, stderr, rc = run_ghostscript(args)
    
    if rc != 0:
        print(f"    GhostScript PDF/A-3 conversion error: {stderr[:500]}")
        return False
    
    return os.path.exists(output_pdf) and os.path.getsize(output_pdf) > 1000

def combine_with_xml(pdfa3_pdf, xml_file, output):
    """
    Step 4: Combine PDF/A-3 with ZUGFeRD XML (re-embed)
    """
    _, stderr, rc = run_mustang([
        "--action", "combine",
        "--source", pdfa3_pdf,
        "--source-xml", xml_file,
        "--out", output,
        "--format", "zf",
        "--version", "1",  # User specified version 1
        "--profile", "T",
        "--no-additional-attachments"
    ])
    
    if rc != 0:
        print(f"    MustangProject combine error: {stderr[:500]}")
    
    return os.path.exists(output) and os.path.getsize(output) > 1000

def workflow(invoice_pdf, attachment_pdf, output_path):
    """Main workflow with sichtbare Seiten"""
    WORK_DIR.mkdir(parents=True, exist_ok=True)
    
    print("=" * 70)
    print("ZUGFeRD Workflow mit SICHTBAREN SEITEN")
    print("=" * 70)
    print(f"Rechnung:   {Path(invoice_pdf).name}")
    print(f"Anhang:     {Path(attachment_pdf).name}")
    print(f"Output:     {output_path}")
    print("=" * 70)
    
    # Step 1: Identify e-invoice and extract XML
    print("\n[1/5] Extrahiere ZUGFeRD XML...")
    
    # Test both files - the one with XML is the invoice
    xml_temp = WORK_DIR / "extracted_zugferd.xml"
    
    invoice_file = None
    for test_file in [invoice_pdf, attachment_pdf]:
        print(f"  Teste: {Path(test_file).name}")
        if extract_xml(test_file, str(xml_temp)):
            invoice_file = test_file
            print(f"  ✓ XML gefunden in: {Path(test_file).name}")
            break
    
    if not invoice_file:
        print("  ❌ Keine ZUGFeRD XML in beiden Dateien gefunden!")
        return False
    
    other_file = attachment_pdf if invoice_file == invoice_pdf else invoice_pdf
    
    # Step 2: Merge PDFs
    print("\n[2/5] Merge PDFs mit GhostScript...")
    merged_pdf = WORK_DIR / "merged.pdf"
    if not merge_pdfs_ghostscript(invoice_file, other_file, str(merged_pdf)):
        print("  ❌ Merge fehlgeschlagen!")
        return False
    print(f"  ✓ Gemerged: {merged_pdf} ({merged_pdf.stat().st_size / 1024:.1f} KB)")
    
    # Step 3: Convert to PDF/A-3
    print("\n[3/5] Konvertiere zu PDF/A-3...")
    pdfa3_pdf = WORK_DIR / "merged_pdfa3.pdf"
    if not convert_to_pdfa3(str(merged_pdf), str(pdfa3_pdf)):
        print("  ❌ PDF/A-3 Konvertierung fehlgeschlagen!")
        return False
    print(f"  ✓ PDF/A-3: {pdfa3_pdf} ({pdfa3_pdf.stat().st_size / 1024:.1f} KB)")
    
    # Validate intermediate PDF/A-3
    print("    Validiere PDF/A-3...")
    v_mid = validate(str(pdfa3_pdf))
    print(f"    PDF/A-3 kompliant: {'✅' if v_mid['pdf_compliant'] else '❌'} ({v_mid['pdf_flavour']})")
    
    # Step 4: Re-embed XML
    print("\n[4/5] Bette ZUGFeRD XML ein...")
    combined_pdf = WORK_DIR / "zugferd_combined.pdf"
    if not combine_with_xml(str(pdfa3_pdf), str(xml_temp), str(combined_pdf)):
        print("  ❌ XML Einbettung fehlgeschlagen!")
        return False
    print(f"  ✓ Kombiniert: {combined_pdf}")
    
    # Step 5: Validate final
    print("\n[5/5] Validiere finale ZUGFeRD PDF...")
    result = validate(str(combined_pdf))
    
    # Copy to final destination
    os.makedirs(os.path.dirname(os.path.abspath(output_path)) or ".", exist_ok=True)
    shutil.copy(combined_pdf, output_path)
    
    # Results
    print("\n" + "=" * 70)
    print("ERGEBNIS")
    print("=" * 70)
    print(f"Output:          {os.path.abspath(output_path)}")
    print(f"Größe:           {os.path.getsize(output_path) / 1024:.1f} KB")
    print(f"Seiten:          ✅ Sichtbar gemerged")
    print(f"PDF/A-3:         {'✅ COMPLIANT' if result['pdf_compliant'] else '❌ FAILED'}")
    print(f"ZUGFeRD XML:     {'✅ Eingebettet' if result['has_xml'] else '❌ Fehlt'}")
    print(f"Validierung:     {'✅ VALID' if result['xml_valid'] else '⚠️ MIT WARNUNGEN'}")
    print(f"Status:          {'✅ ZUGFeRD-konform mit sichtbaren Seiten!' if result['xml_valid'] else '❌ Prüfung erforderlich'}")
    print("=" * 70)
    
    if result['xml_valid']:
        print("\n🎉 Erfolgreich! Mehrseitige ZUGFeRD-Rechnung erstellt.")
    else:
        print("\n⚠️ Achtung: Validierungswarnungen vorhanden.")
    
    return result['xml_valid']

def main():
    parser = argparse.ArgumentParser(
        description="ZUGFeRD Workflow mit sichtbaren Seiten (alle Tools)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Workflow:
  1. XML extrahieren (mustangproject)
  2. PDFs mergen (GhostScript) ← sichtbare Seiten!
  3. Zu PDF/A-3 konvertieren (GhostScript)
  4. XML einbetten (mustangproject combine)
  5. Validieren

Example:
    %(prog)s --invoice Rechnung.pdf --attachment Zeitnachweis.pdf \\
             --output Rechnung_komplett.pdf
        """
    )
    parser.add_argument("--invoice", "-i", required=True, 
                       help="PDF mit ZUGFeRD XML (Rechnung)")
    parser.add_argument("--attachment", "-a", required=True,
                       help="Zusätzliche PDF (Zeitnachweis)")
    parser.add_argument("--output", "-o", required=True,
                       help="Output Pfad")
    
    args = parser.parse_args()
    
    # Check dependencies
    missing = []
    if not os.path.exists(MUSTANG_JAR):
        missing.append(f"mustang.jar nicht gefunden: {MUSTANG_JAR}")
    
    gs_check = subprocess.run(["which", "gs"], capture_output=True)
    if gs_check.returncode != 0:
        missing.append("GhostScript (gs) nicht installiert: brew install ghostscript")
    
    if missing:
        print("❌ Fehlende Abhängigkeiten:")
        for m in missing:
            print(f"   - {m}")
        sys.exit(1)
    
    success = workflow(args.invoice, args.attachment, args.output)
    sys.exit(0 if success else 1)

if __name__ == "__main__":
    main()


path: scripts/zugferd_workflow.py
content: #!/usr/bin/env python3
"""
ZUGFeRD Invoice Workflow - Sauberer Anhang-Ansatz

Der Zeitnachweis wird NICHT als Seiten gemergt, sondern als Datei-Anhang
via --attachments eingebettet. Das ist der einzig valide Weg für ZUGFeRD-Factur-X.

Ablauf:
1. Identifiziere eRechnung (XML-Träger) via Validation
2. Extrahiere ZUGFeRD XML
3. Kombiniere: eRechnung-PDF + XML + Zeitnachweis als Attachment
4. Validiere Finale-PDF

Usage:
    python3 zugferd_workflow.py --invoice RE.pdf --attachment TIME.pdf --output final.pdf

Requirements:
    - Java 11+ (für MustangProject)
    - mustang.jar: ~/.openclaw/tools/mustang/mustang.jar
"""

import argparse
import subprocess
import sys
import os
import shutil
from pathlib import Path

MUSTANG_JAR = os.path.expanduser("~/.openclaw/tools/mustang/mustang.jar")
WORK_DIR = Path(__file__).parent.parent / "temp"

def run_mustang(args):
    """Execute mustang.jar with Java 21"""
    cmd = ["java", "-jar", MUSTANG_JAR] + args
    env = os.environ.copy()
    env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "")
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60)
    return result.stdout, result.stderr, result.returncode

def validate(pdf_path):
    """Validate PDF - returns dict with status"""
    stdout, stderr, _ = run_mustang(["--action", "validate", "--source", pdf_path, "--no-notices"])
    output = stdout + stderr
    
    return {
        "pdf_compliant": "isCompliant=true" in output,
        "pdf_flavour": "pdf-a/3" if "pdf-a/3" in output.lower() else "unknown",
        "has_xml": "XML:valid" in output or "attached" in output.lower(),
        "xml_valid": "<summary status=\"valid\"/>" in output,
        "raw": output
    }

def extract_xml(pdf_path, out_xml):
    """Extract ZUGFeRD XML from PDF"""
    _, _, rc = run_mustang([
        "--action", "extract",
        "--source", pdf_path,
        "--out", out_xml
    ])
    return rc == 0 and os.path.exists(out_xml)

def combine(invoice_pdf, xml_file, attachment_pdf, output):
    """
    Main: Combine invoice PDF + ZUGFeRD XML + attachment file
    
    This creates a valid ZUGFeRD/Factur-X PDF where:
    - The invoice is the primary PDF/A-3 document
    - The XML is embedded (ZUGFeRD data)
    - The attachment is embedded as file attachment (not merged pages)
    """
    _, stderr, rc = run_mustang([
        "--action", "combine",
        "--source", invoice_pdf,      # Base PDF/A-3
        "--source-xml", xml_file,     # ZUGFeRD XML
        "--out", output,
        "--format", "zf",             # ZUGFeRD
        "--version", "2",             # Version 2
        "--profile", "T",             # EXTENDED profile
        "--attachments", attachment_pdf,  # Additional file attachment
        "--no-additional-attachments"
    ])
    
    if rc != 0 and os.path.exists(output):
        # Sometimes output exists even with stderr warnings
        return True
    
    return rc == 0 and os.path.exists(output) and os.path.getsize(output) > 1000

def workflow(invoice_pdf, attachment_pdf, output_path):
    """Main workflow"""
    WORK_DIR.mkdir(parents=True, exist_ok=True)
    
    print("=" * 60)
    print("ZUGFeRD Factur-X Workflow")
    print("=" * 60)
    
    # Step 1: Identify which file is the e-invoice
    print("\n[1/4] Identifying e-invoice...")
    
    print(f"  Checking: {Path(invoice_pdf).name}")
    v1 = validate(invoice_pdf)
    print(f"    PDF/A-3: {'✅' if v1['pdf_compliant'] else '❌'}, XML: {'✅' if v1['has_xml'] else '❌'}")
    
    print(f"  Checking: {Path(attachment_pdf).name}")
    v2 = validate(attachment_pdf)
    print(f"    PDF/A-3: {'✅' if v2['pdf_compliant'] else '❌'}, XML: {'✅' if v2['has_xml'] else '❌'}")
    
    # Determine which is invoice
    if v1['has_xml']:
        einvoice, z_anhang = invoice_pdf, attachment_pdf
        print(f"\n  → e-invoice: {Path(einvoice).name}")
    elif v2['has_xml']:
        einvoice, z_anhang = attachment_pdf, invoice_pdf
        print(f"\n  → e-invoice: {Path(einvoice).name}")
    else:
        print("\n  ❌ No ZUGFeRD XML found in either file")
        return False
    
    # Step 2: Extract XML
    print("\n[2/4] Extracting ZUGFeRD XML...")
    xml_temp = WORK_DIR / "extracted_zugferd.xml"
    if not extract_xml(einvoice, str(xml_temp)):
        print("  ❌ Failed to extract XML")
        return False
    print(f"  ✓ XML extracted: {xml_temp}")
    
    # Step 3: Combine with attachment
    print("\n[3/4] Combining PDF + XML + Attachment...")
    print(f"  Method: Attachment embedding (not page merge)")
    print(f"  Base PDF: {Path(einvoice).name}")
    print(f"  Attachment: {Path(z_anhang).name}")
    
    temp_out = WORK_DIR / "combined.pdf"
    if not combine(einvoice, str(xml_temp), z_anhang, str(temp_out)):
        print("  ❌ Combine failed")
        print(f"  Error details available in: {WORK_DIR}/")
        return False
    print(f"  ✓ Combined: {temp_out}")
    
    # Step 4: Validate
    print("\n[4/4] Validating output...")
    result = validate(str(temp_out))
    
    # Copy to final destination
    os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
    shutil.copy(temp_out, output_path)
    
    # Results
    print("\n" + "=" * 60)
    print("RESULT")
    print("=" * 60)
    print(f"Output:      {os.path.abspath(output_path)}")
    print(f"Size:        {os.path.getsize(output_path) / 1024:.1f} KB")
    print(f"PDF/A-3:     {'✅ COMPLIANT' if result['pdf_compliant'] else '⚠️ NON-COMPLIANT'}")
    print(f"Has XML:     {'✅' if result['has_xml'] else '❌'}")
    print(f"ZUGFeRD:     {'✅ VALID' if result['xml_valid'] else '⚠️ WARNINGS'}")
    print(f"Status:      {'✅ READY FOR EN16931' if result['xml_valid'] else '⚠️ REVIEW NEEDED'}")
    print("=" * 60)
    
    return result['xml_valid']

def main():
    parser = argparse.ArgumentParser(
        description="Create ZUGFeRD compliant PDF with file attachment",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Example:
    %(prog)s --invoice Rechnung.pdf --attachment Zeitnachweis.pdf \\
             --output Rechnung_mit_Anhang.pdf
        
Note:
    The attachment is embedded as a FILE attachment, not merged as pages.
    This preserves PDF/A-3 compliance and follows ZUGFeRD/Factur-X spec.
        """
    )
    parser.add_argument("--invoice", required=True, help="PDF containing ZUGFeRD XML")
    parser.add_argument("--attachment", required=True, help="Additional PDF to attach")
    parser.add_argument("--output", "-o", required=True, help="Output path")
    
    args = parser.parse_args()
    
    success = workflow(args.invoice, args.attachment, args.output)
    sys.exit(0 if success else 1)

if __name__ == "__main__":
    main()


path: temp/extracted.xml
content: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rsm:CrossIndustryInvoice xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100">
    <rsm:ExchangedDocumentContext>
        <ram:BusinessProcessSpecifiedDocumentContextParameter>
            <ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>
        </ram:BusinessProcessSpecifiedDocumentContextParameter>
        <ram:GuidelineSpecifiedDocumentContextParameter>
            <ram:ID>urn:cen.eu:en16931:2017</ram:ID>
        </ram:GuidelineSpecifiedDocumentContextParameter>
    </rsm:ExchangedDocumentContext>
    <rsm:ExchangedDocument>
        <ram:ID>RE0182</ram:ID>
        <ram:TypeCode>380</ram:TypeCode>
        <ram:IssueDateTime>
            <udt:DateTimeString format="102">20260228</udt:DateTimeString>
        </ram:IssueDateTime>
        <ram:IncludedNote>
            <ram:Content>Rechnung</ram:Content>
            <ram:SubjectCode>AFM</ram:SubjectCode>
        </ram:IncludedNote>
        <ram:IncludedNote>
            <ram:Content>Unsere Lieferungen/Leistungen stellen wir Ihnen wie folgt in Rechnung.</ram:Content>
            <ram:SubjectCode>AAI</ram:SubjectCode>
        </ram:IncludedNote>
        <ram:IncludedNote>
            <ram:Content>Vielen Dank für die gute Zusammenarbeit.</ram:Content>
            <ram:SubjectCode>SUR</ram:SubjectCode>
        </ram:IncludedNote>
    </rsm:ExchangedDocument>
    <rsm:SupplyChainTradeTransaction>
        <ram:IncludedSupplyChainTradeLineItem>
            <ram:AssociatedDocumentLineDocument>
                <ram:LineID>1</ram:LineID>
            </ram:AssociatedDocumentLineDocument>
            <ram:SpecifiedTradeProduct>
                <ram:Name>SAP Entwicklung Remote für Projekt 36773</ram:Name>
                <ram:Description>gemäß beigefügtem Tätigkeitsnachweis</ram:Description>
            </ram:SpecifiedTradeProduct>
            <ram:SpecifiedLineTradeAgreement>
                <ram:NetPriceProductTradePrice>
                    <ram:ChargeAmount>95.00</ram:ChargeAmount>
                    <ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
                </ram:NetPriceProductTradePrice>
            </ram:SpecifiedLineTradeAgreement>
            <ram:SpecifiedLineTradeDelivery>
                <ram:BilledQuantity unitCode="HUR">76.5000</ram:BilledQuantity>
            </ram:SpecifiedLineTradeDelivery>
            <ram:SpecifiedLineTradeSettlement>
                <ram:ApplicableTradeTax>
                    <ram:TypeCode>VAT</ram:TypeCode>
                    <ram:CategoryCode>S</ram:CategoryCode>
                    <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
                </ram:ApplicableTradeTax>
                <ram:SpecifiedTradeSettlementLineMonetarySummation>
                    <ram:LineTotalAmount>7267.50</ram:LineTotalAmount>
                </ram:SpecifiedTradeSettlementLineMonetarySummation>
            </ram:SpecifiedLineTradeSettlement>
        </ram:IncludedSupplyChainTradeLineItem>
        <ram:ApplicableHeaderTradeAgreement>
            <ram:BuyerReference></ram:BuyerReference>
            <ram:SellerTradeParty>
                <ram:ID/>
                <ram:Name>QuantX GmbH</ram:Name>
                <ram:DefinedTradeContact>
                    <ram:PersonName>Heiko Hänsel</ram:PersonName>
                    <ram:TelephoneUniversalCommunication>
                        <ram:CompleteNumber>+491604784131</ram:CompleteNumber>
                    </ram:TelephoneUniversalCommunication>
                    <ram:EmailURIUniversalCommunication>
                        <ram:URIID>[REDACTED_SECRET]</ram:URIID>
                    </ram:EmailURIUniversalCommunication>
                </ram:DefinedTradeContact>
                <ram:PostalTradeAddress>
                    <ram:PostcodeCode>02826</ram:PostcodeCode>
                    <ram:LineOne>Am Hirschwinkel 5b</ram:LineOne>
                    <ram:CityName>Görlitz</ram:CityName>
                    <ram:CountryID>DE</ram:CountryID>
                </ram:PostalTradeAddress>
                <ram:URIUniversalCommunication>
                    <ram:URIID schemeID="EM">[REDACTED_SECRET]</ram:URIID>
                </ram:URIUniversalCommunication>
                <ram:SpecifiedTaxRegistration>
                    <ram:ID schemeID="FC">207/116/00362</ram:ID>
                </ram:SpecifiedTaxRegistration>
                <ram:SpecifiedTaxRegistration>
                    <ram:ID schemeID="VA">DE328725694</ram:ID>
                </ram:SpecifiedTaxRegistration>
            </ram:SellerTradeParty>
            <ram:BuyerTradeParty>
                <ram:Name>ProTIP ProjektService AG</ram:Name>
                <ram:PostalTradeAddress>
                    <ram:PostcodeCode>51429</ram:PostcodeCode>
                    <ram:LineOne>Moitzfeld 96</ram:LineOne>
                    <ram:CityName>Bergisch Gladbach</ram:CityName>
                    <ram:CountryID>DE</ram:CountryID>
                </ram:PostalTradeAddress>
            </ram:BuyerTradeParty>
        </ram:ApplicableHeaderTradeAgreement>
        <ram:ApplicableHeaderTradeDelivery>
            <ram:ActualDeliverySupplyChainEvent>
                <ram:OccurrenceDateTime>
                    <udt:DateTimeString format="102">20260228</udt:DateTimeString>
                </ram:OccurrenceDateTime>
            </ram:ActualDeliverySupplyChainEvent>
        </ram:ApplicableHeaderTradeDelivery>
        <ram:ApplicableHeaderTradeSettlement>
            <ram:PaymentReference>RE0182</ram:PaymentReference>
            <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
            <ram:SpecifiedTradeSettlementPaymentMeans>
                <ram:TypeCode>1</ram:TypeCode>
                <ram:Information>Überweisung</ram:Information>
                <ram:PayeePartyCreditorFinancialAccount>
                    <ram:IBANID>DE43110101015027834960</ram:IBANID>
                    <ram:AccountName>QuantX GmbH</ram:AccountName>
                </ram:PayeePartyCreditorFinancialAccount>
                <ram:PayeeSpecifiedCreditorFinancialInstitution>
                    <ram:BICID>SOBKDEB2XXX</ram:BICID>
                </ram:PayeeSpecifiedCreditorFinancialInstitution>
            </ram:SpecifiedTradeSettlementPaymentMeans>
            <ram:ApplicableTradeTax>
                <ram:CalculatedAmount>1380.83</ram:CalculatedAmount>
                <ram:TypeCode>VAT</ram:TypeCode>
                <ram:BasisAmount>7267.50</ram:BasisAmount>
                <ram:CategoryCode>S</ram:CategoryCode>
                <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
            </ram:ApplicableTradeTax>
            <ram:BillingSpecifiedPeriod>
                <ram:StartDateTime>
                    <udt:DateTimeString format="102">20260201</udt:DateTimeString>
                </ram:StartDateTime>
                <ram:EndDateTime>
                    <udt:DateTimeString format="102">20260228</udt:DateTimeString>
                </ram:EndDateTime>
            </ram:BillingSpecifiedPeriod>
            <ram:SpecifiedTradePaymentTerms>
                <ram:Description>Zahlbar in  28 Tagen netto.
#SKONTO#TAGE=28#PROZENT=0.00#
</ram:Description>
                <ram:DueDateDateTime>
                    <udt:DateTimeString format="102">20260328</udt:DateTimeString>
                </ram:DueDateDateTime>
            </ram:SpecifiedTradePaymentTerms>
            <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
                <ram:LineTotalAmount>7267.50</ram:LineTotalAmount>
                <ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
                <ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
                <ram:TaxBasisTotalAmount>7267.50</ram:TaxBasisTotalAmount>
                <ram:TaxTotalAmount currencyID="EUR">1380.83</ram:TaxTotalAmount>
                <ram:GrandTotalAmount>8648.33</ram:GrandTotalAmount>
                <ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
                <ram:DuePayableAmount>8648.33</ram:DuePayableAmount>
            </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
        </ram:ApplicableHeaderTradeSettlement>
    </rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>


path: temp/extracted_zugferd.xml
content: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rsm:CrossIndustryInvoice xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100">
    <rsm:ExchangedDocumentContext>
        <ram:BusinessProcessSpecifiedDocumentContextParameter>
            <ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>
        </ram:BusinessProcessSpecifiedDocumentContextParameter>
        <ram:GuidelineSpecifiedDocumentContextParameter>
            <ram:ID>urn:cen.eu:en16931:2017</ram:ID>
        </ram:GuidelineSpecifiedDocumentContextParameter>
    </rsm:ExchangedDocumentContext>
    <rsm:ExchangedDocument>
        <ram:ID>RE0182</ram:ID>
        <ram:TypeCode>380</ram:TypeCode>
        <ram:IssueDateTime>
            <udt:DateTimeString format="102">20260228</udt:DateTimeString>
        </ram:IssueDateTime>
        <ram:IncludedNote>
            <ram:Content>Rechnung</ram:Content>
            <ram:SubjectCode>AFM</ram:SubjectCode>
        </ram:IncludedNote>
        <ram:IncludedNote>
            <ram:Content>Unsere Lieferungen/Leistungen stellen wir Ihnen wie folgt in Rechnung.</ram:Content>
            <ram:SubjectCode>AAI</ram:SubjectCode>
        </ram:IncludedNote>
        <ram:IncludedNote>
            <ram:Content>Vielen Dank für die gute Zusammenarbeit.</ram:Content>
            <ram:SubjectCode>SUR</ram:SubjectCode>
        </ram:IncludedNote>
    </rsm:ExchangedDocument>
    <rsm:SupplyChainTradeTransaction>
        <ram:IncludedSupplyChainTradeLineItem>
            <ram:AssociatedDocumentLineDocument>
                <ram:LineID>1</ram:LineID>
            </ram:AssociatedDocumentLineDocument>
            <ram:SpecifiedTradeProduct>
                <ram:Name>SAP Entwicklung Remote für Projekt 36773</ram:Name>
                <ram:Description>gemäß beigefügtem Tätigkeitsnachweis</ram:Description>
            </ram:SpecifiedTradeProduct>
            <ram:SpecifiedLineTradeAgreement>
                <ram:NetPriceProductTradePrice>
                    <ram:ChargeAmount>95.00</ram:ChargeAmount>
                    <ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
                </ram:NetPriceProductTradePrice>
            </ram:SpecifiedLineTradeAgreement>
            <ram:SpecifiedLineTradeDelivery>
                <ram:BilledQuantity unitCode="HUR">76.5000</ram:BilledQuantity>
            </ram:SpecifiedLineTradeDelivery>
            <ram:SpecifiedLineTradeSettlement>
                <ram:ApplicableTradeTax>
                    <ram:TypeCode>VAT</ram:TypeCode>
                    <ram:CategoryCode>S</ram:CategoryCode>
                    <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
                </ram:ApplicableTradeTax>
                <ram:SpecifiedTradeSettlementLineMonetarySummation>
                    <ram:LineTotalAmount>7267.50</ram:LineTotalAmount>
                </ram:SpecifiedTradeSettlementLineMonetarySummation>
            </ram:SpecifiedLineTradeSettlement>
        </ram:IncludedSupplyChainTradeLineItem>
        <ram:ApplicableHeaderTradeAgreement>
            <ram:BuyerReference></ram:BuyerReference>
            <ram:SellerTradeParty>
                <ram:ID/>
                <ram:Name>QuantX GmbH</ram:Name>
                <ram:DefinedTradeContact>
                    <ram:PersonName>Heiko Hänsel</ram:PersonName>
                    <ram:TelephoneUniversalCommunication>
                        <ram:CompleteNumber>+491604784131</ram:CompleteNumber>
                    </ram:TelephoneUniversalCommunication>
                    <ram:EmailURIUniversalCommunication>
                        <ram:URIID>[REDACTED_SECRET]</ram:URIID>
                    </ram:EmailURIUniversalCommunication>
                </ram:DefinedTradeContact>
                <ram:PostalTradeAddress>
                    <ram:PostcodeCode>02826</ram:PostcodeCode>
                    <ram:LineOne>Am Hirschwinkel 5b</ram:LineOne>
                    <ram:CityName>Görlitz</ram:CityName>
                    <ram:CountryID>DE</ram:CountryID>
                </ram:PostalTradeAddress>
                <ram:URIUniversalCommunication>
                    <ram:URIID schemeID="EM">[REDACTED_SECRET]</ram:URIID>
                </ram:URIUniversalCommunication>
                <ram:SpecifiedTaxRegistration>
                    <ram:ID schemeID="FC">207/116/00362</ram:ID>
                </ram:SpecifiedTaxRegistration>
                <ram:SpecifiedTaxRegistration>
                    <ram:ID schemeID="VA">DE328725694</ram:ID>
                </ram:SpecifiedTaxRegistration>
            </ram:SellerTradeParty>
            <ram:BuyerTradeParty>
                <ram:Name>ProTIP ProjektService AG</ram:Name>
                <ram:PostalTradeAddress>
                    <ram:PostcodeCode>51429</ram:PostcodeCode>
                    <ram:LineOne>Moitzfeld 96</ram:LineOne>
                    <ram:CityName>Bergisch Gladbach</ram:CityName>
                    <ram:CountryID>DE</ram:CountryID>
                </ram:PostalTradeAddress>
            </ram:BuyerTradeParty>
        </ram:ApplicableHeaderTradeAgreement>
        <ram:ApplicableHeaderTradeDelivery>
            <ram:ActualDeliverySupplyChainEvent>
                <ram:OccurrenceDateTime>
                    <udt:DateTimeString format="102">20260228</udt:DateTimeString>
                </ram:OccurrenceDateTime>
            </ram:ActualDeliverySupplyChainEvent>
        </ram:ApplicableHeaderTradeDelivery>
        <ram:ApplicableHeaderTradeSettlement>
            <ram:PaymentReference>RE0182</ram:PaymentReference>
            <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
            <ram:SpecifiedTradeSettlementPaymentMeans>
                <ram:TypeCode>1</ram:TypeCode>
                <ram:Information>Überweisung</ram:Information>
                <ram:PayeePartyCreditorFinancialAccount>
                    <ram:IBANID>DE43110101015027834960</ram:IBANID>
                    <ram:AccountName>QuantX GmbH</ram:AccountName>
                </ram:PayeePartyCreditorFinancialAccount>
                <ram:PayeeSpecifiedCreditorFinancialInstitution>
                    <ram:BICID>SOBKDEB2XXX</ram:BICID>
                </ram:PayeeSpecifiedCreditorFinancialInstitution>
            </ram:SpecifiedTradeSettlementPaymentMeans>
            <ram:ApplicableTradeTax>
                <ram:CalculatedAmount>1380.83</ram:CalculatedAmount>
                <ram:TypeCode>VAT</ram:TypeCode>
                <ram:BasisAmount>7267.50</ram:BasisAmount>
                <ram:CategoryCode>S</ram:CategoryCode>
                <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
            </ram:ApplicableTradeTax>
            <ram:BillingSpecifiedPeriod>
                <ram:StartDateTime>
                    <udt:DateTimeString format="102">20260201</udt:DateTimeString>
                </ram:StartDateTime>
                <ram:EndDateTime>
                    <udt:DateTimeString format="102">20260228</udt:DateTimeString>
                </ram:EndDateTime>
            </ram:BillingSpecifiedPeriod>
            <ram:SpecifiedTradePaymentTerms>
                <ram:Description>Zahlbar in  28 Tagen netto.
#SKONTO#TAGE=28#PROZENT=0.00#
</ram:Description>
                <ram:DueDateDateTime>
                    <udt:DateTimeString format="102">20260328</udt:DateTimeString>
                </ram:DueDateDateTime>
            </ram:SpecifiedTradePaymentTerms>
            <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
                <ram:LineTotalAmount>7267.50</ram:LineTotalAmount>
                <ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
                <ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
                <ram:TaxBasisTotalAmount>7267.50</ram:TaxBasisTotalAmount>
                <ram:TaxTotalAmount currencyID="EUR">1380.83</ram:TaxTotalAmount>
                <ram:GrandTotalAmount>8648.33</ram:GrandTotalAmount>
                <ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
                <ram:DuePayableAmount>8648.33</ram:DuePayableAmount>
            </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
        </ram:ApplicableHeaderTradeSettlement>
    </rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)