diff --git a/Abrechnung/form.json b/Abrechnung/form.json index 5b71e57..c848bc2 100644 --- a/Abrechnung/form.json +++ b/Abrechnung/form.json @@ -1,150 +1,141 @@ { - "elements": [ - { - "type": "Label", - "caption": "🧾 Abrechnungseinstellungen" - }, + "elements": [ + { + "type": "Label", + "caption": "🧾 Abrechnungseinstellungen" + }, + { + "type": "ExpansionPanel", + "caption": "Benutzer", + "items": [ { - "type": "ExpansionPanel", - "caption": "Benutzer", - "items": [ - { - "type": "List", - "name": "Users", - "caption": "Benutzerliste", - "add": true, - "delete": true, - "sortable": true, - "columns": [ - { - "caption": "ID", - "name": "id", - "width": "10%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Name", - "name": "name", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Adresse", - "name": "address", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Ort", - "name": "city", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - } - ] - } - ] - }, - - { - "type": "ExpansionPanel", - "caption": "Stromzaehler", - "items": [ - { - "type": "List", - "name": "PowerMeters", - "caption": "Stromzählerliste", - "add": true, - "delete": true, - "sortable": true, - "columns": [ - { - "caption": "ID", - "name": "id", - "width": "10%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Name", - "name": "name", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Var. Verbrauch", - "name": "var_consumption", - "width": "20%", - "add": 0, - "edit": { "type": "SelectVariable" } - }, - { - "caption": "Var. Bezug", - "name": "var_feed", - "width": "20%", - "add": 0, - "edit": { "type": "SelectVariable" } - }, - { - "caption": "Benutzer-ID", - "name": "user_id", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - } - ] - } - ] - }, - - { - "type": "ExpansionPanel", - "caption": "Wasserzaehler / Verbrauchszaehler", - "items": [ - { - "type": "List", - "name": "WaterMeters", - "caption": "Verbrauchszählerliste", - "add": true, - "delete": true, - "sortable": true, - "columns": [ - { - "caption": "ID", - "name": "id", - "width": "10%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Name", - "name": "name", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - }, - { - "caption": "Var. Verbrauch", - "name": "var_consumption", - "width": "20%", - "add": 0, - "edit": { "type": "SelectVariable" } - }, - { - "caption": "Benutzer-ID", - "name": "user_id", - "width": "20%", - "add": "", - "edit": { "type": "ValidationTextBox" } - } - ] - } - ] + "type": "List", + "name": "Users", + "caption": "Benutzerliste", + "add": true, + "delete": true, + "sortable": true, + "columns": [ + { "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Name", "name": "name", "width": "25%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Adresse", "name": "address", "width": "25%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Ort", "name": "city", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } } + ] } - ] + ] + }, + + { + "type": "ExpansionPanel", + "caption": "Stromzähler", + "items": [ + { + "type": "List", + "name": "PowerMeters", + "caption": "Stromzählerliste", + "add": true, + "delete": true, + "sortable": true, + "columns": [ + { "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Var. Verbrauch", "name": "var_consumption", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } }, + { "caption": "Var. Bezug", "name": "var_feed", "width": "20%", "add": 0, "edit": { "type": "SelectVariable" } }, + { "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } } + ] + } + ] + }, + + { + "type": "ExpansionPanel", + "caption": "Wasserzähler / Verbrauchszähler", + "items": [ + { + "type": "List", + "name": "WaterMeters", + "caption": "Verbrauchszählerliste", + "add": true, + "delete": true, + "sortable": true, + "columns": [ + { "caption": "ID", "name": "id", "width": "10%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Name", "name": "name", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { "caption": "Var. Verbrauch", "name": "var_consumption", "width": "25%", "add": 0, "edit": { "type": "SelectVariable" } }, + { "caption": "Benutzer-ID", "name": "user_id", "width": "20%", "add": "", "edit": { "type": "ValidationTextBox" } }, + { + "caption": "Zählertyp", + "name": "meter_type", + "width": "20%", + "add": "Warmwasser", + "edit": { + "type": "Select", + "options": [ + { "caption": "Warmwasser", "value": "Warmwasser" }, + { "caption": "Kaltwasser", "value": "Kaltwasser" }, + { "caption": "Wärme", "value": "Wärme" } + ] + } + } + ] + } + ] + }, + + { + "type": "ExpansionPanel", + "caption": "Tarife", + "items": [ + { + "type": "List", + "name": "Tariffs", + "caption": "Tarifübersicht", + "add": true, + "delete": true, + "sortable": true, + "columns": [ + { + "caption": "Startdatum", + "name": "start", + "width": "20%", + "add": "", + "edit": { "type": "DateTime" } + }, + { + "caption": "Enddatum", + "name": "end", + "width": "20%", + "add": "", + "edit": { "type": "DateTime" } + }, + { + "caption": "Tarif (Rp/Einheit)", + "name": "price", + "width": "20%", + "add": 0, + "edit": { "type": "NumberSpinner", "digits": 3, "minimum": 0 } + }, + { + "caption": "Einheit", + "name": "unit_type", + "width": "30%", + "add": "Strombezug", + "edit": { + "type": "Select", + "options": [ + { "caption": "Strombezug", "value": "Strombezug" }, + { "caption": "Stromproduktion", "value": "Stromproduktion" }, + { "caption": "Solarstrombezug", "value": "Solarstrombezug" }, + { "caption": "Warmwasser", "value": "Warmwasser" }, + { "caption": "Kaltwasser", "value": "Kaltwasser" }, + { "caption": "Wärme", "value": "Wärme" } + ] + } + } + ] + } + ] + } + ] } diff --git a/Abrechnung/module.php b/Abrechnung/module.php index 68c8366..35ea043 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -1,44 +1,18 @@ 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->RegisterPropertyString('Users', '[]'); + $this->RegisterPropertyString('PowerMeters', '[]'); + $this->RegisterPropertyString('WaterMeters', '[]'); + $this->RegisterPropertyString('Tariffs', '[]'); - // This is used to return status message in the form - $this->RegisterVariableString('LastPDF', 'Letztes PDF', '', 10); } public function ApplyChanges() @@ -49,260 +23,5 @@ class Abrechnung extends IPSModule // 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; - } -} +} \ No newline at end of file