no message

This commit is contained in:
2025-10-31 11:53:48 +01:00
parent dd48587425
commit 60fc376d22
3 changed files with 439 additions and 0 deletions

119
Abrechnung/form.json Normal file
View File

@@ -0,0 +1,119 @@
{
"elements": [
{
"type": "Label",
"caption": "Einstellungen für Abrechnung"
},
{
"type": "ExpansionPanel",
"caption": "Stammdaten",
"items": [
{
"type": "Row",
"items": [
{
"type": "Column",
"items": [
{
"type": "List",
"name": "Users",
"caption": "Benutzer (Empfänger)",
"add": true,
"columns": [
{ "caption": "ID", "name": "id", "width": "10%" },
{ "caption": "Name", "name": "name", "width": "35%" },
{ "caption": "E-Mail", "name": "email", "width": "35%" },
{ "caption": "Adresse", "name": "address", "width": "20%" }
],
"rowProperties": [
{ "label": "Name", "name": "name", "type": "TextBox" },
{ "label": "E-Mail", "name": "email", "type": "TextBox" },
{ "label": "Adresse", "name": "address", "type": "TextBox" },
{ "label": "Benutzer-ID (frei)", "name": "id", "type": "ValidationTextBox" }
]
}
]
},
{
"type": "Column",
"items": [
{
"type": "List",
"name": "Meters",
"caption": "Zähler",
"add": true,
"columns": [
{ "caption": "ID", "name": "id", "width": "10%" },
{ "caption": "Name", "name": "name", "width": "30%" },
{ "caption": "IPS-VariableID", "name": "variableid", "width": "30%" },
{ "caption": "Benutzer-ID", "name": "user_id", "width": "15%" },
{ "caption": "Einheit", "name": "unit", "width": "15%" }
],
"rowProperties": [
{ "label": "Name", "name": "name", "type": "TextBox" },
{ "label": "IPS-VariableID (Zählerstand)", "name": "variableid", "type": "ValidationTextBox" },
{ "label": "Zugehöriger Benutzer-ID", "name": "user_id", "type": "ValidationTextBox" },
{ "label": "Einheit (z.B. kWh)", "name": "unit", "type": "TextBox" },
{ "label": "Zähler-ID (frei)", "name": "id", "type": "ValidationTextBox" }
]
}
]
}
]
}
]
},
{
"type": "ExpansionPanel",
"caption": "Abrechnung (Schnellstart)",
"items": [
{
"type": "Row",
"items": [
{
"type": "Column",
"items": [
{
"type": "DateTimePicker",
"name": "from",
"caption": "Von",
"format": "yyyy-MM-dd HH:mm"
},
{
"type": "DateTimePicker",
"name": "to",
"caption": "Bis",
"format": "yyyy-MM-dd HH:mm"
},
{
"type": "NumberSpinner",
"name": "work_price",
"caption": "Arbeitspreis (pro Einheit)",
"digits": 4,
"minimum": 0
},
{
"type": "NumberSpinner",
"name": "fixed_fee",
"caption": "Fixbetrag pro Benutzer (optional)",
"digits": 2,
"minimum": 0
},
{
"type": "Button",
"caption": "Abrechnung erzeugen (PDF)",
"onClick": "GenerateInvoices"
},
{
"type": "Label",
"name": "result",
"caption": ""
}
]
}
]
}
]
}
]
}

12
Abrechnung/module.json Normal file
View File

@@ -0,0 +1,12 @@
{
"id": "{9DAC29C7-F381-F2A4-7CE2-F391C4123F38}",
"name": "Abrechnung",
"type": 3,
"vendor": "Belevo AG",
"aliases": [],
"parentRequirements": [],
"childRequirements": [],
"implemented": [],
"prefix": "ABR",
"url": ""
}

308
Abrechnung/module.php Normal file
View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
trait AbrechnungHelper {
private function jsonDecodeProperty(string $name): array {
$s = $this->ReadPropertyString($name);
$a = json_decode($s, true);
return is_array($a) ? $a : [];
}
private function saveTempFile(string $filename, string $content): string {
$dir = IPS_GetKernelDir() . 'media' . DIRECTORY_SEPARATOR . 'Abrechnung';
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
$path = $dir . DIRECTORY_SEPARATOR . $filename;
file_put_contents($path, $content);
return $path;
}
private function formatCurrency(float $v): string {
return number_format($v, 2, '.', '');
}
}
class Abrechnung extends IPSModule
{
use AbrechnungHelper;
public function Create()
{
// Never delete this line!
parent::Create();
// Properties to store JSON arrays for Users and Meters
$this->RegisterPropertyString('Users', json_encode([], JSON_THROW_ON_ERROR));
$this->RegisterPropertyString('Meters', json_encode([], JSON_THROW_ON_ERROR));
// This is used to return status message in the form
$this->RegisterVariableString('LastPDF', 'Letztes PDF', '', 10);
}
public function ApplyChanges()
{
parent::ApplyChanges();
}
// Called by the button in form.json
public function GenerateInvoices($data)
{
// $data contains the values from the form (from, to, work_price, fixed_fee)
try {
$from = isset($data['from']) ? strtotime($data['from']) : null;
$to = isset($data['to']) ? strtotime($data['to']) : null;
$workPrice = isset($data['work_price']) ? floatval($data['work_price']) : 0.0;
$fixedFee = isset($data['fixed_fee']) ? floatval($data['fixed_fee']) : 0.0;
if (!$from || !$to || $to <= $from) {
return $this->ReturnResult(false, 'Ungültiger Zeitraum angegeben.');
}
$users = $this->jsonDecodeProperty('Users');
$meters = $this->jsonDecodeProperty('Meters');
// Map meters to users
$userMeters = [];
foreach ($meters as $m) {
$uid = $m['user_id'] ?? null;
if ($uid === null) continue;
$userMeters[$uid][] = $m;
}
// Prepare per-user statements
$invoiceData = [];
foreach ($users as $u) {
$uid = $u['id'] ?? null;
if ($uid === null) continue;
$name = $u['name'] ?? 'Unbenannt';
$email = $u['email'] ?? '';
$address = $u['address'] ?? '';
$entries = [];
$sumCost = 0.0;
$sumConsumption = 0.0;
$mList = $userMeters[$uid] ?? [];
foreach ($mList as $m) {
$varId = intval($m['variableid']);
if ($varId <= 0) {
$entries[] = [
'name' => $m['name'],
'error' => 'keine gültige Variable'
];
continue;
}
// We read the first value before period and last value at end period
// If counters are absolute cumulative, we look up the last value before period and last value at 'to'.
$fromValue = $this->GetVariableValueAt($varId, $from);
$toValue = $this->GetVariableValueAt($varId, $to);
if ($fromValue === null || $toValue === null) {
$entries[] = [
'name' => $m['name'],
'error' => 'keine historischen Werte gefunden'
];
continue;
}
$consumption = $toValue - $fromValue;
if ($consumption < 0) {
// possibly rollover or wrong counter — keep absolute
$consumption = 0;
}
$cost = $consumption * $workPrice;
$sumConsumption += $consumption;
$sumCost += $cost;
$entries[] = [
'name' => $m['name'],
'variableid' => $varId,
'unit' => $m['unit'] ?? '',
'fromValue' => $fromValue,
'toValue' => $toValue,
'consumption' => $consumption,
'cost' => $cost
];
}
// add fixed fee per user
$sumCost += $fixedFee;
$invoiceData[] = [
'user' => [
'id' => $uid,
'name' => $name,
'email' => $email,
'address' => $address
],
'entries' => $entries,
'summary' => [
'consumption' => $sumConsumption,
'work_price' => $workPrice,
'fixed_fee' => $fixedFee,
'total' => $sumCost
]
];
}
// generate PDF content
$filename = $this->generatePdfFilename($from, $to);
$path = $this->generatePdf($invoiceData, $from, $to, $filename);
if ($path === false) {
return $this->ReturnResult(false, 'PDF-Erzeugung fehlgeschlagen (TCPDF fehlt?).');
}
// Save path in a variable for easy retrieval
SetValue($this->GetIDForIdent('LastPDF'), basename($path));
$result = [
'success' => true,
'message' => 'PDF erstellt',
'file' => $path
];
return $result;
} catch (Exception $e) {
return $this->ReturnResult(false, 'Fehler: ' . $e->getMessage());
}
}
private function ReturnResult(bool $ok, string $msg)
{
return ['success' => $ok, 'message' => $msg];
}
private function GetVariableValueAt(int $variableId, int $timestamp)
{
// Try to get the value at timestamp using IPS_GetVariableProfile or historic values
// We use AC_GetLoggedValues if available (Symcon built-in)
if (!function_exists('AC_GetLoggedValues')) {
// fallback: try current value if historical not available
return GetValue($variableId);
}
// get a single value at timestamp +/- 1 second
$data = AC_GetLoggedValues($variableId, $timestamp - 60, $timestamp + 60, 1);
if (!is_array($data) || count($data) === 0) {
// no historical value -> try current value
return GetValue($variableId);
}
// AC_GetLoggedValues returns an array of arrays with 'Value' => ...
if (isset($data[0]['Value'])) {
return floatval($data[0]['Value']);
}
return null;
}
private function generatePdfFilename(int $from, int $to): string {
return 'Abrechnung_' . date('Ymd_His', $from) . '_' . date('Ymd_His', $to) . '.pdf';
}
private function generatePdf(array $invoiceData, int $from, int $to, string $filename)
{
// Try to include TCPDF
if (!class_exists('\TCPDF')) {
// Attempt to include from common locations (you might need to adjust)
$possible = [
IPS_GetKernelDir() . 'libs' . DIRECTORY_SEPARATOR . 'tcpdf' . DIRECTORY_SEPARATOR . 'tcpdf.php',
IPS_GetKernelDir() . 'libs' . DIRECTORY_SEPARATOR . 'TCPDF' . DIRECTORY_SEPARATOR . 'tcpdf.php'
];
$found = false;
foreach ($possible as $p) {
if (file_exists($p)) {
require_once $p;
$found = true;
break;
}
}
if (!$found && !class_exists('\TCPDF')) {
// TCPDF not available
IPS_LogMessage('Abrechnung', 'TCPDF nicht gefunden. Bitte installiere tcpdf in ' . IPS_GetKernelDir() . 'libs/tcpdf. Composer: composer require tecnickcom/tcpdf');
return false;
}
}
// create new PDF document
$pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
$pdf->SetCreator('IPSymcon Abrechnung');
$pdf->SetAuthor('Abrechnung Modul');
$pdf->SetTitle('Zählerabrechnung');
$pdf->SetMargins(15, 20, 15);
$pdf->SetHeaderMargin(5);
$pdf->SetFooterMargin(10);
$pdf->AddPage();
// cover page
$pdf->SetFont('helvetica', 'B', 16);
$pdf->Cell(0, 10, 'Abrechnung', 0, 1, 'C');
$pdf->SetFont('helvetica', '', 10);
$pdf->Ln(2);
$pdf->Cell(0, 6, 'Zeitraum: ' . date('Y-m-d H:i', $from) . ' - ' . date('Y-m-d H:i', $to), 0, 1);
$pdf->Ln(6);
foreach ($invoiceData as $inv) {
$user = $inv['user'];
$entries = $inv['entries'];
$summary = $inv['summary'];
$pdf->AddPage();
$pdf->SetFont('helvetica', 'B', 12);
$pdf->Cell(0, 8, $user['name'] . ' (ID: ' . $user['id'] . ')', 0, 1);
$pdf->SetFont('helvetica', '', 10);
if (!empty($user['address'])) {
$pdf->MultiCell(0, 5, $user['address'], 0, 'L', 0, 1);
}
if (!empty($user['email'])) {
$pdf->Cell(0, 5, 'E-Mail: ' . $user['email'], 0, 1);
}
$pdf->Ln(4);
// Table header
$pdf->SetFont('helvetica', 'B', 10);
$pdf->Cell(60, 7, 'Zähler', 1);
$pdf->Cell(30, 7, 'Von', 1);
$pdf->Cell(30, 7, 'Bis', 1);
$pdf->Cell(30, 7, 'Verbrauch', 1);
$pdf->Cell(30, 7, 'Kosten', 1);
$pdf->Ln();
// Entries
$pdf->SetFont('helvetica', '', 9);
foreach ($entries as $e) {
if (isset($e['error'])) {
$pdf->Cell(60, 6, $e['name'] . ' (' . $e['error'] . ')', 1);
$pdf->Cell(30, 6, '-', 1);
$pdf->Cell(30, 6, '-', 1);
$pdf->Cell(30, 6, '-', 1);
$pdf->Cell(30, 6, '-', 1);
$pdf->Ln();
continue;
}
$pdf->Cell(60, 6, ($e['name'] ?? 'Zähler'), 1);
$pdf->Cell(30, 6, (string)round($e['fromValue'], 4) . ' ' . ($e['unit'] ?? ''), 1);
$pdf->Cell(30, 6, (string)round($e['toValue'], 4) . ' ' . ($e['unit'] ?? ''), 1);
$pdf->Cell(30, 6, (string)round($e['consumption'], 4), 1);
$pdf->Cell(30, 6, $this->formatCurrency($e['cost']), 1);
$pdf->Ln();
}
$pdf->Ln(4);
$pdf->SetFont('helvetica', 'B', 10);
$pdf->Cell(0, 6, 'Zusammenfassung', 0, 1);
$pdf->SetFont('helvetica', '', 9);
$pdf->Cell(80, 6, 'Gesamtverbrauch: ' . round($summary['consumption'], 4) . ' Einheiten', 0, 1);
$pdf->Cell(80, 6, 'Arbeitspreis: ' . $this->formatCurrency($summary['work_price']) . ' / Einheit', 0, 1);
$pdf->Cell(80, 6, 'Fixbetrag: ' . $this->formatCurrency($summary['fixed_fee']), 0, 1);
$pdf->Ln(2);
$pdf->SetFont('helvetica', 'B', 11);
$pdf->Cell(80, 7, 'Gesamtbetrag: ' . $this->formatCurrency($summary['total']), 0, 1);
}
// output PDF to file
$content = $pdf->Output('', 'S'); // return as string
$path = $this->saveTempFile($filename, $content);
return $path;
}
}