This commit is contained in:
@@ -55,10 +55,13 @@ class Verbrauchskostenreport extends IPSModule
|
||||
$this->RegisterPropertyBoolean('EnableVAT', false);
|
||||
$this->RegisterPropertyFloat('VATPercentage', 8.1);
|
||||
|
||||
// Neue Felder für den QR-Schein
|
||||
$this->RegisterPropertyString('CreditorName', 'Belevo AG');
|
||||
$this->RegisterPropertyString('CreditorAddress', 'Musterstrasse 1');
|
||||
$this->RegisterPropertyString('CreditorCity', '6122 Menznau');
|
||||
// Strukturierte Adressfelder für den QR-Zahlteil nach SIX SPS 2.3
|
||||
$this->RegisterPropertyString('CreditorName', '');
|
||||
$this->RegisterPropertyString('CreditorStreet', '');
|
||||
$this->RegisterPropertyString('CreditorBuildingNumber', '');
|
||||
$this->RegisterPropertyString('CreditorPostalCode', '');
|
||||
$this->RegisterPropertyString('CreditorTown', '');
|
||||
$this->RegisterPropertyString('CreditorCountry', 'CH');
|
||||
$this->RegisterPropertyString('BankIBAN', '');
|
||||
|
||||
$this->RegisterTariffVariableProfiles();
|
||||
@@ -703,12 +706,15 @@ form.onsubmit=function(){
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style='width:18%'>Name</th>
|
||||
<th style='width:18%'>Adresse</th>
|
||||
<th style='width:18%'>Ort</th>
|
||||
<th style='width:19%'>Stromzaehler</th>
|
||||
<th style='width:19%'>Nebenzaehler</th>
|
||||
<th style='width:8%'>Aktion</th>
|
||||
<th style='width:13%'>Name</th>
|
||||
<th style='width:13%'>Strasse</th>
|
||||
<th style='width:6%'>Nr.</th>
|
||||
<th style='width:7%'>PLZ</th>
|
||||
<th style='width:11%'>Ort</th>
|
||||
<th style='width:6%'>Land</th>
|
||||
<th style='width:17%'>Stromzaehler</th>
|
||||
<th style='width:17%'>Nebenzaehler</th>
|
||||
<th style='width:10%'>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>" . $rows . "</tbody>
|
||||
@@ -831,8 +837,11 @@ refreshPickers();
|
||||
|
||||
return '<tr>'
|
||||
. '<td><input type="hidden" name="user_id[]" value="' . $this->h($userId) . '"><input type="hidden" name="row_key[]" value="' . $this->h($rowKey) . '"><input name="user_name[]" value="' . $this->h($user['name'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_address[]" value="' . $this->h($user['address'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_city[]" value="' . $this->h($user['city'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_street[]" value="' . $this->h($user['street'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_building_number[]" value="' . $this->h($user['building_number'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_postal_code[]" value="' . $this->h($user['postal_code'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_town[]" value="' . $this->h($user['town'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_country[]" value="' . $this->h($user['country'] ?? 'CH') . '"></td>'
|
||||
. '<td>' . $this->BuildMeterPicker('power', 'power_assign', $rowKey, $powerMeters, $powerSelected) . '</td>'
|
||||
. '<td>' . $this->BuildMeterPicker('water', 'water_assign', $rowKey, $waterMeters, $waterSelected) . '</td>'
|
||||
. '<td><button type="button" class="danger" data-action="delete-user">Loeschen</button></td>'
|
||||
@@ -923,13 +932,18 @@ refreshPickers();
|
||||
{
|
||||
$ids = $post['user_id'] ?? [];
|
||||
$names = $post['user_name'] ?? [];
|
||||
$addresses = $post['user_address'] ?? [];
|
||||
$cities = $post['user_city'] ?? [];
|
||||
$streets = $post['user_street'] ?? [];
|
||||
$buildingNumbers = $post['user_building_number'] ?? [];
|
||||
$postalCodes = $post['user_postal_code'] ?? [];
|
||||
$towns = $post['user_town'] ?? [];
|
||||
$countries = $post['user_country'] ?? [];
|
||||
$rowKeys = $post['row_key'] ?? [];
|
||||
$powerAssign = $post['power_assign'] ?? [];
|
||||
$waterAssign = $post['water_assign'] ?? [];
|
||||
|
||||
if (!is_array($ids) || !is_array($names) || !is_array($addresses) || !is_array($cities) || !is_array($rowKeys)) {
|
||||
if (!is_array($ids) || !is_array($names) || !is_array($streets)
|
||||
|| !is_array($buildingNumbers) || !is_array($postalCodes)
|
||||
|| !is_array($towns) || !is_array($countries) || !is_array($rowKeys)) {
|
||||
throw new Exception('Ungueltige Benutzerwerte.');
|
||||
}
|
||||
if (!is_array($powerAssign)) {
|
||||
@@ -968,18 +982,31 @@ refreshPickers();
|
||||
}
|
||||
$assignedPower = [];
|
||||
$assignedWater = [];
|
||||
$count = max(count($ids), count($names), count($addresses), count($cities), count($rowKeys));
|
||||
$count = max(
|
||||
count($ids),
|
||||
count($names),
|
||||
count($streets),
|
||||
count($buildingNumbers),
|
||||
count($postalCodes),
|
||||
count($towns),
|
||||
count($countries),
|
||||
count($rowKeys)
|
||||
);
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$rowKey = (string)($rowKeys[$i] ?? $i);
|
||||
$id = trim((string)($ids[$i] ?? ''));
|
||||
$name = trim((string)($names[$i] ?? ''));
|
||||
$address = trim((string)($addresses[$i] ?? ''));
|
||||
$city = trim((string)($cities[$i] ?? ''));
|
||||
$street = trim((string)($streets[$i] ?? ''));
|
||||
$buildingNumber = trim((string)($buildingNumbers[$i] ?? ''));
|
||||
$postalCode = trim((string)($postalCodes[$i] ?? ''));
|
||||
$town = trim((string)($towns[$i] ?? ''));
|
||||
$country = strtoupper(trim((string)($countries[$i] ?? 'CH')));
|
||||
$selectedPower = $this->NormalizeMeterSelection($powerAssign[$rowKey] ?? []);
|
||||
$selectedWater = $this->NormalizeMeterSelection($waterAssign[$rowKey] ?? []);
|
||||
|
||||
if ($id === '' && $name === '' && $address === '' && $city === '' && empty($selectedPower) && empty($selectedWater)) {
|
||||
if ($id === '' && $name === '' && $street === '' && $buildingNumber === ''
|
||||
&& $postalCode === '' && $town === '' && empty($selectedPower) && empty($selectedWater)) {
|
||||
continue;
|
||||
}
|
||||
if ($id === '') {
|
||||
@@ -993,8 +1020,12 @@ refreshPickers();
|
||||
$user = $existingUsers[$id] ?? [];
|
||||
$user['id'] = $id;
|
||||
$user['name'] = $name;
|
||||
$user['address'] = $address;
|
||||
$user['city'] = $city;
|
||||
$user['street'] = $street;
|
||||
$user['building_number'] = $buildingNumber;
|
||||
$user['postal_code'] = $postalCode;
|
||||
$user['town'] = $town;
|
||||
$user['country'] = $country;
|
||||
unset($user['address'], $user['city']);
|
||||
$users[] = $user;
|
||||
|
||||
foreach ($selectedPower as $meterIndex) {
|
||||
@@ -1144,11 +1175,96 @@ refreshPickers();
|
||||
}
|
||||
}
|
||||
|
||||
if (trim($this->ReadPropertyString('BankIBAN')) !== '') {
|
||||
$this->ValidateQRConfiguration($users, $errors);
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new Exception(implode(' ', array_values(array_unique($errors))));
|
||||
}
|
||||
}
|
||||
|
||||
private function ValidateQRConfiguration(array $users, array &$errors)
|
||||
{
|
||||
$iban = strtoupper(str_replace(' ', '', $this->ReadPropertyString('BankIBAN')));
|
||||
if (!preg_match('/^(CH|LI)[0-9]{19}$/', $iban) || !$this->IsValidIBAN($iban)) {
|
||||
$errors[] = 'Die IBAN für den QR-Zahlteil ist ungültig.';
|
||||
} elseif ($this->IsQRIBAN($iban)) {
|
||||
$errors[] = 'Eine QR-IBAN erfordert eine QR-Referenz; für den Report ist eine normale CH/LI-IBAN zu verwenden.';
|
||||
}
|
||||
|
||||
$this->ValidateStructuredQRAddress([
|
||||
'name' => $this->ReadPropertyString('CreditorName'),
|
||||
'street' => $this->ReadPropertyString('CreditorStreet'),
|
||||
'building_number' => $this->ReadPropertyString('CreditorBuildingNumber'),
|
||||
'postal_code' => $this->ReadPropertyString('CreditorPostalCode'),
|
||||
'town' => $this->ReadPropertyString('CreditorTown'),
|
||||
'country' => $this->ReadPropertyString('CreditorCountry')
|
||||
], 'Zahlungsempfänger', $errors);
|
||||
|
||||
foreach ($users as $index => $user) {
|
||||
if (!is_array($user)) {
|
||||
continue;
|
||||
}
|
||||
$label = 'Benutzer ' . trim((string)($user['id'] ?? ($index + 1)));
|
||||
$this->ValidateStructuredQRAddress($user, $label, $errors);
|
||||
}
|
||||
}
|
||||
|
||||
private function ValidateStructuredQRAddress(array $address, $label, array &$errors)
|
||||
{
|
||||
$name = trim((string)($address['name'] ?? ''));
|
||||
$postalCode = trim((string)($address['postal_code'] ?? ''));
|
||||
$town = trim((string)($address['town'] ?? ''));
|
||||
$country = strtoupper(trim((string)($address['country'] ?? '')));
|
||||
|
||||
if ($name === '') {
|
||||
$errors[] = $label . ': Name fehlt für den QR-Zahlteil.';
|
||||
}
|
||||
if ($postalCode === '') {
|
||||
$errors[] = $label . ': PLZ fehlt für den QR-Zahlteil.';
|
||||
}
|
||||
if ($town === '') {
|
||||
$errors[] = $label . ': Ort fehlt für den QR-Zahlteil.';
|
||||
}
|
||||
if (!preg_match('/^[A-Z]{2}$/', $country)) {
|
||||
$errors[] = $label . ': Land muss als ISO-2-Code angegeben werden.';
|
||||
}
|
||||
|
||||
$limits = [
|
||||
'name' => 70,
|
||||
'street' => 70,
|
||||
'building_number' => 16,
|
||||
'postal_code' => 16,
|
||||
'town' => 35
|
||||
];
|
||||
foreach ($limits as $field => $limit) {
|
||||
if (mb_strlen(trim((string)($address[$field] ?? ''))) > $limit) {
|
||||
$errors[] = $label . ': Feld ' . $field . ' überschreitet ' . $limit . ' Zeichen.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function IsValidIBAN($iban)
|
||||
{
|
||||
$rearranged = substr($iban, 4) . substr($iban, 0, 4);
|
||||
$remainder = 0;
|
||||
for ($i = 0, $length = strlen($rearranged); $i < $length; $i++) {
|
||||
$character = $rearranged[$i];
|
||||
$numeric = ctype_alpha($character) ? (string)(ord($character) - 55) : $character;
|
||||
for ($j = 0, $numericLength = strlen($numeric); $j < $numericLength; $j++) {
|
||||
$remainder = (($remainder * 10) + (int)$numeric[$j]) % 97;
|
||||
}
|
||||
}
|
||||
return $remainder === 1;
|
||||
}
|
||||
|
||||
private function IsQRIBAN($iban)
|
||||
{
|
||||
$iid = (int)substr($iban, 4, 5);
|
||||
return $iid >= 30000 && $iid <= 31999;
|
||||
}
|
||||
|
||||
private function RequireTariffCoverage(array $tariffs, $type, $from, $to, array &$errors)
|
||||
{
|
||||
$gap = $this->GetTariffCoverageGap($tariffs, $type, (int)$from, (int)$to);
|
||||
@@ -1419,7 +1535,14 @@ refreshPickers();
|
||||
|
||||
$periodText = date('d.m.Y', $from) . ' - ' . date('d.m.Y', $to);
|
||||
$meterText = empty($meterNames) ? '-' : implode("\n", $meterNames);
|
||||
$addressText = trim(($user['name'] ?? '') . "\n" . ($user['address'] ?? '') . "\n" . ($user['city'] ?? ''));
|
||||
$addressText = implode("\n", array_filter([
|
||||
trim((string)($user['name'] ?? '')),
|
||||
trim((string)($user['street'] ?? '') . ' ' . (string)($user['building_number'] ?? '')),
|
||||
trim((string)($user['postal_code'] ?? '') . ' ' . (string)($user['town'] ?? '')),
|
||||
strtoupper(trim((string)($user['country'] ?? 'CH')))
|
||||
], function ($value) {
|
||||
return $value !== '';
|
||||
}));
|
||||
$meterLineCount = max(1, count(preg_split('/\r\n|\r|\n/', $meterText)));
|
||||
$addressLineCount = max(1, count(preg_split('/\r\n|\r|\n/', $addressText)));
|
||||
$meterBlockHeight = 22 + ($meterLineCount * 4.6);
|
||||
@@ -1754,14 +1877,26 @@ refreshPickers();
|
||||
$pdf->Line(62, $yStart, 62, 297);
|
||||
$pdf->SetLineStyle(['dash' => 0, 'color' => [0, 0, 0]]);
|
||||
|
||||
if ($amount > 999999999.99) {
|
||||
throw new Exception('Der Betrag überschreitet das Maximum für den QR-Zahlteil.');
|
||||
}
|
||||
|
||||
$creditorName = $this->ReadPropertyString('CreditorName');
|
||||
$creditorAddress = $this->ReadPropertyString('CreditorAddress');
|
||||
$creditorCity = $this->ReadPropertyString('CreditorCity');
|
||||
$creditorStreetLine = trim(
|
||||
$this->ReadPropertyString('CreditorStreet') . ' '
|
||||
. $this->ReadPropertyString('CreditorBuildingNumber')
|
||||
);
|
||||
$creditorTownLine = trim(
|
||||
$this->ReadPropertyString('CreditorPostalCode') . ' '
|
||||
. $this->ReadPropertyString('CreditorTown')
|
||||
);
|
||||
$creditorCountry = strtoupper($this->ReadPropertyString('CreditorCountry'));
|
||||
$bankIBANFormatted = wordwrap($iban, 4, ' ', true);
|
||||
$debtorLines = [
|
||||
$user['name'] ?? '',
|
||||
$user['address'] ?? '',
|
||||
$user['city'] ?? ''
|
||||
trim((string)($user['street'] ?? '') . ' ' . (string)($user['building_number'] ?? '')),
|
||||
trim((string)($user['postal_code'] ?? '') . ' ' . (string)($user['town'] ?? '')),
|
||||
strtoupper((string)($user['country'] ?? 'CH'))
|
||||
];
|
||||
|
||||
// Linker Teil: Empfangsschein
|
||||
@@ -1772,8 +1907,9 @@ refreshPickers();
|
||||
$this->WriteQrTextBlock($pdf, 5, $yStart + 12, 52, 'Konto / Zahlbar an', [
|
||||
$bankIBANFormatted,
|
||||
$creditorName,
|
||||
$creditorAddress,
|
||||
$creditorCity
|
||||
$creditorStreetLine,
|
||||
$creditorTownLine,
|
||||
$creditorCountry
|
||||
], 6, 7, 3.2);
|
||||
|
||||
$this->WriteQrTextBlock($pdf, 5, $yStart + 39, 52, 'Zahlbar durch', $debtorLines, 6, 7, 3.2);
|
||||
@@ -1823,8 +1959,9 @@ refreshPickers();
|
||||
$this->WriteQrTextBlock($pdf, 118, $yStart + 5, 84, 'Konto / Zahlbar an', [
|
||||
$bankIBANFormatted,
|
||||
$creditorName,
|
||||
$creditorAddress,
|
||||
$creditorCity
|
||||
$creditorStreetLine,
|
||||
$creditorTownLine,
|
||||
$creditorCountry
|
||||
], 8, 9, 4);
|
||||
|
||||
$this->WriteQrTextBlock($pdf, 118, $yStart + 34, 84, 'Zusätzliche Informationen', [
|
||||
@@ -1852,31 +1989,62 @@ refreshPickers();
|
||||
$pdf->MultiCell($width, $lineHeight, $line, 0, 'L', false, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private function GenerateQRString($iban, $amount, $user)
|
||||
{
|
||||
// Daten nach SIX Swiss Payment Standard beschneiden (max 70 Zeichen pro Zeile)
|
||||
$creditorName = mb_substr($this->ReadPropertyString('CreditorName'), 0, 70);
|
||||
$creditorStr = mb_substr($this->ReadPropertyString('CreditorAddress'), 0, 70);
|
||||
$creditorCity = mb_substr($this->ReadPropertyString('CreditorCity'), 0, 70);
|
||||
$sanitize = function ($value, $maxLength) {
|
||||
$value = preg_replace('/[\r\n]+/', ' ', trim((string)$value));
|
||||
return mb_substr((string)$value, 0, $maxLength);
|
||||
};
|
||||
|
||||
// Fallback falls ein Mieter keinen Namen oder Ort hinterlegt hat (Banken verlangen das zwingend)
|
||||
$debtorName = mb_substr(empty($user['name']) ? 'Unbekannt' : $user['name'], 0, 70);
|
||||
$debtorStr = mb_substr($user['address'] ?? '', 0, 70);
|
||||
$debtorCity = mb_substr(empty($user['city']) ? 'Unbekannt' : $user['city'], 0, 70);
|
||||
|
||||
$amountStr = number_format($amount, 2, '.', '');
|
||||
|
||||
// EPC / Swiss QR Code String Format (Exakte Reihenfolge ist kritisch!)
|
||||
$lines = [
|
||||
'SPC', '0200', '1', $iban, 'K',
|
||||
$creditorName, $creditorStr, $creditorCity, '', '', 'CH',
|
||||
'', '', '', '', '', '', '', // Ultimate Creditor (leer)
|
||||
$amountStr, 'CHF', 'K',
|
||||
$debtorName, $debtorStr, $debtorCity, '', '', 'CH',
|
||||
'NON', '', // Reference Type (NON = Ohne Referenz)
|
||||
'Verbrauchskostenreport Liegenschaft', // 30. Unstrukturierte Mitteilung
|
||||
'EPD' // 31. Trailer (Zwingend exakt 'EPD' am Schluss!)
|
||||
$creditor = [
|
||||
'name' => $sanitize($this->ReadPropertyString('CreditorName'), 70),
|
||||
'street' => $sanitize($this->ReadPropertyString('CreditorStreet'), 70),
|
||||
'building_number' => $sanitize($this->ReadPropertyString('CreditorBuildingNumber'), 16),
|
||||
'postal_code' => $sanitize($this->ReadPropertyString('CreditorPostalCode'), 16),
|
||||
'town' => $sanitize($this->ReadPropertyString('CreditorTown'), 35),
|
||||
'country' => strtoupper($sanitize($this->ReadPropertyString('CreditorCountry'), 2))
|
||||
];
|
||||
$debtor = [
|
||||
'name' => $sanitize($user['name'] ?? '', 70),
|
||||
'street' => $sanitize($user['street'] ?? '', 70),
|
||||
'building_number' => $sanitize($user['building_number'] ?? '', 16),
|
||||
'postal_code' => $sanitize($user['postal_code'] ?? '', 16),
|
||||
'town' => $sanitize($user['town'] ?? '', 35),
|
||||
'country' => strtoupper($sanitize($user['country'] ?? 'CH', 2))
|
||||
];
|
||||
|
||||
$lines = [
|
||||
'SPC',
|
||||
'0200',
|
||||
'1',
|
||||
$iban,
|
||||
'S',
|
||||
$creditor['name'],
|
||||
$creditor['street'],
|
||||
$creditor['building_number'],
|
||||
$creditor['postal_code'],
|
||||
$creditor['town'],
|
||||
$creditor['country'],
|
||||
'', '', '', '', '', '', '', // Ultimate Creditor
|
||||
number_format($amount, 2, '.', ''),
|
||||
'CHF',
|
||||
'S',
|
||||
$debtor['name'],
|
||||
$debtor['street'],
|
||||
$debtor['building_number'],
|
||||
$debtor['postal_code'],
|
||||
$debtor['town'],
|
||||
$debtor['country'],
|
||||
'NON',
|
||||
'',
|
||||
$sanitize('Verbrauchskostenreport Liegenschaft', 140),
|
||||
'EPD'
|
||||
];
|
||||
|
||||
if (count($lines) !== 31) {
|
||||
throw new LogicException('Swiss-QR-Payload muss genau 31 Basisfelder enthalten.');
|
||||
}
|
||||
|
||||
return implode("\r\n", $lines);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user