feat(erp): annoter un objet figé — la date URSSAF portée par une pièce jointe (#90)
Co-authored-by: Gabriel Radureau <[email protected]>
This commit was merged in pull request #90.
This commit is contained in:
@@ -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 @- <<JSON
|
||||
{"filename":"NOTE-<sujet>-<date>.md","modulepart":"tax","subdir":"<ID DE LA CHARGE>",
|
||||
"filecontent":"<BASE64>","fileencoding":"base64","overwriteifexists":1}
|
||||
JSON
|
||||
```
|
||||
|
||||
Vérifier sur `/compta/sociales/document.php?id=<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
|
||||
|
||||
@@ -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).
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user