prepare('SELECT * FROM invoices WHERE id = ?');
$stmt->execute([$id]);
$invoice = $stmt->fetch();
if (!$invoice) {
header('Content-Type: application/json; charset=utf-8');
errorResponse('Faktura nebyla nalezena', 404);
}
// Polozky
$stmt = $pdo->prepare('SELECT * FROM invoice_items WHERE invoice_id = ? ORDER BY position');
$stmt->execute([$id]);
$items = $stmt->fetchAll();
// Zakaznik
$customer = null;
if ($invoice['customer_id']) {
$stmt = $pdo->prepare('SELECT * FROM customers WHERE id = ?');
$stmt->execute([$invoice['customer_id']]);
$customer = $stmt->fetch();
}
// Firemni udaje
$stmt = $pdo->query('SELECT * FROM company_settings LIMIT 1');
$settings = $stmt->fetch();
// Logo
$logoBase64 = '';
$logoMime = 'image/png';
if (!empty($settings['logo_data'])) {
$logoBase64 = base64_encode($settings['logo_data']);
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detected = $finfo->buffer($settings['logo_data']);
$allowedMimes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
if ($detected && in_array($detected, $allowedMimes)) {
$logoMime = $detected;
}
}
// Helpery
$esc = function ($str) {
return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
};
$formatNum = function ($number, $decimals = 2) {
return number_format((float)$number, $decimals, ',', "\xC2\xA0");
};
$formatDate = function ($dateStr) {
if (!$dateStr) {
return '';
}
$d = strtotime($dateStr);
return $d !== false ? date('d.m.Y', $d) : $dateStr;
};
// Preklady statickych textu
$translations = [
'cs' => [
'title' => 'Faktura',
'heading' => 'FAKTURA - DAŇOVÝ DOKLAD č.',
'supplier' => 'Dodavatel',
'customer' => 'Odběratel',
'bank' => 'Banka:',
'swift' => 'SWIFT:',
'iban' => 'IBAN:',
'account_no' => 'Číslo účtu:',
'var_symbol' => 'Variabilní s.:',
'const_symbol' => 'Konstantní s.:',
'order_no' => 'Objednávka č.:',
'issue_date' => 'Datum vystavení:',
'due_date' => 'Datum splatnosti:',
'tax_date' => 'Datum uskutečnění plnění:',
'payment_method' => 'Forma úhrady:',
'billing' => 'Fakturujeme Vám za:',
'col_no' => 'Č.',
'col_desc' => 'Popis',
'col_qty' => 'Množství',
'col_unit_price' => 'Jedn. cena',
'col_price' => 'Cena',
'col_vat_pct' => '%DPH',
'col_vat' => 'DPH',
'col_total' => 'Celkem',
'subtotal' => 'Mezisoučet:',
'vat_label' => 'DPH',
'total' => 'Celkem k úhradě',
'amounts_in' => 'Částky jsou uvedeny v',
'notes' => 'Poznámky',
'issued_by' => 'Vystavil:',
'notice' => 'Dovolujeme si Vás upozornit, že v případě nedodržení data splatnosti'
. ' uvedeného na faktuře Vám budeme účtovat úrok z prodlení v dohodnuté, resp.'
. ' zákonné výši a smluvní pokutu (byla-li sjednána).',
'vat_recap' => 'Rekapitulace DPH v Kč:',
'vat_base' => 'Základ v Kč',
'vat_rate' => 'Sazba',
'vat_amount' => 'DPH v Kč',
'vat_with_total' => 'Celkem s DPH v Kč',
'received_by' => 'Převzal:',
'stamp' => 'Razítko:',
'ico' => 'IČ: ',
'dic' => 'DIČ: ',
],
'en' => [
'title' => 'Invoice',
'heading' => 'INVOICE - TAX DOCUMENT No.',
'supplier' => 'Supplier',
'customer' => 'Customer',
'bank' => 'Bank:',
'swift' => 'SWIFT:',
'iban' => 'IBAN:',
'account_no' => 'Account No.:',
'var_symbol' => 'Variable symbol:',
'const_symbol' => 'Constant symbol:',
'order_no' => 'Order No.:',
'issue_date' => 'Issue date:',
'due_date' => 'Due date:',
'tax_date' => 'Tax point date:',
'payment_method' => 'Payment method:',
'billing' => 'We invoice you for:',
'col_no' => 'No.',
'col_desc' => 'Description',
'col_qty' => 'Quantity',
'col_unit_price' => 'Unit price',
'col_price' => 'Price',
'col_vat_pct' => 'VAT%',
'col_vat' => 'VAT',
'col_total' => 'Total',
'subtotal' => 'Subtotal:',
'vat_label' => 'VAT',
'total' => 'Total to pay',
'amounts_in' => 'Amounts are in',
'notes' => 'Notes',
'issued_by' => 'Issued by:',
'notice' => 'Please note that in case of late payment, we will charge default interest'
. ' at the agreed or statutory rate and a contractual penalty (if agreed).',
'vat_recap' => 'VAT recapitulation in CZK:',
'vat_base' => 'Tax base in CZK',
'vat_rate' => 'Rate',
'vat_amount' => 'VAT in CZK',
'vat_with_total' => 'Total incl. VAT in CZK',
'received_by' => 'Received by:',
'stamp' => 'Stamp:',
'ico' => 'Reg. No.: ',
'dic' => 'Tax ID: ',
],
];
$t = $translations[$lang];
$currency = $invoice['currency'] ?? 'CZK';
$applyVat = !empty($invoice['apply_vat']);
// Vypocty
$vatSummary = [];
$subtotal = 0;
foreach ($items as $item) {
$lineSubtotal = (float)$item['quantity'] * (float)$item['unit_price'];
$subtotal += $lineSubtotal;
$rate = (float)$item['vat_rate'];
if (!isset($vatSummary[(string)$rate])) {
$vatSummary[(string)$rate] = ['base' => 0, 'vat' => 0];
}
$vatSummary[(string)$rate]['base'] += $lineSubtotal;
if ($applyVat) {
$vatSummary[(string)$rate]['vat'] += $lineSubtotal * $rate / 100;
}
}
$totalVat = 0;
foreach ($vatSummary as $data) {
$totalVat += $data['vat'];
}
$totalToPay = $subtotal + $totalVat;
// Rekapitulace DPH - vzdy v CZK (cesky danovy doklad)
$isForeign = strtoupper($currency) !== 'CZK';
$cnbRate = 1.0;
if ($isForeign) {
$cnb = CnbRates::getInstance();
$issueDate = $invoice['issue_date'] ?? '';
// Kurz CNB k datu vystaveni
$cnbRate = $cnb->toCzk(1.0, $currency, $issueDate);
}
$vatRates = [21, 12, 0];
$vatRecap = [];
foreach ($vatRates as $rate) {
$key = (string)(float)$rate;
$base = $vatSummary[$key]['base'] ?? 0;
$vat = $vatSummary[$key]['vat'] ?? 0;
$vatRecap[] = [
'rate' => $rate,
'base' => round($base * $cnbRate, 2),
'vat' => round($vat * $cnbRate, 2),
'total' => round(($base + $vat) * $cnbRate, 2),
];
}
// QR kod - SPAYD
$spaydParts = [
'SPD*1.0',
'ACC:' . str_replace(' ', '', $invoice['bank_iban']),
'AM:' . number_format($totalToPay, 2, '.', ''),
'CC:' . $currency,
'X-VS:' . $invoice['invoice_number'],
'X-KS:' . ($invoice['constant_symbol'] ?: '0308'),
'MSG:' . $t['title'] . ' ' . $invoice['invoice_number'],
];
$spaydString = implode('*', $spaydParts);
$qrSvg = '';
try {
$qrOptions = new QROptions([
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
'eccLevel' => QRCode::ECC_M,
'scale' => 3,
'addQuietzone' => true,
'svgUseCssProperties' => false,
]);
$qrCode = new QRCode($qrOptions);
$qrSvg = $qrCode->render($spaydString);
} catch (Exception $e) {
error_log('QR code generation failed: ' . $e->getMessage());
}
// Sanitizace HTML z Rich editoru
$cleanQuillHtml = function (string $html): string {
$allowedTags = '
- '
. '
';
$html = strip_tags($html, $allowedTags);
// Odstranit event handlery (quoted i unquoted hodnoty)
$html = preg_replace('/\s+on\w+\s*=\s*["\'][^"\']*["\']/i', '', $html);
$html = preg_replace('/\s+on\w+\s*=\s*[^\s>]*/i', '', $html);
// Odstranit javascript: v href a jinych atributech
$html = preg_replace('/href\s*=\s*["\']?\s*javascript\s*:[^"\'>\s]*/i', 'href="#"', $html);
$html = preg_replace('/\s+javascript\s*:/i', '', $html);
$html = preg_replace_callback(
'/(<[^>]*>)|( )/u',
function ($m) {
if (!empty($m[1])) {
return $m[1];
}
return ' ';
},
$html
);
$prev = null;
while ($prev !== $html) {
$prev = $html;
$html = preg_replace_callback(
'/]*)>(.*?)<\/span>\s*/su',
fn ($m) => '' . $m[2],
$html
);
}
return $html;
};
$indentCSS = '';
for ($n = 1; $n <= 9; $n++) {
$pad = $n * 3;
$liPad = $n * 3 + 1.5;
$indentCSS .= " .ql-indent-{$n} { padding-left: {$pad}em; }\n";
$indentCSS .= " li.ql-indent-{$n} { padding-left: {$liPad}em; }\n";
}
// Logo img
$logoImg = '';
if ($logoBase64) {
$logoImg = '
';
}
// Polozky HTML
$itemsHtml = '';
foreach ($items as $i => $item) {
$lineSubtotal = (float)$item['quantity'] * (float)$item['unit_price'];
$vatRate = (float)$item['vat_rate'];
$lineVat = $applyVat ? $lineSubtotal * $vatRate / 100 : 0;
$lineTotal = $lineSubtotal + $lineVat;
$rowNum = $i + 1;
$itemsHtml .= '
| ' . $rowNum . ' |
' . $esc($item['description']) . ' |
' . $formatNum(
$item['quantity'],
(floor((float)$item['quantity']) == (float)$item['quantity'] ? 0 : 2)
) . ' |
' . $formatNum($item['unit_price']) . ' |
' . $formatNum($lineSubtotal) . ' |
' . ($applyVat ? intval($vatRate) : 0) . '% |
' . $formatNum($lineVat) . ' |
' . $formatNum($lineTotal) . ' |
';
}
// DPH rekapitulace HTML
$vatRecapHtml = '';
foreach ($vatRecap as $vr) {
$vatRecapHtml .= '
| ' . $formatNum($vr['base']) . ' |
' . intval($vr['rate']) . '% |
' . $formatNum($vr['vat']) . ' |
' . $formatNum($vr['total']) . ' |
';
}
$invoiceNumber = $esc($invoice['invoice_number']);
// Odberatel
$custName = $customer ? $esc($customer['name']) : '';
$custStreet = $customer ? $esc($customer['street'] ?? '') : '';
$custCity = $customer ? trim(($customer['postal_code'] ?? '') . ' ' . ($customer['city'] ?? '')) : '';
$custCountry = $customer ? $esc($customer['country'] ?? '') : '';
$custIco = $customer ? $esc($customer['company_id'] ?? '') : '';
$custDic = $customer ? $esc($customer['vat_id'] ?? '') : '';
// Dodavatel
$suppName = $esc($settings['company_name'] ?? '');
$suppStreet = $esc($settings['street'] ?? '');
$suppCity = trim(($settings['postal_code'] ?? '') . ' ' . ($settings['city'] ?? ''));
$suppCountry = $esc($settings['country'] ?? 'Česká republika');
$suppIco = $esc($settings['company_id'] ?? '');
$suppDic = $esc($settings['vat_id'] ?? '');
$suppEmail = $esc($settings['Email'] ?? '');
$suppWeb = $esc($settings['Website'] ?? '');
// Sestaveni adresovych radku (stejne jako v nabidkach)
$buildAddressLines = function ($entity, $isSupplier) use ($t) {
if (!$entity) {
return ['name' => '', 'lines' => []];
}
$nameKey = $isSupplier ? 'company_name' : 'name';
$name = $entity[$nameKey] ?? '';
$cfData = [];
$fieldOrder = null;
$raw = $entity['custom_fields'] ?? null;
if ($raw) {
$parsed = is_string($raw) ? json_decode($raw, true) : $raw;
if (isset($parsed['fields'])) {
$cfData = $parsed['fields'] ?? [];
$fieldOrder = $parsed['field_order'] ?? $parsed['fieldOrder'] ?? null;
} elseif (is_array($parsed) && isset($parsed[0])) {
$cfData = $parsed;
}
}
// Zpetna kompatibilita - stare DB zaznamy maji PascalCase klice
if (is_array($fieldOrder)) {
$legacyMap = [
'Name' => 'name', 'CompanyName' => 'company_name',
'Street' => 'street', 'CityPostal' => 'city_postal',
'Country' => 'country', 'CompanyId' => 'company_id', 'VatId' => 'vat_id',
];
$fieldOrder = array_map(fn ($k) => $legacyMap[$k] ?? $k, $fieldOrder);
}
$fieldMap = [];
if ($name) {
$fieldMap[$nameKey] = $name;
}
if (!empty($entity['street'])) {
$fieldMap['street'] = $entity['street'];
}
$cityParts = array_filter([$entity['city'] ?? '', $entity['postal_code'] ?? '']);
$cityPostal = trim(implode(' ', $cityParts));
if ($cityPostal) {
$fieldMap['city_postal'] = $cityPostal;
}
if (!empty($entity['country'])) {
$fieldMap['country'] = $entity['country'];
}
if (!empty($entity['company_id'])) {
$fieldMap['company_id'] = $t['ico'] . $entity['company_id'];
}
if (!empty($entity['vat_id'])) {
$fieldMap['vat_id'] = $t['dic'] . $entity['vat_id'];
}
foreach ($cfData as $i => $cf) {
$cfName = trim($cf['name'] ?? '');
$cfValue = trim($cf['value'] ?? '');
$showLabel = $cf['showLabel'] ?? true;
if ($cfValue) {
$fieldMap["custom_{$i}"] = ($showLabel && $cfName) ? "{$cfName}: {$cfValue}" : $cfValue;
}
}
$lines = [];
if (is_array($fieldOrder) && count($fieldOrder)) {
foreach ($fieldOrder as $key) {
if ($key === $nameKey) {
continue;
}
if (isset($fieldMap[$key])) {
$lines[] = $fieldMap[$key];
}
}
foreach ($fieldMap as $key => $line) {
if ($key === $nameKey) {
continue;
}
if (!in_array($key, $fieldOrder)) {
$lines[] = $line;
}
}
} else {
foreach ($fieldMap as $key => $line) {
if ($key === $nameKey) {
continue;
}
$lines[] = $line;
}
}
return ['name' => $name, 'lines' => $lines];
};
$supp = $buildAddressLines($settings, true);
$cust = $buildAddressLines($customer, false);
$suppLinesHtml = '';
foreach ($supp['lines'] as $line) {
$suppLinesHtml .= '' . $esc($line) . '
';
}
$custLinesHtml = '';
foreach ($cust['lines'] as $line) {
$custLinesHtml .= '' . $esc($line) . '
';
}
// Objednavka
$orderNumber = '';
$orderDate = '';
if ($invoice['order_id']) {
$stmtOrder = $pdo->prepare('SELECT order_number, customer_order_number, created_at FROM orders WHERE id = ?');
$stmtOrder->execute([$invoice['order_id']]);
$orderRow = $stmtOrder->fetch();
if ($orderRow) {
$orderNumber = $esc($orderRow['customer_order_number'] ?: $orderRow['order_number']);
$orderDate = $formatDate($orderRow['created_at'] ?? '');
}
}
$html = '
' . $esc($t['title']) . ' ' . $invoiceNumber . '
' . $esc($t['supplier']) . '
' . $esc($supp['name']) . '
' . $suppLinesHtml . '
' . $esc($t['customer']) . '
' . $esc($cust['name']) . '
' . $custLinesHtml . '
' . $esc($t['bank']) . ' ' . $esc($invoice['bank_name']) . '
' . $esc($t['swift']) . ' ' . $esc($invoice['bank_swift']) . '
' . $esc($t['iban']) . ' ' . $esc($invoice['bank_iban']) . '
' . $esc($t['account_no']) . ' ' . $esc($invoice['bank_account']) . '
' . $esc($t['var_symbol']) . ' ' . $invoiceNumber . '
' . $esc($t['const_symbol'])
. ' ' . $esc($invoice['constant_symbol']) . '
' . ($orderNumber ? $esc($t['order_no']) . ' ' . $orderNumber : '') . '
' . $esc($t['issue_date']) . ' '
. $esc($formatDate($invoice['issue_date'])) . '
' . $esc($t['due_date']) . ' '
. $esc($formatDate($invoice['due_date'])) . '
' . $esc($t['tax_date']) . ' '
. $esc($formatDate($invoice['tax_date'])) . '
' . $esc($t['payment_method']) . ' '
. $esc($invoice['payment_method']) . '
' . $esc($t['billing']) . '
| ' . $esc($t['col_no']) . ' |
' . $esc($t['col_desc']) . ' |
' . $esc($t['col_qty']) . ' |
' . $esc($t['col_unit_price']) . ' |
' . $esc($t['col_price']) . ' |
' . $esc($t['col_vat_pct']) . ' |
' . $esc($t['col_vat']) . ' |
' . $esc($t['col_total']) . ' |
' . $itemsHtml . '
' . $esc($t['subtotal']) . '
' . $formatNum($subtotal) . ' ' . $esc($currency) . '
';
// DPH radky
if ($applyVat) {
foreach ($vatSummary as $rate => $data) {
if ($data['vat'] > 0) {
$html .= '
' . $esc($t['vat_label']) . ' ' . intval((float)$rate) . '%:
' . $formatNum($data['vat']) . ' ' . $esc($currency) . '
';
}
}
}
$html .= '
' . $esc($t['total']) . '
' . $formatNum($totalToPay) . ' ' . $esc($currency) . '
' . $esc($t['amounts_in']) . ' ' . $esc($currency) . '
' . (!empty(trim(strip_tags($invoice['notes'] ?? ''))) ? '
' . $esc($t['notes']) . '
' . $cleanQuillHtml($invoice['notes']) . '
' : '') . '
';
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit();
} catch (PDOException $e) {
error_log('Invoices PDF API error: ' . $e->getMessage());
header('Content-Type: application/json; charset=utf-8');
if (DEBUG_MODE) {
errorResponse('Chyba databáze: ' . $e->getMessage(), 500);
} else {
errorResponse('Chyba generování PDF', 500);
}
} catch (Exception $e) {
error_log('Invoices PDF generation error: ' . $e->getMessage());
header('Content-Type: application/json; charset=utf-8');
if (DEBUG_MODE) {
errorResponse('Chyba PDF: ' . $e->getMessage(), 500);
} else {
errorResponse('Chyba generování PDF', 500);
}
}