130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
/*
|
|
Écrit la note publique d'un objet Dolibarr, par l'onglet Notes.
|
|
|
|
POURQUOI CE CHEMIN EXISTE. Certains objets ne sont plus modifiables sur ce
|
|
déploiement : toute soumission du formulaire d'édition d'une charge sociale
|
|
échoue sur `ERROR 42601: multiple assignments to same column "fk_user_modif"`
|
|
(erp#87). La NOTE, elle, passe par `update_note()` — un UPDATE ciblé, une seule
|
|
affectation — et **échappe au bug**. Vérifié en sandbox le 2026-08-13.
|
|
|
|
D'où la doctrine, posée par l'opérateur : quand une donnée fautive est déjà en
|
|
production et qu'elle n'a pas d'incidence insurmontable sur l'exercice, on ne
|
|
force pas la correction — **on annote l'objet**. Le registre reste
|
|
append-only, la vérité est portée par la pièce et par la note, et l'écriture
|
|
n'est pas maquillée.
|
|
|
|
Deux pièges :
|
|
- le lien d'édition de la note est un `a.editfielda`, VISIBLE AU SURVOL
|
|
seulement : le cliquer expire. On lit son href — il porte le token de
|
|
session — et on y navigue directement ;
|
|
- la note s'écrase. Le script refuse d'écrire par-dessus une note existante
|
|
différente, sauf --force : personne ne doit perdre une annotation
|
|
antérieure sans l'avoir voulu.
|
|
|
|
Usage :
|
|
deno run -A test/annotateObject.ts --module compta/sociales --id 1 \
|
|
--note "…" [--force] [--dry-run]
|
|
*/
|
|
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 mod = pick("--module", "compta/sociales"); // chemin du module Dolibarr
|
|
const id = pick("--id");
|
|
const note = pick("--note");
|
|
const force = argv.includes("--force");
|
|
const dryRun = argv.includes("--dry-run");
|
|
|
|
if (!id || !note) {
|
|
console.error("--id et --note sont requis");
|
|
Deno.exit(2);
|
|
}
|
|
|
|
const dolibarrAddress = assertSandbox();
|
|
console.log(`cible : ${dolibarrAddress}`);
|
|
console.log(`objet : /${mod}/note.php?id=${id}`);
|
|
console.log(`note : ${note.slice(0, 120)}${note.length > 120 ? "…" : ""}`);
|
|
if (dryRun) {
|
|
console.log("\n--dry-run : rien n'est soumis.");
|
|
Deno.exit(0);
|
|
}
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ locale: "fr-FR" });
|
|
const page = await context.newPage();
|
|
|
|
const normalise = (s: string) => s.replace(/\s+/g, " ").trim();
|
|
|
|
try {
|
|
await login.doAdminLogin({
|
|
page,
|
|
dolibarrAddress,
|
|
adminCredentials: {
|
|
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
|
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
|
},
|
|
});
|
|
|
|
const notePage = `${dolibarrAddress}/${mod}/note.php?id=${id}`;
|
|
await page.goto(notePage);
|
|
if (!page.url().includes("note.php")) {
|
|
console.error(`objet introuvable — ${notePage} a redirigé vers ${page.url()}`);
|
|
Deno.exit(1);
|
|
}
|
|
|
|
const before = normalise(await page.locator("body").innerText());
|
|
if (before.includes(normalise(note).slice(0, 60))) {
|
|
console.log("note déjà présente à l'identique, rien à faire.");
|
|
Deno.exit(0);
|
|
}
|
|
|
|
// Le lien d'édition porte le token de session ; on le lit plutôt que de le
|
|
// cliquer, car il n'est rendu visible qu'au survol.
|
|
const href = await page.locator('a[href*="action=editnote_public"]').first()
|
|
.getAttribute("href").catch(() => null);
|
|
if (!href) {
|
|
console.error("aucun lien d'édition de note public — droits insuffisants, ou module sans note.");
|
|
Deno.exit(1);
|
|
}
|
|
await page.goto(dolibarrAddress + href);
|
|
|
|
const area = page.locator("form textarea").first();
|
|
if (!(await area.count())) {
|
|
console.error("pas de zone de saisie sur la page d'édition.");
|
|
Deno.exit(1);
|
|
}
|
|
const existing = normalise(await area.inputValue());
|
|
if (existing && !force) {
|
|
console.error(`une note différente existe déjà — refus d'écraser sans --force :\n ${existing.slice(0, 200)}`);
|
|
Deno.exit(1);
|
|
}
|
|
|
|
await area.fill(note);
|
|
await Promise.all([
|
|
page.waitForNavigation({ waitUntil: "networkidle" }).catch(() => null),
|
|
page.locator('input[type="submit"]').first().click(),
|
|
]);
|
|
|
|
const after = normalise(await page.locator("body").innerText());
|
|
const dup = after.match(/multiple assignments to same column '?"?(\w+)/);
|
|
if (dup) {
|
|
console.error(`\nBLOQUÉ — le bug amont touche aussi ce chemin (colonne ${dup[1]}). Voir erp#87.`);
|
|
Deno.exit(1);
|
|
}
|
|
// Relire la page de note plutôt que de croire la page de retour.
|
|
await page.goto(notePage);
|
|
const check = normalise(await page.locator("body").innerText());
|
|
if (!check.includes(normalise(note).slice(0, 60))) {
|
|
console.error("\nNON ENREGISTRÉE — la note n'apparaît pas après relecture.");
|
|
Deno.exit(1);
|
|
}
|
|
console.log("\nnote enregistrée et relue sur l'objet.");
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
}
|