Steuerkram/src/Services/ElsterXmlService.php

162 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Steuerkram\Services;
use Steuerkram\Models\Setting;
/**
* Erzeugt eine ELSTER-konforme XML-Datei für die UStVA.
* Format: Anmeldungssteuern Upload-Schema v{year} (kein ERiC nötig).
*/
class ElsterXmlService
{
public function generate(int $year, int $month, array $kz): string
{
$settings = Setting::get();
$steuernummer = preg_replace('/[^0-9]/', '', $settings['ustva_steuernummer'] ?? '');
$inhaber = $settings['inhaber'] ?? $settings['firma'] ?? '';
$firma = $settings['firma'] ?? '';
[$strasse, $hausnummer] = $this->splitStrasse($settings['strasse'] ?? '');
[$vorname, $nachname] = $this->splitName($inhaber);
$x = fn(string $v): string => htmlspecialchars($v, ENT_XML1, 'UTF-8');
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$ns = sprintf('http://finkonsens.de/elster/elsteranmeldung/ustva/v%d', $year);
$anm = $dom->createElementNS($ns, 'Anmeldungssteuern');
$anm->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', $ns);
$anm->setAttribute('version', (string)$year);
$dom->appendChild($anm);
// DatenLieferant
$dl = $dom->createElement('DatenLieferant');
foreach ([
'Name' => $x($inhaber),
'Strasse' => $x($settings['strasse'] ?? ''),
'PLZ' => $x($settings['plz'] ?? ''),
'Ort' => $x($settings['ort'] ?? ''),
'Telefon' => $x($settings['telefon'] ?? ''),
'Email' => $x($settings['email'] ?? ''),
] as $tag => $val) {
$dl->appendChild($dom->createElement($tag, $val));
}
$anm->appendChild($dl);
$anm->appendChild($dom->createElement('Erstellungsdatum', date('Ymd')));
// Steuerfall
$sf = $dom->createElement('Steuerfall');
$anm->appendChild($sf);
// Berater (leer)
$berater = $dom->createElement('Berater');
foreach ([
'Bezeichnung','Name','Vorname','Namensvorsatz','Namenszusatz',
'Str','Hausnummer','HNrZusatz','AnschriftenZusatz','Ort','PLZ',
'AuslandsPLZ','Land','PostfachOrt','Postfach','PostfachPLZ',
'GKPLZ','Telefon','Email',
] as $tag) {
$berater->appendChild($dom->createElement($tag));
}
$sf->appendChild($berater);
// Mandant
$mandant = $dom->createElement('Mandant');
foreach ([
'Name' => $x($nachname),
'Vorname' => $x($vorname),
'MandantenNr' => '',
'Bearbeiterkennzeichen' => '',
] as $tag => $val) {
$mandant->appendChild($dom->createElement($tag, $val));
}
$sf->appendChild($mandant);
// Unternehmer
$unternehmer = $dom->createElement('Unternehmer');
foreach ([
'Bezeichnung' => $x($firma),
'Name' => $x($nachname),
'Vorname' => $x($vorname),
'Namensvorsatz' => '',
'Namenszusatz' => '',
'Str' => $x($strasse),
'Hausnummer' => $x($hausnummer),
'HNrZusatz' => '',
'AnschriftenZusatz' => '',
'Ort' => $x($settings['ort'] ?? ''),
'PLZ' => $x($settings['plz'] ?? ''),
'AuslandsPLZ' => '',
'Land' => 'Deutschland',
'PostfachOrt' => '',
'Postfach' => '',
'PostfachPLZ' => '',
'GKPLZ' => '',
'Telefon' => $x($settings['telefon'] ?? ''),
'Email' => $x($settings['email'] ?? ''),
] as $tag => $val) {
$unternehmer->appendChild($dom->createElement($tag, $val));
}
$sf->appendChild($unternehmer);
// Umsatzsteuervoranmeldung
$ustva = $dom->createElement('Umsatzsteuervoranmeldung');
$sf->appendChild($ustva);
$ustva->appendChild($dom->createElement('Jahr', (string)$year));
$ustva->appendChild($dom->createElement('Zeitraum', sprintf('%02d', $month)));
$ustva->appendChild($dom->createElement('Steuernummer', $steuernummer));
// Kz-Felder: Bemessungsgrundlagen ganzzahlig, Steuerbeträge mit 2 Dezimalstellen.
// ust81 und ust89 (abgeleitete Anzeigewerte) werden nicht übertragen.
$kzBetrag = fn(float $v): string => (string)(int)round($v);
$kzSteuer = fn(float $v): string => number_format($v, 2, '.', '');
$betragsfelder = ['81', '89'];
foreach ($betragsfelder as $nr) {
$val = (float)($kz[$nr]['betrag'] ?? 0.0);
if ($val != 0.0) {
$ustva->appendChild($dom->createElement('Kz' . $nr, $kzBetrag($val)));
}
}
$steuerfelder = ['66', '61', '83'];
foreach ($steuerfelder as $nr) {
$val = (float)($kz[$nr]['steuer'] ?? 0.0);
if ($val != 0.0) {
$ustva->appendChild($dom->createElement('Kz' . $nr, $kzSteuer($val)));
}
}
return $dom->saveXML();
}
/**
* Trennt Straßenname und Hausnummer. "Bietigheimer Str. 67" → ["Bietigheimer Str.", "67"]
*/
private function splitStrasse(string $strasse): array
{
if (preg_match('/^(.*?)\s+(\d+\w*)$/', trim($strasse), $m)) {
return [$m[1], $m[2]];
}
return [$strasse, ''];
}
/**
* Trennt Vorname und Nachname. "Felix Schepmann" → ["Felix", "Schepmann"]
*/
private function splitName(string $name): array
{
$parts = explode(' ', trim($name), 2);
if (count($parts) === 2) {
return [$parts[0], $parts[1]];
}
return ['', $name];
}
}