fix(profile): cotisations TNS en 641, pas 646 — et charge sociale immuable
L'appel de cotisations URSSAF 2026 (PDF officiel du 09/07) a permis de qualifier l'imputation, restée explicitement ouverte dans le runbook. La réponse contredit ce qui y était écrit. Le compte 646 est réservé à l'entreprise individuelle et aux sociétés à l'IR : il ne s'applique pas à une SARL à l'IS. La prise en charge par la société des cotisations de son gérant majoritaire est un complément de rémunération — compte 641, sous-compte dédié, déductible. 645 et 631/633 restent vides : aucun salarié, l'opérateur n'est pas employeur. Piège corrigé au passage : la mention « CSG déductible fiscalement » de l'appel vise l'IR personnel du gérant (art. 62 CGI), pas l'IS de la société. Pour la SARL la CSG/CRDS est intégralement déductible — la réintégration annoncée dans un premier temps était un raisonnement d'entreprise individuelle mal transposé. Le runbook affirmait aussi que le type de charge Dolibarr pilotait le compte. Faux : la colonne « Code comptable » du dictionnaire est vide pour tous les types, TAXSSI compris (vérifié cellule par cellule), et le module comptabilité n'est pas déployé. La carte affiche « Code comptable: Inconnu ». L'ERP porte les faits, pas les écritures. Deux corrections de fond sur les dates et les montants : - les dates de l'échéancier sont des dates d'ÉCHÉANCE (le 5 du mois), pas de débit bancaire — 05/05 et non 22/05, ce qui rend visible le retard de 17 jours ; - les 3 041 EUR sont PROVISOIRES, assis sur un forfait début d'activité, et seront régularisés. L'assiette est la rémunération du gérant, jamais le CA. Enfin, découvert en tentant la correction de date : aucune charge sociale n'est modifiable sur ce déploiement. Toute édition, même du seul montant, échoue sur « multiple assignments to same column fk_user_modif » — ChargeSociales::update() génère un UPDATE que PostgreSQL rejette (42601) là où MySQL passe. Le défaut est propre à cet objet ; la mise à jour d'un tiers via REST fonctionne. La date doit donc être juste à la création. updateSocialCharge.ts conserve le cas de reproduction et diagnostique l'erreur au lieu de la subir. Le justificatif officiel est attaché aux trois charges de production. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Correct the dates of an existing social charge through the Dolibarr UI.
|
||||
|
||||
!! BLOQUÉ PAR UN BUG AMONT — CE SCRIPT NE PEUT PAS ABOUTIR AUJOURD'HUI. !!
|
||||
|
||||
Sur ce déploiement (Dolibarr 22.0.4 + PostgreSQL), AUCUNE charge sociale n'est
|
||||
modifiable : toute soumission du formulaire d'édition, même en ne touchant que
|
||||
le montant, échoue sur
|
||||
|
||||
ERROR 42601: multiple assignments to same column "fk_user_modif"
|
||||
|
||||
ChargeSociales::update() construit un UPDATE qui affecte deux fois la même
|
||||
colonne. MySQL l'accepte, PostgreSQL le rejette. Vérifié le 2026-08-13 en
|
||||
sandbox sur la date ET sur le montant ; la mise à jour d'un tiers via REST
|
||||
fonctionne, donc le défaut est propre à cet objet, pas systémique.
|
||||
|
||||
CONSÉQUENCE OPÉRATOIRE : une charge sociale est de fait IMMUABLE. Sa date doit
|
||||
être juste à la création — voir RUNBOOK_charges_sociales.md, « échéance ≠
|
||||
prélèvement ». Ce script est conservé comme cas de reproduction : il
|
||||
diagnostique l'erreur au lieu de la subir, et redeviendra utile dès que le bug
|
||||
amont sera corrigé.
|
||||
|
||||
WHY THIS EXISTS: recordSocialCharge.ts only creates. When a charge was booked
|
||||
on the wrong date the record must be corrected in place — deleting and
|
||||
recreating is not an option, since no agent scope grants DELETE and a ledger
|
||||
is append-only by doctrine.
|
||||
|
||||
THE CORRECTION THIS WAS WRITTEN FOR: the URSSAF échéancier gives DUE dates on
|
||||
the 5th of the month. The bank debit lands later (22/05 for the 05/05 due
|
||||
date). The first échéance had been recorded at its debit date, which makes a
|
||||
17-day late payment look punctual and puts the charge in the wrong month.
|
||||
|
||||
Same double-bottom date trap as the create form: the visible `ech` / `period`
|
||||
inputs are decorative, the backend reads ONLY the hidden day/month/year
|
||||
triplets fed by the jQuery datepicker. See RUNBOOK_charges_sociales.md.
|
||||
|
||||
Usage:
|
||||
deno run -A test/updateSocialCharge.ts --id 1 --due 2026-05-05 \
|
||||
--period 2026-05-05 [--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 id = pick("--id");
|
||||
const due = pick("--due"); // yyyy-mm-dd
|
||||
const period = pick("--period"); // yyyy-mm-dd
|
||||
const dryRun = argv.includes("--dry-run");
|
||||
|
||||
if (!id || !due) {
|
||||
console.error("--id and --due (yyyy-mm-dd) are required");
|
||||
Deno.exit(2);
|
||||
}
|
||||
|
||||
const dolibarrAddress = assertSandbox();
|
||||
const fr = (iso: string) => {
|
||||
const [y, m, d] = iso.split("-");
|
||||
return `${d}/${m}/${y}`;
|
||||
};
|
||||
|
||||
console.log(`target : ${dolibarrAddress}`);
|
||||
console.log(`charge ${id} : échéance → ${fr(due)}${period ? `, période → ${fr(period)}` : ""}`);
|
||||
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();
|
||||
|
||||
async function setDolibarrDate(name: string, iso: string): Promise<void> {
|
||||
const [y, m, d] = iso.split("-");
|
||||
await page.fill(`input[name="${name}"]`, fr(iso)).catch(() => {});
|
||||
await page.evaluate(
|
||||
({ n, dd, mm, yy }: { n: string; dd: string; mm: string; yy: string }) => {
|
||||
const doc = (globalThis as unknown as {
|
||||
document: { querySelector(s: string): { value: string } | null };
|
||||
}).document;
|
||||
const set = (suffix: string, v: string) => {
|
||||
const el = doc.querySelector(`input[name="${n}${suffix}"]`);
|
||||
if (el) el.value = v;
|
||||
};
|
||||
set("day", String(Number(dd)));
|
||||
set("month", String(Number(mm)));
|
||||
set("year", yy);
|
||||
},
|
||||
{ n: name, dd: d, mm: m, yy: y },
|
||||
);
|
||||
}
|
||||
|
||||
/** Read the dates back off the card — the only trustworthy confirmation. */
|
||||
async function readDates(): Promise<string> {
|
||||
await page.goto(`${dolibarrAddress}/compta/sociales/card.php?id=${id}`);
|
||||
return (await page.locator("div.fichecenter, table").first().innerText())
|
||||
.replace(/\s+/g, " ").trim().slice(0, 300);
|
||||
}
|
||||
|
||||
try {
|
||||
await login.doAdminLogin({
|
||||
page,
|
||||
dolibarrAddress,
|
||||
adminCredentials: {
|
||||
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
||||
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`avant : ${await readDates()}`);
|
||||
|
||||
await page.goto(`${dolibarrAddress}/compta/sociales/card.php?id=${id}&action=edit`);
|
||||
await setDolibarrDate("ech", due);
|
||||
if (period) await setDolibarrDate("period", period);
|
||||
await page.locator('input[name="save"], input[type="submit"][value="Enregistrer"]').first().click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Diagnose the known upstream failure rather than reporting a bare mismatch:
|
||||
// "la date n'a pas changé" sends the reader hunting through the date fields,
|
||||
// which are not the problem.
|
||||
const submitted = (await page.locator("body").innerText()).replace(/\s+/g, " ");
|
||||
const dup = submitted.match(/multiple assignments to same column '?"?(\w+)/);
|
||||
if (dup) {
|
||||
console.error(
|
||||
`\nBLOQUÉ PAR LE BUG AMONT — UPDATE invalide sur la colonne « ${dup[1]} ».\n` +
|
||||
`ChargeSociales::update() l'affecte deux fois ; PostgreSQL rejette (42601).\n` +
|
||||
`Une charge sociale est immuable sur ce déploiement : la recréer avec la\n` +
|
||||
`bonne date, ou corriger en base, sont les seules voies.`,
|
||||
);
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
const after = await readDates();
|
||||
console.log(`après : ${after}`);
|
||||
if (!after.includes(fr(due))) {
|
||||
console.error(`\nÉCHEC — la carte ne porte pas ${fr(due)}.`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
console.log(`\ncorrigée et vérifiée : échéance ${fr(due)}`);
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user