added pdf creation
This commit is contained in:
43
Layout/default/WarehouseOffer/PDF_FOOTER.html
Normal file
43
Layout/default/WarehouseOffer/PDF_FOOTER.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Xinon Rechnung</title>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body style="border:0; margin: 0;font-family: sans-serif, Verdana;font-size: 11px;" onload="subst()">
|
||||
|
||||
<script>
|
||||
function subst() {
|
||||
var vars = {};
|
||||
var query_strings_from_url = document.location.search.substring(1).split('&');
|
||||
for (var query_string in query_strings_from_url) {
|
||||
if (query_strings_from_url.hasOwnProperty(query_string)) {
|
||||
var temp_var = query_strings_from_url[query_string].split('=', 2);
|
||||
vars[temp_var[0]] = decodeURI(temp_var[1]);
|
||||
}
|
||||
}
|
||||
var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];
|
||||
for (var css_class in css_selector_classes) {
|
||||
if (css_selector_classes.hasOwnProperty(css_class)) {
|
||||
var element = document.getElementsByClassName(css_selector_classes[css_class]);
|
||||
for (var j = 0; j < element.length; ++j) {
|
||||
element[j].textContent = vars[css_selector_classes[css_class]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="margin-bottom: 16px;height: 1px"></div>
|
||||
<div style="color:grey;text-align: center;margin-bottom: 0">
|
||||
<span>XINON GmbH | Fladnitz 150 | 8322 Studenzen</span><br>
|
||||
<span>Tel.: +43 3115 40800 | E-Mail: office@xinon.at</span><br>
|
||||
<span>UID: ATU68711968 | FN: 416556h | LG: Feldbach</span><br>
|
||||
<span>IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}</span><br>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right">Seite <span class="page"></span> von <span class="topage"></span></div>
|
||||
|
||||
<div style="margin-top: 16px;height: 1px"></div>
|
||||
</body>
|
||||
</html>
|
||||
87
Layout/default/WarehouseOffer/PDF_HEADER.html
Normal file
87
Layout/default/WarehouseOffer/PDF_HEADER.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>XINON Shipping Note Header</title>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif, Verdana;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.customer-details {
|
||||
vertical-align: bottom;
|
||||
font-size: 14px;
|
||||
padding-left: 30pt;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.invoice-details {
|
||||
border: 2px solid #e1e1e1;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.invoice-details td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.invoice-details td:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-top: 24px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
#topSpacer {
|
||||
margin-bottom: 32px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="topSpacer"></div>
|
||||
|
||||
<div style="height: 50px; margin-bottom: 48px">
|
||||
<img alt="Xinon Logo" src="{{ basedir }}/public/assets/images/xinon-full.png" style="text-align:left;height: 85px;">
|
||||
</div>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td class="customer-details">
|
||||
<h3>{{ addressLine_header }}</h3>
|
||||
<div>{{ addressLine_1 }}</div>
|
||||
<div>{{ addressLine_2 }}</div>
|
||||
<div>{{ addressLine_3 }}</div>
|
||||
<div>{{ addressLine_4 }}</div>
|
||||
<div>{{ addressLine_5 }}</div>
|
||||
<div style="margin-bottom: 12pt"></div>
|
||||
<div>{{ externalReference }}</div>
|
||||
</td>
|
||||
<td class="customer-details">
|
||||
<h3>{{ billingAddressLine_header }}</h3>
|
||||
<div>{{ billingAddressLine_1 }}</div>
|
||||
<div>{{ billingAddressLine_2 }}</div>
|
||||
<div>{{ billingAddressLine_3 }}</div>
|
||||
<div>{{ billingAddressLine_4 }}</div>
|
||||
<div>{{ billingAddressLine_5 }}</div>
|
||||
<div>{{ billingAddressLine_6 }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
330
Layout/default/WarehouseOffer/PDF_MAIN.php
Normal file
330
Layout/default/WarehouseOffer/PDF_MAIN.php
Normal file
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Template for Warehouse Offer PDF Main Content
|
||||
*
|
||||
* @var \App\Models\WarehouseOfferModel $offer The offer object
|
||||
* @var array $entries Grouped positions array [groupKey => [position1, position2,...]]
|
||||
* @var float $subTotal Calculated subtotal of all positions
|
||||
* @var string $offerNumber Offer number
|
||||
* @var int $offerDate Timestamp of offer creation
|
||||
* @var int|null $validUntilDate Timestamp of offer validity end, or null
|
||||
* @var bool $includeTax Whether to calculate and show VAT
|
||||
* @var float $vatRate The VAT rate (e.g., 0.20 for 20%)
|
||||
* @var string $offerText Additional text from the offer record
|
||||
*
|
||||
* // Bank details are also available but typically used in footer
|
||||
* @var string $bank_iban
|
||||
* @var string $bank_bic
|
||||
* @var string $bank_bank
|
||||
* @var string $bank_owner
|
||||
*/
|
||||
|
||||
// Set filename for download (optional, can also be set in controller)
|
||||
// $this->setReturnValue(['filename' => ($offerNumber ?? $offer->id) . "_Angebot.pdf"]);
|
||||
|
||||
// --- Text Elements (Simple German Example - Extend for EN like in Order) ---
|
||||
$texts = [
|
||||
'DE' => [
|
||||
'title' => 'Angebot',
|
||||
'offerNumberLabel' => 'Angebotsnr.:',
|
||||
'offerDateLabel' => 'Datum:',
|
||||
'validUntilLabel' => 'Gültig bis:',
|
||||
'pageLabel' => 'Seite', // For page numbering in content if needed
|
||||
'table' => [
|
||||
'pos' => 'Pos',
|
||||
'article' => 'Artikel / Beschreibung',
|
||||
// 'description' => 'Beschreibung', // Combined with Article
|
||||
'amount' => 'Menge',
|
||||
'unit' => 'Einheit',
|
||||
'unitPrice' => 'Einzelpreis',
|
||||
'totalPrice' => 'Gesamtpreis'
|
||||
],
|
||||
'summary' => [
|
||||
'subTotal' => 'Zwischensumme',
|
||||
'vatFormatted' => 'zzgl. {VAT_RATE}% MwSt.', // Placeholder for rate
|
||||
'total' => 'Gesamtbetrag',
|
||||
'currency' => '€' // Currency symbol
|
||||
],
|
||||
'notes' => 'Anmerkungen:',
|
||||
'defaultOfferText' => 'Vielen Dank für Ihre Anfrage. Es gelten unsere Allgemeinen Geschäftsbedingungen.',
|
||||
'taxInfoNet' => '(Alle Preise exkl. MwSt.)', // Info if tax is added at the end
|
||||
'taxInfoGross' => '(Alle Preise inkl. MwSt.)', // Info if prices already include tax (less common in B2B offers)
|
||||
]
|
||||
// Add 'EN' => [...] section if needed
|
||||
];
|
||||
// Simple language selection (default to DE) - enhance if customer language is known
|
||||
$lang = 'DE';
|
||||
$text = $texts[$lang];
|
||||
$currencySymbol = $text['summary']['currency'];
|
||||
|
||||
// --- Calculations ---
|
||||
$vatAmount = 0;
|
||||
$grandTotal = $subTotal;
|
||||
if ($includeTax) {
|
||||
$vatAmount = $subTotal * $vatRate;
|
||||
$grandTotal = $subTotal + $vatAmount;
|
||||
}
|
||||
|
||||
// Format dates
|
||||
$formattedOfferDate = date("d.m.Y", $offerDate);
|
||||
$formattedValidUntil = $validUntilDate ? date("d.m.Y", $validUntilDate) : date("d.m.Y", strtotime("+14 days", $offerDate));
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><?= $text['title'] ?> <?= $offerNumber ?></title>
|
||||
<meta charset="utf-8"/>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif, Verdana;
|
||||
font-size: 10px; /* Slightly smaller base font */
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #005384; /* Xinon Blue */
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header-info {
|
||||
margin-bottom: 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.header-info table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.header-info td {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.header-info .label {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
width: 100px; /* Fixed width for labels */
|
||||
}
|
||||
|
||||
table#positionsTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
table#positionsTable th {
|
||||
border-bottom: 2px solid #005384;
|
||||
padding: 8px 4px;
|
||||
text-align: left;
|
||||
background-color: #f2f2f2;
|
||||
font-size: 10px;
|
||||
}
|
||||
table#positionsTable td {
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
padding: 6px 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
table#positionsTable tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Column Alignment */
|
||||
table#positionsTable th.pos, table#positionsTable td.pos { text-align: center; width: 30px; }
|
||||
table#positionsTable th.amount, table#positionsTable td.amount { text-align: right; width: 50px;}
|
||||
table#positionsTable th.unit, table#positionsTable td.unit { text-align: center; width: 40px;}
|
||||
table#positionsTable th.price, table#positionsTable td.price { text-align: right; width: 80px;}
|
||||
table#positionsTable th.total, table#positionsTable td.total { text-align: right; width: 90px;}
|
||||
table#positionsTable th.article, table#positionsTable td.article { text-align: left; }
|
||||
|
||||
.position-group-header td {
|
||||
background-color: #e8f0f8; /* Light blue for group headers */
|
||||
font-weight: bold;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.article-description {
|
||||
font-size: 9px; /* Smaller font for description */
|
||||
color: #555;
|
||||
padding-left: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
table#summaryTable {
|
||||
width: 45%; /* Adjust width as needed */
|
||||
float: right; /* Align to the right */
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
table#summaryTable td {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
table#summaryTable td.label {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
table#summaryTable td.value {
|
||||
text-align: right;
|
||||
}
|
||||
table#summaryTable tr.grand-total td {
|
||||
border-top: 1px solid #333;
|
||||
border-bottom: 3px double #333; /* Double line for grand total */
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
table#summaryTable tr.subtotal td {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.offer-text {
|
||||
margin-top: 30px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
font-size: 10px;
|
||||
clear: both; /* Ensure it clears the floated summary table */
|
||||
}
|
||||
.offer-text p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.tax-info {
|
||||
font-style: italic;
|
||||
font-size: 9px;
|
||||
margin-top: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Page break avoidance */
|
||||
tr, .position-group-header {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
h1, .header-info, table#summaryTable, .offer-text {
|
||||
page-break-before: auto;
|
||||
page-break-after: auto;
|
||||
}
|
||||
table#positionsTable {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1><?= $text['title'] ?></h1>
|
||||
|
||||
<div class="header-info">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label"><?= $text['offerNumberLabel'] ?></td>
|
||||
<td><?= htmlspecialchars($offerNumber) ?></td>
|
||||
<td class="label"><?= $text['offerDateLabel'] ?></td>
|
||||
<td><?= $formattedOfferDate ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td><td></td> <td class="label"><?= $text['validUntilLabel'] ?></td>
|
||||
<td><?= $formattedValidUntil ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table id="positionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="pos"><?= $text['table']['pos'] ?></th>
|
||||
<th class="article"><?= $text['table']['article'] ?></th>
|
||||
<th class="amount"><?= $text['table']['amount'] ?></th>
|
||||
<th class="unit"><?= $text['table']['unit'] ?></th>
|
||||
<th class="price"><?= $text['table']['unitPrice'] ?></th>
|
||||
<th class="total"><?= $text['table']['totalPrice'] ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$posCounter = 0;
|
||||
foreach ($entries as $groupName => $positions):
|
||||
// Optional: Display group name if it exists
|
||||
if (!empty($groupName)): ?>
|
||||
<tr class="position-group-header">
|
||||
<td colspan="6"><?= htmlspecialchars($groupName) ?></td>
|
||||
</tr>
|
||||
<?php endif;
|
||||
|
||||
foreach ($positions as $p):
|
||||
$posCounter++;
|
||||
?>
|
||||
<tr>
|
||||
<td class="pos"><?= $posCounter ?></td>
|
||||
<td class="article">
|
||||
<div class="article-title"><?= htmlspecialchars($p['articleText']) ?></div>
|
||||
<?php if (!empty($p['articleDescription'])): ?>
|
||||
<div class="article-description"><?= nl2br(htmlspecialchars($p['articleDescription'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="amount"><?= number_format($p['amount'], 2, ',', '.') // Format amount as needed ?></td>
|
||||
<td class="unit"><?= htmlspecialchars($p['articleUnit']) ?></td>
|
||||
<td class="price"><?= number_format($p['price'], 2, ',', '.') ?> <?= $currencySymbol ?></td>
|
||||
<td class="total"><?= number_format($p['totalPrice'], 2, ',', '.') ?> <?= $currencySymbol ?></td>
|
||||
</tr>
|
||||
<?php endforeach; // End positions loop ?>
|
||||
<?php endforeach; // End entries (groups) loop ?>
|
||||
|
||||
<?php if ($posCounter == 0): // Show message if no positions ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 20px;">Keine Positionen im Angebot enthalten.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table id="summaryTable">
|
||||
<tbody>
|
||||
<tr class="subtotal">
|
||||
<td class="label"><?= $text['summary']['subTotal'] ?>:</td>
|
||||
<td class="value"><?= number_format($subTotal, 2, ',', '.') ?> <?= $currencySymbol ?></td>
|
||||
</tr>
|
||||
<?php if ($includeTax):
|
||||
$vatLabel = str_replace('{VAT_RATE}', number_format($vatRate * 100, 0), $text['summary']['vatFormatted']);
|
||||
?>
|
||||
<tr>
|
||||
<td class="label"><?= $vatLabel ?>:</td>
|
||||
<td class="value"><?= number_format($vatAmount, 2, ',', '.') ?> <?= $currencySymbol ?></td>
|
||||
</tr>
|
||||
<tr class="grand-total">
|
||||
<td class="label"><?= $text['summary']['total'] ?>:</td>
|
||||
<td class="value"><?= number_format($grandTotal, 2, ',', '.') ?> <?= $currencySymbol ?></td>
|
||||
</tr>
|
||||
<?php else: // If tax not included, Total is same as Subtotal ?>
|
||||
<tr class="grand-total">
|
||||
<td class="label"><?= $text['summary']['total'] ?>:</td>
|
||||
<td class="value"><?= number_format($grandTotal, 2, ',', '.') ?> <?= $currencySymbol ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php // Tax Information Note
|
||||
if ($includeTax) {
|
||||
echo '<div class="tax-info" style="clear: both;">' . $text['taxInfoNet'] . '</div>';
|
||||
}
|
||||
// Add taxInfoGross if your prices *include* tax, which is less common for B2B offers.
|
||||
?>
|
||||
|
||||
|
||||
<div class="offer-text">
|
||||
<?php if (!empty($offerText)): ?>
|
||||
<p><strong><?= $text['notes'] ?></strong></p>
|
||||
<div><?= nl2br(htmlspecialchars($offerText)) // Use nl2br to preserve line breaks ?></div>
|
||||
<br>
|
||||
<?php endif; ?>
|
||||
<p><?= $text['defaultOfferText'] // Add default closing text ?></p>
|
||||
<p>Zahlungsbedingungen: 14 Tage netto.</p>
|
||||
<p>Lieferzeit: nach Vereinbarung.</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -80,57 +80,243 @@ class WarehouseOfferController extends TTCrud {
|
||||
}
|
||||
|
||||
public function createPDFAction($returnFilename = false, $idOverride = null) {
|
||||
//display errors
|
||||
// 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 (strlen($id) < 1) self::sendError('ID fehlt');
|
||||
|
||||
$offer = WarehouseOfferModel::get($id);
|
||||
if (!$offer->id) self::sendError('Angebot nicht gefunden');
|
||||
|
||||
$positions = json_decode($offer->positions, true);
|
||||
$entries = [];
|
||||
|
||||
foreach ($positions as $position) {
|
||||
if (!isset($position['article'])) continue;
|
||||
$article = WarehouseArticleModel::get($position['article']);
|
||||
$position['articleText'] = WarehouseArticleModel::get($position['article'])->title;
|
||||
$position['articleDescription'] = $article->description === $article->title ? "" : $article->description;
|
||||
$position['articleUnit'] = $position['unit'] ?? $article->unit ?? 'Stk.';
|
||||
|
||||
if (isset($position['_group'])) {
|
||||
$entries[$position['_group']][] = $position;
|
||||
} else {
|
||||
$entries[''][] = $position;
|
||||
}
|
||||
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,
|
||||
"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
|
||||
];
|
||||
|
||||
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOffer/PDF_HEADER.html");
|
||||
$headerHtml = str_replace("{{ addressLine_header }}",
|
||||
// {{ addressLine_1 }} {{ addressLine_2 }} {{ addressLine_3 }} {{ addressLine_4 }} {{ externalReference }}
|
||||
$headerHtml = str_replace("{{ addressLine_1 }}", $offer->customerName, $headerHtml));
|
||||
$headerHtml = str_replace("{{ addressLine_2 }}", $offer->customerStreet, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_3 }}", $offer->customerZip . ' ' . $offer->customerCity, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_5 }}", $offer->customerVAT, $headerHtml);
|
||||
$headerHtml = str_replace("{{ externalReference }}", $offer->customerReference, $headerHtml);
|
||||
// --- 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}";
|
||||
|
||||
exit;
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -233,7 +233,10 @@ Vue.component('warehouse-offer', {
|
||||
|
||||
|
||||
|
||||
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
|
||||
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table"
|
||||
@openpdf="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseOffer/createPDF?id=' + $event.id)"
|
||||
|
||||
>
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`,
|
||||
|
||||
Reference in New Issue
Block a user