148 lines
6.0 KiB
TypeScript
148 lines
6.0 KiB
TypeScript
/*
|
||
Supprime le RÈGLEMENT d'une facture client, pour pouvoir le ressaisir juste.
|
||
|
||
POURQUOI CE CHEMIN EXISTE. Quand une facture a été encaissée pour un montant
|
||
différent de celui qu'on lui a donné — le cas classique ici : un prix
|
||
contractuel en USD, converti en euros au mauvais taux — il ne suffit pas de
|
||
corriger la facture. Le règlement, lui, porte l'ancien montant, et la ligne
|
||
bancaire qu'il a engendrée aussi. Tant qu'on ne le refait pas, le solde de
|
||
l'ERP diverge de celui de la banque.
|
||
|
||
L'API REST ne sait pas supprimer un règlement : `PUT /invoices/{id}/payments`
|
||
ne met à jour que son NUMÉRO. Il faut donc l'interface. Et deux verrous s'y
|
||
opposent, dans cet ordre :
|
||
|
||
1. tant que la facture est marquée PAYÉE, la fiche du règlement n'affiche
|
||
AUCUN lien de suppression — pas de message, pas d'erreur, le lien est
|
||
simplement absent. Il faut d'abord « Rouvrir » la facture ;
|
||
2. la confirmation n'est PAS un formulaire mais une boîte MODALE jQuery UI,
|
||
dont les boutons « Oui » / « Non » n'ont ni `name` ni `value` — un
|
||
sélecteur sur `input[type=submit]` ne trouve rien et la suppression
|
||
n'a pas lieu, silencieusement. On clique sur le TEXTE du bouton.
|
||
|
||
Le script vérifie en RELISANT la fiche du règlement : supprimée, elle ne
|
||
répond plus. On ne croit pas la page de retour.
|
||
|
||
Usage :
|
||
deno run -A test/deleteInvoicePayment.ts --invoice 14 [--payment 7] [--dry-run]
|
||
|
||
Sans --payment, le règlement est retrouvé sur la fiche de la facture. C'est
|
||
préférable : les identifiants de règlement diffèrent d'une instance à l'autre,
|
||
et en coder un en dur pour la production après l'avoir relevé en bac à sable
|
||
est une façon commode de supprimer le mauvais.
|
||
*/
|
||
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 invoice = pick("--invoice");
|
||
const payment = pick("--payment");
|
||
const dryRun = argv.includes("--dry-run");
|
||
|
||
if (!invoice) {
|
||
console.error("--invoice est requis (identifiant Dolibarr de la facture)");
|
||
Deno.exit(2);
|
||
}
|
||
|
||
const dolibarrAddress = assertSandbox();
|
||
console.log(`cible : ${dolibarrAddress}`);
|
||
console.log(`facture : id=${invoice} règlement : ${payment || "(à retrouver sur la fiche)"}`);
|
||
|
||
const browser = await chromium.launch({ headless: true });
|
||
const context = await browser.newContext({ locale: "fr-FR" });
|
||
const page = await context.newPage();
|
||
|
||
let pid = payment;
|
||
|
||
/** Le règlement existe-t-il encore ? Sa fiche est la seule source de vérité. */
|
||
async function reglementExiste(): Promise<boolean> {
|
||
const res = await page.goto(`${dolibarrAddress}/compta/paiement/card.php?id=${pid}`);
|
||
if (res && res.status() >= 400) return false;
|
||
const txt = (await page.locator("body").innerText()).replace(/\s+/g, " ");
|
||
return /Montant/.test(txt) && !/n'existe pas|does not exist|Erreur/i.test(txt);
|
||
}
|
||
|
||
try {
|
||
await login.doAdminLogin({
|
||
page,
|
||
dolibarrAddress,
|
||
adminCredentials: {
|
||
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
||
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
||
},
|
||
});
|
||
|
||
if (!pid) {
|
||
await page.goto(`${dolibarrAddress}/compta/facture/card.php?facid=${invoice}`);
|
||
const hrefs = await page.locator('a[href*="/compta/paiement/card.php?id="]')
|
||
.evaluateAll((as: { getAttribute(n: string): string | null }[]) =>
|
||
as.map((a) => a.getAttribute("href") ?? ""));
|
||
const ids = [...new Set(hrefs.map((h) => (h.match(/id=(\d+)/) || [])[1]).filter(Boolean))];
|
||
if (ids.length === 0) {
|
||
console.log("aucun règlement sur cette facture, rien à faire.");
|
||
Deno.exit(0);
|
||
}
|
||
if (ids.length > 1) {
|
||
console.error(`${ids.length} règlements sur cette facture (${ids.join(", ")}) — préciser --payment.`);
|
||
Deno.exit(1);
|
||
}
|
||
pid = ids[0];
|
||
console.log(`règlement : id=${pid} (retrouvé sur la fiche)`);
|
||
}
|
||
|
||
if (!await reglementExiste()) {
|
||
console.log("règlement déjà absent, rien à faire.");
|
||
Deno.exit(0);
|
||
}
|
||
const montant = ((await page.locator("body").innerText()).match(/Montant\s+([\d\s ]+,\d{2})/) || [])[1];
|
||
console.log(`montant : ${montant ?? "?"} €`);
|
||
if (dryRun) {
|
||
console.log("\n--dry-run : rien n'est supprimé.");
|
||
Deno.exit(0);
|
||
}
|
||
|
||
// Verrou 1 — rouvrir la facture, sinon le lien de suppression n'existe pas.
|
||
await page.goto(`${dolibarrAddress}/compta/facture/card.php?facid=${invoice}`);
|
||
const reopen = await page.locator('a[href*="action=reopen"]').first()
|
||
.getAttribute("href").catch(() => null);
|
||
if (reopen) {
|
||
await page.goto(new URL(reopen, dolibarrAddress).toString());
|
||
console.log("facture rouverte (elle était marquée payée).");
|
||
}
|
||
|
||
await page.goto(`${dolibarrAddress}/compta/paiement/card.php?id=${pid}`);
|
||
const del = await page.locator('a[href*="action=delete"]').first()
|
||
.getAttribute("href").catch(() => null);
|
||
if (!del) {
|
||
console.error(
|
||
"aucun lien de suppression sur la fiche du règlement.\n" +
|
||
" La facture est-elle encore marquée payée ? Le lien n'apparaît qu'une fois rouverte.",
|
||
);
|
||
Deno.exit(1);
|
||
}
|
||
await page.goto(new URL(del, dolibarrAddress).toString());
|
||
|
||
// Verrou 2 — la page de confirmation. Sans elle, rien n'est supprimé.
|
||
const oui = page.getByRole("button", { name: /^(Oui|Yes)$/ }).first();
|
||
await oui.waitFor({ state: "visible", timeout: 10_000 }).catch(() => {});
|
||
if (await oui.count() === 0) {
|
||
console.error("boîte de confirmation introuvable — le bouton « Oui » n'est pas rendu.");
|
||
Deno.exit(1);
|
||
}
|
||
await oui.click();
|
||
await page.waitForLoadState("networkidle");
|
||
|
||
if (await reglementExiste()) {
|
||
console.error("ÉCHEC — le règlement répond toujours.");
|
||
Deno.exit(1);
|
||
}
|
||
console.log("ok — règlement supprimé, vérifié par relecture de sa fiche.");
|
||
} finally {
|
||
await context.close();
|
||
await browser.close();
|
||
}
|