Steuerkram/src/Services/ZugferdService.php

163 lines
5.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Steuerkram\Services;
use DateTime;
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
use horstoeko\zugferd\ZugferdProfiles;
class ZugferdService
{
/**
* Bettet ZUGFeRD EN16931-XML in den übergebenen PDF-String ein
* und gibt das resultierende PDF als String zurück.
*/
public function attachToPdf(string $pdfContent, array $inv, array $lines, array $settings): string
{
$doc = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_EN16931);
$rechnungsDatum = new DateTime($inv['rechnungs_datum']);
$faelligDatum = !empty($inv['faellig_datum']) ? new DateTime($inv['faellig_datum']) : null;
// Dokumentkopf
$doc->setDocumentInformation(
$inv['rechnungs_nr'],
'380', // UN/EDIFACT: Handelsrechnung
$rechnungsDatum,
'EUR'
);
// Käuferreferenz (BT-10)
$doc->setDocumentBuyerReference($inv['kundennummer']);
// Verkäufer (BG-4)
$doc->setDocumentSeller($settings['firma'] ?? '');
$doc->setDocumentSellerAddress(
$settings['strasse'] ?? '',
null,
null,
$settings['plz'] ?? '',
$settings['ort'] ?? '',
'DE'
);
if (!empty($settings['ust_id'])) {
$doc->addDocumentSellerVATRegistrationNumber($settings['ust_id']);
} elseif (!empty($settings['steuernummer'])) {
$doc->addDocumentSellerTaxNumber($settings['steuernummer']);
}
// Käufer (BG-7)
$buyerName = !empty($inv['firma'])
? $inv['firma']
: trim(($inv['anrede'] ?? '') . ' ' . ($inv['vorname'] ?? '') . ' ' . ($inv['nachname'] ?? ''));
$doc->setDocumentBuyer($buyerName, $inv['kundennummer']);
$doc->setDocumentBuyerAddress(
$inv['strasse'] ?? '',
null,
null,
$inv['plz'] ?? '',
$inv['ort'] ?? '',
$inv['land'] ?? 'DE'
);
if (!empty($inv['ust_id'])) {
$doc->addDocumentBuyerVATRegistrationNumber($inv['ust_id']);
}
// Zahlungsart: Überweisung (BG-16)
$iban = preg_replace('/\s+/', '', $settings['iban'] ?? '');
$bic = preg_replace('/\s+/', '', $settings['bic'] ?? '');
if (!empty($iban)) {
$doc->addDocumentPaymentMeanToCreditTransfer(
$iban,
$settings['bank_name'] ?? null,
null,
$bic ?: null,
$inv['rechnungs_nr']
);
}
// Zahlungsziel (BT-9)
if ($faelligDatum !== null) {
$doc->addDocumentPaymentTerm(null, $faelligDatum);
}
// USt-Zusammenfassung nach Steuersatz gruppieren (BG-23)
$taxGroups = [];
foreach ($lines as $line) {
if (($line['type'] ?? 'position') === 'heading') {
continue;
}
$rate = (float)$line['ust_satz'];
$key = number_format($rate, 2);
if (!isset($taxGroups[$key])) {
$taxGroups[$key] = ['rate' => $rate, 'basis' => 0.0, 'tax' => 0.0];
}
$taxGroups[$key]['basis'] += (float)$line['betrag_netto'];
$taxGroups[$key]['tax'] += (float)$line['betrag_ust'];
}
foreach ($taxGroups as $group) {
$categoryCode = $group['rate'] > 0 ? 'S' : 'E';
$exemption = $group['rate'] > 0 ? null : 'Kleinunternehmer gemäß § 19 UStG';
$doc->addDocumentTax(
$categoryCode,
'VAT',
round($group['basis'], 2),
round($group['tax'], 2),
$group['rate'] > 0 ? $group['rate'] : null,
$exemption
);
}
// Gesamtsummen (BG-22)
$doc->setDocumentSummation(
round((float)$inv['betrag_brutto'], 2), // Brutto (Zahlbetrag)
round((float)$inv['betrag_brutto'], 2), // fällig
round((float)$inv['betrag_netto'], 2), // Positionssumme Netto
null,
null,
round((float)$inv['betrag_netto'], 2), // Steuerbemessungsgrundlage
round((float)$inv['betrag_ust'], 2) // Steuer gesamt
);
// Positionen (BG-25) – Überschriften werden übersprungen
$posNr = 0;
foreach ($lines as $line) {
if (($line['type'] ?? 'position') === 'heading') {
continue;
}
$posNr++;
$doc->addNewPosition((string)$posNr);
$doc->setDocumentPositionProductDetails($line['bezeichnung']);
$doc->setDocumentPositionNetPrice((float)$line['einzelpreis']);
$doc->setDocumentPositionQuantity(
(float)$line['menge'],
$this->mapEinheit($line['einheit'] ?? '')
);
$rate = (float)$line['ust_satz'];
$categoryCode = $rate > 0 ? 'S' : 'E';
$doc->addDocumentPositionTax($categoryCode, 'VAT', $rate);
$doc->setDocumentPositionLineSummation(round((float)$line['betrag_netto'], 2));
}
$pdfBuilder = new ZugferdDocumentPdfBuilder($doc, $pdfContent);
$pdfBuilder->generateDocument();
return $pdfBuilder->downloadString();
}
private function mapEinheit(string $einheit): string
{
return match (strtolower(trim($einheit))) {
'h', 'std', 'stunden', 'stunde' => 'HUR',
'tag', 'tage', 'd' => 'DAY',
'km' => 'KMT',
'monat', 'monate', 'mon' => 'MON',
'kg' => 'KGM',
'l', 'liter' => 'LTR',
default => 'C62', // Stück / Pauschale
};
}
}