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