From 3a4bb00b5792111feae8070e5b3edd492dbd4dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Wed, 5 Nov 2025 14:29:00 +0100 Subject: [PATCH] no message --- Abrechnung/module.php | 446 +++++++++++++++++------------------------- 1 file changed, 183 insertions(+), 263 deletions(-) diff --git a/Abrechnung/module.php b/Abrechnung/module.php index ddc4fcb..6b8eccd 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -25,14 +25,14 @@ class Abrechnung extends IPSModule // Abrechnungs-Button $this->RegisterScript('StartBilling', 'Abrechnung starten', "InstanceID . ", 'StartBilling', ''); ?>"); - // PDF-Media-Objekt + // 🧾 Media-Objekt für PDF-Ergebnis $this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf'); } public function ApplyChanges() { parent::ApplyChanges(); - IPS_LogMessage('Abrechnung', '✅ Modul geladen'); + IPS_LogMessage('Abrechnung', 'Modul geladen'); } private function RegisterMediaDocument($Ident, $Name, $Extension, $Position = 0) @@ -50,6 +50,8 @@ class Abrechnung extends IPSModule public function RequestAction($Ident, $Value) { + IPS_LogMessage('Abrechnung', "RequestAction: $Ident"); + switch ($Ident) { case 'FromDate': case 'ToDate': @@ -57,26 +59,29 @@ class Abrechnung extends IPSModule break; case 'StartBilling': + IPS_LogMessage('Abrechnung', 'Starte Abrechnung...'); try { $pdfContent = $this->GenerateInvoices(); - if ($pdfContent && strlen($pdfContent) > 100) { - $mediaID = $this->GetIDForIdent('InvoicePDF'); - IPS_SetMediaContent($mediaID, base64_encode($pdfContent)); - SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung vom ' . date('d.m.Y H:i')); - echo "✅ PDF erfolgreich erstellt."; - } else { - echo "❌ Fehler bei der PDF-Erstellung."; + if (!$pdfContent) { + echo "❌ Fehler bei der PDF-Erstellung"; + return; } + + $mediaID = $this->GetIDForIdent('InvoicePDF'); + IPS_SetMediaContent($mediaID, base64_encode($pdfContent)); + SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung vom ' . date('d.m.Y H:i')); + IPS_LogMessage('Abrechnung', '✅ Abrechnung erfolgreich erstellt'); + echo "✅ PDF erfolgreich erstellt."; } catch (Throwable $e) { + IPS_LogMessage('Abrechnung', '💥 Fehler: ' . $e->getMessage()); echo "❌ Ausnahmefehler: " . $e->getMessage(); } break; } } - // =========================================================== - // 🧾 Hauptlogik: Rechnungserstellung für alle Benutzer - // =========================================================== + // ====================== PDF-Erstellung ====================== + public function GenerateInvoices() { $from = GetValue($this->GetIDForIdent('FromDate')); @@ -87,7 +92,10 @@ class Abrechnung extends IPSModule $water = json_decode($this->ReadPropertyString('WaterMeters'), true); $tariffs = json_decode($this->ReadPropertyString('Tariffs'), true); - if (!class_exists('TCPDF')) return false; + if (!class_exists('TCPDF')) { + IPS_LogMessage('Abrechnung', '❌ TCPDF nicht gefunden'); + return false; + } $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); $pdf->SetCreator('IPSymcon Abrechnung'); @@ -95,121 +103,189 @@ class Abrechnung extends IPSModule $pdf->SetAutoPageBreak(true, 20); $pdf->SetFont('dejavusans', '', 9); - foreach ($users as $user) { - IPS_LogMessage('Abrechnung', '→ Erstelle Seite für Benutzer: ' . $user['name']); - $this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to); - } + foreach ($users as $user) { + $this->BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to); + } return $pdf->Output('Abrechnung.pdf', 'S'); } - // =========================================================== - // 🧱 Layout / PDF-Aufbau pro Benutzerseite - // =========================================================== - private function CalculatePowerCosts(array $powerMeters, array $tariffs, int $userId, int $from, int $to): array -{ - $html = " - - - - - - - - - - - "; - $total = 0.0; + private function BuildUserInvoice($pdf, array $user, array $power, array $water, array $tariffs, int $from, int $to) + { + $pdf->AddPage(); - foreach ($powerMeters as $m) { - if ($m['user_id'] != $userId) continue; - $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, 'Strombezug'); - $html .= $cost['row']; - $total += $cost['value']; + $html = " +

Rechnung fĂĽr {$user['name']}

+

{$user['address']}
{$user['city']}

+

Zeitraum: " . date('d.m.Y', $from) . " – " . date('d.m.Y', $to) . "


"; + + // ⚡ Stromkosten + $html .= "

⚡ Stromkosten

"; + $powerResult = $this->CalculatePowerCosts($power, $tariffs, $user['id'], $from, $to); + $html .= $powerResult['html']; + $totalPower = $powerResult['sum']; + + // đź’§ Nebenkosten + $html .= "

💧 Nebenkosten (Wasser/Wärme)

"; + $additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to); + $html .= $additionalResult['html']; + $totalAdditional = $additionalResult['sum']; + + $grandTotal = $totalPower + $totalAdditional; + $html .= "

Gesamttotal: " . number_format($grandTotal, 2) . " CHF

"; + + $pdf->writeHTML($html, true, false, true, false, ''); } - $html .= " - + // ====================== Stromkosten ====================== + + private function CalculatePowerCosts(array $powerMeters, array $tariffs, int $userId, int $from, int $to): array + { + $html = "
ZählerTypStartEndeZählerstand StartZählerstand EndeVerbrauchTarif (Rp)Kosten (CHF)
+ + + + "; + + $total = 0.0; + foreach ($powerMeters as $m) { + if ($m['user_id'] != $userId) continue; + $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, 'Strombezug'); + $html .= $cost['row']; + $total += $cost['value']; + } + + $html .= " - -
ZählerTypStartEndeZähler StartZähler EndeVerbrauchTarif (Rp)Kosten (CHF)
Total Stromkosten: " . number_format($total, 2) . "

"; +
"; - return ['html' => $html, 'sum' => $total]; -} - -private function CalculatePowerCosts(array $powerMeters, array $tariffs, int $userId, int $from, int $to): array -{ - $html = " - - - - - - - - - - - "; - $total = 0.0; - - foreach ($powerMeters as $m) { - if ($m['user_id'] != $userId) continue; - $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, 'Strombezug'); - $html .= $cost['row']; - $total += $cost['value']; + return ['html' => $html, 'sum' => $total]; } - $html .= " - - - - -
ZählerTypStartEndeZählerstand StartZählerstand EndeVerbrauchTarif (Rp)Kosten (CHF)
Total Stromkosten:" . number_format($total, 2) . "

"; + // ====================== Nebenkosten ====================== - return ['html' => $html, 'sum' => $total]; -} - -private function CalculateAdditionalCosts(array $waterMeters, array $tariffs, int $userId, int $from, int $to): array -{ - $html = " + private function CalculateAdditionalCosts(array $waterMeters, array $tariffs, int $userId, int $from, int $to): array + { + $html = "
- - - - - - - - - + + "; - $total = 0.0; - foreach ($waterMeters as $m) { - if ($m['user_id'] != $userId) continue; - $type = $m['meter_type'] ?? 'Warmwasser'; - $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type); - $html .= $cost['row']; - $total += $cost['value']; - } + $total = 0.0; + foreach ($waterMeters as $m) { + if ($m['user_id'] != $userId) continue; + $type = $m['meter_type'] ?? 'Warmwasser'; + $cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type); + $html .= $cost['row']; + $total += $cost['value']; + } - $html .= " - + $html .= " - -
ZählerTypStartEndeZählerstand StartZählerstand EndeVerbrauchTarif (Rp)Kosten (CHF)ZählerTypStartEndeZähler StartZähler EndeVerbrauchTarif (Rp)Kosten (CHF)
Total Nebenkosten: " . number_format($total, 2) . "

"; +
"; - return ['html' => $html, 'sum' => $total]; -} + return ['html' => $html, 'sum' => $total]; + } + // ====================== Kernberechnung ====================== + + private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type) + { + $rows = ''; + $totalCost = 0.0; + $varId = $meter['var_consumption']; + + if (!IPS_VariableExists($varId)) return ['row' => '', 'value' => 0]; + + date_default_timezone_set('Europe/Zurich'); + + $filteredTariffs = array_filter($tariffs, fn($t) => + strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type)) + ); + + if (empty($filteredTariffs)) return ['row' => '', 'value' => 0]; + + foreach ($filteredTariffs as &$t) { + $t['start_ts'] = $this->toUnixTs($t['start'], false); + $t['end_ts'] = $this->toUnixTs($t['end'], true); + } + unset($t); + usort($filteredTariffs, fn($a, $b) => ($a['start_ts'] ?? 0) <=> ($b['start_ts'] ?? 0)); + + $currentStart = $from; + while ($currentStart < $to) { + $activeTariff = null; + foreach ($filteredTariffs as $t) { + if (($t['start_ts'] ?? 0) <= $currentStart && $currentStart <= ($t['end_ts'] ?? 0)) { + $activeTariff = $t; + break; + } + } + + if (!$activeTariff) { + $activeTariff = ['start_ts' => $currentStart, 'end_ts' => $to, 'price' => 0.0]; + } + + $tariffEnd = intval($activeTariff['end_ts']); + $tariffPrice = floatval($activeTariff['price']); + + $startValue = $this->GetValueAt($varId, $currentStart, true); + $segmentEnd = ($tariffEnd < $to) ? $tariffEnd : $to; + $endValue = $this->GetValueAt($varId, $segmentEnd, true); + + if ($startValue === null || $endValue === null) break; + + $verbrauch = max(0, $endValue - $startValue); + $kosten = round(($tariffPrice / 100) * $verbrauch, 2); + $totalCost += $kosten; + + $rows .= " + {$meter['name']} + {$type} + " . date('d.m.Y', $currentStart) . " + " . date('d.m.Y', $segmentEnd) . " + " . number_format($startValue, 2) . " + " . number_format($endValue, 2) . " + " . number_format($verbrauch, 2) . " + " . number_format($tariffPrice, 2) . " + " . number_format($kosten, 2) . " + "; + + if ($tariffEnd < $to) $currentStart = $tariffEnd + 1; + else break; + } + + if ($rows === '') return ['row' => '', 'value' => 0]; + + return ['row' => $rows, 'value' => $totalCost]; + } + + // ====================== Hilfsfunktionen ====================== + + private function GetValueAt(int $varId, int $timestamp, bool $nearestAfter = true) + { + $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; + if (!$archiveID || !IPS_VariableExists($varId)) return null; + + $values = @AC_GetLoggedValues($archiveID, $varId, $timestamp - 30 * 86400, $timestamp + 30 * 86400, 0); + if (empty($values)) return floatval(GetValue($varId)); + + $closest = null; + foreach ($values as $v) { + if ($nearestAfter && $v['TimeStamp'] >= $timestamp) { + $closest = $v['Value']; + break; + } elseif (!$nearestAfter && $v['TimeStamp'] <= $timestamp) { + $closest = $v['Value']; + } + } + + return $closest ?? floatval(GetValue($varId)); + } - // =========================================================== - // 🧮 Datumskonvertierung – robust für JSON, String, Timestamp - // =========================================================== private function toUnixTs($val, bool $endOfDay = false): ?int { if (is_int($val)) return $val; @@ -219,7 +295,7 @@ private function CalculateAdditionalCosts(array $waterMeters, array $tariffs, in $s = trim($val); if ($s !== '' && $s[0] === '{') { $obj = json_decode($s, true); - if (is_array($obj) && isset($obj['year'], $obj['month'], $obj['day'])) { + if (isset($obj['year'], $obj['month'], $obj['day'])) { $time = $endOfDay ? '23:59:59' : '00:00:00'; return strtotime(sprintf('%04d-%02d-%02d %s', $obj['year'], $obj['month'], $obj['day'], $time)); } @@ -229,160 +305,4 @@ private function CalculateAdditionalCosts(array $waterMeters, array $tariffs, in } return null; } - - // =========================================================== - // 🔍 Archivwertsuche (deine getestete Version) - // =========================================================== - private function GetValueAt(int $varId, int $timestamp, bool $nearestAfter = true) - { - $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0]; - if (!$archiveID || !IPS_VariableExists($varId)) return null; - - $maxDays = 365; - $stepDays = 30; - $valueFound = null; - - if ($nearestAfter) { - for ($offset = 0; $offset < $maxDays; $offset += $stepDays) { - $from = $timestamp + $offset * 86400; - $to = $from + $stepDays * 86400; - $values = @AC_GetLoggedValues($archiveID, $varId, $from, $to, 0); - if (!empty($values)) { - foreach ($values as $v) { - if ($v['TimeStamp'] >= $timestamp) { - return floatval($v['Value']); - } - } - } - } - } else { - for ($offset = 0; $offset < $maxDays; $offset += $stepDays) { - $from = $timestamp - ($offset + $stepDays) * 86400; - $to = $timestamp - $offset * 86400; - $values = @AC_GetLoggedValues($archiveID, $varId, $from, $to, 0); - if (!empty($values)) { - $last = end($values); - return floatval($last['Value']); - } - } - } - - return floatval(GetValue($varId)); - } - -private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type) -{ - $rows = ''; - $totalCost = 0.0; - $varId = $meter['var_consumption']; - - if (!IPS_VariableExists($varId)) { - IPS_LogMessage('Abrechnung', "❌ Variable {$varId} für {$meter['name']} nicht gefunden"); - return ['row' => '', 'value' => 0]; - } - - date_default_timezone_set('Europe/Zurich'); - - // 🔹 Filtere relevante Tarife nach Typ - $filteredTariffs = array_filter($tariffs, function ($t) use ($type) { - return strtolower(trim($t['unit_type'] ?? '')) === strtolower(trim($type)); - }); - - if (empty($filteredTariffs)) { - IPS_LogMessage('Abrechnung', "⚠ Keine passenden Tarife für {$type} gefunden"); - return ['row' => '', 'value' => 0]; - } - - // 🔹 Normiere Zeiträume - foreach ($filteredTariffs as &$t) { - $t['start_ts'] = $this->toUnixTs($t['start'], false); - $t['end_ts'] = $this->toUnixTs($t['end'], true); - } - unset($t); - usort($filteredTariffs, fn($a, $b) => ($a['start_ts'] ?? 0) <=> ($b['start_ts'] ?? 0)); - - $currentStart = $from; - - while ($currentStart < $to) { - // 🔹 Aktiven Tarif suchen - $activeTariff = null; - foreach ($filteredTariffs as $t) { - if (($t['start_ts'] ?? 0) <= $currentStart && $currentStart <= ($t['end_ts'] ?? 0)) { - $activeTariff = $t; - break; - } - } - - if (!$activeTariff) { - $activeTariff = [ - 'start_ts' => $currentStart, - 'end_ts' => $to, - 'price' => 0.0, - 'unit_type'=> $type - ]; - } - - $tariffEnd = intval($activeTariff['end_ts']); - $tariffPrice = floatval($activeTariff['price']); - $tariffLabel = number_format($tariffPrice, 2, ',', ''); - - // 🔹 Werte aus Archiv lesen - $startValue = $this->GetValueAt($varId, $currentStart, true); - $segmentEnd = ($tariffEnd < $to) ? $tariffEnd : $to; - $endValue = $this->GetValueAt($varId, $segmentEnd, true); - - if ($startValue === null || $endValue === null) break; - - $verbrauch = max(0, $endValue - $startValue); - $kosten = round(($tariffPrice / 100) * $verbrauch, 2); - $totalCost += $kosten; - - // 🔹 Kurze Datumsangaben - $startDate = date('d.m.Y', $currentStart); - $endDate = date('d.m.Y', $segmentEnd); - - // 🔹 Tabellenzeile mit sichtbaren Linien - $rows .= " - - {$meter['name']} - {$type} - {$startDate} - {$endDate} - " . number_format($startValue, 2) . " - " . number_format($endValue, 2) . " - " . number_format($verbrauch, 2) . " - " . $tariffLabel . " - " . number_format($kosten, 2) . " - "; - - if ($tariffEnd < $to) { - $currentStart = $tariffEnd + 1; - } else { - break; - } - } - - // 🔹 Fallback – kein Segment gefunden - if ($rows === '') { - $startVal = $this->GetValueAt($varId, $from, false); - $endVal = $this->GetValueAt($varId, $to, true); - $verbrauch = max(0, $endVal - $startVal); - $rows = " - - {$meter['name']} - {$type} - " . date('d.m.Y', $from) . " - " . date('d.m.Y', $to) . " - " . number_format($startVal, 2) . " - " . number_format($endVal, 2) . " - " . number_format($verbrauch, 2) . " - 0.00 - 0.00 - "; - } - - return ['row' => $rows, 'value' => $totalCost]; } - - -} \ No newline at end of file