Files
erp/test/buildInvoicePdf.ts
T

107 lines
4.0 KiB
TypeScript

/*
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();
}