/* Fait produire à Dolibarr le PDF d'une facture client, puis l'enregistre. POURQUOI CE CHEMIN EXISTE. Valider une facture par l'API **ne génère aucun PDF** : le fichier n'existe sur le disque que lorsque quelqu'un a demandé sa production. `PUT /documents/builddoc` le ferait, mais ce droit n'est accordé à aucun scope agent (voir test/scopes.ts) et répond 403. Le seul chemin ouvert est donc l'interface — celui que l'opérateur emprunterait lui-même. Deux pièges : - le PDF n'est PAS servi par la page : on demande sa production par l'UI, puis on le relit par l'API `/documents/download`, qui prouve du même coup qu'il est bien déposé et lisible par un tiers ; - `action=builddoc` exige le jeton CSRF de session. On ne le devine pas : on le relit sur un lien de la fiche. Usage : deno run -A test/buildInvoicePdf.ts --ref FAC009-CL0001009 --out dossier/ */ import "load_dotenv"; import { chromium } from "playwright"; import login from "./scripts/login.ts"; import { assertSandbox } from "./scripts/guard.ts"; const argv = Deno.args; const pick = (f: string, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : d); const ref = pick("--ref"); const out = pick("--out", "."); const model = pick("--model", "sponge"); const dryRun = argv.includes("--dry-run"); if (!ref) { console.error("--ref est requis (ex. FAC009-CL0001009)"); Deno.exit(2); } const dolibarrAddress = assertSandbox(); console.log(`cible : ${dolibarrAddress}`); console.log(`facture: ${ref}`); console.log(`modèle : ${model}`); if (dryRun) { console.log("\n--dry-run : rien n'est produit."); Deno.exit(0); } const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ locale: "fr-FR" }); const page = await context.newPage(); try { await login.doAdminLogin({ page, dolibarrAddress, adminCredentials: { username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined", password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined", }, }); const card = `${dolibarrAddress}/compta/facture/card.php?ref=${encodeURIComponent(ref)}`; await page.goto(card); const body = await page.locator("body").innerText(); if (!body.includes(ref)) { console.error(`facture introuvable : ${card}`); Deno.exit(1); } // Identifiant réel de la facture, tel que l'URL le porte après résolution du ref. const idMatch = (await page.locator('a[href*="facid="]').first().getAttribute("href") .catch(() => null))?.match(/facid=(\d+)/); const anyToken = (await page.locator('a[href*="token="]').first().getAttribute("href") .catch(() => null))?.match(/token=([a-zA-Z0-9]+)/); if (!anyToken) { console.error("aucun jeton de session lisible sur la fiche — droits insuffisants ?"); Deno.exit(1); } const token = anyToken[1]; const id = idMatch?.[1]; const buildUrl = `${dolibarrAddress}/compta/facture/card.php` + `?${id ? `facid=${id}` : `ref=${encodeURIComponent(ref)}`}` + `&action=builddoc&token=${token}&model=${model}`; await page.goto(buildUrl); // On ne croit pas la page de retour : on relit le fichier par l'API. const apiUrl = Deno.env.get("DOLIBARR_URL") || dolibarrAddress; const apiKey = Deno.env.get("DOLIBARR_API_KEY") || ""; const res = await fetch( `${apiUrl}/api/index.php/documents/download` + `?modulepart=facture&original_file=${encodeURIComponent(`${ref}/${ref}.pdf`)}`, { headers: { DOLAPIKEY: apiKey, Accept: "application/json" } }, ); const payload = await res.json(); if (!res.ok || !payload.content) { console.error(`le PDF n'est pas déposé : HTTP ${res.status} ${JSON.stringify(payload).slice(0, 200)}`); Deno.exit(1); } const bytes = Uint8Array.from(atob(payload.content), (c) => c.charCodeAt(0)); const dest = out.endsWith(".pdf") ? out : `${out.replace(/\/$/, "")}/${ref}.pdf`; await Deno.writeFile(dest, bytes); console.log(`ok — ${dest} (${bytes.length} octets)`); } finally { await browser.close(); }