163 lines
5.9 KiB
PHP
163 lines
5.9 KiB
PHP
<?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
|
||
};
|
||
}
|
||
}
|