no message

This commit is contained in:
2026-03-19 16:44:55 +01:00
parent a57a76972d
commit 724f3b0d2a
2 changed files with 406 additions and 318 deletions
+27 -32
View File
@@ -19,9 +19,31 @@
"name": "FooterText", "name": "FooterText",
"caption": "Fusszeile" "caption": "Fusszeile"
}, },
{ {
"type": "Label",
"caption": "🏦 QR-Einzahlungsschein Daten"
},
{
"type": "ValidationTextBox",
"name": "CreditorName",
"caption": "Rechnungssteller Name"
},
{
"type": "ValidationTextBox",
"name": "CreditorAddress",
"caption": "Rechnungssteller Strasse/Nr."
},
{
"type": "ValidationTextBox",
"name": "CreditorCity",
"caption": "Rechnungssteller PLZ/Ort"
},
{
"type": "ValidationTextBox",
"name": "BankIBAN",
"caption": "Bank IBAN"
},
{
"type": "List", "type": "List",
"name": "Users", "name": "Users",
"caption": "Benutzerliste", "caption": "Benutzerliste",
@@ -34,11 +56,8 @@
{ "caption": "Adresse", "name": "address", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }, { "caption": "Adresse", "name": "address", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } },
{ "caption": "Ort", "name": "city", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } } { "caption": "Ort", "name": "city", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }
] ]
}, },
{ {
"type": "List", "type": "List",
"name": "PowerMeters", "name": "PowerMeters",
"caption": "Stromzählerliste", "caption": "Stromzählerliste",
@@ -53,9 +72,7 @@
{ "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } } { "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }
] ]
}, },
{ {
"type": "List", "type": "List",
"name": "WaterMeters", "name": "WaterMeters",
"caption": "Verbrauchszählerliste", "caption": "Verbrauchszählerliste",
@@ -81,12 +98,9 @@
] ]
} }
} }
] ]
}, },
{ {
"type": "List", "type": "List",
"name": "Tariffs", "name": "Tariffs",
"caption": "Tarifübersicht", "caption": "Tarifübersicht",
@@ -94,27 +108,9 @@
"delete": true, "delete": true,
"sortable": true, "sortable": true,
"columns": [ "columns": [
{ { "caption": "Startdatum", "name": "start", "width": "20%", "add": "", "edit": { "type": "SelectDate" } },
"caption": "Startdatum", { "caption": "Enddatum", "name": "end", "width": "20%", "add": "", "edit": { "type": "SelectDate" } },
"name": "start", { "caption": "Tarif (Rp/Einheit)", "name": "price", "width": "20%", "add": 0, "edit": { "type": "NumberSpinner", "digits": 3, "minimum": 0 } },
"width": "20%",
"add": "",
"edit": { "type": "SelectDate" }
},
{
"caption": "Enddatum",
"name": "end",
"width": "20%",
"add": "",
"edit": { "type": "SelectDate" }
},
{
"caption": "Tarif (Rp/Einheit)",
"name": "price",
"width": "20%",
"add": 0,
"edit": { "type": "NumberSpinner", "digits": 3, "minimum": 0 }
},
{ {
"caption": "Einheit", "caption": "Einheit",
"name": "unit_type", "name": "unit_type",
@@ -134,6 +130,5 @@
} }
] ]
} }
] ]
} }
+150 -57
View File
@@ -44,6 +44,12 @@ class Abrechnung extends IPSModule
$this->RegisterPropertyString('PropertyText', 'Liegenschaft'); $this->RegisterPropertyString('PropertyText', 'Liegenschaft');
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch'); $this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
// Neue Felder für den QR-Schein
$this->RegisterPropertyString('CreditorName', 'Belevo AG');
$this->RegisterPropertyString('CreditorAddress', 'Musterstrasse 1');
$this->RegisterPropertyString('CreditorCity', '6122 Menznau');
$this->RegisterPropertyString('BankIBAN', '');
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1); $this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
$this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2); $this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2);
$this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3); $this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3);
@@ -139,7 +145,11 @@ class Abrechnung extends IPSModule
foreach ($users as $user) { foreach ($users as $user) {
$pdf->AddPage(); $pdf->AddPage();
$this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to); // Hole das berechnete Total aus der Rechnung
$grandTotal = $this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to);
// Generiere den Einzahlungsschein, wenn eine IBAN vorhanden ist
$this->BuildQRBill($pdf, $user, $grandTotal);
} }
return $pdf->Output('Abrechnung.pdf', 'S'); return $pdf->Output('Abrechnung.pdf', 'S');
@@ -223,6 +233,136 @@ class Abrechnung extends IPSModule
</h2> </h2>
"; ";
$pdf->writeHTML($html, true, false, true, false, ''); $pdf->writeHTML($html, true, false, true, false, '');
// WICHTIG: Total zurückgeben für den Einzahlungsschein
return $grandTotal;
}
// ====================== QR-Einzahlungsschein (Schweiz) ======================
private function BuildQRBill($pdf, $user, $amount)
{
$iban = str_replace(' ', '', $this->ReadPropertyString('BankIBAN'));
// Kein Einzahlungsschein, wenn IBAN leer ist oder Betrag <= 0
if (empty($iban) || $amount <= 0) {
return;
}
$pdf->AddPage();
$pdf->SetAutoPageBreak(false);
// Position des Einzahlungsscheins unten auf A4 (Höhe 105mm)
$yStart = 192;
// Trennlinien zeichnen
$pdf->SetLineStyle(['dash' => '2,2', 'color' => [0, 0, 0]]);
$pdf->Line(0, $yStart, 210, $yStart); // Horizontal
$pdf->Line(62, $yStart, 62, 297); // Vertikal
$pdf->SetLineStyle(['dash' => 0, 'color' => [0, 0, 0]]); // Reset
$creditorName = $this->ReadPropertyString('CreditorName');
$creditorCity = $this->ReadPropertyString('CreditorCity');
$bankIBAN_formatted = wordwrap($iban, 4, ' ', true);
// --- LINKER TEIL: Empfangsschein ---
$pdf->SetFont('dejavusans', 'B', 11);
$pdf->SetXY(5, $yStart + 5);
$pdf->Cell(52, 5, 'Empfangsschein', 0, 1);
$pdf->SetFont('dejavusans', 'B', 6);
$pdf->SetXY(5, $yStart + 12);
$pdf->Cell(52, 3, 'Konto / Zahlbar an', 0, 1);
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetX(5); $pdf->Cell(52, 3, $bankIBAN_formatted, 0, 1);
$pdf->SetX(5); $pdf->Cell(52, 3, $creditorName, 0, 1);
$pdf->SetX(5); $pdf->Cell(52, 3, $creditorCity, 0, 1);
$pdf->SetFont('dejavusans', 'B', 6);
$pdf->SetXY(5, $yStart + 35);
$pdf->Cell(52, 3, 'Zahlbar durch', 0, 1);
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetX(5); $pdf->Cell(52, 3, $user['name'] ?? '', 0, 1);
$pdf->SetX(5); $pdf->Cell(52, 3, $user['address'] ?? '', 0, 1);
$pdf->SetX(5); $pdf->Cell(52, 3, $user['city'] ?? '', 0, 1);
$pdf->SetFont('dejavusans', 'B', 6);
$pdf->SetXY(5, $yStart + 60);
$pdf->Cell(15, 3, 'Währung', 0, 0);
$pdf->Cell(20, 3, 'Betrag', 0, 1);
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetX(5);
$pdf->Cell(15, 4, 'CHF', 0, 0);
$pdf->Cell(20, 4, number_format($amount, 2, '.', ''), 0, 1);
// --- RECHTER TEIL: Zahlteil ---
$pdf->SetFont('dejavusans', 'B', 11);
$pdf->SetXY(118, $yStart + 5);
$pdf->Cell(50, 5, 'Zahlteil', 0, 1);
// QR Code String generieren & rendern (46x46mm)
$qrString = $this->GenerateQRString($iban, $amount, $user);
$style = ['border' => false, 'padding' => 0, 'fgcolor' => [0, 0, 0], 'bgcolor' => false];
$pdf->write2DBarcode($qrString, 'QRCODE,M', 67, $yStart + 17, 46, 46, $style, 'N');
// Schweizer Kreuz über den QR Code zeichnen (7x7mm Block)
$pdf->SetFillColor(0, 0, 0);
$pdf->Rect(67 + 19.5, $yStart + 17 + 19.5, 7, 7, 'F');
$pdf->SetFillColor(255, 255, 255);
$pdf->Rect(67 + 19.5 + 3, $yStart + 17 + 19.5 + 1, 1, 5, 'F');
$pdf->Rect(67 + 19.5 + 1, $yStart + 17 + 19.5 + 3, 5, 1, 'F');
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetXY(118, $yStart + 17);
$pdf->Cell(80, 4, 'Konto / Zahlbar an', 0, 1);
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetX(118); $pdf->Cell(80, 4, $bankIBAN_formatted, 0, 1);
$pdf->SetX(118); $pdf->Cell(80, 4, $creditorName, 0, 1);
$pdf->SetX(118); $pdf->Cell(80, 4, $creditorCity, 0, 1);
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetXY(118, $yStart + 40);
$pdf->Cell(80, 4, 'Zahlbar durch', 0, 1);
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetX(118); $pdf->Cell(80, 4, $user['name'] ?? '', 0, 1);
$pdf->SetX(118); $pdf->Cell(80, 4, $user['address'] ?? '', 0, 1);
$pdf->SetX(118); $pdf->Cell(80, 4, $user['city'] ?? '', 0, 1);
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetXY(67, $yStart + 68);
$pdf->Cell(15, 4, 'Währung', 0, 0);
$pdf->Cell(20, 4, 'Betrag', 0, 1);
$pdf->SetFont('dejavusans', '', 10);
$pdf->SetX(67);
$pdf->Cell(15, 5, 'CHF', 0, 0);
$pdf->Cell(20, 5, number_format($amount, 2, '.', ''), 0, 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);
$debtorName = mb_substr($user['name'] ?? '', 0, 70);
$debtorStr = mb_substr($user['address'] ?? '', 0, 70);
$debtorCity = mb_substr($user['city'] ?? '', 0, 70);
$amountStr = number_format($amount, 2, '.', '');
// EPC / Swiss QR Code String Format
$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)
'EPD', 'Abrechnung Liegenschaft'
];
return implode("\r\n", $lines);
} }
// ====================== Stromkosten (15-Minuten, alle User) ====================== // ====================== Stromkosten (15-Minuten, alle User) ======================
@@ -396,8 +536,8 @@ class Abrechnung extends IPSModule
} }
} }
private function GetCalculatedPowerCosts($userId) private function GetCalculatedPowerCosts($userId)
{ {
$html = " $html = "
<style> <style>
table.powercost { table.powercost {
@@ -509,7 +649,7 @@ private function GetCalculatedPowerCosts($userId)
</table>"; </table>";
return ['html' => $html, 'sum' => $sum]; return ['html' => $html, 'sum' => $sum];
} }
// ====================== Nebenkosten Wasser/Wärme ====================== // ====================== Nebenkosten Wasser/Wärme ======================
@@ -638,8 +778,8 @@ private function GetCalculatedPowerCosts($userId)
// ====================== Hilfsfunktionen ====================== // ====================== Hilfsfunktionen ======================
private function GetValueAt($varId, $timestamp, $nearestAfter = true) private function GetValueAt($varId, $timestamp, $nearestAfter = true)
{ {
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
if (!$archiveID || !IPS_VariableExists($varId)) { if (!$archiveID || !IPS_VariableExists($varId)) {
return null; return null;
@@ -671,11 +811,10 @@ private function GetValueAt($varId, $timestamp, $nearestAfter = true)
// Fallback → Live-Wert // Fallback → Live-Wert
return (float)GetValue($varId); return (float)GetValue($varId);
} }
private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float
private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float {
{
// Werte holen // Werte holen
$startValue = $this->GetValueAt($varId, $tStart, false); $startValue = $this->GetValueAt($varId, $tStart, false);
$endValue = $this->GetValueAt($varId, $tEnd, false); $endValue = $this->GetValueAt($varId, $tEnd, false);
@@ -692,7 +831,7 @@ private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float
} }
return (float)$diff; return (float)$diff;
} }
private function toUnixTs($val, $endOfDay = false) private function toUnixTs($val, $endOfDay = false)
{ {
@@ -724,52 +863,6 @@ private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float
return null; return null;
} }
private function readDelta($varId, $tStart, $tEnd)
{
if (!is_int($tStart)) {
$tStart = strtotime($tStart);
}
if (!is_int($tEnd)) {
$tEnd = strtotime($tEnd);
}
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
if (!$archiveID || !IPS_VariableExists($varId)) {
return 0.0;
}
$values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0);
if (empty($values)) {
return 0.0;
}
usort($values, fn($a, $b) => (int)$a['TimeStamp'] <=> (int)$b['TimeStamp']);
$vStart = null;
$vEnd = null;
foreach ($values as $v) {
if ($v['TimeStamp'] <= $tStart) {
$vStart = $v['Value'];
}
if ($v['TimeStamp'] <= $tEnd) {
$vEnd = $v['Value'];
}
if ($v['TimeStamp'] > $tEnd) {
break;
}
}
if ($vStart === null) {
$vStart = (float)GetValue($varId);
}
if ($vEnd === null) {
$vEnd = (float)GetValue($varId);
}
$diff = $vEnd - $vStart;
return ($diff < 0) ? 0.0 : $diff;
}
private function getTariffPriceAt($tariffs, $typeSynonyms, $ts) private function getTariffPriceAt($tariffs, $typeSynonyms, $ts)
{ {
$wanted = array_map('strtolower', $typeSynonyms); $wanted = array_map('strtolower', $typeSynonyms);