Files
Symcon_Belevo_Energiemanage…/Abrechnung/module.php
2025-11-05 14:19:06 +01:00

413 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
include_once __DIR__ . '/libs/vendor/autoload.php'; // TCPDF via Composer
class Abrechnung extends IPSModule
{
public function Create()
{
parent::Create();
// Eigenschaften
$this->RegisterPropertyString('Users', '[]');
$this->RegisterPropertyString('PowerMeters', '[]');
$this->RegisterPropertyString('WaterMeters', '[]');
$this->RegisterPropertyString('Tariffs', '[]');
// Variablen
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
$this->RegisterVariableInteger('ToDate', 'Enddatum', '~UnixTimestamp', 2);
$this->RegisterVariableString('LastResult', 'Letzte Abrechnung', '', 3);
$this->EnableAction('FromDate');
$this->EnableAction('ToDate');
// Abrechnungs-Button
$this->RegisterScript('StartBilling', 'Abrechnung starten', "<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>");
// PDF-Media-Objekt
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
}
public function ApplyChanges()
{
parent::ApplyChanges();
IPS_LogMessage('Abrechnung', '✅ Modul geladen');
}
private function RegisterMediaDocument($Ident, $Name, $Extension, $Position = 0)
{
$mid = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID);
if ($mid === false) {
$mid = IPS_CreateMedia(5); // 5 = Document
IPS_SetParent($mid, $this->InstanceID);
IPS_SetIdent($mid, $Ident);
IPS_SetName($mid, $Name);
IPS_SetPosition($mid, $Position);
IPS_SetMediaFile($mid, 'media/' . $mid . '.' . $Extension, false);
}
}
public function RequestAction($Ident, $Value)
{
switch ($Ident) {
case 'FromDate':
case 'ToDate':
SetValue($this->GetIDForIdent($Ident), $Value);
break;
case 'StartBilling':
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.";
}
} catch (Throwable $e) {
echo "❌ Ausnahmefehler: " . $e->getMessage();
}
break;
}
}
// ===========================================================
// 🧾 Hauptlogik: Rechnungserstellung für alle Benutzer
// ===========================================================
public function GenerateInvoices()
{
$from = GetValue($this->GetIDForIdent('FromDate'));
$to = GetValue($this->GetIDForIdent('ToDate'));
$users = json_decode($this->ReadPropertyString('Users'), true);
$power = json_decode($this->ReadPropertyString('PowerMeters'), true);
$water = json_decode($this->ReadPropertyString('WaterMeters'), true);
$tariffs = json_decode($this->ReadPropertyString('Tariffs'), true);
if (!class_exists('TCPDF')) return false;
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->SetCreator('IPSymcon Abrechnung');
$pdf->SetMargins(15, 15, 15);
$pdf->SetAutoPageBreak(true, 20);
$pdf->SetFont('dejavusans', '', 9);
foreach ($users as $user) {
$pdf->AddPage();
$html = $this->BuildUserInvoice($user, $power, $water, $tariffs, $from, $to);
$pdf->writeHTML($html);
}
return $pdf->Output('Abrechnung.pdf', 'S');
}
// ===========================================================
// 🧱 Layout / PDF-Aufbau pro Benutzerseite
// ===========================================================
private function BuildUserInvoice($user, $power, $water, $tariffs, $from, $to)
{
$stromRows = '';
$stromTotal = 0.0;
$stromTariffsUsed = [];
$nebenRows = '';
$nebenTotal = 0.0;
$nebenTariffsUsed = [];
// ---------- Stromkosten ----------
foreach ($power as $m) {
if ($m['user_id'] != $user['id']) continue;
$res = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, 'Strombezug');
$stromRows .= $res['row'];
$stromTotal += $res['value'];
}
// ---------- Nebenkosten ----------
foreach ($water as $m) {
if ($m['user_id'] != $user['id']) continue;
$type = $m['meter_type'] ?? 'Warmwasser';
$res = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
$nebenRows .= $res['row'];
$nebenTotal += $res['value'];
}
// ---------- Alle Tarife gruppieren ----------
$tariffGroups = [];
foreach ($tariffs as $t) {
$type = ucfirst(strtolower($t['unit_type'] ?? 'Unbekannt'));
$sTs = $this->toUnixTs($t['start'], false);
$eTs = $this->toUnixTs($t['end'], true);
if ($sTs && $eTs) {
$period = date('d.m.Y', $sTs) . ' ' . date('d.m.Y', $eTs);
$price = number_format(floatval($t['price']), 2);
$tariffGroups[$type][] = "{$period}: {$price} Rp";
}
}
// ---------- Gesamtsummen ----------
$gesamtTotal = $stromTotal + $nebenTotal;
// ---------- Tabellenstil ----------
$tableStyle = "border-collapse:collapse;width:100%;font-size:8px;";
$cellStyle = "border:0.3px solid #bbb;padding:3px;vertical-align:middle;";
// ---------- Kopfbereich ----------
$html = "
<h2 style='text-align:center;'>Rechnung für {$user['name']}</h2>
<p>{$user['address']}<br>{$user['city']}</p>
<p><b>Abrechnungszeitraum:</b> " . date('d.m.Y', $from) . " " . date('d.m.Y', $to) . "</p>";
// ---------- Stromkosten ----------
$html .= "
<h3 style='margin-top:10px;color:#004085;'>⚡ Stromkosten</h3>
<table style='{$tableStyle}'>
<tr style='background-color:#e0e0e0;text-align:center;'>
<th style='{$cellStyle}'>Zähler</th>
<th style='{$cellStyle}'>Typ</th>
<th style='{$cellStyle}'>Startzeit</th>
<th style='{$cellStyle}'>Endzeit</th>
<th style='{$cellStyle}'>Start</th>
<th style='{$cellStyle}'>Ende</th>
<th style='{$cellStyle}'>Verbrauch</th>
<th style='{$cellStyle}'>Tarif (Rp)</th>
<th style='{$cellStyle}'>Kosten (CHF)</th>
</tr>
{$stromRows}
<tr style='border-top:1px solid black;font-weight:bold;background-color:#f8f9fa;'>
<td colspan='8' style='text-align:right;{$cellStyle}'>Total Stromkosten:</td>
<td style='{$cellStyle}text-align:right;'>" . number_format($stromTotal, 2) . "</td>
</tr>
</table>";
// ---------- Nebenkosten ----------
$html .= "
<h3 style='margin-top:15px;color:#6c757d;'>💧 Nebenkosten (Wasser & Wärme)</h3>
<table style='{$tableStyle}'>
<tr style='background-color:#e0e0e0;text-align:center;'>
<th style='{$cellStyle}'>Zähler</th>
<th style='{$cellStyle}'>Typ</th>
<th style='{$cellStyle}'>Startzeit</th>
<th style='{$cellStyle}'>Endzeit</th>
<th style='{$cellStyle}'>Start</th>
<th style='{$cellStyle}'>Ende</th>
<th style='{$cellStyle}'>Verbrauch</th>
<th style='{$cellStyle}'>Tarif (Rp)</th>
<th style='{$cellStyle}'>Kosten (CHF)</th>
</tr>
{$nebenRows}
<tr style='border-top:1px solid black;font-weight:bold;background-color:#f8f9fa;'>
<td colspan='8' style='text-align:right;{$cellStyle}'>Total Nebenkosten:</td>
<td style='{$cellStyle}text-align:right;'>" . number_format($nebenTotal, 2) . "</td>
</tr>
</table>";
// ---------- Tarife (alle Typen) ----------
if (!empty($tariffGroups)) {
$html .= "<h4 style='margin-top:15px;'>📋 Angewendete Tarife</h4><table style='{$tableStyle}'>";
foreach ($tariffGroups as $type => $entries) {
$html .= "<tr><td colspan='2' style='{$cellStyle}background-color:#efefef;'><b>{$type}</b></td></tr>";
foreach ($entries as $line) {
$html .= "<tr><td colspan='2' style='{$cellStyle}border-top:none;'>{$line}</td></tr>";
}
}
$html .= "</table>";
}
// ---------- Gesamttotal ----------
$html .= "
<h3 style='text-align:right;margin-top:15px;background-color:#f1f1f1;padding:6px;border:0.5px solid #aaa;'>
💰 <u>Gesamttotal:</u> " . number_format($gesamtTotal, 2) . " CHF
</h3>";
return $html;
}
// ===========================================================
// 🧮 Datumskonvertierung robust für JSON, String, Timestamp
// ===========================================================
private function toUnixTs($val, bool $endOfDay = false): ?int
{
if (is_int($val)) return $val;
if (is_numeric($val)) return intval($val);
if (is_string($val)) {
$s = trim($val);
if ($s !== '' && $s[0] === '{') {
$obj = json_decode($s, true);
if (is_array($obj) && 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));
}
}
$ts = strtotime($s);
return $ts === false ? null : $ts;
}
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');
// 🔹 Passende Tarife nach Typ filtern
$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];
}
// 🔹 Zeitstempel konvertieren und sortieren
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,
'unit_type'=> $type
];
}
$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;
$startDate = date('d.m.Y', $currentStart);
$endDate = date('d.m.Y', $segmentEnd);
// 🧾 TCPDF-kompatible Tabellenzeile (sichtbare Linien)
$rows .= "
<tr nobr='true'>
<td align='left' border='1' style='padding:2px;'>{$meter['name']}</td>
<td align='left' border='1' style='padding:2px;'>{$type}</td>
<td align='center' border='1' style='padding:2px;'>{$startDate}</td>
<td align='center' border='1' style='padding:2px;'>{$endDate}</td>
<td align='right' border='1' style='padding:2px;'>" . number_format($startValue, 2) . "</td>
<td align='right' border='1' style='padding:2px;'>" . number_format($endValue, 2) . "</td>
<td align='right' border='1' style='padding:2px;'>" . number_format($verbrauch, 2) . "</td>
<td align='right' border='1' style='padding:2px;'>" . number_format($tariffPrice, 2) . "</td>
<td align='right' border='1' style='padding:2px;'>" . number_format($kosten, 2) . "</td>
</tr>";
if ($tariffEnd < $to) {
$currentStart = $tariffEnd + 1;
} else {
break;
}
}
// Fallback (keine Segmente)
if ($rows === '') {
$startVal = $this->GetValueAt($varId, $from, false);
$endVal = $this->GetValueAt($varId, $to, true);
$verbrauch = max(0, $endVal - $startVal);
$rows = "
<tr nobr='true'>
<td align='left' border='1'>{$meter['name']}</td>
<td align='left' border='1'>{$type}</td>
<td align='center' border='1'>" . date('d.m.Y', $from) . "</td>
<td align='center' border='1'>" . date('d.m.Y', $to) . "</td>
<td align='right' border='1'>" . number_format($startVal, 2) . "</td>
<td align='right' border='1'>" . number_format($endVal, 2) . "</td>
<td align='right' border='1'>" . number_format($verbrauch, 2) . "</td>
<td align='right' border='1'>0.00</td>
<td align='right' border='1'>0.00</td>
</tr>";
}
// 🧾 Totalsumme in separater, deutlich sichtbarer Zeile
$rows .= "
<tr nobr='true' style='font-weight:bold; background-color:#f0f0f0;'>
<td colspan='8' align='right' border='1' style='padding:4px;'>Total {$type}:</td>
<td align='right' border='1' style='padding:4px;'>" . number_format($totalCost, 2) . "</td>
</tr>";
// Komplett in eine sichtbare Tabelle einbetten (damit TCPDF Linien rendert)
$table = "<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:8px;'>{$rows}</table>";
return ['row' => $table, 'value' => $totalCost];
}
}