Files
erp/test/deleteInvoicePayment.ts
T

148 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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();
}