From 60fc376d229ba855abf82a5df1365a12e99191ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Fri, 31 Oct 2025 11:53:48 +0100 Subject: [PATCH] no message --- Abrechnung/form.json | 119 ++++++++++++++++ Abrechnung/module.json | 12 ++ Abrechnung/module.php | 308 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 439 insertions(+) create mode 100644 Abrechnung/form.json create mode 100644 Abrechnung/module.json create mode 100644 Abrechnung/module.php diff --git a/Abrechnung/form.json b/Abrechnung/form.json new file mode 100644 index 0000000..0c8fb62 --- /dev/null +++ b/Abrechnung/form.json @@ -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": "" + } + ] + } + ] + } + ] + } + ] +} diff --git a/Abrechnung/module.json b/Abrechnung/module.json new file mode 100644 index 0000000..5bfa2fd --- /dev/null +++ b/Abrechnung/module.json @@ -0,0 +1,12 @@ +{ + "id": "{9DAC29C7-F381-F2A4-7CE2-F391C4123F38}", + "name": "Abrechnung", + "type": 3, + "vendor": "Belevo AG", + "aliases": [], + "parentRequirements": [], + "childRequirements": [], + "implemented": [], + "prefix": "ABR", + "url": "" +} diff --git a/Abrechnung/module.php b/Abrechnung/module.php new file mode 100644 index 0000000..68c8366 --- /dev/null +++ b/Abrechnung/module.php @@ -0,0 +1,308 @@ +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; + } +}