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 = "
-
- | Zähler |
- Typ |
- Start |
- Ende |
- Zählerstand Start |
- Zählerstand Ende |
- Verbrauch |
- Tarif (Rp) |
- Kosten (CHF) |
-
";
- $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ähler | Typ | Start | Ende |
+ Zähler Start | Zähler Ende | Verbrauch | Tarif (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 .= "
| 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 = "
-
- | Zähler |
- Typ |
- Start |
- Ende |
- Zählerstand Start |
- Zählerstand Ende |
- Verbrauch |
- Tarif (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'];
+ return ['html' => $html, 'sum' => $total];
}
- $html .= "
-
- | 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 = "
- | Zähler |
- Typ |
- Start |
- Ende |
- Zählerstand Start |
- Zählerstand Ende |
- Verbrauch |
- Tarif (Rp) |
- Kosten (CHF) |
+ Zähler | Typ | Start | Ende |
+ Zähler Start | Zähler Ende | Verbrauch | Tarif (Rp) | Kosten (CHF) |
";
- $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 .= "
| 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