From d83d63cc14880bbe553857acf9903be89b0f9ab2 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Fri, 31 Jul 2026 18:35:20 +0200 Subject: [PATCH] Fix #xml-rechnung: USt-IdNr. des Kunden (BT-48) landet wieder im XML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invoice::find() selektierte c.ust_id nicht, invoices hat keine eigene Spalte dafür - der Zweig addDocumentBuyerVATRegistrationNumber() lief deshalb nie. Bei Kategorie S optional, bei Reverse Charge und innergemeinschaftlichen Lieferungen aber Pflicht. --- Changes.md | 1 + src/Models/Invoice.php | 2 +- tests/72_InvoiceXmlExportTest.php | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index aa017f4..37563fb 100644 --- a/Changes.md +++ b/Changes.md @@ -21,6 +21,7 @@ Der E-Rechnungsvalidator (KoSIT-Prüfkonfiguration) meldete für die heruntergel - **PEPPOL-EN16931-R001/R010/R020 (Hinweise):** Geschäftsprozess (BT-23) sowie die elektronischen Adressen von Verkäufer (BT-34) und Käufer (BT-49, aus der Kunden-E-Mail) werden jetzt mit ausgegeben. - **Die PDF-Einbettung bleibt unverändert** beim Profil EN16931 (Factur-X, `factur-x.xml`) – dem üblichen Format für hybride B2B-Rechnungen. Die neuen Kontakt- und Adressangaben stehen aber auch dort, das PDF-XML wird dadurch nur vollständiger. - `collectMissingData()` prüft zusätzlich Ansprechpartner (BT-41), Telefon (BT-42), E-Mail (BT-43/BT-34) und die Kunden-E-Mail (BT-49); Test 72 deckt die neuen Elemente sowie eine XSD-Prüfung des erzeugten XML ab. +- **Fix: USt-IdNr. des Kunden (BT-48) fehlte immer im XML.** `Invoice::find()` selektierte vom Kunden zwar Adresse, E-Mail und Zahlungsziel, aber nicht `c.ust_id` – und `invoices` hat keine eigene Spalte dafür. Der Zweig `addDocumentBuyerVATRegistrationNumber()` in `ZugferdService` lief deshalb nie, auch wenn beim Kunden eine USt-IdNr. gepflegt war (Bestandsfehler, betraf auch das in die PDF eingebettete XML). Bei inländischen Leistungen mit Steuerkategorie „S" ist BT-48 optional, weshalb es nicht auffiel; bei Reverse Charge (§ 13b UStG) und innergemeinschaftlichen Lieferungen ist die Angabe dagegen Pflicht. `Invoice::find()` liefert `c.ust_id` jetzt mit, Test 72 prüft BT-31 und BT-48 samt Schemakennung „VA". ## 2026-07-31 – Firmenlogo persistent in `data/logo`, Nutzdaten gebündelt unter `data/` diff --git a/src/Models/Invoice.php b/src/Models/Invoice.php index f326f6a..6c0c5e2 100644 --- a/src/Models/Invoice.php +++ b/src/Models/Invoice.php @@ -54,7 +54,7 @@ class Invoice { return Database::fetchOne( 'SELECT i.*, i.storno_of_id, c.firma, c.nachname, c.vorname, c.anrede, c.kundennummer, - c.strasse, c.plz, c.ort, c.land, c.email, c.payment_days + c.strasse, c.plz, c.ort, c.land, c.email, c.payment_days, c.ust_id FROM invoices i JOIN customers c ON i.customer_id = c.id WHERE i.id = ?', diff --git a/tests/72_InvoiceXmlExportTest.php b/tests/72_InvoiceXmlExportTest.php index 9aacf1a..7170d11 100644 --- a/tests/72_InvoiceXmlExportTest.php +++ b/tests/72_InvoiceXmlExportTest.php @@ -25,6 +25,7 @@ class InvoiceXmlExportTest extends TestCase 'ort' => 'Normstadt', 'land' => 'DE', 'email' => 'buchhaltung@xmltest.example', + 'ust_id' => 'DE987654321', 'payment_days' => 14, ]); $invoiceId = \Steuerkram\Models\Invoice::create([ @@ -123,6 +124,16 @@ class InvoiceXmlExportTest extends TestCase 'E-Mail des Ansprechpartners (BT-43) vorhanden – BR-DE-7' ); + // USt-IdNr. beider Parteien (BT-31 / BT-48) – BT-48 nur, wenn beim Kunden gepflegt + $sellerVat = $xp->query('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]')->item(0); + $buyerVat = $xp->query('//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]')->item(0); + $this->assertEquals('DE123456789', $sellerVat?->nodeValue, 'USt-IdNr. des Verkäufers als BT-31 mit Schema "VA"'); + $this->assertEquals('DE987654321', $buyerVat?->nodeValue, 'USt-IdNr. des Kunden als BT-48 mit Schema "VA"'); + $this->assertNotEmpty( + $invoice['ust_id'] ?? '', + 'Invoice::find() liefert die USt-IdNr. des Kunden mit (Voraussetzung für BT-48)' + ); + $sellerUri = $xp->query('//ram:SellerTradeParty/ram:URIUniversalCommunication/ram:URIID')->item(0); $buyerUri = $xp->query('//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID')->item(0); $this->assertEquals('rechnung@muster.example', $sellerUri?->nodeValue, 'Elektronische Adresse des Verkäufers (BT-34)');