From 775daacd18d89dc28009b4cdac5ca36992aef57f Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Thu, 13 Aug 2026 23:57:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(erp):=20annoter=20un=20objet=20fig=C3=A9?= =?UTF-8?q?=20=E2=80=94=20la=20date=20URSSAF=20port=C3=A9e=20par=20une=20p?= =?UTF-8?q?i=C3=A8ce=20jointe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doctrine posée par l'opérateur le 13/08 : si une donnée fautive est déjà en production et sans incidence insurmontable sur l'exercice, on ne force pas la correction — on annote l'objet. Le registre reste append-only et l'écriture n'est pas maquillée. La règle est juste, son canal habituel est fermé ici : sur ce déploiement, AUCUNE écriture sur une charge sociale n'aboutit — ni la date, ni le montant, NI MÊME LA NOTE PUBLIQUE. Toutes passent par le même UPDATE et échouent sur « multiple assignments to same column fk_user_modif » (erp#87). Ce qui fonctionne est un autre chemin : attacher un document, le téléversement n'étant pas un UPDATE sur l'objet. La charge n°1 (URSSAF 1re échéance, 493 EUR) porte désormais deux pièces : l'appel de cotisations officiel, et une note de correction expliquant que le 22/05 est la date du PRÉLÈVEMENT quand l'échéance officielle est le 05/05 — 17 jours de retard, écart resté dans le même mois et le même exercice. annotateObject.ts écrit la note publique d'un objet et REFUSE explicitement quand le bug la bloque. Ce garde-fou vient d'un faux positif qui a bien failli passer : la page de retour de Dolibarr ré-affiche le texte soumis, si bien qu'un contrôle « le texte est là » réussit sur un enregistrement qui n'a jamais eu lieu. J'ai d'abord conclu que le chemin des notes échappait au bug — il ne lui échappe pas. Relire l'objet, jamais la page de retour. Le script reste utile sur les objets non affectés par erp#87. Co-Authored-By: Claude Opus 5 (1M context) --- .../RUNBOOK_quel_instrument.md | 39 ++++++ .../NOTE-urssaf-date-echeance-2026-08-13.md | 39 ++++++ test/annotateObject.ts | 129 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 fleet/profile/notes/NOTE-urssaf-date-echeance-2026-08-13.md create mode 100644 test/annotateObject.ts diff --git a/.claude/skills/dolibarr-sandbox-write/RUNBOOK_quel_instrument.md b/.claude/skills/dolibarr-sandbox-write/RUNBOOK_quel_instrument.md index 257f485..ac690de 100644 --- a/.claude/skills/dolibarr-sandbox-write/RUNBOOK_quel_instrument.md +++ b/.claude/skills/dolibarr-sandbox-write/RUNBOOK_quel_instrument.md @@ -32,6 +32,45 @@ réels payées personnellement par le gérant — le tiers y est le fournisseur, jamais le gérant. Un modèle qui contredit celui déjà en place est presque toujours le mauvais. +## Une charge sociale est TOTALEMENT figée — écrire juste du premier coup + +> [!CAUTION] +> Sur ce déploiement (Dolibarr 22.0.4 + PostgreSQL), **aucune écriture sur une +> charge sociale n'aboutit** : ni la date, ni le montant, **ni même la note +> publique**. Toutes échouent sur +> `ERROR 42601: multiple assignments to same column "fk_user_modif"` — +> `ChargeSociales::update()` affecte deux fois la même colonne, ce que MySQL +> tolère et PostgreSQL rejette. Suivi : [erp#87](https://gitea.arcodange.lab/arcodange-org/erp/issues/87). + +**La règle générale — « si c'est déjà en production et sans incidence sur +l'exercice, annoter l'objet suffit » — reste juste, mais son canal habituel est +fermé ici.** Le champ note passe par le même `UPDATE`. + +Ce qui fonctionne, parce que c'est un autre chemin : **attacher un document**. +Le téléversement n'est pas un `UPDATE` sur l'objet. + +```bash +curl -s -X POST https://erp.arcodange.lab/api/index.php/documents/upload \ + -H "DOLAPIKEY: $(cat test/.ai_agent_prod_prod_write.key)" \ + -H 'Content-Type: application/json' -d @- <-.md","modulepart":"tax","subdir":"", + "filecontent":"","fileencoding":"base64","overwriteifexists":1} +JSON +``` + +Vérifier sur `/compta/sociales/document.php?id=` que le compteur a bougé. +Première application : la charge n°1 (URSSAF 1re échéance) porte le 22/05, date +du *prélèvement*, quand l'échéance officielle est le 05/05 — l'écart, resté dans +le même mois et le même exercice, est porté par une note attachée plutôt que +forcé dans la donnée. + +`test/annotateObject.ts` écrit la note publique d'un objet **et refuse +explicitement** quand le bug la bloque, au lieu de croire une page qui ré-affiche +le formulaire. Ce faux positif a bien failli passer : la page de retour contient +le texte soumis, donc un contrôle naïf « le texte est là » réussit sur un +enregistrement qui n'a jamais eu lieu. **Relire l'objet, jamais la page de +retour.** Le script reste utile sur les objets non affectés par erp#87. + ## Le compte courant d'associé : deux usages à ne pas confondre Le compte bancaire **`CCA1` (id 3)** porte le numéro comptable **45511** et son diff --git a/fleet/profile/notes/NOTE-urssaf-date-echeance-2026-08-13.md b/fleet/profile/notes/NOTE-urssaf-date-echeance-2026-08-13.md new file mode 100644 index 0000000..9a29911 --- /dev/null +++ b/fleet/profile/notes/NOTE-urssaf-date-echeance-2026-08-13.md @@ -0,0 +1,39 @@ +# Note de correction — date de la 1re échéance URSSAF 2026 + +**Objet :** charge sociale n°1 — « URSSAF 2026 — 1re échéance », 493,00 € +**Établie le :** 13 août 2026 + +## Ce que la charge porte, et ce qui fait foi + +La charge est enregistrée au **22/05/2026**. Cette date est celle du +**prélèvement bancaire**, non celle de l'échéance. + +La date d'**échéance officielle est le 05/05/2026**, portée par l'appel de +cotisations de l'URSSAF Île-de-France du 09/07/2026, n° TI 117 1583346778 4, +joint à cette même charge. + +Le règlement a donc été effectué avec **17 jours de retard** sur l'échéance. + +## Pourquoi la date n'a pas été corrigée + +Sur ce déploiement (Dolibarr 22.0.4 + PostgreSQL), **aucune modification d'une +charge sociale n'aboutit** — ni la date, ni le montant, ni la note publique. +Toute écriture échoue sur : + + ERROR 42601: multiple assignments to same column "fk_user_modif" + +`ChargeSociales::update()` affecte deux fois la même colonne ; MySQL l'accepte, +PostgreSQL le rejette. Suivi : erp#87. + +L'écart restant dans le **même mois et le même exercice**, il est sans incidence +sur l'exercice 2026. Conformément à la doctrine append-only du registre, la +donnée n'est pas forcée : **la présente note et le justificatif joint font foi.** + +## Rappel sur les montants + +Les **3 041 €** appelés au titre de 2026 sont **provisoires** : ils sont calculés +sur la base forfaitaire applicable aux assurés en début d'activité (8 856 €) et +seront **régularisés** après la déclaration des revenus 2026. + +Échéancier officiel : 0 € au 05/02 · 493 € au 05/05 · 1 215 € au 05/08 · +1 333 € au 05/11 (dont 120 € de contribution à la formation professionnelle). diff --git a/test/annotateObject.ts b/test/annotateObject.ts new file mode 100644 index 0000000..27f84bc --- /dev/null +++ b/test/annotateObject.ts @@ -0,0 +1,129 @@ +/* + É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(); +} -- 2.54.0