'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' => false, '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'] = "Empfänger"; // 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'] = "Rechnungsadresse"; $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'] = "Rechnungsadresse"; } // --- 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['articleNumber'] = $article->articleNumber ?? null; $position['articleText'] = $article->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); } $editor = UserModel::getOne($offer->editor); $editorName = $editor ? $editor->name : 'Unbekannt'; // Fallback if editor not found // --- 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 "offerEditorName" => $editorName, // Editor name for the offer "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) ? "Ihre Referenz: " . 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; } }