323 lines
16 KiB
PHP
323 lines
16 KiB
PHP
<?php
|
|
|
|
class WarehouseOfferController extends TTCrud {
|
|
protected string $headerTitle = 'Angebote';
|
|
protected string $singleText = 'Angebot';
|
|
protected bool $createText = false;
|
|
|
|
protected array $columns = [
|
|
['key' => 'id', 'text' => 'ID', 'modal' => false, 'table' => false],
|
|
['key' => 'offerNumber', 'text' => 'Angebotsnummer', 'required' => true, 'modal' => false],
|
|
['key' => 'customerNumber', 'text' => 'Kundennummer', 'required' => true, 'modal' => false],
|
|
['key' => 'customerName', 'text' => 'Kundenname', 'required' => true, 'modal' => false],
|
|
['key' => 'customerCity', 'text' => 'Stadt', 'required' => true, 'modal' => false],
|
|
['key' => 'customerVAT', 'text' => 'UID', 'required' => true, 'modal' => false],
|
|
['key' => 'editor', 'text' => 'Sachbearbeiter', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
|
|
['key' => 'totalAmount', 'text' => 'Gesamtbetrag', 'required' => true, 'modal' => false],
|
|
['key' => 'status', 'text' => 'Status', 'required' => true],
|
|
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
|
|
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']],
|
|
['key' => 'actions',
|
|
'text' => 'Aktionen',
|
|
'required' => false,
|
|
'modal' => false,
|
|
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
|
];
|
|
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
|
|
protected array $permissionCheck = ['WarehouseAdmin'];
|
|
protected array $additionalActions = [['key' => 'openpdf', 'title' => 'PDF öffnen', 'class' => 'fas fa-file-pdf', 'color' => 'primary']];
|
|
|
|
protected function prepareCrudConfig(): void {
|
|
$editorColumnIndex = array_search('editor', array_column($this->columns, 'key'));
|
|
$this->columns[$editorColumnIndex]['modal']['items'] = array_map(function ($user) {
|
|
return ['value' => intval($user->id), 'text' => $user->name];
|
|
}, UserModel::search(['employee' => true]));
|
|
if (!$this->user->can('WarehouseAdmin')) $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
|
|
}
|
|
|
|
protected function beforeCreate(): bool {
|
|
$currentCount = WarehouseOfferModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]);
|
|
$this->postData['offerNumber'] = 'AN' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
|
|
$this->postData['status'] = 'new';
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function beforeUpdate($postData): bool {
|
|
(new WarehouseHistoryController)->create($postData, $this->mod);
|
|
return true;
|
|
}
|
|
|
|
protected function getHistoryAction() {
|
|
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
|
}
|
|
|
|
protected function createTemplateAction() {
|
|
if (!$this->user->can('WarehouseAdmin')) self::sendError("Keine Berechtigung");
|
|
$_POST = json_decode(file_get_contents('php://input'), true);
|
|
|
|
$templateId = WarehouseOfferTemplateModel::create([
|
|
'templateName' => $_POST['name'],
|
|
'positions' => $_POST['positions'],
|
|
'totalDiscount' => $_POST['totalDiscount'],
|
|
'paymentTerms' => $_POST['paymentTerms'],
|
|
'deliveryTerms' => $_POST['deliveryTerms'],
|
|
'closingText' => $_POST['closingText'],
|
|
'notes' => $_POST['notes'],
|
|
]);
|
|
|
|
self::returnJson(['success' => true, 'id' => $templateId]);
|
|
}
|
|
|
|
protected function deleteTemplateAction() {
|
|
if (!$this->user->can('WarehouseAdmin')) self::sendError("Keine Berechtigung");
|
|
WarehouseOfferTemplateModel::delete($this->request->id);
|
|
self::returnJson(['success' => true]);
|
|
}
|
|
|
|
protected function getTemplatesAction() {
|
|
self::returnJson(WarehouseOfferTemplateModel::getAll());
|
|
}
|
|
|
|
public function createPDFAction($returnFilename = false, $idOverride = null) {
|
|
// Display errors (Keep for debugging, consider removing for production)
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
$id = $idOverride ?? $this->request->id;
|
|
if (empty($id)) {
|
|
self::sendError('ID fehlt'); // Use empty() for better checks
|
|
}
|
|
|
|
$offer = WarehouseOfferModel::get($id);
|
|
if (!$offer || !$offer->id) { // Check if offer object is valid
|
|
self::sendError('Angebot nicht gefunden');
|
|
}
|
|
|
|
// --- Customer Data (Assuming fields exist on $offer) ---
|
|
// You might need to fetch a CustomerModel if data isn't directly on the offer
|
|
// $customer = CustomerModel::get($offer->customerId);
|
|
// $customerName = $customer->name; // Example
|
|
$customerName = $offer->customerName ?? 'N/A';
|
|
$customerStreet = $offer->customerStreet ?? '';
|
|
$customerZip = $offer->customerZip ?? '';
|
|
$customerCity = $offer->customerCity ?? '';
|
|
$customerCountryId = $offer->customerCountryId ?? null; // Assuming country ID exists
|
|
$customerCountry = $customerCountryId ? (new Country($customerCountryId))->name : '';
|
|
|
|
// Construct address lines (adjust based on your actual data fields)
|
|
$addressLines = [];
|
|
$addressLines['header'] = "<strong>Empfänger</strong>"; // Or "Kunde"
|
|
$addressLines['1'] = $customerName;
|
|
$addressLines['2'] = $customerStreet;
|
|
$addressLines['3'] = $customerZip . ' ' . $customerCity;
|
|
$addressLines['4'] = $customerCountry;
|
|
$addressLines['5'] = ''; // Add more lines if needed (e.g., contact person)
|
|
|
|
// --- Billing Address (Assuming fields exist on $offer, check if different) ---
|
|
// Example: Check if specific billing fields exist and are filled
|
|
$useBillingAddress = !empty($offer->billingName) || !empty($offer->billingStreet);
|
|
if ($useBillingAddress) {
|
|
$billingAddressLines = [];
|
|
$billingAddressLines['header'] = "<strong>Rechnungsadresse</strong>";
|
|
$billingAddressLines['1'] = $offer->billingName ?? $customerName;
|
|
$billingAddressLines['2'] = $offer->billingStreet ?? '';
|
|
$billingAddressLines['3'] = ($offer->billingZip ?? '') . ' ' . ($offer->billingCity ?? '');
|
|
$billingAddressLines['4'] = $offer->billingCountryId ? (new Country($offer->billingCountryId))->name : '';
|
|
$billingAddressLines['5'] = '';
|
|
$billingAddressLines['6'] = ''; // Add more lines if needed
|
|
} else {
|
|
// If no specific billing address, maybe hide the section or repeat customer address
|
|
$billingAddressLines = ['header' => '', '1' => '', '2' => '', '3' => '', '4' => '', '5' => '', '6' => '']; // Effectively hides it
|
|
// Or repeat customer address:
|
|
// $billingAddressLines = $addressLines;
|
|
// $billingAddressLines['header'] = "<strong>Rechnungsadresse</strong>";
|
|
}
|
|
|
|
|
|
// --- Process Positions ---
|
|
$positionsRaw = json_decode($offer->positions, true);
|
|
$entries = [];
|
|
$subTotal = 0; // Calculate subtotal here
|
|
|
|
if (is_array($positionsRaw)) {
|
|
foreach ($positionsRaw as $position) {
|
|
if (!isset($position['article']) || empty($position['article'])) continue;
|
|
|
|
$article = WarehouseArticleModel::get($position['article']);
|
|
if (!$article) continue; // Skip if article not found
|
|
|
|
$position['articleText'] = $article->title ?? 'N/A';
|
|
// Avoid showing description if same as title
|
|
$position['articleDescription'] = ($article->description !== $article->title) ? ($article->description ?? '') : '';
|
|
$position['articleUnit'] = $position['unit'] ?? $article->unit ?? 'Stk.'; // Default to 'Stk.'
|
|
$position['price'] = (float)($position['unitPrice'] ?? 0); // Ensure price is float
|
|
$position['amount'] = (float)($position['amount'] ?? 0); // Ensure amount is float
|
|
$position['totalPrice'] = $position['unitPrice'] * $position['amount'];
|
|
|
|
$subTotal += $position['totalPrice']; // Add to subtotal
|
|
|
|
// Grouping logic
|
|
$groupKey = $position['_group'] ?? ''; // Use empty string as default group
|
|
if (!isset($entries[$groupKey])) {
|
|
$entries[$groupKey] = []; // Initialize group if not exists
|
|
}
|
|
$entries[$groupKey][] = $position;
|
|
}
|
|
} else {
|
|
// Handle case where positions JSON is invalid or empty
|
|
trigger_error("Invalid or empty positions JSON for offer ID: " . $id, E_USER_WARNING);
|
|
}
|
|
|
|
|
|
// --- Prepare PDF Variables ---
|
|
$pdf_vars = [
|
|
"offer" => $offer,
|
|
"entries" => $entries, // Grouped entries
|
|
"subTotal" => $subTotal,
|
|
// Add other offer details needed in PDF_MAIN
|
|
"offerNumber" => $offer->offerNumber ?? $offer->id, // Use offerNumber if available
|
|
"offerDate" => $offer->createDate ?? time(), // Assuming createDate is a timestamp
|
|
"validUntilDate" => $offer->validUntilDate ?? null, // Assuming validUntilDate is a timestamp
|
|
"includeTax" => $offer->includeTax ?? true, // Default to including tax (e.g., 20% VAT)
|
|
"vatRate" => 0.20, // Example VAT rate (20%) - make this configurable if needed
|
|
"offerText" => $offer->offerText ?? '', // Optional text block from the offer
|
|
"bank_iban" => TT_INVOICE_BANK_IBAN,
|
|
"bank_bic" => TT_INVOICE_BANK_BIC,
|
|
"bank_bank" => TT_INVOICE_BANK_BANK,
|
|
"bank_owner" => TT_INVOICE_BANK_OWNER
|
|
// Add any other variables needed in PDF_MAIN.php
|
|
];
|
|
|
|
// --- Prepare Replacements for Header/Footer ---
|
|
$replacements = [
|
|
'basedir' => BASEDIR, // Crucial for images/assets in header/footer
|
|
'externalReference' => !empty($offer->extReference) ?
|
|
"<strong>Ihre Referenz:</strong> " . htmlspecialchars($offer->extReference) : "", // Added htmlspecialchars
|
|
|
|
// Customer Address
|
|
'addressLine_header' => $addressLines['header'],
|
|
'addressLine_1' => htmlspecialchars($addressLines['1']),
|
|
'addressLine_2' => htmlspecialchars($addressLines['2']),
|
|
'addressLine_3' => htmlspecialchars($addressLines['3']),
|
|
'addressLine_4' => htmlspecialchars($addressLines['4']),
|
|
'addressLine_5' => htmlspecialchars($addressLines['5']),
|
|
|
|
// Billing Address
|
|
'billingAddressLine_header' => $billingAddressLines['header'],
|
|
'billingAddressLine_1' => htmlspecialchars($billingAddressLines['1']),
|
|
'billingAddressLine_2' => htmlspecialchars($billingAddressLines['2']),
|
|
'billingAddressLine_3' => htmlspecialchars($billingAddressLines['3']),
|
|
'billingAddressLine_4' => htmlspecialchars($billingAddressLines['4']),
|
|
'billingAddressLine_5' => htmlspecialchars($billingAddressLines['5']),
|
|
'billingAddressLine_6' => htmlspecialchars($billingAddressLines['6']),
|
|
|
|
// Footer variables (assuming PDF_FOOTER.HTML needs these)
|
|
'bank_iban' => defined('TT_INVOICE_BANK_IBAN_FORMATTED') ? TT_INVOICE_BANK_IBAN_FORMATTED : TT_INVOICE_BANK_IBAN,
|
|
'bank_bic' => TT_INVOICE_BANK_BIC,
|
|
'bank_bank' => TT_INVOICE_BANK_BANK,
|
|
'bank_owner' => TT_INVOICE_BANK_OWNER,
|
|
// Add other company info if needed in footer (e.g., VAT ID, address)
|
|
'company_vat_id' => 'ATU68711968', // Example
|
|
'company_address' => 'Fladnitz im Raabtal 150, 8322 Studenzen', // Example
|
|
'company_phone' => '+43 1 2345678', // Example
|
|
'company_email' => 'office@xinon.at' // Example
|
|
];
|
|
|
|
// --- Generate Header/Footer Temp Files ---
|
|
// Ensure the temp directory exists and is writable
|
|
$tempDir = BASEDIR . "/var/temp";
|
|
if (!is_dir($tempDir)) {
|
|
mkdir($tempDir, 0775, true);
|
|
}
|
|
|
|
$headerFile = $tempDir . "/offer_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
|
$footerFile = $tempDir . "/offer_footer-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
|
|
|
// Assume generateTemplate exists and works like in the Order example
|
|
// You need 'WarehouseOffer/PDF_HEADER' and 'WarehouseOffer/PDF_FOOTER' templates
|
|
// Using the provided 'PDF_HEADER.HTML' content directly as the template source path
|
|
// **Important:** Adjust 'WarehouseOffer/PDF_HEADER' and 'WarehouseOffer/PDF_FOOTER'
|
|
// to match the actual paths or keys your `generateTemplate` function expects.
|
|
// If generateTemplate just takes file paths, you might need to save the HTML content
|
|
// from the prompt into actual files first (e.g., `views/WarehouseOffer/PDF_HEADER.HTML`).
|
|
|
|
// Check if template generation succeeds
|
|
if (file_put_contents($headerFile, $this->generateTemplate('WarehouseOffer/PDF_HEADER', $replacements)) === false) {
|
|
self::sendError('Fehler beim Erstellen der Header-Datei.');
|
|
}
|
|
if (file_put_contents($footerFile, $this->generateTemplate('WarehouseOffer/PDF_FOOTER', $replacements)) === false) {
|
|
// Attempt cleanup before erroring
|
|
if (file_exists($headerFile)) unlink($headerFile);
|
|
self::sendError('Fehler beim Erstellen der Footer-Datei.');
|
|
}
|
|
|
|
|
|
// --- Generate PDF ---
|
|
try {
|
|
// Use the correct path for your PDF_MAIN template
|
|
$pdf = new PdfForm("WarehouseOffer/PDF_MAIN", $pdf_vars);
|
|
|
|
// Construct options for wkhtmltopdf
|
|
// Adjust margins as needed (T=Top, R=Right, B=Bottom, L=Left)
|
|
$options = "--header-html {$headerFile} --footer-html {$footerFile}";
|
|
|
|
$filename = $pdf->render($options); // Pass options to render
|
|
|
|
} catch (\Exception $e) {
|
|
// Log the error for debugging
|
|
error_log("PDF Generation Error: " . $e->getMessage());
|
|
// Attempt cleanup before erroring
|
|
if (file_exists($headerFile)) unlink($headerFile);
|
|
if (file_exists($footerFile)) unlink($footerFile);
|
|
self::sendError('Fehler beim Erstellen des PDFs: ' . $e->getMessage());
|
|
exit; // Ensure script stops
|
|
}
|
|
|
|
// --- Clean up temporary files ---
|
|
// if (file_exists($headerFile)) unlink($headerFile);
|
|
// if (file_exists($footerFile)) unlink($footerFile);
|
|
|
|
// --- Output or Return Filename ---
|
|
if ($returnFilename === true) {
|
|
return $filename;
|
|
}
|
|
|
|
if (!file_exists($filename)) {
|
|
self::sendError('Generierte PDF-Datei nicht gefunden.');
|
|
}
|
|
|
|
// Send PDF to browser
|
|
header('Content-Type: application/pdf');
|
|
// Use offer number in filename if available
|
|
$outputFilename = ($offer->offerNumber ?? $offer->id) . ".pdf";
|
|
header('Content-Disposition: inline; filename="' . basename($outputFilename) . '"');
|
|
header('Content-Length: ' . filesize($filename)); // Good practice
|
|
readfile($filename);
|
|
// Optionally delete the generated PDF after sending if it's temporary
|
|
// unlink($filename);
|
|
exit; // Crucial to prevent further output
|
|
}
|
|
|
|
protected function generateTemplate(string $templateName, array $replacements): string
|
|
{
|
|
// Example Implementation (Very Basic - Adapt!)
|
|
// Assumes templates are in a specific directory and use {{ key }} placeholders
|
|
$templatePath = BASEDIR . "/Layout/default/" . $templateName . ".html"; // Adjust path as needed
|
|
if (!file_exists($templatePath)) {
|
|
self::sendError('Template nicht gefunden: ' . $templatePath);
|
|
} else {
|
|
$content = file_get_contents($templatePath);
|
|
}
|
|
|
|
foreach ($replacements as $key => $value) {
|
|
$content = str_replace('{{ ' . $key . ' }}', $value ?? '', $content);
|
|
}
|
|
return $content;
|
|
}
|
|
|
|
|
|
|
|
}
|