no message

This commit is contained in:
2026-06-28 12:14:18 +02:00
parent dca9447480
commit 6950b924ac
+523 -253
View File
@@ -63,7 +63,7 @@ class Abrechnung extends IPSModule
$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->RegisterVariableString('VisuTariffOverview', 'Tarife bearbeiten', '~HTMLBox', 10);
$this->RegisterVariableInteger('VisuTariffType', 'Tarifart', 'BELEVO.TariffType', 11);
$this->RegisterVariableInteger('VisuTariffStart', 'Tarif Startdatum', '~UnixTimestamp', 12);
$this->RegisterVariableInteger('VisuTariffEnd', 'Tarif Enddatum', '~UnixTimestamp', 13);
@@ -81,16 +81,7 @@ class Abrechnung extends IPSModule
'Abrechnung starten',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>"
);
$this->RegisterScript(
'SaveVisuTariff',
'Tarif speichern',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'SaveVisuTariff', ''); ?>"
);
$this->RegisterScript(
'DeleteVisuTariff',
'Tarif löschen',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'DeleteVisuTariff', ''); ?>"
);
$this->EnsureVisuTariffObjects();
$this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf');
}
@@ -105,7 +96,7 @@ class Abrechnung extends IPSModule
private function EnsureVisuTariffObjects()
{
$this->RegisterVariableString('VisuTariffOverview', 'Tarifübersicht', '~HTMLBox', 10);
$this->RegisterVariableString('VisuTariffOverview', 'Tarife bearbeiten', '~HTMLBox', 10);
$this->RegisterVariableInteger('VisuTariffType', 'Tarifart', 'BELEVO.TariffType', 11);
$this->RegisterVariableInteger('VisuTariffStart', 'Tarif Startdatum', '~UnixTimestamp', 12);
$this->RegisterVariableInteger('VisuTariffEnd', 'Tarif Enddatum', '~UnixTimestamp', 13);
@@ -117,16 +108,17 @@ class Abrechnung extends IPSModule
$this->EnableAction('VisuTariffEnd');
$this->EnableAction('VisuTariffPrice');
$this->RegisterScript(
'SaveVisuTariff',
'Tarif speichern',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'SaveVisuTariff', ''); ?>"
);
$this->RegisterScript(
'DeleteVisuTariff',
'Tarif löschen',
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'DeleteVisuTariff', ''); ?>"
);
foreach (['VisuTariffType', 'VisuTariffStart', 'VisuTariffEnd', 'VisuTariffPrice', 'VisuTariffStatus', 'SaveVisuTariff', 'DeleteVisuTariff'] as $ident) {
$this->HideObjectByIdent($ident);
}
}
private function HideObjectByIdent($Ident)
{
$id = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID);
if ($id !== false) {
IPS_SetHidden($id, true);
}
}
private function RegisterTariffVariableProfiles()
@@ -329,34 +321,183 @@ class Abrechnung extends IPSModule
return;
}
$types = $this->GetTariffTypes();
$tariffs = $this->ReadTariffs();
$html = "<table width='100%' cellspacing='0' cellpadding='5' style='border-collapse:collapse;font-size:12px;'>
<tr style='background-color:#f0f0f0;font-weight:bold;'>
<td width='28%' style='border:1px solid #ccc;'>Tarifart</td>
<td width='22%' style='border:1px solid #ccc;'>Gültig von</td>
<td width='22%' style='border:1px solid #ccc;'>Gültig bis</td>
<td width='28%' style='border:1px solid #ccc;text-align:right;'>Preis</td>
</tr>";
$rows = '';
if (empty($tariffs)) {
$html .= "<tr><td colspan='4' style='border:1px solid #ccc;'>Keine Tarife erfasst.</td></tr>";
} else {
foreach ($tariffs as $tariff) {
$start = $this->toUnixTs($tariff['start'] ?? 0, false);
$end = $this->toUnixTs($tariff['end'] ?? 0, false);
$html .= "<tr>
<td style='border:1px solid #ddd;'>" . $this->h($tariff['unit_type'] ?? '') . "</td>
<td style='border:1px solid #ddd;'>" . ($start ? date('d.m.Y', $start) : '-') . "</td>
<td style='border:1px solid #ddd;'>" . ($end ? date('d.m.Y', $end) : '-') . "</td>
<td align='right' style='border:1px solid #ddd;'>" . number_format((float)($tariff['price'] ?? 0), 3, '.', "'") . " Rp/Einheit</td>
</tr>";
}
foreach ($tariffs as $tariff) {
$rows .= $this->BuildTariffEditorRow(
(string)($tariff['unit_type'] ?? 'Netztarif'),
$this->FormatDateInput($this->toUnixTs($tariff['start'] ?? 0, false)),
$this->FormatDateInput($this->toUnixTs($tariff['end'] ?? 0, false)),
number_format((float)($tariff['price'] ?? 0), 3, '.', '')
);
}
$html .= '</table>';
if ($rows === '') {
$rows = $this->BuildTariffEditorRow('Netztarif', date('Y-01-01'), date('Y-12-31'), '0.000');
}
$typeOptions = '';
foreach ($types as $type) {
$typeOptions .= '<option value="' . $this->h($type) . '">' . $this->h($type) . '</option>';
}
$instanceID = (int)$this->InstanceID;
$status = $this->h(@GetValue($this->GetIDForIdent('VisuTariffStatus')));
$html = "
<div class='belevo-tariffs' data-instance='" . $instanceID . "'>
<style>
.belevo-tariffs{font-family:Arial,Helvetica,sans-serif;font-size:13px;color:#202124;max-width:980px}
.belevo-tariffs table{width:100%;border-collapse:collapse;background:#fff}
.belevo-tariffs th{background:#f3f4f6;border:1px solid #d6d9de;text-align:left;padding:7px;font-weight:700}
.belevo-tariffs td{border:1px solid #e0e2e6;padding:5px;vertical-align:middle}
.belevo-tariffs input,.belevo-tariffs select{width:100%;box-sizing:border-box;border:1px solid #c7cbd1;border-radius:3px;padding:5px;font:inherit;background:#fff}
.belevo-tariffs .num{text-align:right}
.belevo-tariffs .actions{display:flex;gap:8px;margin-top:10px;align-items:center}
.belevo-tariffs button{border:1px solid #9aa0a6;background:#f8f9fa;border-radius:3px;padding:6px 10px;font:inherit;cursor:pointer}
.belevo-tariffs button.primary{background:#1f4e79;border-color:#1f4e79;color:#fff;font-weight:700}
.belevo-tariffs button.danger{color:#9b1c1c;border-color:#d7a6a6;background:#fff5f5}
.belevo-tariffs .status{margin-left:8px;color:#4b5563}
</style>
<table>
<thead>
<tr>
<th style='width:23%'>Tarifart</th>
<th style='width:20%'>Startdatum</th>
<th style='width:20%'>Enddatum</th>
<th style='width:22%'>Rp/Einheit</th>
<th style='width:15%'>Aktion</th>
</tr>
</thead>
<tbody>" . $rows . "</tbody>
</table>
<div class='actions'>
<button type='button' data-action='add'>Zeile hinzufügen</button>
<button type='button' data-action='save' class='primary'>Tarife speichern</button>
<span class='status'>" . $status . "</span>
</div>
<template data-row-template>" . $this->BuildTariffEditorRow('Netztarif', date('Y-01-01'), date('Y-12-31'), '0.000') . "</template>
<script>
(function(){
var root=document.currentScript.closest('.belevo-tariffs');
if(!root||root.dataset.bound==='1'){return;}
root.dataset.bound='1';
var tbody=root.querySelector('tbody');
var status=root.querySelector('.status');
function bindDelete(row){
var btn=row.querySelector('[data-action=delete]');
if(btn){btn.onclick=function(){row.parentNode.removeChild(row);};}
}
Array.prototype.forEach.call(tbody.querySelectorAll('tr'),bindDelete);
root.querySelector('[data-action=add]').onclick=function(){
var tpl=root.querySelector('template[data-row-template]').innerHTML;
var temp=document.createElement('tbody');
temp.innerHTML=tpl;
var row=temp.querySelector('tr');
tbody.appendChild(row);
bindDelete(row);
};
root.querySelector('[data-action=save]').onclick=function(){
var rows=[];
Array.prototype.forEach.call(tbody.querySelectorAll('tr'),function(row){
var type=row.querySelector('[data-field=unit_type]').value;
var start=row.querySelector('[data-field=start]').value;
var end=row.querySelector('[data-field=end]').value;
var price=row.querySelector('[data-field=price]').value.replace(',', '.');
if(type&&start&&end&&price!==''){
rows.push({unit_type:type,start:start,end:end,price:price});
}
});
status.textContent='Speichern...';
fetch('/api/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',method:'IPS_RequestAction',params:[" . $instanceID . ",'SaveTariffTable',JSON.stringify(rows)],id:Date.now()})})
.then(function(r){return r.json();})
.then(function(j){
if(j.error){throw new Error(j.error.message||'API-Fehler');}
status.textContent=(typeof j.result==='string'&&j.result)?j.result:'Gespeichert.';
})
.catch(function(e){status.textContent='Fehler: '+e.message;});
};
})();
</script>
</div>";
SetValue($overviewID, $html);
}
private function BuildTariffEditorRow($type, $start, $end, $price)
{
$options = '';
foreach ($this->GetTariffTypes() as $tariffType) {
$selected = strtolower($tariffType) === strtolower((string)$type) ? ' selected' : '';
$options .= '<option value="' . $this->h($tariffType) . '"' . $selected . '>' . $this->h($tariffType) . '</option>';
}
return '<tr>'
. '<td><select data-field="unit_type">' . $options . '</select></td>'
. '<td><input data-field="start" type="date" value="' . $this->h($start) . '"></td>'
. '<td><input data-field="end" type="date" value="' . $this->h($end) . '"></td>'
. '<td><input data-field="price" class="num" type="number" min="0" step="0.001" value="' . $this->h($price) . '"></td>'
. '<td><button type="button" class="danger" data-action="delete">Löschen</button></td>'
. '</tr>';
}
private function SaveTariffTableFromPayload($payload)
{
$rows = json_decode((string)$payload, true);
if (!is_array($rows)) {
throw new Exception('Ungültige Tarifdaten.');
}
$allowedTypes = $this->GetTariffTypes();
$tariffs = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$type = trim((string)($row['unit_type'] ?? ''));
$startText = trim((string)($row['start'] ?? ''));
$endText = trim((string)($row['end'] ?? ''));
$priceText = str_replace(',', '.', trim((string)($row['price'] ?? '')));
if ($type === '' && $startText === '' && $endText === '' && $priceText === '') {
continue;
}
if (!in_array($type, $allowedTypes, true)) {
throw new Exception('Ungültige Tarifart: ' . $type);
}
if ($startText === '' || $endText === '') {
throw new Exception('Start- und Enddatum müssen gesetzt sein.');
}
if (!is_numeric($priceText) || (float)$priceText < 0) {
throw new Exception('Tarifpreis ungültig.');
}
$start = strtotime($startText . ' 00:00:00');
$end = strtotime($endText . ' 00:00:00');
if ($start === false || $end === false || $end < $start) {
throw new Exception('Datumsbereich ungültig.');
}
$tariffs[] = [
'start' => $start,
'end' => $end,
'price' => round((float)$priceText, 3),
'unit_type' => $type
];
}
$this->WriteTariffs($tariffs);
return count($tariffs) . ' Tarif(e) gespeichert.';
}
private function FormatDateInput($timestamp)
{
return $timestamp ? date('Y-m-d', (int)$timestamp) : '';
}
private function RegisterMediaDocument($Ident, $Name, $Extension, $Position = 0)
{
$mid = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID);
@@ -406,6 +547,18 @@ class Abrechnung extends IPSModule
}
break;
case 'SaveTariffTable':
try {
$message = $this->SaveTariffTableFromPayload($Value);
SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
echo $message;
} catch (Throwable $e) {
$message = 'Fehler beim Speichern der Tarifliste: ' . $e->getMessage();
SetValue($this->GetIDForIdent('VisuTariffStatus'), $message);
echo $message;
}
break;
case 'StartBilling':
try {
$pdfContent = $this->GenerateInvoices();
@@ -479,123 +632,327 @@ public function GenerateInvoices()
private function BuildUserInvoice($pdf, $user, $power, $water, $tariffs, $from, $to)
{
$pdf->SetTextColor(20, 20, 20);
$pdf->SetDrawColor(190, 190, 190);
$pdf->SetLineWidth(0.15);
$pdf->SetFont('dejavusans', 'B', 16);
$pdf->SetY(max($pdf->GetY(), 38));
$pdf->Cell(0, 8, 'Elektro- und Nebenkostenabrechnung', 0, 1, 'L');
$pdf->SetFont('dejavusans', '', 8.5);
$pdf->Cell(0, 5, 'Liegenschaft: ' . $this->ReadPropertyString('PropertyText'), 0, 1, 'L');
$pdf->Ln(3);
$meterNames = [];
foreach ($power as $m) {
if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
$meterNames[] = $this->h($m['name'] ?? '');
$meterNames[] = (string)($m['name'] ?? '');
}
}
foreach ($water as $m) {
if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
$meterNames[] = $this->h($m['name'] ?? '');
$meterNames[] = (string)($m['name'] ?? '');
}
}
$meterText = empty($meterNames) ? '-' : implode('<br>', $meterNames);
$periodText = date('d.m.Y', $from) . ' - ' . date('d.m.Y', $to);
$meterText = empty($meterNames) ? '-' : implode("\n", $meterNames);
$addressText = trim(($user['name'] ?? '') . "\n" . ($user['address'] ?? '') . "\n" . ($user['city'] ?? ''));
$html = "
<h1 style='text-align:center;font-size:18px;margin-bottom:4px;'>Elektro- und Nebenkostenabrechnung</h1>
<p style='text-align:center;font-size:10px;margin-top:0;'><strong>Liegenschaft:</strong> " . $this->h($this->ReadPropertyString('PropertyText')) . "</p>
<table width='100%' cellpadding='5' cellspacing='0' style='font-size:9px;border-collapse:collapse;'>
<tr>
<td width='56%' style='border:1px solid #d8d8d8;background-color:#fafafa;'>
<strong>Abrechnungszeitraum</strong><br>" . $periodText . "<br><br>
<strong>Zählpunkte</strong><br>" . $meterText . "
</td>
<td width='44%' style='border:1px solid #d8d8d8;'>
<strong>Rechnungsadresse</strong><br>
" . $this->h($user['name'] ?? '') . "<br>
" . $this->h($user['address'] ?? '') . "<br>
" . $this->h($user['city'] ?? '') . "
</td>
</tr>
</table>
<br>";
$y = $pdf->GetY();
$pdf->SetFillColor(248, 249, 250);
$pdf->Rect(15, $y, 88, 32, 'DF');
$pdf->Rect(107, $y, 88, 32, 'D');
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetXY(18, $y + 3);
$pdf->MultiCell(82, 4, "Abrechnungszeitraum", 0, 'L');
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetX(18);
$pdf->MultiCell(82, 4, $periodText, 0, 'L');
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetX(18);
$pdf->MultiCell(82, 4, "Zählpunkte", 0, 'L');
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetX(18);
$pdf->MultiCell(82, 4, $meterText, 0, 'L');
// ========================= Elektrizität =========================
$html .= "<h2 style='font-size:12px;margin-bottom:5px;'>Elektrizität</h2>";
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetXY(110, $y + 3);
$pdf->MultiCell(82, 4, "Rechnungsadresse", 0, 'L');
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetX(110);
$pdf->MultiCell(82, 4, $addressText, 0, 'L');
$pdf->SetY($y + 38);
$this->PdfSectionTitle($pdf, 'Elektrizität');
$powerResult = $this->GetCalculatedPowerCosts($user['id']);
$html .= $powerResult['html'];
$this->WritePowerPdfTables($pdf, $powerResult);
$totalPower = $powerResult['sum'];
$appliedTariffs = $this->CollectTariffsForUser($tariffs, ['Netztarif', 'Solartarif', 'Einspeisetarif']);
if (!empty($appliedTariffs)) {
$html .= "<p style='font-size:8px;margin-top:6px;'><strong>Angewendete Elektrizitätstarife</strong></p><ul style='font-size:8px;'>";
foreach ($appliedTariffs as $t) {
if ($t['start'] === null || $t['end'] === null) {
continue;
}
$html .= "<li>"
. $this->h($t['unit_type'] ?? '') . ': '
. date('d.m.Y', $t['start']) . ' - ' . date('d.m.Y', $t['end'])
. ' - <strong>' . number_format($t['price'], 3, '.', "'") . ' Rp/kWh</strong>'
. "</li>";
}
$html .= "</ul><br>";
}
// ========================= Nebenkosten =========================
$html .= "<h2 style='font-size:12px;margin-bottom:5px;'>Nebenkosten</h2>";
$this->WriteTariffPdfTable($pdf, 'Angewendete Elektrizitätstarife', $appliedTariffs, 'Rp/kWh');
$this->PdfSectionTitle($pdf, 'Nebenkosten');
$additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to);
$html .= $additionalResult['html'];
$this->WriteAdditionalPdfTable($pdf, $additionalResult);
$totalAdditional = $additionalResult['sum'];
$this->WriteTariffPdfTable($pdf, 'Angewendete Nebenkostentarife', $additionalResult['tariffs'], 'Rp/Einheit');
// ========================= Gesamttotal & MWST =========================
$subTotal = $totalPower + $totalAdditional;
$enableVAT = $this->ReadPropertyBoolean('EnableVAT');
$html .= "<br><table width='100%' style='font-size:10px;border-collapse:collapse;' cellpadding='5' cellspacing='0'>
<tr style='background-color:#f0f0f0;'>
<td colspan='2' style='border:1px solid #ccc;'><strong>Zusammenfassung</strong></td>
</tr>
<tr>
<td width='72%' style='border:1px solid #ddd;'>Zwischentotal Elektrizität</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($totalPower) . "</td>
</tr>
<tr>
<td width='72%' style='border:1px solid #ddd;'>Zwischentotal Nebenkosten</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($totalAdditional) . "</td>
</tr>";
$summaryRows = [
['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 .= "
<tr>
<td width='72%' style='border:1px solid #ddd;'>Total exkl. MWST</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($subTotal) . "</td>
</tr>
<tr>
<td width='72%' style='border:1px solid #ddd;'>MWST " . number_format($vatPercent, 1, '.', "'") . " %</td>
<td width='28%' align='right' style='border:1px solid #ddd;'>" . $this->FormatCurrency($vatAmount) . "</td>
</tr>
<tr style='background-color:#e9e9e9;'>
<td width='72%' style='border:1px solid #bbb;font-size:12px;'><strong>Gesamttotal inkl. MWST</strong></td>
<td width='28%' align='right' style='border:1px solid #bbb;font-size:12px;'><strong>" . $this->FormatCurrency($grandTotal) . "</strong></td>
</tr>
</table>";
$summaryRows[] = ['Total exkl. MWST', $this->FormatCurrency($subTotal)];
$summaryRows[] = ['MWST ' . number_format($vatPercent, 1, '.', "'") . ' %', $this->FormatCurrency($vatAmount)];
$summaryRows[] = ['Gesamttotal inkl. MWST', $this->FormatCurrency($grandTotal)];
} else {
$grandTotal = $subTotal;
$html .= "
<tr style='background-color:#e9e9e9;'>
<td width='72%' style='border:1px solid #bbb;font-size:12px;'><strong>Gesamttotal</strong></td>
<td width='28%' align='right' style='border:1px solid #bbb;font-size:12px;'><strong>" . $this->FormatCurrency($grandTotal) . "</strong></td>
</tr>
<tr>
<td colspan='2' align='right' style='font-size:8px;color:#555;border:1px solid #ddd;'>Nicht mehrwertsteuerpflichtig.</td>
</tr>
</table>";
$summaryRows[] = ['Gesamttotal', $this->FormatCurrency($grandTotal)];
}
$pdf->writeHTML($html, true, false, true, false, '');
$this->WriteSummaryPdfTable($pdf, $summaryRows);
if (!$enableVAT) {
$pdf->SetFont('dejavusans', '', 7.5);
$pdf->SetTextColor(80, 80, 80);
$pdf->Cell(0, 5, 'Nicht mehrwertsteuerpflichtig.', 0, 1, 'L');
$pdf->SetTextColor(20, 20, 20);
}
return $grandTotal;
}
private function PdfSectionTitle($pdf, $title)
{
$this->PdfEnsureSpace($pdf, 12);
$pdf->Ln(2);
$pdf->SetFont('dejavusans', 'B', 11);
$pdf->SetFillColor(235, 238, 242);
$pdf->Cell(0, 7, $title, 0, 1, 'L', true);
$pdf->Ln(1);
}
private function PdfEnsureSpace($pdf, $height)
{
if ($pdf->GetY() + $height > 276) {
$pdf->AddPage();
$pdf->SetY(38);
}
}
private function PdfTableHeader($pdf, array $columns)
{
$this->PdfEnsureSpace($pdf, 8);
$pdf->SetFont('dejavusans', 'B', 7.2);
$pdf->SetFillColor(231, 234, 238);
$values = array_map(function ($column) {
return $column['label'];
}, $columns);
$this->PdfTableRow($pdf, $columns, $values, true, true);
}
private function PdfTableRow($pdf, array $columns, array $values, $bold = false, $fill = false)
{
$pdf->SetFont('dejavusans', $bold ? 'B' : '', 7.2);
$lineHeights = [];
foreach ($columns as $index => $column) {
$lineHeights[] = $pdf->getStringHeight($column['width'] - 2, (string)($values[$index] ?? '')) + 2;
}
$height = max(5.5, max($lineHeights));
$this->PdfEnsureSpace($pdf, $height + 1);
$x = 15;
$y = $pdf->GetY();
$pdf->SetFillColor($fill ? 245 : 255, $fill ? 246 : 255, $fill ? 248 : 255);
foreach ($columns as $index => $column) {
$align = $column['align'] ?? 'L';
$pdf->MultiCell(
$column['width'],
$height,
(string)($values[$index] ?? ''),
1,
$align,
$fill,
0,
$x,
$y,
true,
0,
false,
true,
$height,
'M'
);
$x += $column['width'];
}
$pdf->SetY($y + $height);
}
private function WritePowerPdfTables($pdf, array $result)
{
$rows = $result['rows'];
if (empty($rows)) {
$this->PdfTableRow($pdf, [['width' => 180, 'align' => 'C']], ['Keine Stromzähler für diesen Benutzer.']);
return;
}
$energyColumns = [
['width' => 48, 'label' => 'Zählpunkt'],
['width' => 30, 'label' => 'Import kWh', 'align' => 'R'],
['width' => 30, 'label' => 'Export kWh', 'align' => 'R'],
['width' => 36, 'label' => 'Solarbezug kWh', 'align' => 'R'],
['width' => 36, 'label' => 'Netzbezug kWh', 'align' => 'R']
];
$this->PdfTableHeader($pdf, $energyColumns);
foreach ($rows as $row) {
$this->PdfTableRow($pdf, $energyColumns, [
$row['name'],
$this->FormatNumber($row['imp'], 3),
$this->FormatNumber($row['exp'], 3),
$this->FormatNumber($row['solar_bezug'], 3),
$this->FormatNumber($row['netz_bezug'], 3)
]);
}
$this->PdfTableRow($pdf, $energyColumns, [
'Total',
$this->FormatNumber($result['totals']['imp'], 3),
$this->FormatNumber($result['totals']['exp'], 3),
$this->FormatNumber($result['totals']['solar_bezug'], 3),
$this->FormatNumber($result['totals']['netz_bezug'], 3)
], true, true);
$pdf->Ln(2);
$exportColumns = [
['width' => 60, 'label' => 'Zählpunkt'],
['width' => 60, 'label' => 'Einspeisung Netz kWh', 'align' => 'R'],
['width' => 60, 'label' => 'ZEV-Verkauf kWh', 'align' => 'R']
];
$this->PdfTableHeader($pdf, $exportColumns);
foreach ($rows as $row) {
$this->PdfTableRow($pdf, $exportColumns, [
$row['name'],
$this->FormatNumber($row['solareinspeisung'], 3),
$this->FormatNumber($row['solarverkauf'], 3)
]);
}
$this->PdfTableRow($pdf, $exportColumns, [
'Total',
$this->FormatNumber($result['totals']['solareinspeisung'], 3),
$this->FormatNumber($result['totals']['solarverkauf'], 3)
], true, true);
$pdf->Ln(2);
$amountColumns = [
['width' => 42, 'label' => 'Zählpunkt'],
['width' => 25, 'label' => 'Solar CHF', 'align' => 'R'],
['width' => 25, 'label' => 'Netz CHF', 'align' => 'R'],
['width' => 31, 'label' => 'Gutschrift Netz', 'align' => 'R'],
['width' => 31, 'label' => 'Gutschrift ZEV', 'align' => 'R'],
['width' => 26, 'label' => 'Total CHF', 'align' => 'R']
];
$this->PdfTableHeader($pdf, $amountColumns);
foreach ($rows as $row) {
$this->PdfTableRow($pdf, $amountColumns, [
$row['name'],
$this->FormatCurrency($row['cost_solar'], false),
$this->FormatCurrency($row['cost_grid'], false),
$this->FormatCurrency($row['rev_feedin'], false),
$this->FormatCurrency($row['rev_zev'], false),
$this->FormatCurrency($row['subtotal'], false)
]);
}
$this->PdfTableRow($pdf, $amountColumns, [
'Zwischentotal Elektrizität', '', '', '', '', $this->FormatCurrency($result['sum'], false)
], true, true);
}
private function WriteAdditionalPdfTable($pdf, array $result)
{
$columns = [
['width' => 30, 'label' => 'Zähler'],
['width' => 20, 'label' => 'Typ'],
['width' => 30, 'label' => 'Zeitraum'],
['width' => 20, 'label' => 'Start', 'align' => 'R'],
['width' => 20, 'label' => 'Ende', 'align' => 'R'],
['width' => 20, 'label' => 'Verbrauch', 'align' => 'R'],
['width' => 18, 'label' => 'Tarif', 'align' => 'R'],
['width' => 22, 'label' => 'Kosten CHF', 'align' => 'R']
];
$this->PdfTableHeader($pdf, $columns);
if (empty($result['rows'])) {
$this->PdfTableRow($pdf, [['width' => 180, 'align' => 'C']], ['Keine Nebenkostenzähler für diesen Benutzer.']);
} else {
foreach ($result['rows'] as $row) {
$this->PdfTableRow($pdf, $columns, [
$row['name'],
$row['type'],
date('d.m.Y', $row['from']) . "\n" . date('d.m.Y', $row['to']),
$this->FormatNumber($row['start_value'], 2),
$this->FormatNumber($row['end_value'], 2),
$this->FormatNumber($row['consumption'], 2),
number_format($row['tariff_price'], 3, '.', "'"),
$this->FormatCurrency($row['cost'], false)
]);
}
}
$this->PdfTableRow($pdf, $columns, [
'Zwischentotal Nebenkosten', '', '', '', '', '', '', $this->FormatCurrency($result['sum'], false)
], true, true);
}
private function WriteTariffPdfTable($pdf, $title, array $tariffs, $unitLabel)
{
if (empty($tariffs)) {
return;
}
$pdf->Ln(2);
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->Cell(0, 5, $title, 0, 1, 'L');
$columns = [
['width' => 48, 'label' => 'Tarifart'],
['width' => 38, 'label' => 'Von'],
['width' => 38, 'label' => 'Bis'],
['width' => 32, 'label' => 'Preis', 'align' => 'R'],
['width' => 24, 'label' => 'Einheit']
];
$this->PdfTableHeader($pdf, $columns);
foreach ($tariffs as $tariff) {
if (($tariff['start'] ?? null) === null || ($tariff['end'] ?? null) === null) {
continue;
}
$this->PdfTableRow($pdf, $columns, [
$tariff['unit_type'] ?? '',
date('d.m.Y', $tariff['start']),
date('d.m.Y', $tariff['end']),
number_format((float)($tariff['price'] ?? 0), 3, '.', "'"),
$unitLabel
]);
}
}
private function WriteSummaryPdfTable($pdf, array $rows)
{
$this->PdfSectionTitle($pdf, 'Zusammenfassung');
$columns = [
['width' => 130, 'label' => 'Position'],
['width' => 50, 'label' => 'Betrag', 'align' => 'R']
];
foreach ($rows as $index => $row) {
$isTotal = $index === count($rows) - 1;
$this->PdfTableRow($pdf, $columns, [$row[0], $row[1]], $isTotal, $isTotal);
}
}
// ====================== QR-Einzahlungsschein (Schweiz) ======================
private function BuildQRBill($pdf, $user, $amount)
@@ -918,16 +1275,6 @@ public function GenerateInvoices()
}
private function GetCalculatedPowerCosts($userId)
{
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
return [
'html' => "<table border='1' cellspacing='0' cellpadding='4' width='100%' style='font-size:8px;'><tr><td align='center'>Keine Stromzähler für diesen Benutzer</td></tr></table><br>",
'sum' => 0.0
];
}
$energyRows = '';
$amountRows = '';
$sum = 0.0;
$totals = [
'imp' => 0.0,
'exp' => 0.0,
@@ -940,7 +1287,12 @@ public function GenerateInvoices()
'rev_feedin' => 0.0,
'rev_zev' => 0.0
];
$rowIndex = 0;
$rows = [];
$sum = 0.0;
if (empty($this->powerCostCache) || !isset($this->powerCostCache[$userId])) {
return ['rows' => [], 'totals' => $totals, 'sum' => 0.0];
}
foreach ($this->powerCostCache[$userId] as $name => $a) {
$subtotal = $a['cost_grid'] + $a['cost_solar'] - ($a['rev_feedin'] + $a['rev_zev']);
@@ -950,90 +1302,31 @@ public function GenerateInvoices()
$totals[$key] += (float)($a[$key] ?? 0.0);
}
$rowStyle = ($rowIndex % 2 === 0) ? " style='background-color:#fbfbfb;'" : '';
$rowIndex++;
$label = $this->h($a['name'] ?? $name);
$energyRows .= "<tr" . $rowStyle . ">
<td width='20%'>" . $label . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['imp'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['exp'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($a['solar_bezug'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($a['netz_bezug'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['solareinspeisung'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($a['solarverkauf'], 3) . "</td>
</tr>";
$amountRows .= "<tr" . $rowStyle . ">
<td width='24%'>" . $label . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['cost_solar'], false) . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['cost_grid'], false) . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['rev_feedin'], false) . "</td>
<td width='16%' align='right'>" . $this->FormatCurrency($a['rev_zev'], false) . "</td>
<td width='12%' align='right'><strong>" . $this->FormatCurrency($subtotal, false) . "</strong></td>
</tr>";
$rows[] = [
'name' => (string)($a['name'] ?? $name),
'imp' => (float)$a['imp'],
'exp' => (float)$a['exp'],
'solar_bezug' => (float)$a['solar_bezug'],
'netz_bezug' => (float)$a['netz_bezug'],
'solareinspeisung' => (float)$a['solareinspeisung'],
'solarverkauf' => (float)$a['solarverkauf'],
'cost_solar' => (float)$a['cost_solar'],
'cost_grid' => (float)$a['cost_grid'],
'rev_feedin' => (float)$a['rev_feedin'],
'rev_zev' => (float)$a['rev_zev'],
'subtotal' => (float)$subtotal
];
}
$html = "
<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:7.5px;border-collapse:collapse;'>
<tr style='background-color:#eeeeee;font-weight:bold;'>
<th width='20%'>Zählpunkt</th>
<th width='13%'>Import kWh</th>
<th width='13%'>Export kWh</th>
<th width='14%'>Solarbezug kWh</th>
<th width='14%'>Netzbezug kWh</th>
<th width='13%'>Einspeisung kWh</th>
<th width='13%'>ZEV-Verkauf kWh</th>
</tr>" . $energyRows . "
<tr style='background-color:#f4f4f4;font-weight:bold;'>
<td width='20%'>Total</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['imp'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['exp'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($totals['solar_bezug'], 3) . "</td>
<td width='14%' align='right'>" . $this->FormatNumber($totals['netz_bezug'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['solareinspeisung'], 3) . "</td>
<td width='13%' align='right'>" . $this->FormatNumber($totals['solarverkauf'], 3) . "</td>
</tr>
</table>
<br>
<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:8px;border-collapse:collapse;'>
<tr style='background-color:#eeeeee;font-weight:bold;'>
<th width='24%'>Zählpunkt</th>
<th width='16%'>Kauf Solar CHF</th>
<th width='16%'>Kauf Netz CHF</th>
<th width='16%'>Gutschrift Netz CHF</th>
<th width='16%'>Gutschrift ZEV CHF</th>
<th width='12%'>Total CHF</th>
</tr>" . $amountRows . "
<tr style='background-color:#e9e9e9;font-weight:bold;'>
<td colspan='5' align='right'>Zwischentotal Elektrizität</td>
<td align='right'>" . $this->FormatCurrency($sum, false) . "</td>
</tr>
</table><br>";
return ['html' => $html, 'sum' => $sum];
return ['rows' => $rows, 'totals' => $totals, 'sum' => $sum];
}
// ====================== Nebenkosten Wasser/Wärme ======================
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
{
$html = "
<table border='1' cellspacing='0' cellpadding='3' width='100%' style='font-size:8px;border-collapse:collapse;'>
<tr style='background-color:#eeeeee;font-weight:bold;'>
<th width='16%'>Zähler</th>
<th width='11%'>Typ</th>
<th width='11%'>Von</th>
<th width='11%'>Bis</th>
<th width='10%'>Start</th>
<th width='10%'>Ende</th>
<th width='10%'>Verbrauch</th>
<th width='10%'>Tarif</th>
<th width='11%'>Kosten CHF</th>
</tr>";
$rows = [];
$total = 0.0;
$usedTariffs = [];
$hasRows = false;
foreach ($waterMeters as $m) {
if (($m['user_id'] ?? null) != $userId) {
@@ -1041,46 +1334,21 @@ public function GenerateInvoices()
}
$type = $m['meter_type'] ?? 'Warmwasser';
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
if ($cost['row'] !== '') {
$hasRows = true;
if (!empty($cost['row'])) {
$rows[] = $cost['row'];
}
$html .= $cost['row'];
$total += $cost['value'];
$usedTariffs = array_merge($usedTariffs, $cost['tariffs']);
}
if (!$hasRows) {
$html .= "<tr><td colspan='9' align='center'>Keine Nebenkostenzähler für diesen Benutzer</td></tr>";
}
$html .= "
<tr style='background-color:#e9e9e9;font-weight:bold;'>
<td colspan='8' align='right'>Zwischentotal Nebenkosten</td>
<td align='right'>" . $this->FormatCurrency($total) . "</td>
</tr>
</table>";
if (!empty($usedTariffs)) {
$html .= "<p style='font-size:8px;margin-top:6px;'><strong>Angewendete Nebenkostentarife</strong></p><ul style='font-size:8px;'>";
foreach ($usedTariffs as $t) {
if ($t['start'] === null || $t['end'] === null) {
continue;
}
$html .= "<li>" . $this->h($t['unit_type'] ?? '') . ': '
. date('d.m.Y', $t['start']) . ' - ' . date('d.m.Y', $t['end'])
. ' - <strong>' . number_format($t['price'], 3, '.', "'") . " Rp/Einheit</strong></li>";
}
$html .= "</ul><br>";
}
return ['html' => $html, 'sum' => $total];
return ['rows' => $rows, 'sum' => $total, 'tariffs' => $usedTariffs];
}
private function AddMeterToPDFRow($meter, $tariffs, $from, $to, $type)
{
$varId = (int)($meter['var_consumption'] ?? 0);
if ($varId <= 0 || !IPS_VariableExists($varId)) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
return ['row' => null, 'value' => 0, 'tariffs' => []];
}
$filteredTariffs = array_filter($tariffs, function ($t) use ($type) {
@@ -1113,25 +1381,27 @@ public function GenerateInvoices()
$endValue = $this->GetValueAt($varId, $to, false);
if ($startValue === null || $endValue === null) {
return ['row' => '', 'value' => 0, 'tariffs' => []];
return ['row' => null, 'value' => 0, 'tariffs' => []];
}
$verbrauch = max(0, $endValue - $startValue);
$kosten = round(($tariffPrice / 100) * $verbrauch, 2);
$row = "<tr>
<td width='16%'>" . $this->h($meter['name'] ?? '') . "</td>
<td width='11%'>" . $this->h($type) . "</td>
<td width='11%' align='center'>" . date('d.m.Y', $from) . "</td>
<td width='11%' align='center'>" . date('d.m.Y', $to) . "</td>
<td width='10%' align='right'>" . $this->FormatNumber($startValue, 2) . "</td>
<td width='10%' align='right'>" . $this->FormatNumber($endValue, 2) . "</td>
<td width='10%' align='right'>" . $this->FormatNumber($verbrauch, 2) . "</td>
<td width='10%' align='right'>" . number_format($tariffPrice, 3, '.', "'") . "</td>
<td width='11%' align='right'>" . $this->FormatCurrency($kosten, false) . "</td>
</tr>";
return ['row' => $row, 'value' => $kosten, 'tariffs' => array_values($usedTariffs)];
return [
'row' => [
'name' => (string)($meter['name'] ?? ''),
'type' => (string)$type,
'from' => (int)$from,
'to' => (int)$to,
'start_value' => (float)$startValue,
'end_value' => (float)$endValue,
'consumption' => (float)$verbrauch,
'tariff_price' => (float)$tariffPrice,
'cost' => (float)$kosten
],
'value' => $kosten,
'tariffs' => array_values($usedTariffs)
];
}
// ====================== Hilfsfunktionen ======================