diff --git a/Abrechnung/README.md b/Abrechnung/README.md
new file mode 100644
index 0000000..4cd05d4
--- /dev/null
+++ b/Abrechnung/README.md
@@ -0,0 +1,40 @@
+# Abrechnung
+
+IP-Symcon-Modul zur Erstellung von Energie- und Nebenkostenabrechnungen als PDF.
+
+## Funktion
+
+- erstellt pro Benutzer eine Abrechnung
+- berechnet Stromkosten aus Archivwerten in 15-Minuten-Schritten
+- berücksichtigt Netz-, Solar- und Einspeisetarife
+- berechnet zusätzliche Zählerkosten für Warmwasser, Kaltwasser und Wärme
+- erzeugt optional einen Schweizer QR-Einzahlungsschein
+- unterstützt MWST-Ausweis
+
+## Konfiguration
+
+| Property | Beschreibung |
+|---|---|
+| `Users` | Benutzerliste |
+| `PowerMeters` | Stromzählerliste |
+| `WaterMeters` | Nebenkostenzähler |
+| `Tariffs` | Tarifliste |
+| `LogoMediaID` | Logo für PDF-Kopf |
+| `PropertyText` | Liegenschaftstext |
+| `FooterText` | PDF-Fusszeile |
+| `EnableVAT` | aktiviert MWST |
+| `VATPercentage` | MWST-Satz |
+| `CreditorName` | Zahlungsempfänger |
+| `CreditorAddress` | Adresse Zahlungsempfänger |
+| `CreditorCity` | PLZ/Ort Zahlungsempfänger |
+| `BankIBAN` | IBAN für QR-Zahlteil |
+
+## Bedienung
+
+| Ident/Action | Beschreibung |
+|---|---|
+| `FromDate` | Startdatum |
+| `ToDate` | Enddatum |
+| `StartBilling` | erzeugt das PDF |
+| `LastResult` | letzter Lauf |
+| `InvoicePDF` | erzeugtes PDF-Dokument |
diff --git a/Abrechnung/module.php b/Abrechnung/module.php
index f7c9c24..65ed869 100644
--- a/Abrechnung/module.php
+++ b/Abrechnung/module.php
@@ -28,9 +28,9 @@ class InvoicePDF extends TCPDF
if (!$this->printFooterText) {
return; // Auf QR-Seite keine Rechnungs-Fusszeile drucken!
}
- $this->SetY(-15);
+ $this->SetY(-18);
$this->SetFont('dejavusans', '', 8);
- $this->Cell(0, 10, $this->footerText, 0, 0, 'C');
+ $this->MultiCell(0, 8, $this->footerText, 0, 'C', false, 1);
}
}
@@ -58,23 +58,303 @@ class Abrechnung extends IPSModule
$this->RegisterPropertyString('CreditorCity', '6122 Menznau');
$this->RegisterPropertyString('BankIBAN', '');
+ $this->RegisterTariffVariableProfiles();
+
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
$this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2);
$this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3);
+ $this->RegisterVariableString('VisuTariffOverview', 'Tarifübersicht', '~HTMLBox', 10);
+ $this->RegisterVariableInteger('VisuTariffType', 'Tarifart', 'BELEVO.TariffType', 11);
+ $this->RegisterVariableInteger('VisuTariffStart', 'Tarif Startdatum', '~UnixTimestamp', 12);
+ $this->RegisterVariableInteger('VisuTariffEnd', 'Tarif Enddatum', '~UnixTimestamp', 13);
+ $this->RegisterVariableFloat('VisuTariffPrice', 'Tarif Rp/Einheit', 'BELEVO.TariffPrice', 14);
+ $this->RegisterVariableString('VisuTariffStatus', 'Tarif Status', '', 15);
$this->EnableAction('FromDate');
$this->EnableAction('ToDate');
+ $this->EnableAction('VisuTariffType');
+ $this->EnableAction('VisuTariffStart');
+ $this->EnableAction('VisuTariffEnd');
+ $this->EnableAction('VisuTariffPrice');
$this->RegisterScript(
'StartBilling',
'Abrechnung starten',
"InstanceID . ", 'StartBilling', ''); ?>"
);
+ $this->RegisterScript(
+ 'SaveVisuTariff',
+ 'Tarif speichern',
+ "InstanceID . ", 'SaveVisuTariff', ''); ?>"
+ );
+ $this->RegisterScript(
+ 'DeleteVisuTariff',
+ 'Tarif löschen',
+ "InstanceID . ", 'DeleteVisuTariff', ''); ?>"
+ );
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
}
public function ApplyChanges()
{
parent::ApplyChanges();
+ $this->RegisterTariffVariableProfiles();
+ $this->EnsureVisuTariffObjects();
+ $this->InitializeVisuTariffDefaults();
+ $this->UpdateTariffOverview();
+ }
+
+ private function EnsureVisuTariffObjects()
+ {
+ $this->RegisterVariableString('VisuTariffOverview', 'Tarifübersicht', '~HTMLBox', 10);
+ $this->RegisterVariableInteger('VisuTariffType', 'Tarifart', 'BELEVO.TariffType', 11);
+ $this->RegisterVariableInteger('VisuTariffStart', 'Tarif Startdatum', '~UnixTimestamp', 12);
+ $this->RegisterVariableInteger('VisuTariffEnd', 'Tarif Enddatum', '~UnixTimestamp', 13);
+ $this->RegisterVariableFloat('VisuTariffPrice', 'Tarif Rp/Einheit', 'BELEVO.TariffPrice', 14);
+ $this->RegisterVariableString('VisuTariffStatus', 'Tarif Status', '', 15);
+
+ $this->EnableAction('VisuTariffType');
+ $this->EnableAction('VisuTariffStart');
+ $this->EnableAction('VisuTariffEnd');
+ $this->EnableAction('VisuTariffPrice');
+
+ $this->RegisterScript(
+ 'SaveVisuTariff',
+ 'Tarif speichern',
+ "InstanceID . ", 'SaveVisuTariff', ''); ?>"
+ );
+ $this->RegisterScript(
+ 'DeleteVisuTariff',
+ 'Tarif löschen',
+ "InstanceID . ", 'DeleteVisuTariff', ''); ?>"
+ );
+ }
+
+ private function RegisterTariffVariableProfiles()
+ {
+ $typeProfile = 'BELEVO.TariffType';
+ $types = $this->GetTariffTypes();
+
+ if (!IPS_VariableProfileExists($typeProfile)) {
+ IPS_CreateVariableProfile($typeProfile, 1);
+ }
+ IPS_SetVariableProfileValues($typeProfile, 0, count($types) - 1, 1);
+ foreach ($types as $index => $caption) {
+ IPS_SetVariableProfileAssociation($typeProfile, $index, $caption, '', -1);
+ }
+
+ $priceProfile = 'BELEVO.TariffPrice';
+ if (!IPS_VariableProfileExists($priceProfile)) {
+ IPS_CreateVariableProfile($priceProfile, 2);
+ }
+ IPS_SetVariableProfileDigits($priceProfile, 3);
+ IPS_SetVariableProfileText($priceProfile, '', ' Rp/Einheit');
+ IPS_SetVariableProfileValues($priceProfile, 0, 0, 0);
+ }
+
+ private function GetTariffTypes()
+ {
+ return [
+ 'Netztarif',
+ 'Einspeisetarif',
+ 'Solartarif',
+ 'Warmwasser',
+ 'Kaltwasser',
+ 'Wärme'
+ ];
+ }
+
+ private function ReadTariffs()
+ {
+ $tariffs = json_decode($this->ReadPropertyString('Tariffs'), true);
+ return is_array($tariffs) ? array_values($tariffs) : [];
+ }
+
+ private function WriteTariffs(array $tariffs)
+ {
+ usort($tariffs, function ($a, $b) {
+ $typeCmp = strcmp((string)($a['unit_type'] ?? ''), (string)($b['unit_type'] ?? ''));
+ if ($typeCmp !== 0) {
+ return $typeCmp;
+ }
+ return ($this->toUnixTs($a['start'] ?? 0, false) ?? 0) <=> ($this->toUnixTs($b['start'] ?? 0, false) ?? 0);
+ });
+
+ IPS_SetProperty($this->InstanceID, 'Tariffs', json_encode(array_values($tariffs), JSON_UNESCAPED_UNICODE));
+ IPS_ApplyChanges($this->InstanceID);
+ $this->UpdateTariffOverview();
+ }
+
+ private function InitializeVisuTariffDefaults()
+ {
+ $startID = @IPS_GetObjectIDByIdent('VisuTariffStart', $this->InstanceID);
+ $endID = @IPS_GetObjectIDByIdent('VisuTariffEnd', $this->InstanceID);
+ $typeID = @IPS_GetObjectIDByIdent('VisuTariffType', $this->InstanceID);
+ $priceID = @IPS_GetObjectIDByIdent('VisuTariffPrice', $this->InstanceID);
+
+ if ($startID === false || $endID === false || $typeID === false || $priceID === false) {
+ return;
+ }
+
+ if ((int)GetValue($startID) > 0 && (int)GetValue($endID) > 0) {
+ return;
+ }
+
+ $tariffs = $this->ReadTariffs();
+ $first = $tariffs[0] ?? null;
+
+ if (is_array($first)) {
+ $start = $this->toUnixTs($first['start'] ?? 0, false) ?? strtotime(date('Y-01-01 00:00:00'));
+ $end = $this->toUnixTs($first['end'] ?? 0, false) ?? strtotime(date('Y-12-31 00:00:00'));
+ SetValue($typeID, $this->GetTariffTypeIndex((string)($first['unit_type'] ?? 'Netztarif')));
+ SetValue($priceID, (float)($first['price'] ?? 0));
+ } else {
+ $start = strtotime(date('Y-01-01 00:00:00'));
+ $end = strtotime(date('Y-12-31 00:00:00'));
+ SetValue($typeID, 0);
+ SetValue($priceID, 0.0);
+ }
+
+ SetValue($startID, $start);
+ SetValue($endID, $end);
+ }
+
+ private function SaveVisuTariffFromVariables()
+ {
+ $tariff = $this->BuildTariffFromVisuVariables();
+ $tariffs = $this->ReadTariffs();
+ $replaced = false;
+
+ foreach ($tariffs as $index => $existing) {
+ if (!$this->IsSameTariffSlot($existing, $tariff)) {
+ continue;
+ }
+ $tariffs[$index] = $tariff;
+ $replaced = true;
+ break;
+ }
+
+ if (!$replaced) {
+ $tariffs[] = $tariff;
+ }
+
+ $this->WriteTariffs($tariffs);
+
+ return ($replaced ? 'Tarif aktualisiert: ' : 'Tarif gespeichert: ')
+ . $tariff['unit_type'] . ' '
+ . date('d.m.Y', $tariff['start']) . ' - ' . date('d.m.Y', $tariff['end'])
+ . ', ' . number_format($tariff['price'], 3, '.', "'") . ' Rp/Einheit';
+ }
+
+ private function DeleteVisuTariffFromVariables()
+ {
+ $tariff = $this->BuildTariffFromVisuVariables();
+ $tariffs = $this->ReadTariffs();
+ $filtered = [];
+ $deleted = false;
+
+ foreach ($tariffs as $existing) {
+ if ($this->IsSameTariffSlot($existing, $tariff)) {
+ $deleted = true;
+ continue;
+ }
+ $filtered[] = $existing;
+ }
+
+ if (!$deleted) {
+ return 'Kein passender Tarif gefunden.';
+ }
+
+ $this->WriteTariffs($filtered);
+
+ return 'Tarif gelöscht: '
+ . $tariff['unit_type'] . ' '
+ . date('d.m.Y', $tariff['start']) . ' - ' . date('d.m.Y', $tariff['end']);
+ }
+
+ private function BuildTariffFromVisuVariables()
+ {
+ $types = $this->GetTariffTypes();
+ $typeIndex = (int)GetValue($this->GetIDForIdent('VisuTariffType'));
+ $type = $types[$typeIndex] ?? $types[0];
+ $start = (int)GetValue($this->GetIDForIdent('VisuTariffStart'));
+ $end = (int)GetValue($this->GetIDForIdent('VisuTariffEnd'));
+ $price = (float)GetValue($this->GetIDForIdent('VisuTariffPrice'));
+
+ if ($start > 0) {
+ $start = strtotime(date('Y-m-d 00:00:00', $start));
+ }
+ if ($end > 0) {
+ $end = strtotime(date('Y-m-d 00:00:00', $end));
+ }
+
+ if ($start <= 0 || $end <= 0) {
+ throw new Exception('Start- und Enddatum müssen gesetzt sein.');
+ }
+ if ($end < $start) {
+ throw new Exception('Enddatum darf nicht vor dem Startdatum liegen.');
+ }
+ if ($price < 0) {
+ throw new Exception('Tarifpreis darf nicht negativ sein.');
+ }
+
+ return [
+ 'start' => $start,
+ 'end' => $end,
+ 'price' => round($price, 3),
+ 'unit_type' => $type
+ ];
+ }
+
+ private function IsSameTariffSlot($a, $b)
+ {
+ return strtolower(trim((string)($a['unit_type'] ?? ''))) === strtolower(trim((string)($b['unit_type'] ?? '')))
+ && ($this->toUnixTs($a['start'] ?? 0, false) ?? 0) === ($this->toUnixTs($b['start'] ?? 0, false) ?? 0)
+ && ($this->toUnixTs($a['end'] ?? 0, false) ?? 0) === ($this->toUnixTs($b['end'] ?? 0, false) ?? 0);
+ }
+
+ private function GetTariffTypeIndex($type)
+ {
+ foreach ($this->GetTariffTypes() as $index => $tariffType) {
+ if (strtolower($tariffType) === strtolower(trim($type))) {
+ return $index;
+ }
+ }
+ return 0;
+ }
+
+ private function UpdateTariffOverview()
+ {
+ $overviewID = @IPS_GetObjectIDByIdent('VisuTariffOverview', $this->InstanceID);
+ if ($overviewID === false) {
+ return;
+ }
+
+ $tariffs = $this->ReadTariffs();
+ $html = "
+
+ | Tarifart |
+ Gültig von |
+ Gültig bis |
+ Preis |
+
";
+
+ if (empty($tariffs)) {
+ $html .= "| Keine Tarife erfasst. |
";
+ } else {
+ foreach ($tariffs as $tariff) {
+ $start = $this->toUnixTs($tariff['start'] ?? 0, false);
+ $end = $this->toUnixTs($tariff['end'] ?? 0, false);
+ $html .= "
+ | " . $this->h($tariff['unit_type'] ?? '') . " |
+ " . ($start ? date('d.m.Y', $start) : '-') . " |
+ " . ($end ? date('d.m.Y', $end) : '-') . " |
+ " . number_format((float)($tariff['price'] ?? 0), 3, '.', "'") . " Rp/Einheit |
+
";
+ }
+ }
+
+ $html .= '
';
+ SetValue($overviewID, $html);
}
private function RegisterMediaDocument($Ident, $Name, $Extension, $Position = 0)
@@ -95,9 +375,37 @@ class Abrechnung extends IPSModule
switch ($Ident) {
case 'FromDate':
case 'ToDate':
+ case 'VisuTariffType':
+ case 'VisuTariffStart':
+ case 'VisuTariffEnd':
+ case 'VisuTariffPrice':
SetValue($this->GetIDForIdent($Ident), $Value);
break;
+ case 'SaveVisuTariff':
+ try {
+ $message = $this->SaveVisuTariffFromVariables();
+ SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
+ echo $message;
+ } catch (Throwable $e) {
+ $message = 'Fehler beim Speichern des Tarifs: ' . $e->getMessage();
+ SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
+ echo $message;
+ }
+ break;
+
+ case 'DeleteVisuTariff':
+ try {
+ $message = $this->DeleteVisuTariffFromVariables();
+ SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
+ echo $message;
+ } catch (Throwable $e) {
+ $message = 'Fehler beim Löschen des Tarifs: ' . $e->getMessage();
+ SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
+ echo $message;
+ }
+ break;
+
case 'StartBilling':
try {
$pdfContent = $this->GenerateInvoices();
@@ -129,6 +437,11 @@ public function GenerateInvoices()
$water = json_decode($this->ReadPropertyString('WaterMeters'), true);
$tariffs = json_decode($this->ReadPropertyString('Tariffs'), true);
+ $users = is_array($users) ? $users : [];
+ $power = is_array($power) ? $power : [];
+ $water = is_array($water) ? $water : [];
+ $tariffs = is_array($tariffs) ? $tariffs : [];
+
if (!class_exists('TCPDF')) {
return false;
}
@@ -153,6 +466,7 @@ public function GenerateInvoices()
foreach ($users as $user) {
$pdf->AddPage();
+ $pdf->SetAutoPageBreak(true, 20);
$pdf->printFooterText = true; // Fusszeile für diese Rechnungsseite EINSCHALTEN
$grandTotal = $this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to);
@@ -165,69 +479,65 @@ public function GenerateInvoices()
private function BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to)
{
- // Kopfbereich
- $html = "
- Elektro- und Nebenkostenabrechnung
- Betrifft Liegenschaft: " . $this->ReadPropertyString("PropertyText") . "
-
-
-
-
-
-
- Zählpunkte: ";
-
- // Alle Zählerpunkte des Users auflisten
+ $meterNames = [];
foreach ($power as $m) {
- if ($m['user_id'] == $user['id']) {
- $html .= htmlspecialchars($m['name']) . " ";
+ if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
+ $meterNames[] = $this->h($m['name'] ?? '');
}
}
foreach ($water as $m) {
- if ($m['user_id'] == $user['id']) {
- $html .= htmlspecialchars($m['name']) . " ";
+ if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
+ $meterNames[] = $this->h($m['name'] ?? '');
}
}
- $html .= "
+ $meterText = empty($meterNames) ? '-' : implode(' ', $meterNames);
+ $periodText = date('d.m.Y', $from) . ' - ' . date('d.m.Y', $to);
+
+ $html = "
+ Elektro- und Nebenkostenabrechnung
+ Liegenschaft: " . $this->h($this->ReadPropertyString('PropertyText')) . "
+
+
+
+ Abrechnungszeitraum " . $periodText . "
+ Zählpunkte " . $meterText . "
|
-
- Rechnungsadresse:
- {$user['name']}
- {$user['address']}
- {$user['city']}
+ |
+ Rechnungsadresse
+ " . $this->h($user['name'] ?? '') . "
+ " . $this->h($user['address'] ?? '') . "
+ " . $this->h($user['city'] ?? '') . "
|
-
- Abrechnungszeitraum: " . date('d.m.Y', $from) . " – " . date('d.m.Y', $to) . "
-
-
- ";
+ ";
// ========================= Elektrizität =========================
- $html .= "
Elektrizität";
+ $html .= "Elektrizität";
$powerResult = $this->GetCalculatedPowerCosts($user['id']);
$html .= $powerResult['html'];
$totalPower = $powerResult['sum'];
- // Elektrizitätstarife anzeigen
$appliedTariffs = $this->CollectTariffsForUser($tariffs, ['Netztarif', 'Solartarif', 'Einspeisetarif']);
if (!empty($appliedTariffs)) {
- $html .= "Angewendete Elektrizitätstarife:
- ";
}
// ========================= Nebenkosten =========================
- $html .= "
Nebenkosten";
+ $html .= "Nebenkosten";
$additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to);
$html .= $additionalResult['html'];
@@ -236,126 +546,127 @@ public function GenerateInvoices()
// ========================= Gesamttotal & MWST =========================
$subTotal = $totalPower + $totalAdditional;
$enableVAT = $this->ReadPropertyBoolean('EnableVAT');
-
- $html .= "";
-
+
+ $html .= "
+
+ | Zusammenfassung |
+
+
+ | Zwischentotal Elektrizität |
+ " . $this->FormatCurrency($totalPower) . " |
+
+
+ | Zwischentotal Nebenkosten |
+ " . $this->FormatCurrency($totalAdditional) . " |
+ ";
+
if ($enableVAT) {
$vatPercent = $this->ReadPropertyFloat('VATPercentage');
$vatAmount = $subTotal * ($vatPercent / 100);
$grandTotal = $subTotal + $vatAmount;
-
+
$html .= "
- | Zwischentotal (exkl. MWST): |
- CHF " . number_format($subTotal, 2, '.', "'") . " |
+ Total exkl. MWST |
+ " . $this->FormatCurrency($subTotal) . " |
- | zuzüglich " . number_format($vatPercent, 1, '.', "'") . " % MWST: |
- CHF " . number_format($vatAmount, 2, '.', "'") . " |
+ MWST " . number_format($vatPercent, 1, '.', "'") . " % |
+ " . $this->FormatCurrency($vatAmount) . " |
-
- Gesamttotal (inkl. MWST): |
- CHF " . number_format($grandTotal, 2, '.', "'") . " |
+
+ | Gesamttotal inkl. MWST |
+ " . $this->FormatCurrency($grandTotal) . " |
";
} else {
$grandTotal = $subTotal;
$html .= "
-
- | Gesamttotal: |
- CHF " . number_format($grandTotal, 2, '.', "'") . " |
+
+ | Gesamttotal |
+ " . $this->FormatCurrency($grandTotal) . " |
- Nicht mehrwertsteuerpflichtig. |
+ Nicht mehrwertsteuerpflichtig. |
-
- ";
+ |
";
}
$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)
+ private function BuildQRBill($pdf, $user, $amount)
{
$iban = str_replace(' ', '', $this->ReadPropertyString('BankIBAN'));
-
+
if (empty($iban) || $amount <= 0) {
return;
}
- $pdf->AddPage(); // Schliesst die vorherige Seite (Fusszeile wird dort noch gedruckt)
- $pdf->printFooterText = false; // Fusszeile für diese QR-Seite AUSSCHALTEN
+ $pdf->AddPage();
+ $pdf->printFooterText = false;
$pdf->SetAutoPageBreak(false);
- // Wir nutzen Helvetica für den QR-Schein (Standard gemäss Richtlinien)
$font = 'helvetica';
- $yStart = 192;
+ $yStart = 192;
+ $amountText = number_format($amount, 2, '.', ' ');
- // Trennlinien zeichnen (Scherensymbol)
$pdf->SetLineStyle(['dash' => '2,2', 'color' => [0, 0, 0], 'width' => 0.1]);
- $pdf->Line(0, $yStart, 210, $yStart); // Horizontal
- $pdf->Line(62, $yStart, 62, 297); // Vertikal
- $pdf->SetLineStyle(['dash' => 0, 'color' => [0, 0, 0]]); // Reset
+ $pdf->Line(0, $yStart, 210, $yStart);
+ $pdf->Line(62, $yStart, 62, 297);
+ $pdf->SetLineStyle(['dash' => 0, 'color' => [0, 0, 0]]);
$creditorName = $this->ReadPropertyString('CreditorName');
+ $creditorAddress = $this->ReadPropertyString('CreditorAddress');
$creditorCity = $this->ReadPropertyString('CreditorCity');
- $bankIBAN_formatted = wordwrap($iban, 4, ' ', true);
+ $bankIBANFormatted = wordwrap($iban, 4, ' ', true);
+ $debtorLines = [
+ $user['name'] ?? '',
+ $user['address'] ?? '',
+ $user['city'] ?? ''
+ ];
- // --- LINKER TEIL: Empfangsschein (0-62mm) ---
+ // Linker Teil: Empfangsschein
$pdf->SetFont($font, 'B', 11);
$pdf->SetXY(5, $yStart + 5);
$pdf->Cell(52, 5, 'Empfangsschein', 0, 1);
- $pdf->SetFont($font, 'B', 6);
- $pdf->SetXY(5, $yStart + 12);
- $pdf->Cell(52, 3, 'Konto / Zahlbar an', 0, 1);
- $pdf->SetFont($font, '', 8);
- $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);
+ $this->WriteQrTextBlock($pdf, 5, $yStart + 12, 52, 'Konto / Zahlbar an', [
+ $bankIBANFormatted,
+ $creditorName,
+ $creditorAddress,
+ $creditorCity
+ ], 6, 7, 3.2);
+
+ $this->WriteQrTextBlock($pdf, 5, $yStart + 39, 52, 'Zahlbar durch', $debtorLines, 6, 7, 3.2);
$pdf->SetFont($font, 'B', 6);
- $pdf->SetXY(5, $yStart + 35);
- $pdf->Cell(52, 3, 'Zahlbar durch', 0, 1);
- $pdf->SetFont($font, '', 8);
- $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);
-
- // Währung und Betrag unten Links
- $pdf->SetFont($font, 'B', 6);
- $pdf->SetXY(5, $yStart + 65);
+ $pdf->SetXY(5, $yStart + 68);
$pdf->Cell(15, 3, 'Währung', 0, 0);
- $pdf->Cell(20, 3, 'Betrag', 0, 1);
+ $pdf->Cell(30, 3, 'Betrag', 0, 1);
$pdf->SetFont($font, '', 8);
$pdf->SetX(5);
$pdf->Cell(15, 4, 'CHF', 0, 0);
- $pdf->Cell(20, 4, number_format($amount, 2, '.', ' '), 0, 1); // Leerzeichen bei Tausender
+ $pdf->Cell(30, 4, $amountText, 0, 1);
- // Annahmestelle
$pdf->SetFont($font, 'B', 6);
- $pdf->SetXY(38, $yStart + 85);
+ $pdf->SetXY(38, $yStart + 90);
$pdf->Cell(19, 3, 'Annahmestelle', 0, 1, 'R');
-
- // --- RECHTER TEIL: Zahlteil ---
+ // Rechter Teil: Zahlteil
$pdf->SetFont($font, 'B', 11);
$pdf->SetXY(67, $yStart + 5);
$pdf->Cell(50, 5, 'Zahlteil', 0, 1);
- // QR Code rendern
$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 (Spezifikation: 7x7mm inkl. weissem Rand)
- $cX = 67 + 23; // Zentrum X des QR Codes (Position 67 + Hälfte von 46)
- $cY = $yStart + 17 + 23; // Zentrum Y des QR Codes (Position 17 + Hälfte von 46)
+ $cX = 67 + 23;
+ $cY = $yStart + 17 + 23;
$pdf->SetFillColor(255, 255, 255);
$pdf->Rect($cX - 3.5, $cY - 3.5, 7, 7, 'F');
@@ -365,41 +676,48 @@ private function BuildQRBill($pdf, $user, $amount)
$pdf->Rect($cX - 0.6, $cY - 2, 1.2, 4, 'F');
$pdf->Rect($cX - 2, $cY - 0.6, 4, 1.2, 'F');
- // Währung und Betrag unter dem QR Code
$pdf->SetFont($font, 'B', 8);
$pdf->SetXY(67, $yStart + 68);
$pdf->Cell(15, 4, 'Währung', 0, 0);
- $pdf->Cell(20, 4, 'Betrag', 0, 1);
+ $pdf->Cell(30, 4, 'Betrag', 0, 1);
$pdf->SetFont($font, '', 10);
$pdf->SetX(67);
$pdf->Cell(15, 5, 'CHF', 0, 0);
- $pdf->Cell(20, 5, number_format($amount, 2, '.', ' '), 0, 1);
+ $pdf->Cell(30, 5, $amountText, 0, 1);
- // --- RECHTE TEXTSPALTE (ab 118mm) ---
- $pdf->SetFont($font, 'B', 8);
- $pdf->SetXY(118, $yStart + 5);
- $pdf->Cell(80, 4, 'Konto / Zahlbar an', 0, 1);
- $pdf->SetFont($font, '', 10);
- $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);
+ $this->WriteQrTextBlock($pdf, 118, $yStart + 5, 84, 'Konto / Zahlbar an', [
+ $bankIBANFormatted,
+ $creditorName,
+ $creditorAddress,
+ $creditorCity
+ ], 8, 9, 4);
- $pdf->SetFont($font, 'B', 8);
- $pdf->SetXY(118, $yStart + 26);
- $pdf->Cell(80, 4, 'Zusätzliche Informationen', 0, 1);
- $pdf->SetFont($font, '', 10);
- $pdf->SetX(118); $pdf->Cell(80, 4, 'Abrechnung Liegenschaft', 0, 1);
+ $this->WriteQrTextBlock($pdf, 118, $yStart + 34, 84, 'Zusätzliche Informationen', [
+ 'Abrechnung Liegenschaft'
+ ], 8, 9, 4);
- $pdf->SetFont($font, 'B', 8);
- $pdf->SetXY(118, $yStart + 43);
- $pdf->Cell(80, 4, 'Zahlbar durch', 0, 1);
- $pdf->SetFont($font, '', 10);
- $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);
+ $this->WriteQrTextBlock($pdf, 118, $yStart + 50, 84, 'Zahlbar durch', $debtorLines, 8, 9, 4);
+
+ $pdf->SetAutoPageBreak(true, 20);
}
-private function GenerateQRString($iban, $amount, $user)
+ private function WriteQrTextBlock($pdf, $x, $y, $width, $label, array $lines, $labelSize = 6, $textSize = 8, $lineHeight = 3.5)
+ {
+ $pdf->SetFont('helvetica', 'B', $labelSize);
+ $pdf->SetXY($x, $y);
+ $pdf->MultiCell($width, 3, $label, 0, 'L', false, 1);
+ $pdf->SetFont('helvetica', '', $textSize);
+
+ foreach ($lines as $line) {
+ $line = trim((string)$line);
+ if ($line === '') {
+ continue;
+ }
+ $pdf->SetX($x);
+ $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);
@@ -600,170 +918,159 @@ private function GenerateQRString($iban, $amount, $user)
}
private function GetCalculatedPowerCosts($userId)
{
- $html = "
-
-
-
-
- | ID |
- Import (kWh) |
- Export (kWh) |
- ZEV-Haus (kWh) |
- Netz-Haus (kWh) |
- Solar-Netz (kWh) |
- Solar-ZEV (kWh) |
- Kauf Solar (CHF) |
- Kauf Netz (CHF) |
- Verkauf Netz(CHF) |
- Verkauf ZEV(CHF) |
- Total CHF |
-
";
-
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
- $html .= "| Keine Stromzähler für diesen Benutzer |
";
- return ['html' => $html, 'sum' => 0.0];
+ return [
+ 'html' => "| Keine Stromzähler für diesen Benutzer |
",
+ 'sum' => 0.0
+ ];
}
+ $energyRows = '';
+ $amountRows = '';
$sum = 0.0;
+ $totals = [
+ 'imp' => 0.0,
+ 'exp' => 0.0,
+ 'solar_bezug' => 0.0,
+ 'netz_bezug' => 0.0,
+ 'solareinspeisung' => 0.0,
+ 'solarverkauf' => 0.0,
+ 'cost_solar' => 0.0,
+ 'cost_grid' => 0.0,
+ 'rev_feedin' => 0.0,
+ 'rev_zev' => 0.0
+ ];
$rowIndex = 0;
foreach ($this->powerCostCache[$userId] as $name => $a) {
-
$subtotal = $a['cost_grid'] + $a['cost_solar'] - ($a['rev_feedin'] + $a['rev_zev']);
$sum += $subtotal;
- $rowClass = ($rowIndex % 2 === 0) ? "row-even" : "row-odd";
- $rowIndex++;
+ foreach ($totals as $key => $_) {
+ $totals[$key] += (float)($a[$key] ?? 0.0);
+ }
- // Datenzeile
- $html .= "
- | {$a['name']} |
- " . number_format($a['imp'], 3, '.', "'") . " |
- " . number_format($a['exp'], 3, '.', "'") . " |
- " . number_format($a['solar_bezug'], 3, '.', "'") . " |
- " . number_format($a['netz_bezug'], 3, '.', "'") . " |
- " . number_format($a['solareinspeisung'], 3, '.', "'") . " |
- " . number_format($a['solarverkauf'], 3, '.', "'") . " |
- " . number_format($a['cost_solar'], 2, '.', "'") . " |
- " . number_format($a['cost_grid'], 2, '.', "'") . " |
- " . number_format($a['rev_feedin'], 2, '.', "'") . " |
- " . number_format($a['rev_zev'], 2, '.', "'") . " |
- " . number_format($subtotal, 2, '.', "'") . " |
+ $rowStyle = ($rowIndex % 2 === 0) ? " style='background-color:#fbfbfb;'" : '';
+ $rowIndex++;
+ $label = $this->h($a['name'] ?? $name);
+
+ $energyRows .= "
+ | " . $label . " |
+ " . $this->FormatNumber($a['imp'], 3) . " |
+ " . $this->FormatNumber($a['exp'], 3) . " |
+ " . $this->FormatNumber($a['solar_bezug'], 3) . " |
+ " . $this->FormatNumber($a['netz_bezug'], 3) . " |
+ " . $this->FormatNumber($a['solareinspeisung'], 3) . " |
+ " . $this->FormatNumber($a['solarverkauf'], 3) . " |
";
- // Leerzeile
- $html .= " |
";
+ $amountRows .= "
+ | " . $label . " |
+ " . $this->FormatCurrency($a['cost_solar'], false) . " |
+ " . $this->FormatCurrency($a['cost_grid'], false) . " |
+ " . $this->FormatCurrency($a['rev_feedin'], false) . " |
+ " . $this->FormatCurrency($a['rev_zev'], false) . " |
+ " . $this->FormatCurrency($subtotal, false) . " |
+
";
}
- // Gesamttotal
- $html .= "
-
- | Total |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- " . number_format($sum, 2, '.', "'") . " |
+ $html = "
+
+
+ | Zählpunkt |
+ Import kWh |
+ Export kWh |
+ Solarbezug kWh |
+ Netzbezug kWh |
+ Einspeisung kWh |
+ ZEV-Verkauf kWh |
+
" . $energyRows . "
+
+ | Total |
+ " . $this->FormatNumber($totals['imp'], 3) . " |
+ " . $this->FormatNumber($totals['exp'], 3) . " |
+ " . $this->FormatNumber($totals['solar_bezug'], 3) . " |
+ " . $this->FormatNumber($totals['netz_bezug'], 3) . " |
+ " . $this->FormatNumber($totals['solareinspeisung'], 3) . " |
+ " . $this->FormatNumber($totals['solarverkauf'], 3) . " |
-
";
+
+
+
+
+ | Zählpunkt |
+ Kauf Solar CHF |
+ Kauf Netz CHF |
+ Gutschrift Netz CHF |
+ Gutschrift ZEV CHF |
+ Total CHF |
+
" . $amountRows . "
+
+ | Zwischentotal Elektrizität |
+ " . $this->FormatCurrency($sum, false) . " |
+
+
";
return ['html' => $html, 'sum' => $sum];
}
-
// ====================== Nebenkosten Wasser/Wärme ======================
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
{
$html = "
-
-
- | ID |
- Typ |
- Datum von |
- Datum bis |
- Zähler Start |
- Zähler Ende |
- Verbrauch |
- Tarif |
- Kosten CHF |
-
";
+
+
+ | Zähler |
+ Typ |
+ Von |
+ Bis |
+ Start |
+ Ende |
+ Verbrauch |
+ Tarif |
+ Kosten CHF |
+
";
- $total = 0.0;
+ $total = 0.0;
$usedTariffs = [];
+ $hasRows = false;
foreach ($waterMeters as $m) {
- if ($m['user_id'] != $userId) {
+ if (($m['user_id'] ?? null) != $userId) {
continue;
}
$type = $m['meter_type'] ?? 'Warmwasser';
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
+ if ($cost['row'] !== '') {
+ $hasRows = true;
+ }
$html .= $cost['row'];
$total += $cost['value'];
$usedTariffs = array_merge($usedTariffs, $cost['tariffs']);
}
- $html .= "
- | Total |
- |
- |
- |
- |
- |
- |
- |
- CHF " . number_format($total, 2) . ".- |
-
";
+ if (!$hasRows) {
+ $html .= "| Keine Nebenkostenzähler für diesen Benutzer |
";
+ }
+
+ $html .= "
+
+ | Zwischentotal Nebenkosten |
+ " . $this->FormatCurrency($total) . " |
+
+
";
if (!empty($usedTariffs)) {
- $html .= "Angewendete Nebenkostentarife
";
}
return ['html' => $html, 'sum' => $total];
@@ -771,74 +1078,78 @@ private function GenerateQRString($iban, $amount, $user)
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
- $rows = '';
- $totalCost = 0.0;
- $usedTariffs = [];
- $varId = $meter['var_consumption'];
-
- if (!IPS_VariableExists($varId)) {
+ $varId = (int)($meter['var_consumption'] ?? 0);
+ if ($varId <= 0 || !IPS_VariableExists($varId)) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
}
- // Relevante Tarife nach Typ filtern
- $filteredTariffs = array_filter($tariffs, fn($t) =>
- strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type))
- );
+ $filteredTariffs = array_filter($tariffs, function ($t) use ($type) {
+ return strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type));
+ });
+ $usedTariffs = [];
$activeTariff = null;
foreach ($filteredTariffs as $t) {
- $startTs = $this->toUnixTs($t['start'], false);
- $endTs = $this->toUnixTs($t['end'], true);
+ $startTs = $this->toUnixTs($t['start'] ?? 0, false);
+ $endTs = $this->toUnixTs($t['end'] ?? 0, true);
if ($startTs === null || $endTs === null) {
continue;
}
- // Tarif überlappt den Abrechnungszeitraum
if ($endTs < $from || $startTs > $to) {
continue;
}
$activeTariff = [
'start' => $startTs,
- 'end' => $endTs,
- 'price' => (float)$t['price']
+ 'end' => $endTs,
+ 'price' => (float)($t['price'] ?? 0),
+ 'unit_type' => $t['unit_type'] ?? $type
];
$usedTariffs[] = $activeTariff;
- break; // erster passender Tarif reicht
+ break;
}
$tariffPrice = $activeTariff ? $activeTariff['price'] : 0.0;
-
- // Verbrauch = letzter Wert vor/gleich "to" minus letzter Wert vor/gleich "from"
$startValue = $this->GetValueAt($varId, $from, false);
- $endValue = $this->GetValueAt($varId, $to, false);
+ $endValue = $this->GetValueAt($varId, $to, false);
if ($startValue === null || $endValue === null) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
}
$verbrauch = max(0, $endValue - $startValue);
- $kosten = round(($tariffPrice / 100) * $verbrauch, 2);
- $totalCost = $kosten;
+ $kosten = round(($tariffPrice / 100) * $verbrauch, 2);
- $rows .= "
- | {$meter['name']} |
- {$type} |
- " . date('d.m.Y', $from) . " |
- " . date('d.m.Y', $to) . " |
- " . number_format($startValue, 2) . " |
- " . number_format($endValue, 2) . " |
- " . number_format($verbrauch, 2) . " |
- " . number_format($tariffPrice, 2) . " |
- " . number_format($kosten, 2) . " |
+ $row = "
+ | " . $this->h($meter['name'] ?? '') . " |
+ " . $this->h($type) . " |
+ " . date('d.m.Y', $from) . " |
+ " . date('d.m.Y', $to) . " |
+ " . $this->FormatNumber($startValue, 2) . " |
+ " . $this->FormatNumber($endValue, 2) . " |
+ " . $this->FormatNumber($verbrauch, 2) . " |
+ " . number_format($tariffPrice, 3, '.', "'") . " |
+ " . $this->FormatCurrency($kosten, false) . " |
";
- if ($rows === '') {
- return ['row' => '', 'value' => 0, 'tariffs' => []];
- }
+ return ['row' => $row, 'value' => $kosten, 'tariffs' => array_values($usedTariffs)];
+ }
+ // ====================== Hilfsfunktionen ======================
- return ['row' => $rows, 'value' => $totalCost, 'tariffs' => array_values($usedTariffs)];
+ private function h($value)
+ {
+ return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
- // ====================== Hilfsfunktionen ======================
+ private function FormatNumber($value, $digits = 2)
+ {
+ return number_format((float)$value, $digits, '.', "'");
+ }
+
+ private function FormatCurrency($value, $withCurrency = true)
+ {
+ $amount = number_format((float)$value, 2, '.', "'");
+ return $withCurrency ? 'CHF ' . $amount : $amount;
+ }
private function GetValueAt($varId, $timestamp, $nearestAfter = true)
{
@@ -898,10 +1209,22 @@ private function GenerateQRString($iban, $amount, $user)
private function toUnixTs($val, $endOfDay = false)
{
if (is_int($val)) {
- return $val;
+ return $endOfDay ? strtotime(date('Y-m-d 23:59:59', $val)) : $val;
}
if (is_numeric($val)) {
- return (int)$val;
+ $ts = (int)$val;
+ return $endOfDay ? strtotime(date('Y-m-d 23:59:59', $ts)) : $ts;
+ }
+
+ if (is_array($val) && isset($val['year'], $val['month'], $val['day'])) {
+ $time = $endOfDay ? '23:59:59' : '00:00:00';
+ return strtotime(sprintf(
+ '%04d-%02d-%02d %s',
+ $val['year'],
+ $val['month'],
+ $val['day'],
+ $time
+ ));
}
if (is_string($val)) {
@@ -969,7 +1292,8 @@ private function GenerateQRString($iban, $amount, $user)
$result[] = [
'start' => $start,
'end' => $end,
- 'price' => (float)$t['price']
+ 'price' => (float)$t['price'],
+ 'unit_type' => $t['unit_type'] ?? ''
];
}
@@ -1006,4 +1330,4 @@ private function GenerateQRString($iban, $amount, $user)
return $path;
}
}
-?>
\ No newline at end of file
+?>