162 lines
5.8 KiB
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];
|
|
}
|
|
}
|