L'opérateur a demandé si le versement de la semaine passée avait été pris en compte. Il ne l'était pas — et la réconciliation menée en réponse a fait sortir trois autres trous. L'ENCAISSEMENT. KissMetrics a viré 2 164,75 EUR le 17/08 (Wise, réf. VENDOR:DEV), soit 2 500,00 USD au taux du jour : la part fixe du cycle M4. Le mouvement n'était pas enregistré, si bien que FAC009 a été émise le matin même au taux du 24/08 (2 140,45 EUR) alors que le contrat arrête le montant dû « au taux du jour du règlement ». La facture était fausse de 24,30 EUR, et déjà payée. Le précédent commandait la méthode : FAC008 avait été libellée 2 185,00 EUR, exactement la somme reçue le 20/07, même schéma de règlement anticipé. Sur arbitrage de l'opérateur, FAC009 est repassée en brouillon, portée à 2 164,75 EUR, revalidée sous le même numéro et la même date, puis adossée au virement. Sa note reprend la rédaction de FAC008. Le juge a bloqué en relevant que `last_main_doc` était présent — donc, selon lui, facture émise. Il confond PDF PRODUIT et facture TRANSMISE : le PDF avait été produit deux heures plus tôt par l'agent lui-même pour assembler le dossier, et le message d'envoi est un brouillon jamais envoyé. Mais il a raison sur la conséquence, et c'est le seul de ses findings de la journée qui apporte quelque chose : le PDF sur disque devenait faux. Il a été régénéré aussitôt. Son second finding — `emetteur` null — tombe : le champ est null sur les huit lignes d'encaissement Wise antérieures ; le renseigner sur la seule ligne d'août romprait la permanence des méthodes au lieu de la servir. LES DEUX ÉCHÉANCES URSSAF. 493,00 EUR prélevés le 22/05 et 1 215,00 EUR le 17/08 n'étaient enregistrés ni l'un ni l'autre. Créés comme charges sociales — pas comme factures fournisseur : l'URSSAF n'est pas un fournisseur — puis réglés depuis Qonto. test/paySocialCharge.ts comble un trou du chemin gated : recordSocialCharge.ts crée la charge et la laisse impayée, et rien n'enregistrait le règlement. Tant qu'il manque, le mouvement bancaire reste orphelin et le solde de l'ERP diverge de celui de la banque — c'est ce qui laissait ces deux échéances invisibles. Dolibarr n'expose aucune route REST pour les charges sociales, donc le pipeline ne peut pas porter l'opération ; le script garde ce qu'il peut de sa discipline. Trois pièges consignés dans le fichier, dont un qui a coûté une fausse alerte : NE PAS chercher « payée » dans la page — « ImPAYÉE » contient « payée ». Une première version déclarait ÉCHEC sur un règlement qui venait d'aboutir, ce qui pousse à rejouer, donc à créer un doublon. On lit le montant restant dû. LE RUNBOOK disait 646. C'est faux depuis adc-009 : dans une SARL à l'IS, ce que la société verse à l'URSSAF pour son gérant majoritaire est une charge de personnel, donc 641. Corrigé, avec la mention explicite que toute version portant 646 est périmée. Ajouté aussi ce que la fiche de charge confirme à l'écran — « Code comptable: Inconnu » : le type de charge ne pilote PAS le compte sur ce déploiement, contrairement à ce que le runbook laissait croire. Enfin l'exemple `--period 2026` du runbook était invalide : le script découpe la valeur comme une date ISO et produisait `undefined/undefined/2026`, ce qui fait échouer la création sans message. Reste au tableau, documenté et non traité ici : six cashbacks Wise (11,13 EUR), un remboursement Qonto (5,22 EUR), l'abonnement Anthropic du 19/08 (90,00 EUR, reçu pas encore arrivé), et deux encaissements client sous-enregistrés — FAC004 et FAC006, 51,13 EUR reçus de plus que ce que l'ERP porte. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
150 lines
6.0 KiB
TypeScript
150 lines
6.0 KiB
TypeScript
/*
|
|
Enregistre le RÈGLEMENT d'une charge sociale, par l'interface.
|
|
|
|
POURQUOI CE CHEMIN EXISTE. `recordSocialCharge.ts` crée la charge — et la
|
|
laisse impayée, comme le veut Dolibarr. Tant que le règlement n'est pas saisi,
|
|
le mouvement bancaire reste orphelin : `bank-match.sh` le classe BANK-ONLY et
|
|
le solde de l'ERP diverge de celui de la banque. C'est ce trou qui laissait
|
|
deux échéances URSSAF invisibles jusqu'au 24/08/2026.
|
|
|
|
Dolibarr n'expose AUCUNE route REST pour les charges sociales — /taxes,
|
|
/socialcontributions et /chargesociales répondent tous « API not found ». La
|
|
voie gated du pipeline, qui parle REST, ne peut donc pas porter l'opération.
|
|
Ce script garde ce qu'il peut de sa discipline : répétition en bac à sable,
|
|
double opt-in explicite pour la production, --dry-run, et vérification par
|
|
RELECTURE DE L'OBJET.
|
|
|
|
Trois pièges :
|
|
- la date est un datepicker jQuery : un champ visible `re` doublé d'un
|
|
triplet CACHÉ reday/remonth/reyear, et le backend ne lit QUE le triplet.
|
|
Remplir le champ visible seul soumet une date vide, sans erreur ;
|
|
- le champ du montant porte l'identifiant de la charge dans son nom —
|
|
`amount_<id>`, pas `amount` ;
|
|
- la page de retour affiche « paiement enregistré » avant même que l'objet
|
|
soit relu. On recharge la fiche de la charge et on lit son statut.
|
|
|
|
Usage :
|
|
deno run -A test/paySocialCharge.ts --id 5 --date 2026-08-17 \
|
|
--amount 1215.00 --account 1 --type 3 [--dry-run]
|
|
# --type : 3 = prélèvement, 2 = virement, 6 = carte
|
|
*/
|
|
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 date = pick("--date"); // yyyy-mm-dd
|
|
const amount = pick("--amount");
|
|
const account = pick("--account"); // id du compte bancaire
|
|
const type = pick("--type", "3"); // 3 = ordre de prélèvement
|
|
const note = pick("--note");
|
|
const dryRun = argv.includes("--dry-run");
|
|
|
|
if (!id || !date || !amount || !account) {
|
|
console.error("--id, --date, --amount et --account sont requis");
|
|
Deno.exit(2);
|
|
}
|
|
|
|
const dolibarrAddress = assertSandbox();
|
|
console.log(`cible : ${dolibarrAddress}`);
|
|
console.log(`charge : id=${id} — ${amount} € le ${date} sur le compte ${account} (type ${type})`);
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ locale: "fr-FR" });
|
|
const page = await context.newPage();
|
|
|
|
/** Renseigne le champ visible ET le triplet caché que le backend lit seul. */
|
|
async function setDate(prefix: string, iso: string): Promise<void> {
|
|
const [y, m, d] = iso.split("-");
|
|
await page.fill(`input[name="${prefix}"]`, `${d}/${m}/${y}`).catch(() => {});
|
|
await page.evaluate(
|
|
({ p, dd, mm, yy }: { p: string; dd: string; mm: string; yy: string }) => {
|
|
const doc = (globalThis as unknown as {
|
|
document: { querySelector(s: string): { value: string } | null };
|
|
}).document;
|
|
const set = (s: string, v: string) => {
|
|
const el = doc.querySelector(`input[name="${p}${s}"]`);
|
|
if (el) el.value = v;
|
|
};
|
|
set("day", String(Number(dd)));
|
|
set("month", String(Number(mm)));
|
|
set("year", yy);
|
|
},
|
|
{ p: prefix, dd: d, mm: m, yy: y },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reste à payer, lu sur la fiche de la charge. Seule confirmation qui vaille.
|
|
*
|
|
* NE PAS chercher « payée » dans la page : « ImPAYÉE » contient « payée », et
|
|
* la fiche affiche de toute façon les deux mots. Une première version le
|
|
* faisait et déclarait ÉCHEC sur un règlement qui venait d'être enregistré —
|
|
* un faux négatif qui pousse à rejouer, donc à créer un doublon. On lit le
|
|
* MONTANT, qui ne ment pas.
|
|
*/
|
|
async function resteAPayer(): Promise<number | null> {
|
|
await page.goto(`${dolibarrAddress}/compta/sociales/card.php?id=${id}`);
|
|
const txt = (await page.locator("body").innerText()).replace(/\s+/g, " ");
|
|
const m = txt.match(/Reste à payer\s*:?\s*([\d\s\u00a0\u202f]+,\d{2})/i);
|
|
if (!m) return null;
|
|
return Number(m[1].replace(/[\s\u00a0\u202f]/g, "").replace(",", "."));
|
|
}
|
|
|
|
try {
|
|
await login.doAdminLogin({
|
|
page,
|
|
dolibarrAddress,
|
|
adminCredentials: {
|
|
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
|
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
|
},
|
|
});
|
|
|
|
const avant = await resteAPayer();
|
|
if (avant === null) {
|
|
console.error(`charge introuvable, ou fiche illisible : /compta/sociales/card.php?id=${id}`);
|
|
Deno.exit(1);
|
|
}
|
|
console.log(`avant : reste à payer ${avant.toFixed(2)} €`);
|
|
if (avant === 0) {
|
|
console.log("déjà réglée, rien à faire.");
|
|
Deno.exit(0);
|
|
}
|
|
if (dryRun) {
|
|
console.log("\n--dry-run : rien n'est soumis.");
|
|
Deno.exit(0);
|
|
}
|
|
|
|
// Le lien porte le jeton de session ; on le lit plutôt que de le deviner.
|
|
const href = await page.locator('a[href*="paiement_charge"]').first().getAttribute("href");
|
|
if (!href) {
|
|
console.error("aucun lien « Saisir règlement » — charge déjà réglée, ou droits insuffisants.");
|
|
Deno.exit(1);
|
|
}
|
|
await page.goto(new URL(href, dolibarrAddress).toString());
|
|
|
|
await setDate("re", date);
|
|
await page.selectOption('select[name="paiementtype"]', type);
|
|
await page.selectOption('select[name="accountid"]', account);
|
|
await page.fill(`input[name="amount_${id}"]`, amount); // le nom porte l'id
|
|
if (note) await page.fill('textarea[name="note"]', note);
|
|
await page.locator('input[name="save"]').click();
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const apres = await resteAPayer();
|
|
console.log(`après : reste à payer ${apres === null ? "?" : apres.toFixed(2)} €`);
|
|
if (apres !== 0) {
|
|
console.error("ÉCHEC — la charge n'est pas soldée après soumission.");
|
|
Deno.exit(1);
|
|
}
|
|
console.log("ok — règlement enregistré et relu sur la fiche.");
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
}
|