no message
This commit is contained in:
119
Abrechnung/form.json
Normal file
119
Abrechnung/form.json
Normal 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
12
Abrechnung/module.json
Normal 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
308
Abrechnung/module.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user