fork download
  1. class PDF(FPDF):
  2. def header(self):
  3. self.set_font('Arial', 'B', 14)
  4. self.cell(0, 10, 'Guía de Equipos de Protección contra Caídas', ln=True, align='C')
  5. self.ln(5)
  6.  
  7. def chapter_title(self, title):
  8. self.set_font('Arial', 'B', 12)
  9. self.set_text_color(30, 30, 30)
  10. self.cell(0, 10, title, ln=True, align='L')
  11. self.ln(2)
  12.  
  13. def chapter_body(self, body):
  14. self.set_font('Arial', '', 11)
  15. self.set_text_color(50, 50, 50)
  16. self.multi_cell(0, 7, body)
  17. self.ln()
  18.  
  19. pdf = PDF()
  20. pdf.add_page()
  21.  
  22. contenido = [
  23. ("1. Introducción a los equipos",
  24. "Los equipos de protección contra caídas son dispositivos diseñados para proteger a los trabajadores que realizan tareas en altura..."),
  25.  
  26. ("2. Descripción de cada componente",
  27. "- Arneses: Sujetan el cuerpo del usuario, distribuyendo la fuerza de una caída.\n"
  28. "- Líneas de vida: Conectan al usuario con el punto de anclaje...\n"
  29. "- Anclajes, cinturones, mosquetones, etc."),
  30.  
  31. ("3. Unidades de medida y resistencia",
  32. "La resistencia se mide principalmente en kilonewtons (kN)...\n"
  33. "- 1 kN ≈ 100 kgf\n- 1 kN ≈ 225 lbf"),
  34.  
  35. ("4. Normas técnicas aplicables",
  36. "Normas importantes:\n- EN 361 (Arnés)\n- EN 362 (Conectores)\n- ANSI Z359...\n- OSHA 1910 / 1926"),
  37.  
  38. ("5. Errores comunes y buenas prácticas",
  39. "- No interpretar bien los valores técnicos.\n- Usar equipos incompatibles...\n- Verificar etiquetas y estado físico."),
  40.  
  41. ("6. Plantilla práctica de revisión",
  42. "Checklist:\n[ ] Arnés certificado\n[ ] Mosquetones seguros\n[ ] Línea de vida correcta\n[ ] Punto de anclaje ≥ 12 kN..."),
  43.  
  44. ("7. Marcas y recursos recomendados",
  45. "Marcas: Petzl, 3M, MSA, Kratos, Skylotec, Honeywell...\nConsulta manuales y videos oficiales.")
  46. ]
  47.  
  48. for titulo, texto in contenido:
  49. pdf.chapter_title(titulo)
  50. pdf.chapter_body(texto)
  51.  
  52. pdf.output("Guia_Equipos_Proteccion_Caidas.pdf")<?php
  53. /**
  54.  * Single-file French Quote & Invoice Generator
  55.  *
  56.  * This script handles two things:
  57.  * 1. If accessed via a POST request, it generates a PDF.
  58.  * 2. If accessed via a GET request, it displays the HTML interface.
  59.  */
  60.  
  61. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  62. // --- MODE 1: PDF GENERATION ---
  63.  
  64. require('lib/fpdf/fpdf.php');
  65.  
  66. // We need a custom class to create a header and footer
  67. class PDF extends FPDF
  68. {
  69. private $docData;
  70.  
  71. function __construct($orientation = 'P', $unit = 'mm', $size = 'A4', $data = []) {
  72. parent::__construct($orientation, $unit, $size);
  73. $this->docData = $data;
  74. }
  75.  
  76. function Header() {
  77. $this->SetFont('Arial', 'B', 20);
  78. $this->Cell(0, 10, utf8_decode(strtoupper($this->docData['doc_type'])), 0, 1, 'L');
  79. $this->SetFont('Arial', '', 12);
  80. $this->Cell(0, 7, utf8_decode($this->docData['doc']['number']), 0, 1, 'L');
  81. $this->Ln(15);
  82.  
  83. $this->SetFont('Arial', 'B', 10);
  84. $this->Cell(95, 7, utf8_decode($this->docData['company']['name']), 0, 0, 'L');
  85. $this->Cell(95, 7, utf8_decode($this->docData['client']['name']), 0, 1, 'R');
  86.  
  87. $this->SetFont('Arial', '', 10);
  88. $yPos = $this->GetY();
  89. $this->MultiCell(95, 5, utf8_decode($this->docData['company']['address']), 0, 'L');
  90. $this->SetXY(115, $yPos); // Set X to the right column
  91. $this->MultiCell(85, 5, utf8_decode($this->docData['client']['address']), 0, 'L');
  92.  
  93. // Use GetY from the longest MultiCell to set the next position correctly
  94. $yPosAfterAddress = $this->GetY();
  95.  
  96. $this->SetY($yPosAfterAddress);
  97. $this->Ln(2);
  98. if(!empty($this->docData['company']['siret'])) $this->Cell(95, 5, utf8_decode('SIRET : ' . $this->docData['company']['siret']), 0, 1, 'L');
  99. if(!empty($this->docData['company']['vat'])) $this->Cell(95, 5, utf8_decode('N° TVA : ' . $this->docData['company']['vat']), 0, 1, 'L');
  100.  
  101. $this->Ln(10);
  102. $this->SetFont('Arial', '', 10);
  103. $this->Cell(0, 5, utf8_decode('Date d\'émission : ' . date("d/m/Y", strtotime($this->docData['doc']['date']))), 0, 1, 'R');
  104. if (!empty($this->docData['doc']['due_date'])) {
  105. $this->Cell(0, 5, utf8_decode('Date d\'échéance : ' . date("d/m/Y", strtotime($this->docData['doc']['due_date']))), 0, 1, 'R');
  106. }
  107. $this->Ln(15);
  108. }
  109.  
  110. function Footer() {
  111. $this->SetY(-30);
  112. if (!empty($this->docData['notes'])) {
  113. $this->SetFont('Arial','',9);
  114. $this->Cell(0, 5, 'Notes :', 0, 1, 'L');
  115. $this->MultiCell(0, 5, utf8_decode($this->docData['notes']), 0, 'L');
  116. }
  117. $this->SetY(-15);
  118. $this->SetFont('Arial','I',8);
  119. $this->Cell(0,10, 'Page '.$this->PageNo().'/{nb}',0,0,'C');
  120. }
  121. }
  122.  
  123. $json = file_get_contents('php://input');
  124. $data = json_decode($json, true);
  125.  
  126. if ($data === null) {
  127. http_response_code(400);
  128. die('Invalid JSON');
  129. }
  130.  
  131. $pdf = new PDF('P', 'mm', 'A4', $data);
  132. $pdf->AliasNbPages();
  133. $pdf->AddPage();
  134.  
  135. $pdf->SetFont('Arial', 'B', 10);
  136. $pdf->SetFillColor(230, 230, 230);
  137. $pdf->Cell(100, 8, 'Description', 1, 0, 'L', true);
  138. $pdf->Cell(20, 8, utf8_decode('Qté'), 1, 0, 'C', true);
  139. $pdf->Cell(35, 8, 'P.U. HT', 1, 0, 'C', true);
  140. $pdf->Cell(35, 8, 'Total HT', 1, 1, 'C', true);
  141.  
  142. $pdf->SetFont('Arial', '', 10);
  143. $currencySymbol = utf8_decode('€');
  144. foreach ($data['items'] as $item) {
  145. $pdf->Cell(100, 8, utf8_decode($item['description']), 1, 0, 'L');
  146. $pdf->Cell(20, 8, $item['quantity'], 1, 0, 'R');
  147. $pdf->Cell(35, 8, number_format((float)$item['price'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 0, 'R');
  148. $pdf->Cell(35, 8, number_format((float)$item['total'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R');
  149. }
  150.  
  151. $pdf->Ln(10);
  152. $pdf->SetFont('Arial', '', 10);
  153. $totalsX = 120;
  154. $totalsLabelWidth = 35;
  155. $totalsValueWidth = 45;
  156.  
  157. $pdf->Cell($totalsX, 8, '', 0, 0);
  158. $pdf->Cell($totalsLabelWidth, 8, 'Total HT', 1, 0, 'L');
  159. $pdf->Cell($totalsValueWidth, 8, number_format((float)$data['totals']['subtotal'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R');
  160.  
  161. $pdf->Cell($totalsX, 8, '', 0, 0);
  162. $pdf->Cell($totalsLabelWidth, 8, 'TVA (' . $data['totals']['vat_rate'] . '%)', 1, 0, 'L');
  163. $pdf->Cell($totalsValueWidth, 8, number_format((float)$data['totals']['vat_total'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R');
  164.  
  165. $pdf->SetFont('Arial', 'B', 12);
  166. $pdf->Cell($totalsX, 10, '', 0, 0);
  167. $pdf->Cell($totalsLabelWidth, 10, 'Total TTC', 1, 0, 'L', true);
  168. $pdf->Cell($totalsValueWidth, 10, number_format((float)$data['totals']['total_ttc'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R', true);
  169.  
  170. $filename = strtoupper($data['doc_type']) . '-' . preg_replace('/[^a-zA-Z0-9-]/', '', $data['doc']['number']) . '.pdf';
  171. $pdf->Output('D', $filename);
  172.  
  173. // Stop execution to prevent HTML from being sent
  174. }
  175.  
  176. // --- MODE 2: HTML INTERFACE ---
  177. ?>
  178. <!DOCTYPE html>
  179. <html lang="fr" data-theme="light">
  180. <head>
  181. <meta charset="UTF-8">
  182. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  183. <title>Générateur de Devis et Factures</title>
  184. <link rel="stylesheet" href="https://c...content-available-to-author-only...r.net/npm/@picocss/pico@1/css/pico.min.css">
  185. <style>
  186. /* Embedded custom CSS */
  187. body {
  188. padding-bottom: 5rem;
  189. }
  190. .table-container {
  191. overflow-x: auto;
  192. }
  193. table th:last-child,
  194. table td:last-child {
  195. text-align: right;
  196. }
  197. table input[type="number"] {
  198. min-width: 80px;
  199. text-align: right;
  200. }
  201. .totals-section {
  202. text-align: right;
  203. padding-top: 1rem;
  204. border-left: 1px solid var(--pico-muted-border-color);
  205. padding-left: 1rem;
  206. }
  207. .totals-section p {
  208. margin-bottom: 0.5rem;
  209. }
  210. .totals-section strong {
  211. margin-right: 1rem;
  212. }
  213. .notes-section {
  214. padding-right: 1rem;
  215. }
  216. .form-actions {
  217. margin-top: 2rem;
  218. display: flex;
  219. justify-content: flex-end;
  220. gap: 1rem;
  221. }
  222. .remove-item {
  223. padding: 0.25rem 0.5rem;
  224. line-height: 1;
  225. }
  226. </style>
  227. </head>
  228. <body>
  229. <main class="container">
  230. <header>
  231. <h1>Générateur de Devis & Factures</h1>
  232. <p>Toutes les données sont sauvegardées localement dans votre navigateur. Aucune information n'est envoyée à un serveur.</p>
  233. </header>
  234.  
  235. <form id="invoice-form">
  236. <fieldset>
  237. <legend>Type de document</legend>
  238. <label for="doc-type-quote">
  239. <input type="radio" id="doc-type-quote" name="doc_type" value="Devis" checked>
  240. Devis
  241. </label>
  242. <label for="doc-type-invoice">
  243. <input type="radio" id="doc-type-invoice" name="doc_type" value="Facture">
  244. Facture
  245. </label>
  246. </fieldset>
  247.  
  248. <div class="grid">
  249. <article>
  250. <h3 id="company-title">Votre Entreprise</h3>
  251. <label for="company_name">Nom de l'entreprise</label>
  252. <input type="text" id="company_name" name="company_name" required>
  253. <label for="company_address">Adresse</label>
  254. <textarea id="company_address" name="company_address" rows="3"></textarea>
  255. <div class="grid">
  256. <label for="company_siret">SIRET <input type="text" id="company_siret" name="company_siret"></label>
  257. <label for="company_vat">N° TVA <input type="text" id="company_vat" name="company_vat"></label>
  258. </div>
  259. <button type="button" id="save-company-info" class="secondary">Enregistrer mes informations</button>
  260. </article>
  261.  
  262. <article>
  263. <h3>Client</h3>
  264. <label for="client_name">Nom du client</label>
  265. <input type="text" id="client_name" name="client_name" required>
  266. <label for="client_address">Adresse du client</label>
  267. <textarea id="client_address" name="client_address" rows="3"></textarea>
  268. </article>
  269. </div>
  270.  
  271. <article>
  272. <div class="grid">
  273. <label for="doc_number"><span id="doc-type-label">Numéro de Devis</span>
  274. <input type="text" id="doc_number" name="doc_number" required>
  275. </label>
  276. <label for="doc_date">Date
  277. <input type="date" id="doc_date" name="doc_date" required>
  278. </label>
  279. <label for="doc_due_date">Date d'échéance
  280. <input type="date" id="doc_due_date" name="doc_due_date">
  281. </label>
  282. </div>
  283. </article>
  284.  
  285. <article>
  286. <h3>Lignes de prestation</h3>
  287. <div class="table-container">
  288. <table>
  289. <thead>
  290. <tr>
  291. <th>Description</th>
  292. <th>Qté</th>
  293. <th>P.U. HT</th>
  294. <th>Total HT</th>
  295. <th></th>
  296. </tr>
  297. </thead>
  298. <tbody id="item-list"></tbody>
  299. </table>
  300. </div>
  301. <button type="button" id="add-item" class="secondary">Ajouter une ligne</button>
  302. </article>
  303.  
  304. <div class="grid">
  305. <div class="notes-section">
  306. <label for="notes">Notes / Conditions de paiement</label>
  307. <textarea id="notes" name="notes" rows="4">Paiement à réception de la facture.</textarea>
  308. </div>
  309. <article class="totals-section">
  310. <div class="grid">
  311. <label for="vat_rate">Taux de TVA (%)</label>
  312. <input type="number" id="vat_rate" name="vat_rate" value="20" step="0.1" required>
  313. </div>
  314. <p><strong>Total HT :</strong> <span id="subtotal">0.00</span> €</p>
  315. <p><strong>TVA :</strong> <span id="vat-total">0.00</span> €</p>
  316. <p><strong>Total TTC :</strong> <span id="total-ttc">0.00</span> €</p>
  317. </article>
  318. </div>
  319.  
  320. <footer class="form-actions">
  321. <button type="submit" id="generate-pdf">Générer le PDF</button>
  322. <button type="button" id="reset-form" class="secondary outline">Réinitialiser</button>
  323. </footer>
  324. </form>
  325. </main>
  326.  
  327. <script>
  328. // --- Embedded JavaScript ---
  329. document.addEventListener('DOMContentLoaded', () => {
  330. const form = document.getElementById('invoice-form');
  331. const itemList = document.getElementById('item-list');
  332. const addItemBtn = document.getElementById('add-item');
  333. const saveCompanyInfoBtn = document.getElementById('save-company-info');
  334. const resetFormBtn = document.getElementById('reset-form');
  335. const subtotalEl = document.getElementById('subtotal');
  336. const vatTotalEl = document.getElementById('vat-total');
  337. const totalTtcEl = document.getElementById('total-ttc');
  338. const docTypeRadios = document.querySelectorAll('input[name="doc_type"]');
  339. const docTypeLabel = document.getElementById('doc-type-label');
  340. const docNumberInput = document.getElementById('doc_number');
  341. const companyTitle = document.getElementById('company-title');
  342.  
  343. const calculateTotals = () => {
  344. let subtotal = 0;
  345. itemList.querySelectorAll('tr').forEach(row => {
  346. const quantity = parseFloat(row.querySelector('.quantity').value) || 0;
  347. const price = parseFloat(row.querySelector('.price').value) || 0;
  348. const rowTotal = quantity * price;
  349. row.querySelector('.row-total').textContent = rowTotal.toFixed(2);
  350. subtotal += rowTotal;
  351. });
  352. const vatRate = parseFloat(document.getElementById('vat_rate').value) || 0;
  353. const vatTotal = subtotal * (vatRate / 100);
  354. const totalTtc = subtotal + vatTotal;
  355. subtotalEl.textContent = subtotal.toFixed(2);
  356. vatTotalEl.textContent = vatTotal.toFixed(2);
  357. totalTtcEl.textContent = totalTtc.toFixed(2);
  358. };
  359.  
  360. const addLineItem = () => {
  361. const row = document.createElement('tr');
  362. row.innerHTML = `
  363. <td><input type="text" class="description" placeholder="Description de la prestation"></td>
  364. <td><input type="number" class="quantity" value="1" step="any"></td>
  365. <td><input type="number" class="price" value="0.00" step="any"></td>
  366. <td><span class="row-total">0.00</span> €</td>
  367. <td><button type="button" class="remove-item secondary outline">×</button></td>
  368. `;
  369. itemList.appendChild(row);
  370. row.querySelector('.remove-item').addEventListener('click', () => {
  371. row.remove();
  372. calculateTotals();
  373. });
  374. };
  375.  
  376. const updateDocType = () => {
  377. const selectedType = document.querySelector('input[name="doc_type"]:checked').value;
  378. docTypeLabel.textContent = `Numéro de ${selectedType}`;
  379. const prefix = selectedType === 'Devis' ? 'DE' : 'FA';
  380. const currentVal = docNumberInput.value;
  381. if (!currentVal.startsWith('DE-') && !currentVal.startsWith('FA-') || currentVal === '') {
  382. const year = new Date().getFullYear();
  383. docNumberInput.value = `${prefix}-${year}-001`;
  384. }
  385. };
  386.  
  387. const saveCompanyInfo = () => {
  388. const companyInfo = {
  389. name: document.getElementById('company_name').value,
  390. address: document.getElementById('company_address').value,
  391. siret: document.getElementById('company_siret').value,
  392. vat: document.getElementById('company_vat').value,
  393. };
  394. localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
  395. companyTitle.textContent = 'Votre Entreprise (Enregistré)';
  396. setTimeout(() => companyTitle.textContent = 'Votre Entreprise', 2000);
  397. };
  398.  
  399. const loadCompanyInfo = () => {
  400. const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
  401. if (companyInfo) {
  402. document.getElementById('company_name').value = companyInfo.name || '';
  403. document.getElementById('company_address').value = companyInfo.address || '';
  404. document.getElementById('company_siret').value = companyInfo.siret || '';
  405. document.getElementById('company_vat').value = companyInfo.vat || '';
  406. }
  407. };
  408.  
  409. const resetForm = () => {
  410. if(confirm("Voulez-vous vraiment réinitialiser le formulaire ? Les informations de votre entreprise resteront enregistrées.")) {
  411. const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
  412. form.reset();
  413. localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
  414. loadCompanyInfo();
  415. itemList.innerHTML = '';
  416. addLineItem();
  417. document.getElementById('doc_date').valueAsDate = new Date();
  418. updateDocType();
  419. calculateTotals();
  420. }
  421. }
  422.  
  423. const generatePDF = async (e) => {
  424. e.preventDefault();
  425. const items = Array.from(itemList.querySelectorAll('tr')).map(row => ({
  426. description: row.querySelector('.description').value,
  427. quantity: row.querySelector('.quantity').value,
  428. price: row.querySelector('.price').value,
  429. total: parseFloat(row.querySelector('.row-total').textContent)
  430. }));
  431. const formData = {
  432. doc_type: document.querySelector('input[name="doc_type"]:checked').value,
  433. company: { name: document.getElementById('company_name').value, address: document.getElementById('company_address').value, siret: document.getElementById('company_siret').value, vat: document.getElementById('company_vat').value },
  434. client: { name: document.getElementById('client_name').value, address: document.getElementById('client_address').value },
  435. doc: { number: document.getElementById('doc_number').value, date: document.getElementById('doc_date').value, due_date: document.getElementById('doc_due_date').value },
  436. items: items,
  437. totals: { subtotal: subtotalEl.textContent, vat_rate: document.getElementById('vat_rate').value, vat_total: vatTotalEl.textContent, total_ttc: totalTtcEl.textContent },
  438. notes: document.getElementById('notes').value
  439. };
  440. const pdfButton = document.getElementById('generate-pdf');
  441. pdfButton.setAttribute('aria-busy', 'true');
  442. pdfButton.textContent = 'Génération...';
  443. try {
  444. const response = await fetch('', { // Post to the same file
  445. method: 'POST',
  446. headers: { 'Content-Type': 'application/json' },
  447. body: JSON.stringify(formData)
  448. });
  449. if (!response.ok) throw new Error(`Erreur du serveur: ${response.statusText}`);
  450. const blob = await response.blob();
  451. const url = window.URL.createObjectURL(blob);
  452. const a = document.createElement('a');
  453. a.style.display = 'none';
  454. a.href = url;
  455. a.download = `${formData.doc_type.toUpperCase()}-${formData.doc.number.replace(/[^a-zA-Z0-9-]/g, '')}.pdf`;
  456. document.body.appendChild(a);
  457. a.click();
  458. window.URL.revokeObjectURL(url);
  459. a.remove();
  460. } catch (error) {
  461. console.error('Erreur lors de la génération du PDF:', error);
  462. alert('Une erreur est survenue lors de la génération du PDF.');
  463. } finally {
  464. pdfButton.removeAttribute('aria-busy');
  465. pdfButton.textContent = 'Générer le PDF';
  466. }
  467. };
  468.  
  469. addItemBtn.addEventListener('click', addLineItem);
  470. form.addEventListener('input', calculateTotals);
  471. form.addEventListener('submit', generatePDF);
  472. saveCompanyInfoBtn.addEventListener('click', saveCompanyInfo);
  473. resetFormBtn.addEventListener('click', resetForm);
  474. docTypeRadios.forEach(radio => radio.addEventListener('change', updateDocType));
  475.  
  476. // --- Initialisation ---
  477. loadCompanyInfo();
  478. addLineItem();
  479. calculateTotals();
  480. document.getElementById('doc_date').valueAsDate = new Date();
  481. updateDocType();
  482. });
  483. </script>
  484. </body>
  485. </html>
Success #stdin #stdout #stderr 0.03s 25924KB
stdin
Standard input is empty
stdout
class PDF(FPDF):
    def header(self):
        self.set_font('Arial', 'B', 14)
        self.cell(0, 10, 'Guía de Equipos de Protección contra Caídas', ln=True, align='C')
        self.ln(5)

    def chapter_title(self, title):
        self.set_font('Arial', 'B', 12)
        self.set_text_color(30, 30, 30)
        self.cell(0, 10, title, ln=True, align='L')
        self.ln(2)

    def chapter_body(self, body):
        self.set_font('Arial', '', 11)
        self.set_text_color(50, 50, 50)
        self.multi_cell(0, 7, body)
        self.ln()

pdf = PDF()
pdf.add_page()

contenido = [
    ("1. Introducción a los equipos", 
     "Los equipos de protección contra caídas son dispositivos diseñados para proteger a los trabajadores que realizan tareas en altura..."),

    ("2. Descripción de cada componente", 
     "- Arneses: Sujetan el cuerpo del usuario, distribuyendo la fuerza de una caída.\n"
     "- Líneas de vida: Conectan al usuario con el punto de anclaje...\n"
     "- Anclajes, cinturones, mosquetones, etc."),

    ("3. Unidades de medida y resistencia", 
     "La resistencia se mide principalmente en kilonewtons (kN)...\n"
     "- 1 kN ≈ 100 kgf\n- 1 kN ≈ 225 lbf"),

    ("4. Normas técnicas aplicables", 
     "Normas importantes:\n- EN 361 (Arnés)\n- EN 362 (Conectores)\n- ANSI Z359...\n- OSHA 1910 / 1926"),

    ("5. Errores comunes y buenas prácticas", 
     "- No interpretar bien los valores técnicos.\n- Usar equipos incompatibles...\n- Verificar etiquetas y estado físico."),

    ("6. Plantilla práctica de revisión", 
     "Checklist:\n[ ] Arnés certificado\n[ ] Mosquetones seguros\n[ ] Línea de vida correcta\n[ ] Punto de anclaje ≥ 12 kN..."),

    ("7. Marcas y recursos recomendados", 
     "Marcas: Petzl, 3M, MSA, Kratos, Skylotec, Honeywell...\nConsulta manuales y videos oficiales.")
]

for titulo, texto in contenido:
    pdf.chapter_title(titulo)
    pdf.chapter_body(texto)

pdf.output("Guia_Equipos_Proteccion_Caidas.pdf")<!DOCTYPE html>
<html lang="fr" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Générateur de Devis et Factures</title>
    <link rel="stylesheet" href="https://c...content-available-to-author-only...r.net/npm/@picocss/pico@1/css/pico.min.css">
    <style>
        /* Embedded custom CSS */
        body {
            padding-bottom: 5rem;
        }
        .table-container {
            overflow-x: auto;
        }
        table th:last-child,
        table td:last-child {
            text-align: right;
        }
        table input[type="number"] {
            min-width: 80px;
            text-align: right;
        }
        .totals-section {
            text-align: right;
            padding-top: 1rem;
            border-left: 1px solid var(--pico-muted-border-color);
            padding-left: 1rem;
        }
        .totals-section p {
            margin-bottom: 0.5rem;
        }
        .totals-section strong {
            margin-right: 1rem;
        }
        .notes-section {
            padding-right: 1rem;
        }
        .form-actions {
            margin-top: 2rem;
            display: flex;
            justify-content: flex-end;
            gap: 1rem;
        }
        .remove-item {
            padding: 0.25rem 0.5rem;
            line-height: 1;
        }
    </style>
</head>
<body>
    <main class="container">
        <header>
            <h1>Générateur de Devis & Factures</h1>
            <p>Toutes les données sont sauvegardées localement dans votre navigateur. Aucune information n'est envoyée à un serveur.</p>
        </header>

        <form id="invoice-form">
            <fieldset>
                <legend>Type de document</legend>
                <label for="doc-type-quote">
                    <input type="radio" id="doc-type-quote" name="doc_type" value="Devis" checked>
                    Devis
                </label>
                <label for="doc-type-invoice">
                    <input type="radio" id="doc-type-invoice" name="doc_type" value="Facture">
                    Facture
                </label>
            </fieldset>

            <div class="grid">
                <article>
                    <h3 id="company-title">Votre Entreprise</h3>
                    <label for="company_name">Nom de l'entreprise</label>
                    <input type="text" id="company_name" name="company_name" required>
                    <label for="company_address">Adresse</label>
                    <textarea id="company_address" name="company_address" rows="3"></textarea>
                    <div class="grid">
                        <label for="company_siret">SIRET <input type="text" id="company_siret" name="company_siret"></label>
                        <label for="company_vat">N° TVA <input type="text" id="company_vat" name="company_vat"></label>
                    </div>
                     <button type="button" id="save-company-info" class="secondary">Enregistrer mes informations</button>
                </article>

                <article>
                    <h3>Client</h3>
                    <label for="client_name">Nom du client</label>
                    <input type="text" id="client_name" name="client_name" required>
                    <label for="client_address">Adresse du client</label>
                    <textarea id="client_address" name="client_address" rows="3"></textarea>
                </article>
            </div>
            
            <article>
                <div class="grid">
                    <label for="doc_number"><span id="doc-type-label">Numéro de Devis</span>
                        <input type="text" id="doc_number" name="doc_number" required>
                    </label>
                    <label for="doc_date">Date
                        <input type="date" id="doc_date" name="doc_date" required>
                    </label>
                    <label for="doc_due_date">Date d'échéance
                        <input type="date" id="doc_due_date" name="doc_due_date">
                    </label>
                </div>
            </article>

            <article>
                <h3>Lignes de prestation</h3>
                <div class="table-container">
                    <table>
                        <thead>
                            <tr>
                                <th>Description</th>
                                <th>Qté</th>
                                <th>P.U. HT</th>
                                <th>Total HT</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody id="item-list"></tbody>
                    </table>
                </div>
                <button type="button" id="add-item" class="secondary">Ajouter une ligne</button>
            </article>

            <div class="grid">
                <div class="notes-section">
                     <label for="notes">Notes / Conditions de paiement</label>
                     <textarea id="notes" name="notes" rows="4">Paiement à réception de la facture.</textarea>
                </div>
                <article class="totals-section">
                    <div class="grid">
                        <label for="vat_rate">Taux de TVA (%)</label>
                        <input type="number" id="vat_rate" name="vat_rate" value="20" step="0.1" required>
                    </div>
                    <p><strong>Total HT :</strong> <span id="subtotal">0.00</span> €</p>
                    <p><strong>TVA :</strong> <span id="vat-total">0.00</span> €</p>
                    <p><strong>Total TTC :</strong> <span id="total-ttc">0.00</span> €</p>
                </article>
            </div>

            <footer class="form-actions">
                <button type="submit" id="generate-pdf">Générer le PDF</button>
                <button type="button" id="reset-form" class="secondary outline">Réinitialiser</button>
            </footer>
        </form>
    </main>

    <script>
        // --- Embedded JavaScript ---
        document.addEventListener('DOMContentLoaded', () => {
            const form = document.getElementById('invoice-form');
            const itemList = document.getElementById('item-list');
            const addItemBtn = document.getElementById('add-item');
            const saveCompanyInfoBtn = document.getElementById('save-company-info');
            const resetFormBtn = document.getElementById('reset-form');
            const subtotalEl = document.getElementById('subtotal');
            const vatTotalEl = document.getElementById('vat-total');
            const totalTtcEl = document.getElementById('total-ttc');
            const docTypeRadios = document.querySelectorAll('input[name="doc_type"]');
            const docTypeLabel = document.getElementById('doc-type-label');
            const docNumberInput = document.getElementById('doc_number');
            const companyTitle = document.getElementById('company-title');

            const calculateTotals = () => {
                let subtotal = 0;
                itemList.querySelectorAll('tr').forEach(row => {
                    const quantity = parseFloat(row.querySelector('.quantity').value) || 0;
                    const price = parseFloat(row.querySelector('.price').value) || 0;
                    const rowTotal = quantity * price;
                    row.querySelector('.row-total').textContent = rowTotal.toFixed(2);
                    subtotal += rowTotal;
                });
                const vatRate = parseFloat(document.getElementById('vat_rate').value) || 0;
                const vatTotal = subtotal * (vatRate / 100);
                const totalTtc = subtotal + vatTotal;
                subtotalEl.textContent = subtotal.toFixed(2);
                vatTotalEl.textContent = vatTotal.toFixed(2);
                totalTtcEl.textContent = totalTtc.toFixed(2);
            };

            const addLineItem = () => {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td><input type="text" class="description" placeholder="Description de la prestation"></td>
                    <td><input type="number" class="quantity" value="1" step="any"></td>
                    <td><input type="number" class="price" value="0.00" step="any"></td>
                    <td><span class="row-total">0.00</span> €</td>
                    <td><button type="button" class="remove-item secondary outline">×</button></td>
                `;
                itemList.appendChild(row);
                row.querySelector('.remove-item').addEventListener('click', () => {
                    row.remove();
                    calculateTotals();
                });
            };

            const updateDocType = () => {
                const selectedType = document.querySelector('input[name="doc_type"]:checked').value;
                docTypeLabel.textContent = `Numéro de ${selectedType}`;
                const prefix = selectedType === 'Devis' ? 'DE' : 'FA';
                const currentVal = docNumberInput.value;
                if (!currentVal.startsWith('DE-') && !currentVal.startsWith('FA-') || currentVal === '') {
                    const year = new Date().getFullYear();
                    docNumberInput.value = `${prefix}-${year}-001`;
                }
            };

            const saveCompanyInfo = () => {
                const companyInfo = {
                    name: document.getElementById('company_name').value,
                    address: document.getElementById('company_address').value,
                    siret: document.getElementById('company_siret').value,
                    vat: document.getElementById('company_vat').value,
                };
                localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
                companyTitle.textContent = 'Votre Entreprise (Enregistré)';
                setTimeout(() => companyTitle.textContent = 'Votre Entreprise', 2000);
            };

            const loadCompanyInfo = () => {
                const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
                if (companyInfo) {
                    document.getElementById('company_name').value = companyInfo.name || '';
                    document.getElementById('company_address').value = companyInfo.address || '';
                    document.getElementById('company_siret').value = companyInfo.siret || '';
                    document.getElementById('company_vat').value = companyInfo.vat || '';
                }
            };
            
            const resetForm = () => {
                if(confirm("Voulez-vous vraiment réinitialiser le formulaire ? Les informations de votre entreprise resteront enregistrées.")) {
                    const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
                    form.reset();
                    localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
                    loadCompanyInfo();
                    itemList.innerHTML = '';
                    addLineItem();
                    document.getElementById('doc_date').valueAsDate = new Date();
                    updateDocType();
                    calculateTotals();
                }
            }

            const generatePDF = async (e) => {
                e.preventDefault();
                const items = Array.from(itemList.querySelectorAll('tr')).map(row => ({
                    description: row.querySelector('.description').value,
                    quantity: row.querySelector('.quantity').value,
                    price: row.querySelector('.price').value,
                    total: parseFloat(row.querySelector('.row-total').textContent)
                }));
                const formData = {
                    doc_type: document.querySelector('input[name="doc_type"]:checked').value,
                    company: { name: document.getElementById('company_name').value, address: document.getElementById('company_address').value, siret: document.getElementById('company_siret').value, vat: document.getElementById('company_vat').value },
                    client: { name: document.getElementById('client_name').value, address: document.getElementById('client_address').value },
                    doc: { number: document.getElementById('doc_number').value, date: document.getElementById('doc_date').value, due_date: document.getElementById('doc_due_date').value },
                    items: items,
                    totals: { subtotal: subtotalEl.textContent, vat_rate: document.getElementById('vat_rate').value, vat_total: vatTotalEl.textContent, total_ttc: totalTtcEl.textContent },
                    notes: document.getElementById('notes').value
                };
                const pdfButton = document.getElementById('generate-pdf');
                pdfButton.setAttribute('aria-busy', 'true');
                pdfButton.textContent = 'Génération...';
                try {
                    const response = await fetch('', { // Post to the same file
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(formData)
                    });
                    if (!response.ok) throw new Error(`Erreur du serveur: ${response.statusText}`);
                    const blob = await response.blob();
                    const url = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.style.display = 'none';
                    a.href = url;
                    a.download = `${formData.doc_type.toUpperCase()}-${formData.doc.number.replace(/[^a-zA-Z0-9-]/g, '')}.pdf`;
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(url);
                    a.remove();
                } catch (error) {
                    console.error('Erreur lors de la génération du PDF:', error);
                    alert('Une erreur est survenue lors de la génération du PDF.');
                } finally {
                    pdfButton.removeAttribute('aria-busy');
                    pdfButton.textContent = 'Générer le PDF';
                }
            };

            addItemBtn.addEventListener('click', addLineItem);
            form.addEventListener('input', calculateTotals);
            form.addEventListener('submit', generatePDF);
            saveCompanyInfoBtn.addEventListener('click', saveCompanyInfo);
            resetFormBtn.addEventListener('click', resetForm);
            docTypeRadios.forEach(radio => radio.addEventListener('change', updateDocType));

            // --- Initialisation ---
            loadCompanyInfo();
            addLineItem();
            calculateTotals();
            document.getElementById('doc_date').valueAsDate = new Date();
            updateDocType();
        });
    </script>
</body>
</html>
stderr
PHP Notice:  Undefined index: REQUEST_METHOD in /home/HX6q5W/prog.php on line 61