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; } }