282 lines
12 KiB
PHP
282 lines
12 KiB
PHP
<?php
|
|
|
|
class ManualInvoiceController extends TTCrud
|
|
{
|
|
protected string $headerTitle = 'Manuelle Rechnungen';
|
|
protected bool $createText = false;
|
|
|
|
protected array $additionalJS = ["js/pages/ManualInvoice/ManualInvoice.js"];
|
|
protected array $additionalHead = ["<link rel='stylesheet' href='/js/pages/ManualInvoice/ManualInvoice.css'>"];
|
|
|
|
//@formatter:off
|
|
protected array $columns = [
|
|
['key' => 'id', 'text' => 'ID', 'table' => ['visible' => false], 'modal' => false],
|
|
['key' => 'invoice_number', 'text' => 'Rechnungsnr.', 'table' => ['sortable' => true, 'filter' => 'search']],
|
|
['key' => 'invoice_date', 'text' => 'Datum', 'type' => 'timestamp', 'table' => ['sortable' => true, 'filter' => 'date', 'formatter' => 'formatDate']],
|
|
['key' => 'company', 'text' => 'Firma', 'table' => ['sortable' => true, 'filter' => 'search']],
|
|
['key' => 'firstname', 'text' => 'Vorname', 'table' => ['visible' => false], 'modal' => false],
|
|
['key' => 'lastname', 'text' => 'Nachname', 'table' => ['visible' => false], 'modal' => false],
|
|
['key' => 'customer_number', 'text' => 'Kundennr.', 'table' => ['sortable' => true, 'filter' => 'search']],
|
|
['key' => 'total', 'text' => 'Netto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']],
|
|
['key' => 'total_gross', 'text' => 'Brutto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']],
|
|
['key' => 'billing_type', 'text' => 'Zahlungsart', 'table' => ['filter' => 'select', 'filterOptions' => [
|
|
['value' => 'invoice', 'text' => 'Rechnung'],
|
|
['value' => 'sepa', 'text' => 'SEPA'],
|
|
]]],
|
|
['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]],
|
|
];
|
|
//@formatter:on
|
|
|
|
protected function createPDFAction($returnFilename = false) {
|
|
// Get data from POST for preview or from database for saved invoice
|
|
$post = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (isset($post['preview']) && $post['preview'] === true) {
|
|
// Create temporary invoice object from POST data for preview
|
|
$invoice = (object)[];
|
|
$invoice->id = 0;
|
|
$invoice->invoice_number = $post['invoice_number'] ?? null;
|
|
$invoice->invoice_date = $post['invoice_date'] ?? time();
|
|
$invoice->customer_number = $post['customer_number'] ?? 0;
|
|
$invoice->fibu_account_number = $post['fibu_account_number'] ?? 0;
|
|
$invoice->company = $post['company'] ?? '';
|
|
$invoice->firstname = $post['firstname'] ?? '';
|
|
$invoice->lastname = $post['lastname'] ?? '';
|
|
$invoice->street = $post['street'] ?? '';
|
|
$invoice->zip = $post['zip'] ?? '';
|
|
$invoice->city = $post['city'] ?? '';
|
|
$invoice->country = $post['country'] ?? 'Österreich';
|
|
$invoice->email = $post['email'] ?? '';
|
|
$invoice->uid = $post['uid'] ?? '';
|
|
$invoice->tax_text = $post['tax_text'] ?? '';
|
|
$invoice->billing_type = $post['billing_type'] ?? 'invoice';
|
|
$invoice->total = $post['total'] ?? 0;
|
|
$invoice->total_gross = $post['total_gross'] ?? 0;
|
|
|
|
$positions = [];
|
|
foreach ($post['positions'] ?? [] as $pos) {
|
|
$positions[] = (object)$pos;
|
|
}
|
|
} else {
|
|
// Load from database
|
|
$id = $this->request->id ?? $post['id'] ?? null;
|
|
if (!$id) {
|
|
http_response_code(500);
|
|
self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']);
|
|
return;
|
|
}
|
|
|
|
$invoice = new ManualInvoice($id);
|
|
if (!$invoice->id) {
|
|
http_response_code(500);
|
|
self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']);
|
|
return;
|
|
}
|
|
|
|
$positions = $invoice->getProperty('positions');
|
|
}
|
|
|
|
// Calculate VAT totals
|
|
$vat = [];
|
|
foreach ($positions as $p) {
|
|
$vatrate = is_object($p) ? $p->vatrate : $p['vatrate'];
|
|
$price_gross = is_object($p) ? $p->price_gross : ($p['price_gross'] ?? 0);
|
|
$price_total = is_object($p) ? $p->price_total : ($p['price_total'] ?? 0);
|
|
|
|
if (!array_key_exists($vatrate, $vat)) {
|
|
$vat[$vatrate] = 0;
|
|
}
|
|
$vat[$vatrate] += $price_gross - $price_total;
|
|
}
|
|
|
|
// Convert positions array to objects if needed
|
|
$invoice->positions = $positions;
|
|
|
|
$pdf_vars = [
|
|
"invoice" => $invoice,
|
|
"vat" => $vat,
|
|
"bank_iban" => TT_INVOICE_BANK_IBAN,
|
|
"bank_bic" => TT_INVOICE_BANK_BIC,
|
|
"bank_bank" => TT_INVOICE_BANK_BANK,
|
|
"bank_owner" => TT_INVOICE_BANK_OWNER
|
|
];
|
|
|
|
// Replace placeholders in header
|
|
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html");
|
|
$headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_1 }}", $invoice->company ? $invoice->company : "", $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_2 }}", trim($invoice->firstname . " " . $invoice->lastname), $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_3 }}", $invoice->street ?? '', $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_4 }}", ($invoice->zip ?? '') . " " . ($invoice->city ?? ''), $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_5 }}", ($invoice->country ?? '') != "Österreich" ? ($invoice->country ?? '') : "", $headerHtml);
|
|
$headerHtml = str_replace("{{ customerNumber }}", $invoice->customer_number ?? '', $headerHtml);
|
|
$headerHtml = str_replace("{{ billingAccount }}", $invoice->fibu_account_number ?? '', $headerHtml);
|
|
$headerHtml = str_replace("{{ invoiceNumber }}", $invoice->invoice_number ?? "VORSCHAU", $headerHtml);
|
|
$headerHtml = str_replace("{{ invoiceDate }}", date("d.m.Y", $invoice->invoice_date ?? time()), $headerHtml);
|
|
$headerHtml = str_replace("{{ vatHtml }}", ($invoice->uid ?? '') ? "<tr><td>Ihre UID:</td><td>" . $invoice->uid . "</td></tr>" : "", $headerHtml);
|
|
|
|
// Generate QR code for SEPA payment
|
|
$qrCode = $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2));
|
|
$headerHtml = str_replace("{{ qrCodeSrc }}", $qrCode, $headerHtml);
|
|
|
|
$headerFile = BASEDIR . "/var/temp/manualinvoice_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
|
file_put_contents($headerFile, $headerHtml);
|
|
|
|
// Replace placeholders in footer
|
|
$footerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_FOOTER.html");
|
|
$footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
|
|
$footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
|
|
$footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
|
|
$footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
|
|
|
|
$footerFile = BASEDIR . "/var/temp/manualinvoice_footer-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
|
file_put_contents($footerFile, $footerHtml);
|
|
|
|
$pdf = new PdfForm("ManualInvoice/PDF_MAIN", $pdf_vars);
|
|
$wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
|
|
$filename = $pdf->render($wkhtmltopdfArgs);
|
|
|
|
if ($returnFilename === true) return $filename;
|
|
|
|
// Return the PDF inline for preview
|
|
header('Content-Type: application/pdf');
|
|
header('Content-Disposition: inline; filename="' . ($invoice->invoice_number ?? 'preview') . '.pdf"');
|
|
readfile($filename);
|
|
die();
|
|
}
|
|
|
|
protected function downloadInvoicePdfAction() {
|
|
$id = $this->request->id;
|
|
if (!is_numeric($id) || !$id) {
|
|
$this->layout()->setFlash("Rechnung nicht gefunden", "error");
|
|
$this->redirect("ManualInvoice");
|
|
}
|
|
|
|
$invoice = new ManualInvoice($id);
|
|
if (!$invoice->id) {
|
|
$this->layout()->setFlash("Rechnung nicht gefunden", "error");
|
|
$this->redirect("ManualInvoice");
|
|
}
|
|
|
|
// Use createPDFAction to get filename
|
|
$pdf_filename = $this->createPDFAction(true);
|
|
|
|
if(!$pdf_filename || !file_exists($pdf_filename)) {
|
|
$this->layout()->setFlash("PDF-Datei konnte nicht erstellt werden");
|
|
$this->redirect("ManualInvoice");
|
|
}
|
|
|
|
header('Content-Type: application/octet-stream');
|
|
header('Content-disposition: attachment; filename="'.$invoice->invoice_number.'.pdf"');
|
|
header('Content-Transfer-Encoding: binary');
|
|
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
|
header('Content-Type: application/pdf');
|
|
header("Content-Length: " . filesize($pdf_filename));
|
|
|
|
readfile($pdf_filename);
|
|
exit;
|
|
}
|
|
|
|
protected function beforeCreate(&$data): bool {
|
|
// Generate invoice number if not provided
|
|
if (empty($data['invoice_number'])) {
|
|
$data['invoice_number'] = ManualInvoiceModel::getNextInvoiceNumber();
|
|
}
|
|
|
|
// Set default values
|
|
if (empty($data['invoice_date'])) {
|
|
$data['invoice_date'] = time();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function afterCreate($data) {
|
|
$invoiceId = $data['id'];
|
|
|
|
// Save positions
|
|
if (isset($data['positions']) && is_array($data['positions'])) {
|
|
foreach ($data['positions'] as $position) {
|
|
$position['manualinvoice_id'] = $invoiceId;
|
|
$posModel = ManualInvoicepositionModel::create($position);
|
|
$posModel->save();
|
|
}
|
|
}
|
|
|
|
// Recalculate totals
|
|
$this->recalculateTotals($invoiceId);
|
|
}
|
|
|
|
protected function beforeUpdate(&$data): bool {
|
|
return true;
|
|
}
|
|
|
|
protected function afterUpdate($data) {
|
|
$invoiceId = $data['id'];
|
|
|
|
// Delete existing positions
|
|
$existingPositions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
|
|
foreach ($existingPositions as $pos) {
|
|
$pos->delete();
|
|
}
|
|
|
|
// Save new positions
|
|
if (isset($data['positions']) && is_array($data['positions'])) {
|
|
foreach ($data['positions'] as $position) {
|
|
$position['manualinvoice_id'] = $invoiceId;
|
|
$posModel = ManualInvoicepositionModel::create($position);
|
|
$posModel->save();
|
|
}
|
|
}
|
|
|
|
// Recalculate totals
|
|
$this->recalculateTotals($invoiceId);
|
|
}
|
|
|
|
protected function recalculateTotals($invoiceId) {
|
|
$invoice = new ManualInvoice($invoiceId);
|
|
$positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
|
|
|
|
$total = 0;
|
|
$total_gross = 0;
|
|
|
|
foreach ($positions as $pos) {
|
|
$total += $pos->price_total;
|
|
$total_gross += $pos->price_gross;
|
|
}
|
|
|
|
$invoice->total = $total;
|
|
$invoice->total_gross = $total_gross;
|
|
$invoice->save();
|
|
}
|
|
|
|
protected function customRowsHandler($rows) {
|
|
foreach ($rows as &$row) {
|
|
// Add customer name
|
|
$row->customerName = trim(($row->company ? $row->company : '') . ' ' . $row->firstname . ' ' . $row->lastname);
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
protected function generateSepaQRCode($paymentReference, $amount) {
|
|
$xinonIBAN = TT_INVOICE_BANK_IBAN;
|
|
$xinonBIC = TT_INVOICE_BANK_BIC;
|
|
$xinonOwner = TT_INVOICE_BANK_OWNER;
|
|
|
|
$epc = "BCD
|
|
001
|
|
1
|
|
SCT
|
|
$xinonBIC
|
|
$xinonOwner
|
|
$xinonIBAN
|
|
EUR$amount
|
|
XINO
|
|
$paymentReference
|
|
|
|
XINON GmbH";
|
|
|
|
return (new \chillerlan\QRCode\QRCode)->render($epc);
|
|
}
|
|
}
|