1604 lines
59 KiB
PHP
1604 lines
59 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
include_once __DIR__ . '/libs/vendor/autoload.php'; // TCPDF via Composer
|
|
|
|
/**
|
|
* Eigene TCPDF-Klasse mit Logo im Header und Text in der Fusszeile
|
|
*/
|
|
|
|
class InvoicePDF extends TCPDF
|
|
{
|
|
public $logoFile = null;
|
|
public $footerText = '';
|
|
public $printFooterText = true; // Neu: Steuert ob die Fusszeile gedruckt wird
|
|
|
|
public function Header()
|
|
{
|
|
if ($this->logoFile !== null && file_exists($this->logoFile)) {
|
|
$this->Image($this->logoFile, 15, 10, 40);
|
|
$this->SetY(28);
|
|
} else {
|
|
$this->SetY(15);
|
|
}
|
|
}
|
|
|
|
public function Footer()
|
|
{
|
|
if (!$this->printFooterText) {
|
|
return; // Auf QR-Seite keine Rechnungs-Fusszeile drucken!
|
|
}
|
|
$this->SetY(-18);
|
|
$this->SetFont('dejavusans', '', 8);
|
|
$this->MultiCell(0, 8, $this->footerText, 0, 'C', false, 1);
|
|
}
|
|
}
|
|
|
|
class Abrechnung extends IPSModule
|
|
{
|
|
public function Create()
|
|
{
|
|
parent::Create();
|
|
|
|
$this->RegisterPropertyString('Users', '[]');
|
|
$this->RegisterPropertyString('PowerMeters', '[]');
|
|
$this->RegisterPropertyString('WaterMeters', '[]');
|
|
$this->RegisterPropertyString('Tariffs', '[]');
|
|
$this->RegisterPropertyInteger('LogoMediaID', 0);
|
|
$this->RegisterPropertyString('PropertyText', 'Liegenschaft');
|
|
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
|
|
|
|
// NEU: MWST Variablen registrieren (Standard 8.1%)
|
|
$this->RegisterPropertyBoolean('EnableVAT', false);
|
|
$this->RegisterPropertyFloat('VATPercentage', 8.1);
|
|
|
|
// Neue Felder für den QR-Schein
|
|
$this->RegisterPropertyString('CreditorName', 'Belevo AG');
|
|
$this->RegisterPropertyString('CreditorAddress', 'Musterstrasse 1');
|
|
$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', '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);
|
|
$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',
|
|
"<?php IPS_RequestAction(" . $this->InstanceID . ", 'StartBilling', ''); ?>"
|
|
);
|
|
$this->EnsureVisuTariffObjects();
|
|
$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', '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);
|
|
$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');
|
|
|
|
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()
|
|
{
|
|
$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;
|
|
}
|
|
|
|
$types = $this->GetTariffTypes();
|
|
$tariffs = $this->ReadTariffs();
|
|
$rows = '';
|
|
|
|
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, '.', '')
|
|
);
|
|
}
|
|
|
|
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);
|
|
if ($mid === false) {
|
|
$mid = IPS_CreateMedia(5);
|
|
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':
|
|
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 '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();
|
|
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'));
|
|
echo "✅ PDF erfolgreich erstellt.";
|
|
} catch (Throwable $e) {
|
|
echo "❌ Ausnahmefehler: " . $e->getMessage();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ====================== PDF-Erstellung ======================
|
|
|
|
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);
|
|
|
|
$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;
|
|
}
|
|
|
|
// Stromkosten einmal für alle User berechnen (15-Minuten-Logik)
|
|
$this->CalculateAllPowerCosts($power, $tariffs, $from, $to);
|
|
|
|
// Logo & Fusszeile vorbereiten
|
|
$logoFile = $this->GetLogoFile();
|
|
$footerText = $this->ReadPropertyString('FooterText');
|
|
|
|
// PDF-Objekt
|
|
$pdf = new InvoicePDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
|
$pdf->logoFile = $logoFile;
|
|
$pdf->footerText = $footerText;
|
|
|
|
$pdf->SetCreator('IPSymcon Abrechnung');
|
|
// Oben mehr Platz für Logo lassen
|
|
$pdf->SetMargins(15, 35, 15);
|
|
$pdf->SetAutoPageBreak(true, 20);
|
|
$pdf->SetFont('dejavusans', '', 8);
|
|
|
|
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);
|
|
|
|
$this->BuildQRBill($pdf, $user, $grandTotal);
|
|
}
|
|
|
|
return $pdf->Output('Abrechnung.pdf', 'S');
|
|
}
|
|
|
|
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[] = (string)($m['name'] ?? '');
|
|
}
|
|
}
|
|
foreach ($water as $m) {
|
|
if (($m['user_id'] ?? null) == ($user['id'] ?? null)) {
|
|
$meterNames[] = (string)($m['name'] ?? '');
|
|
}
|
|
}
|
|
|
|
$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'] ?? ''));
|
|
|
|
$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');
|
|
|
|
$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']);
|
|
$this->WritePowerPdfTables($pdf, $powerResult);
|
|
$totalPower = $powerResult['sum'];
|
|
|
|
$appliedTariffs = $this->CollectTariffsForUser($tariffs, ['Netztarif', 'Solartarif', 'Einspeisetarif']);
|
|
$this->WriteTariffPdfTable($pdf, 'Angewendete Elektrizitätstarife', $appliedTariffs, 'Rp/kWh');
|
|
|
|
$this->PdfSectionTitle($pdf, 'Nebenkosten');
|
|
$additionalResult = $this->CalculateAdditionalCosts($water, $tariffs, $user['id'], $from, $to);
|
|
$this->WriteAdditionalPdfTable($pdf, $additionalResult);
|
|
$totalAdditional = $additionalResult['sum'];
|
|
$this->WriteTariffPdfTable($pdf, 'Angewendete Nebenkostentarife', $additionalResult['tariffs'], 'Rp/Einheit');
|
|
|
|
$subTotal = $totalPower + $totalAdditional;
|
|
$enableVAT = $this->ReadPropertyBoolean('EnableVAT');
|
|
$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;
|
|
$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;
|
|
$summaryRows[] = ['Gesamttotal', $this->FormatCurrency($grandTotal)];
|
|
}
|
|
|
|
$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)
|
|
{
|
|
$iban = str_replace(' ', '', $this->ReadPropertyString('BankIBAN'));
|
|
|
|
if (empty($iban) || $amount <= 0) {
|
|
return;
|
|
}
|
|
|
|
$pdf->AddPage();
|
|
$pdf->printFooterText = false;
|
|
$pdf->SetAutoPageBreak(false);
|
|
|
|
$font = 'helvetica';
|
|
$yStart = 192;
|
|
$amountText = number_format($amount, 2, '.', ' ');
|
|
|
|
$pdf->SetLineStyle(['dash' => '2,2', 'color' => [0, 0, 0], 'width' => 0.1]);
|
|
$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');
|
|
$bankIBANFormatted = wordwrap($iban, 4, ' ', true);
|
|
$debtorLines = [
|
|
$user['name'] ?? '',
|
|
$user['address'] ?? '',
|
|
$user['city'] ?? ''
|
|
];
|
|
|
|
// Linker Teil: Empfangsschein
|
|
$pdf->SetFont($font, 'B', 11);
|
|
$pdf->SetXY(5, $yStart + 5);
|
|
$pdf->Cell(52, 5, 'Empfangsschein', 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 + 68);
|
|
$pdf->Cell(15, 3, 'Währung', 0, 0);
|
|
$pdf->Cell(30, 3, 'Betrag', 0, 1);
|
|
$pdf->SetFont($font, '', 8);
|
|
$pdf->SetX(5);
|
|
$pdf->Cell(15, 4, 'CHF', 0, 0);
|
|
$pdf->Cell(30, 4, $amountText, 0, 1);
|
|
|
|
$pdf->SetFont($font, 'B', 6);
|
|
$pdf->SetXY(38, $yStart + 90);
|
|
$pdf->Cell(19, 3, 'Annahmestelle', 0, 1, 'R');
|
|
|
|
// Rechter Teil: Zahlteil
|
|
$pdf->SetFont($font, 'B', 11);
|
|
$pdf->SetXY(67, $yStart + 5);
|
|
$pdf->Cell(50, 5, 'Zahlteil', 0, 1);
|
|
|
|
$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');
|
|
|
|
$cX = 67 + 23;
|
|
$cY = $yStart + 17 + 23;
|
|
|
|
$pdf->SetFillColor(255, 255, 255);
|
|
$pdf->Rect($cX - 3.5, $cY - 3.5, 7, 7, 'F');
|
|
$pdf->SetFillColor(0, 0, 0);
|
|
$pdf->Rect($cX - 3, $cY - 3, 6, 6, 'F');
|
|
$pdf->SetFillColor(255, 255, 255);
|
|
$pdf->Rect($cX - 0.6, $cY - 2, 1.2, 4, 'F');
|
|
$pdf->Rect($cX - 2, $cY - 0.6, 4, 1.2, 'F');
|
|
|
|
$pdf->SetFont($font, 'B', 8);
|
|
$pdf->SetXY(67, $yStart + 68);
|
|
$pdf->Cell(15, 4, 'Währung', 0, 0);
|
|
$pdf->Cell(30, 4, 'Betrag', 0, 1);
|
|
$pdf->SetFont($font, '', 10);
|
|
$pdf->SetX(67);
|
|
$pdf->Cell(15, 5, 'CHF', 0, 0);
|
|
$pdf->Cell(30, 5, $amountText, 0, 1);
|
|
|
|
$this->WriteQrTextBlock($pdf, 118, $yStart + 5, 84, 'Konto / Zahlbar an', [
|
|
$bankIBANFormatted,
|
|
$creditorName,
|
|
$creditorAddress,
|
|
$creditorCity
|
|
], 8, 9, 4);
|
|
|
|
$this->WriteQrTextBlock($pdf, 118, $yStart + 34, 84, 'Zusätzliche Informationen', [
|
|
'Abrechnung Liegenschaft'
|
|
], 8, 9, 4);
|
|
|
|
$this->WriteQrTextBlock($pdf, 118, $yStart + 50, 84, 'Zahlbar durch', $debtorLines, 8, 9, 4);
|
|
|
|
$pdf->SetAutoPageBreak(true, 20);
|
|
}
|
|
|
|
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);
|
|
$creditorStr = mb_substr($this->ReadPropertyString('CreditorAddress'), 0, 70);
|
|
$creditorCity = mb_substr($this->ReadPropertyString('CreditorCity'), 0, 70);
|
|
|
|
// Fallback falls ein Mieter keinen Namen oder Ort hinterlegt hat (Banken verlangen das zwingend)
|
|
$debtorName = mb_substr(empty($user['name']) ? 'Unbekannt' : $user['name'], 0, 70);
|
|
$debtorStr = mb_substr($user['address'] ?? '', 0, 70);
|
|
$debtorCity = mb_substr(empty($user['city']) ? 'Unbekannt' : $user['city'], 0, 70);
|
|
|
|
$amountStr = number_format($amount, 2, '.', '');
|
|
|
|
// EPC / Swiss QR Code String Format (Exakte Reihenfolge ist kritisch!)
|
|
$lines = [
|
|
'SPC', '0200', '1', $iban, 'K',
|
|
$creditorName, $creditorStr, $creditorCity, '', '', 'CH',
|
|
'', '', '', '', '', '', '', // Ultimate Creditor (leer)
|
|
$amountStr, 'CHF', 'K',
|
|
$debtorName, $debtorStr, $debtorCity, '', '', 'CH',
|
|
'NON', '', // Reference Type (NON = Ohne Referenz)
|
|
'Abrechnung Liegenschaft', // 30. Unstrukturierte Mitteilung
|
|
'EPD' // 31. Trailer (Zwingend exakt 'EPD' am Schluss!)
|
|
];
|
|
|
|
return implode("\r\n", $lines);
|
|
}
|
|
// ====================== Stromkosten (15-Minuten, alle User) ======================
|
|
|
|
private function CalculateAllPowerCosts($powerMeters, $tariffs, $from, $to)
|
|
{
|
|
$this->powerCostCache = [];
|
|
|
|
// Zähler je User aufbauen
|
|
$metersByUser = [];
|
|
foreach ($powerMeters as $m) {
|
|
if (!isset($m['user_id'])) {
|
|
continue;
|
|
}
|
|
|
|
$userId = (int)$m['user_id'];
|
|
$name = $m['name'] ?? ('Meter_' . $m['user_id']);
|
|
|
|
if (!isset($metersByUser[$userId])) {
|
|
$metersByUser[$userId] = [];
|
|
}
|
|
$metersByUser[$userId][$name] = [
|
|
'importVar' => $m['var_consumption'] ?? null,
|
|
'exportVar' => $m['var_feed'] ?? null
|
|
];
|
|
|
|
if (!isset($this->powerCostCache[$userId])) {
|
|
$this->powerCostCache[$userId] = [];
|
|
}
|
|
$this->powerCostCache[$userId][$name] = [
|
|
'name' => $name,
|
|
'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
|
|
];
|
|
}
|
|
|
|
if (empty($metersByUser)) {
|
|
return;
|
|
}
|
|
|
|
// 15-Minuten-Schritte
|
|
for ($ts = $from; $ts < $to; $ts += 900) {
|
|
$slotEnd = min($to, $ts + 900);
|
|
|
|
// Tarife (gleich für alle User)
|
|
$pGrid = $this->getTariffPriceAt($tariffs, ['Netztarif'], $ts);
|
|
$pSolar = $this->getTariffPriceAt($tariffs, ['Solartarif'], $ts);
|
|
$pFeed = $this->getTariffPriceAt($tariffs, ['Einspeisetarif'], $ts);
|
|
|
|
// ==========================================================
|
|
// 1) GLOBAL DELTAS EINMAL BERECHNEN
|
|
// ==========================================================
|
|
$globalImpTotal = 0.0;
|
|
$globalExpTotal = 0.0;
|
|
$globalSlot = [];
|
|
|
|
foreach ($powerMeters as $m) {
|
|
|
|
$name = $m['name'];
|
|
$impDelta = 0.0;
|
|
$expDelta = 0.0;
|
|
|
|
// IMPORT
|
|
if (!empty($m['var_consumption']) && IPS_VariableExists((int)$m['var_consumption'])) {
|
|
$impDelta = $this->getDeltaFromArchive((int)$m['var_consumption'], $ts, $slotEnd);
|
|
}
|
|
|
|
// EXPORT
|
|
if (!empty($m['var_feed']) && IPS_VariableExists((int)$m['var_feed'])) {
|
|
$expDelta = $this->getDeltaFromArchive((int)$m['var_feed'], $ts, $slotEnd);
|
|
}
|
|
|
|
if ($impDelta > 0.0 || $expDelta > 0.0) {
|
|
$globalSlot[$name] = [
|
|
'imp' => $impDelta,
|
|
'exp' => $expDelta,
|
|
'user_id' => (int)$m['user_id']
|
|
];
|
|
|
|
$globalImpTotal += $impDelta;
|
|
$globalExpTotal += $expDelta;
|
|
}
|
|
}
|
|
|
|
// Wenn keine Imp/Exp vorhanden → nächster Slot
|
|
if ($globalImpTotal == 0.0 && $globalExpTotal == 0.0) {
|
|
continue;
|
|
}
|
|
|
|
// ==========================================================
|
|
// 2) Verhältnis PV / Netz
|
|
// ==========================================================
|
|
if ($globalImpTotal == 0.0 && $globalExpTotal > 0.0) {
|
|
// nur Export → PV deckt alles
|
|
$ratio = 0.0;
|
|
$pvCoversAll = true;
|
|
|
|
} elseif ($globalExpTotal == 0.0 && $globalImpTotal > 0.0) {
|
|
// nur Import → Netz deckt alles
|
|
$ratio = 0.0;
|
|
$pvCoversAll = false;
|
|
|
|
} elseif ($globalImpTotal <= $globalExpTotal) {
|
|
// PV produziert genug
|
|
$ratio = $globalExpTotal ? ($globalImpTotal / $globalExpTotal) : 0.0;
|
|
$pvCoversAll = true;
|
|
|
|
} else {
|
|
// Netzbezug notwendig
|
|
$ratio = $globalImpTotal ? ($globalExpTotal / $globalImpTotal) : 0.0;
|
|
$pvCoversAll = false;
|
|
}
|
|
|
|
// ==========================================================
|
|
// 3) PRO USER VERTEILEN
|
|
// ==========================================================
|
|
foreach ($globalSlot as $name => $v) {
|
|
$userId = $v['user_id'];
|
|
|
|
// Falls User nicht im Cache ist → ignorieren
|
|
if (!isset($this->powerCostCache[$userId][$name])) {
|
|
continue;
|
|
}
|
|
|
|
$imp = $v['imp'];
|
|
$exp = $v['exp'];
|
|
|
|
// Totale speichern
|
|
$this->powerCostCache[$userId][$name]['imp'] += $imp;
|
|
$this->powerCostCache[$userId][$name]['exp'] += $exp;
|
|
|
|
// PV deckt alles
|
|
if ($pvCoversAll) {
|
|
$this->powerCostCache[$userId][$name]['solar_bezug'] += $imp;
|
|
$this->powerCostCache[$userId][$name]['solareinspeisung'] += (1 - $ratio) * $exp;
|
|
$this->powerCostCache[$userId][$name]['solarverkauf'] += $ratio * $exp;
|
|
|
|
if ($pSolar !== null) {
|
|
$this->powerCostCache[$userId][$name]['cost_solar'] += ($imp * $pSolar) / 100.0;
|
|
}
|
|
if ($pFeed !== null) {
|
|
$this->powerCostCache[$userId][$name]['rev_feedin'] += ((1 - $ratio) * $exp * $pFeed) / 100.0;
|
|
$this->powerCostCache[$userId][$name]['rev_zev'] += ($ratio * $exp * $pSolar) / 100.0;
|
|
}
|
|
|
|
// Netz deckt einen Teil
|
|
} else {
|
|
$this->powerCostCache[$userId][$name]['solar_bezug'] += $ratio * $imp;
|
|
$this->powerCostCache[$userId][$name]['netz_bezug'] += (1 - $ratio) * $imp;
|
|
$this->powerCostCache[$userId][$name]['solarverkauf'] += $exp;
|
|
|
|
if ($pGrid !== null) {
|
|
$this->powerCostCache[$userId][$name]['cost_grid'] += ((1 - $ratio) * $imp * $pGrid) / 100.0;
|
|
}
|
|
if ($pSolar !== null) {
|
|
$this->powerCostCache[$userId][$name]['cost_solar'] += ($ratio * $imp * $pSolar) / 100.0;
|
|
}
|
|
if ($pFeed !== null) {
|
|
$this->powerCostCache[$userId][$name]['rev_zev'] += ($exp * $pSolar) / 100.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
private function GetCalculatedPowerCosts($userId)
|
|
{
|
|
$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
|
|
];
|
|
$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']);
|
|
$sum += $subtotal;
|
|
|
|
foreach ($totals as $key => $_) {
|
|
$totals[$key] += (float)($a[$key] ?? 0.0);
|
|
}
|
|
|
|
$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
|
|
];
|
|
}
|
|
|
|
return ['rows' => $rows, 'totals' => $totals, 'sum' => $sum];
|
|
}
|
|
// ====================== Nebenkosten Wasser/Wärme ======================
|
|
|
|
private function CalculateAdditionalCosts($waterMeters, $tariffs, $userId, $from, $to)
|
|
{
|
|
$rows = [];
|
|
$total = 0.0;
|
|
$usedTariffs = [];
|
|
|
|
foreach ($waterMeters as $m) {
|
|
if (($m['user_id'] ?? null) != $userId) {
|
|
continue;
|
|
}
|
|
$type = $m['meter_type'] ?? 'Warmwasser';
|
|
$cost = $this->AddMeterToPDFRow($m, $tariffs, $from, $to, $type);
|
|
if (!empty($cost['row'])) {
|
|
$rows[] = $cost['row'];
|
|
}
|
|
$total += $cost['value'];
|
|
$usedTariffs = array_merge($usedTariffs, $cost['tariffs']);
|
|
}
|
|
|
|
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' => null, 'value' => 0, 'tariffs' => []];
|
|
}
|
|
|
|
$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'] ?? 0, false);
|
|
$endTs = $this->toUnixTs($t['end'] ?? 0, true);
|
|
if ($startTs === null || $endTs === null) {
|
|
continue;
|
|
}
|
|
if ($endTs < $from || $startTs > $to) {
|
|
continue;
|
|
}
|
|
$activeTariff = [
|
|
'start' => $startTs,
|
|
'end' => $endTs,
|
|
'price' => (float)($t['price'] ?? 0),
|
|
'unit_type' => $t['unit_type'] ?? $type
|
|
];
|
|
$usedTariffs[] = $activeTariff;
|
|
break;
|
|
}
|
|
|
|
$tariffPrice = $activeTariff ? $activeTariff['price'] : 0.0;
|
|
$startValue = $this->GetValueAt($varId, $from, false);
|
|
$endValue = $this->GetValueAt($varId, $to, false);
|
|
|
|
if ($startValue === null || $endValue === null) {
|
|
return ['row' => null, 'value' => 0, 'tariffs' => []];
|
|
}
|
|
|
|
$verbrauch = max(0, $endValue - $startValue);
|
|
$kosten = round(($tariffPrice / 100) * $verbrauch, 2);
|
|
|
|
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 ======================
|
|
|
|
private function h($value)
|
|
{
|
|
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
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)
|
|
{
|
|
$archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
|
|
if (!$archiveID || !IPS_VariableExists($varId)) {
|
|
return null;
|
|
}
|
|
|
|
if ($nearestAfter) {
|
|
// Erster Wert NACH oder GENAU ab Timestamp
|
|
$values = @AC_GetLoggedValues(
|
|
$archiveID,
|
|
$varId,
|
|
$timestamp, // start
|
|
time(), // end
|
|
1 // LIMIT
|
|
);
|
|
} else {
|
|
// Letzter Wert DAVOR oder GENAU bis Timestamp
|
|
$values = @AC_GetLoggedValues(
|
|
$archiveID,
|
|
$varId,
|
|
0, // start
|
|
$timestamp, // end
|
|
1 // LIMIT
|
|
);
|
|
}
|
|
|
|
if (!empty($values)) {
|
|
return (float)$values[0]['Value'];
|
|
}
|
|
|
|
// Fallback → Live-Wert
|
|
return (float)GetValue($varId);
|
|
}
|
|
|
|
private function getDeltaFromArchive(int $varId, int $tStart, int $tEnd): float
|
|
{
|
|
// Werte holen
|
|
$startValue = $this->GetValueAt($varId, $tStart, false);
|
|
$endValue = $this->GetValueAt($varId, $tEnd, false);
|
|
|
|
if ($startValue === null || $endValue === null) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Delta berechnen
|
|
$diff = $endValue - $startValue;
|
|
|
|
if ($diff < 0) {
|
|
$diff = 0.0;
|
|
}
|
|
|
|
return (float)$diff;
|
|
}
|
|
|
|
private function toUnixTs($val, $endOfDay = false)
|
|
{
|
|
if (is_int($val)) {
|
|
return $endOfDay ? strtotime(date('Y-m-d 23:59:59', $val)) : $val;
|
|
}
|
|
if (is_numeric($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)) {
|
|
$s = trim($val);
|
|
if ($s !== '' && $s[0] === '{') {
|
|
$obj = json_decode($s, true);
|
|
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
|
|
));
|
|
}
|
|
}
|
|
$ts = strtotime($s);
|
|
return $ts === false ? null : $ts;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function getTariffPriceAt($tariffs, $typeSynonyms, $ts)
|
|
{
|
|
$wanted = array_map('strtolower', $typeSynonyms);
|
|
$cands = [];
|
|
|
|
foreach ($tariffs as $t) {
|
|
$u = strtolower(trim($t['unit_type'] ?? ''));
|
|
if (!in_array($u, $wanted, true)) {
|
|
continue;
|
|
}
|
|
|
|
$s = $this->toUnixTs($t['start'], false);
|
|
$e = $this->toUnixTs($t['end'], true);
|
|
if (!$s || !$e) {
|
|
continue;
|
|
}
|
|
if ($s <= $ts && $ts <= $e) {
|
|
$cands[] = (float)$t['price'];
|
|
}
|
|
}
|
|
|
|
if (empty($cands)) {
|
|
return null;
|
|
}
|
|
return end($cands);
|
|
}
|
|
|
|
private function CollectTariffsForUser($tariffs, $types)
|
|
{
|
|
$result = [];
|
|
$wanted = array_map('strtolower', $types);
|
|
|
|
foreach ($tariffs as $t) {
|
|
$type = strtolower(trim($t['unit_type'] ?? ''));
|
|
if (!in_array($type, $wanted, true)) {
|
|
continue;
|
|
}
|
|
|
|
$start = $this->toUnixTs($t['start'], false);
|
|
$end = $this->toUnixTs($t['end'], true);
|
|
|
|
$result[] = [
|
|
'start' => $start,
|
|
'end' => $end,
|
|
'price' => (float)$t['price'],
|
|
'unit_type' => $t['unit_type'] ?? ''
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function GetLogoFile()
|
|
{
|
|
$mediaID = (int)$this->ReadPropertyInteger('LogoMediaID');
|
|
if ($mediaID <= 0 || !IPS_MediaExists($mediaID)) {
|
|
return null;
|
|
}
|
|
|
|
$media = IPS_GetMedia($mediaID);
|
|
$ext = 'png';
|
|
if (!empty($media['MediaFile'])) {
|
|
$extFromFile = pathinfo($media['MediaFile'], PATHINFO_EXTENSION);
|
|
if ($extFromFile !== '') {
|
|
$ext = $extFromFile;
|
|
}
|
|
}
|
|
|
|
$path = IPS_GetKernelDir() . 'media/logo_' . $mediaID . '.' . $ext;
|
|
|
|
// Bild aus der Symcon-Media-Datenbank extrahieren
|
|
$raw = IPS_GetMediaContent($mediaID); // base64
|
|
$bin = base64_decode($raw);
|
|
if ($bin === false) {
|
|
return null;
|
|
}
|
|
|
|
file_put_contents($path, $bin);
|
|
|
|
return $path;
|
|
}
|
|
}
|
|
?>
|