Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68cc5eefb9 | ||
|
|
09e98ea063 | ||
|
|
58705fda56 | ||
|
|
a8b27d2996 | ||
|
|
279092274c | ||
|
|
3f0955a2d3 |
@@ -4,7 +4,7 @@
|
||||
"_match_rules": "Pattern matched case-insensitively as a regex against the bank label. Optional filters: bank (qonto|wise), side (credit|debit), amount_min, amount_max, type (Wise activity type). All present filters must match.",
|
||||
"_classifications": {
|
||||
"capital_deposit": "Apport en capital social. Dolibarr account 1013 (capital souscrit appelé versé).",
|
||||
"social_charges": "URSSAF, retraite complémentaire, etc. Dolibarr compte 646 (cotisations personnelles du dirigeant TNS) — PAS 645x, réservé aux cotisations patronales sur salaires : Arcodange n'a aucun salarié.",
|
||||
"social_charges": "URSSAF, retraite complémentaire, etc. Dolibarr compte 641, sous-compte dédié (la société prend en charge les cotisations personnelles de son gérant majoritaire : c'est un complément de rémunération). PAS 646, réservé à l'exploitant individuel et aux sociétés à l'IR. PAS 645x non plus, qui vise les cotisations sur salaires — Arcodange n'a aucun salarié et l'opérateur n'est pas employeur. Voir adc-009.",
|
||||
"ai_subscription": "Claude / Mistral / OpenAI / similar. Dolibarr account 6262 (frais télécom / abonnements logiciels).",
|
||||
"bank_fee": "Plan bancaire, frais d'opération, refunds. Dolibarr account 627 (services bancaires).",
|
||||
"internal_topup": "Solde Wise/Qonto rechargé pour couvrir un frais immédiat. Often nets out.",
|
||||
@@ -25,7 +25,7 @@
|
||||
"classification": "social_charges",
|
||||
"bank": "qonto",
|
||||
"side": "debit",
|
||||
"note": "Cotisations sociales URSSAF (régime mensuel/trimestriel). Dolibarr: account 645100 (charges de sécurité sociale)."
|
||||
"note": "Cotisations sociales personnelles du gérant, appelées par échéancier (0/493/1215/1333 en 2026). Dolibarr : compte 641, sous-compte dédié — voir adc-009. Montants PROVISOIRES, assis sur un forfait début d'activité, régularisables après déclaration des revenus."
|
||||
},
|
||||
{
|
||||
"pattern": "MISTRAL\\.AI",
|
||||
|
||||
@@ -194,6 +194,22 @@ fi
|
||||
# an earlier date, which is a numbering break. This bites whenever two documents
|
||||
# of the same cycle are issued on different days (a deferred part issued after
|
||||
# the next fixed part, for instance). Refuse rather than create the break.
|
||||
#
|
||||
# CUSTOMER INVOICES ONLY. L'article 289 impose une numérotation chronologique et
|
||||
# continue aux factures que la société ÉMET. Il ne dit rien de la référence de
|
||||
# classement que Dolibarr attribue aux factures qu'elle REÇOIT : le numéro qui
|
||||
# fait foi pour celles-là est celui du fournisseur, porté par ref_supplier, et la
|
||||
# séquence FAF suit l'ordre d'ENREGISTREMENT — c'est sa construction normale.
|
||||
#
|
||||
# Appliquer la garde aux deux registres produisait un faux positif systématique :
|
||||
# la production porte déjà cinq ruptures dans la séquence FAF (FAF2026003 daté du
|
||||
# 4 janvier suit FAF2026002 daté du 9), et zéro dans la séquence FAC. Toute
|
||||
# facture fournisseur enregistrée après coup — le cas ordinaire, cf. adc-008 qui
|
||||
# prescrit d'enregistrer à la date du document — se heurtait au refus et exigeait
|
||||
# ARCO_ALLOW_BACKDATE. Une garde qu'on outrepasse par routine ne garde plus rien.
|
||||
if [[ "${ENDPOINT}" != "/invoices" ]]; then
|
||||
CHRONO_SKIP=1
|
||||
fi
|
||||
NEW_DATE="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['date'])" "${BODY}")"
|
||||
LAST="$("${W}" GET "${ENDPOINT}?sortfield=t.rowid&sortorder=DESC&limit=1" 2>/dev/null \
|
||||
| python3 -c "
|
||||
@@ -204,7 +220,7 @@ try:
|
||||
else: print('0|-')
|
||||
except Exception: print('0|-')" 2>/dev/null || echo "0|-")"
|
||||
LAST_DATE="${LAST%%|*}"; LAST_REF="${LAST##*|}"
|
||||
if [[ "${LAST_DATE}" =~ ^[0-9]+$ ]] && (( LAST_DATE > 0 )) && (( NEW_DATE < LAST_DATE )); then
|
||||
if [[ -z "${CHRONO_SKIP:-}" ]] && [[ "${LAST_DATE}" =~ ^[0-9]+$ ]] && (( LAST_DATE > 0 )) && (( NEW_DATE < LAST_DATE )); then
|
||||
if [[ "${ARCO_ALLOW_BACKDATE:-}" != "I-UNDERSTAND-THIS-BREAKS-CHRONOLOGY" ]]; then
|
||||
printf 'invoice-create.sh: REFUSED — chronology break.\n' >&2
|
||||
printf ' new document dated %s, but %s is already issued at %s.\n' \
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Correspondance commerciale — le format maison
|
||||
|
||||
Le gabarit des lettres qu'Arcodange adresse à ses clients et prospects. Fixé le
|
||||
24 août 2026 avec le dossier KissMetrics, et validé par l'opérateur : « on peut
|
||||
se souvenir de ce format pour les communications avec KM et futurs
|
||||
prospects/clients ».
|
||||
|
||||
## Produire une lettre
|
||||
|
||||
```bash
|
||||
python3 correspondance/lettre.py corps.html sortie.pdf \
|
||||
--titre "Contracts and invoices — cycles M1 to M4" \
|
||||
--date "24 August 2026" --prenom Evan \
|
||||
--destinataire "Evan Sforzo" --fonction "Chief Executive Officer" \
|
||||
--societe "Kissmetrics Inc." \
|
||||
--adresse "2850 34th Street North, 307 — St. Petersburg, Florida 33713 — United States" \
|
||||
--copie "Hendrik Rootering" \
|
||||
--pied "Arcodange × Kissmetrics Inc. — 24 August 2026"
|
||||
```
|
||||
|
||||
`corps.html` ne porte que le corps — les `<h2>`, `<p>`, `<table>`, encadrés.
|
||||
L'en-tête, le bloc destinataire, la signature et le pied viennent du gabarit.
|
||||
Sans `--copie`, la ligne « cc » disparaît entièrement.
|
||||
|
||||
Le script vérifie le nombre de pages et proteste au-delà de deux : une lettre
|
||||
d'affaires qui déborde ne se lit pas. Resserrer le CORPS, jamais la typographie.
|
||||
|
||||
## Les choix, et pourquoi
|
||||
|
||||
**Charter pour le texte, Optima pour les titres.** Charter a été dessinée par
|
||||
Matthew Carter pour tenir le petit corps là où d'autres se délitent — elle reste
|
||||
lisible à l'écran comme sur papier bon marché. Optima lui donne un contrepoint
|
||||
humaniste sans raideur. Les deux sont incorporées au PDF : le rendu est le même
|
||||
chez le destinataire, quelle que soit sa machine. Chiffres elzéviriens activés,
|
||||
pour que les montants dans le texte s'alignent au lieu de faire des bâtons.
|
||||
|
||||
**Deux encadrés, deux usages.** Le gris (`.calme`) porte une question ou une
|
||||
demande — elle ne doit pas se noyer dans un paragraphe. Le rouge sourd
|
||||
(`.encart`) porte ce qui doit être lu même en diagonale. **Un seul par lettre** ;
|
||||
deux, et plus rien ne ressort.
|
||||
|
||||
**Les tableaux portent un `<thead>`.** Sans lui, un tableau qui se coupe entre
|
||||
deux pages perd son en-tête et devient illisible. Avec, il se répète.
|
||||
|
||||
**Les trois emblèmes 🏹💻🪽** viennent du site — *« Gabriel 🪽 Radureau, pour 🏹
|
||||
réussir vos projets 💻 »*. L'arc et l'ange sont dans le nom lui-même. Ils
|
||||
figurent en marque sous le mot-marque, et l'aile seule entre prénom et nom dans
|
||||
la signature.
|
||||
|
||||
## Le piège des emblèmes
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **`weasyprint` ne sait pas rendre Apple Color Emoji.** C'est un format bitmap
|
||||
> `sbix` qu'il ignore : les trois emblèmes sortent en carrés vides, sans erreur
|
||||
> ni avertissement. Vérifié le 24/08/2026.
|
||||
|
||||
D'où `emblemes/*.png`, rendus une fois pour toutes par `test/emoji2png.ts`, qui
|
||||
passe par Chromium — lui lit la police système. Pour les régénérer ou en ajouter :
|
||||
|
||||
```bash
|
||||
cd test && deno run -A emoji2png.ts ../correspondance/emblemes 🏹 💻 🪽
|
||||
```
|
||||
|
||||
Les PNG sont incorporés en base64 **à la génération**, pas dans le gabarit : un
|
||||
gabarit de 400 Ko dont 97 % de charabia ne se relit pas. Le PDF produit, lui,
|
||||
reste autonome.
|
||||
|
||||
## Ce que le format ne fait pas
|
||||
|
||||
Il ne remplace pas le message d'accompagnement. La lettre est le document qu'on
|
||||
joint ; le mot sur Slack ou par courriel reste séparé, plus court, et dit
|
||||
pourquoi on écrit. Les deux doivent rester d'accord — vérifier que la lettre ne
|
||||
mentionne aucune pièce absente de l'envoi.
|
||||
|
||||
## Exemple de référence
|
||||
|
||||
`1_DOCUMENTS/prospects/KissMetrics/relances/2026-08-24_lettre_KM.html` — la
|
||||
lettre du 24/08/2026, dont ce gabarit est extrait. Elle se régénère à
|
||||
l'identique, au mot près, ce qui est le test du gabarit.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Produit une lettre commerciale Arcodange en PDF, depuis le gabarit maison.
|
||||
|
||||
POURQUOI CE SCRIPT EXISTE. Deux choses ne se font pas à la main sans se tromper.
|
||||
|
||||
1. `weasyprint` NE SAIT PAS rendre Apple Color Emoji : c'est un format bitmap
|
||||
`sbix` qu'il ignore, et les trois emblèmes 🏹💻🪽 sortent en carrés vides.
|
||||
Il faut les incorporer en images. Elles sont ici en PNG, rendues une fois
|
||||
pour toutes par `test/emoji2png.ts` (Chromium, lui, lit la police système).
|
||||
2. Un gabarit qui porterait ces images en base64 pèserait 400 Ko dont 97 % de
|
||||
charabia. On les garde en fichiers et on les incorpore À LA GÉNÉRATION,
|
||||
pour que le gabarit reste relisible et que le PDF reste autonome.
|
||||
|
||||
Usage :
|
||||
python3 correspondance/lettre.py corps.html sortie.pdf \
|
||||
--titre "Contracts and invoices — cycles M1 to M4" \
|
||||
--date "24 August 2026" --prenom Evan \
|
||||
--destinataire "Evan Sforzo" --fonction "Chief Executive Officer" \
|
||||
--societe "Kissmetrics Inc." \
|
||||
--adresse "2850 34th Street North, 307 — St. Petersburg, Florida 33713 — United States" \
|
||||
--copie "Hendrik Rootering" \
|
||||
--pied "Arcodange × Kissmetrics Inc. — 24 August 2026"
|
||||
|
||||
`corps.html` ne contient que le corps : les <h2>, <p>, <table>, encadrés. L'en-tête,
|
||||
le bloc destinataire, la signature et le pied de page viennent du gabarit.
|
||||
Sans `--copie`, la ligne « cc » disparaît.
|
||||
"""
|
||||
import argparse, base64, pathlib, re, subprocess, sys
|
||||
|
||||
ICI = pathlib.Path(__file__).parent
|
||||
GABARIT = ICI / "lettre.template.html"
|
||||
EMBLEMES = {"ARC": "arc.png", "LAPTOP": "laptop.png", "AILE": "aile.png"}
|
||||
|
||||
|
||||
def data_uri(chemin: pathlib.Path) -> str:
|
||||
return "data:image/png;base64," + base64.b64encode(chemin.read_bytes()).decode()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
a = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
a.add_argument("corps"); a.add_argument("sortie")
|
||||
for champ in ("titre", "date", "prenom", "destinataire", "fonction",
|
||||
"societe", "adresse", "pied"):
|
||||
a.add_argument(f"--{champ}", required=True)
|
||||
a.add_argument("--copie", default="")
|
||||
a.add_argument("--garder-html", action="store_true",
|
||||
help="conserve le HTML intermédiaire à côté du PDF")
|
||||
o = a.parse_args()
|
||||
|
||||
html = GABARIT.read_text(encoding="utf-8")
|
||||
|
||||
for cle, fichier in EMBLEMES.items():
|
||||
p = ICI / "emblemes" / fichier
|
||||
if not p.exists():
|
||||
print(f"emblème manquant : {p}\n"
|
||||
f" le régénérer : cd test && deno run -A emoji2png.ts <dossier> 🏹 💻 🪽",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
html = html.replace("{{" + cle + "}}", data_uri(p))
|
||||
|
||||
for cle, val in (("TITRE", o.titre), ("DATE", o.date), ("PRENOM", o.prenom),
|
||||
("DESTINATAIRE", o.destinataire), ("FONCTION", o.fonction),
|
||||
("SOCIETE", o.societe), ("ADRESSE", o.adresse),
|
||||
("COPIE", o.copie), ("PIED", o.pied)):
|
||||
html = html.replace("{{" + cle + "}}", val)
|
||||
|
||||
# Pas de destinataire en copie : on retire la ligne entière, pas seulement
|
||||
# son contenu, sinon il reste un « cc » orphelin.
|
||||
if not o.copie:
|
||||
html = re.sub(r'<br>\s*<span class="a"[^>]*>cc</span>\s* \s*', "", html)
|
||||
|
||||
# Le corps remplace tout ce qui sépare le sous-titre de la signature.
|
||||
corps = pathlib.Path(o.corps).read_text(encoding="utf-8")
|
||||
deb = html.index("<h1>")
|
||||
fin = html.index('<div class="signature">')
|
||||
entete = html[deb:html.index("<p>Dear ")]
|
||||
html = html[:deb] + entete + f"<p>Dear {o.prenom},</p>\n\n" + corps + "\n\n" + html[fin:]
|
||||
|
||||
tmp = pathlib.Path(o.sortie).with_suffix(".html")
|
||||
tmp.write_text(html, encoding="utf-8")
|
||||
r = subprocess.run(["weasyprint", str(tmp), o.sortie])
|
||||
if r.returncode == 0 and not o.garder_html:
|
||||
tmp.unlink()
|
||||
if r.returncode == 0:
|
||||
pages = subprocess.run(["pdfinfo", o.sortie], capture_output=True, text=True).stdout
|
||||
n = next((l.split()[1] for l in pages.splitlines() if l.startswith("Pages")), "?")
|
||||
print(f"ok — {o.sortie} ({n} page(s))")
|
||||
if n not in ("1", "2"):
|
||||
print(" ATTENTION : au-delà de deux pages, une lettre d'affaires se lit mal. "
|
||||
"Resserrer le corps plutôt que la typographie.", file=sys.stderr)
|
||||
return r.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
||||
<title>{{TITRE}} — Arcodange</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 15mm 18mm 13mm 18mm;
|
||||
@bottom-center { content: "{{PIED}} — page " counter(page) " / " counter(pages);
|
||||
font-family: "Optima", "Seravek", sans-serif; font-size: 7.8pt;
|
||||
letter-spacing: .4px; color: #8a8a8a; } }
|
||||
body { font-family: "Charter", "Bitstream Charter", "Iowan Old Style", Georgia, serif;
|
||||
font-size: 9.9pt; line-height: 1.44; color: #14161a;
|
||||
font-feature-settings: "kern" 1, "liga" 1, "onum" 1; }
|
||||
.entete { border-bottom: 1.5px solid #14161a; padding-bottom: 8px; margin-bottom: 16px;
|
||||
display: flex; justify-content: space-between; align-items: flex-end; }
|
||||
.entete .nom { font-family: "Optima", "Seravek", sans-serif; font-size: 17pt;
|
||||
font-weight: 600; letter-spacing: 3.5px; }
|
||||
.marque { margin-top: 7px; }
|
||||
.marque img { height: 16px; vertical-align: -3px; margin-right: 7px; }
|
||||
.signature .qui img { height: 13px; vertical-align: -2px; margin: 0 1px; }
|
||||
.entete .mentions { font-family: "Optima", "Seravek", sans-serif; font-size: 8pt; color: #4a4a4a; line-height: 1.45; text-align: right; }
|
||||
.dest { margin-bottom: 16px; font-size: 10pt; }
|
||||
.dest .a { font-family: "Optima", "Seravek", sans-serif; color: #8a8a8a; font-size: 7.8pt; letter-spacing: .6px; text-transform: uppercase; }
|
||||
h1 { font-family: "Optima", "Seravek", sans-serif; font-size: 14pt; font-weight: 600;
|
||||
margin: 0 0 3px; letter-spacing: .2px; }
|
||||
h1 + .sous { font-style: italic; color: #4a4a4a; margin: 0 0 16px; font-size: 10pt; }
|
||||
h2 { font-family: "Optima", "Seravek", sans-serif; font-size: 10.2pt; font-weight: 600;
|
||||
page-break-after: avoid; margin: 14px 0 5px; padding-bottom: 3px; border-bottom: 1px solid #d8d8d8;
|
||||
letter-spacing: .4px; }
|
||||
p { margin: 0 0 7px; text-align: justify; }
|
||||
strong { font-weight: bold; }
|
||||
em { font-style: italic; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 9px 0 10px; font-size: 8.7pt; }
|
||||
th, td { border-bottom: 1px solid #dcdcdc; padding: 3.5px 8px; text-align: left; }
|
||||
thead { display: table-header-group; }
|
||||
th { font-family: "Optima", "Seravek", sans-serif;
|
||||
background: #f4f4f2; border-bottom: 1px solid #999; font-weight: 600; font-size: 8.2pt;
|
||||
letter-spacing: .5px; text-transform: uppercase; color: #333; }
|
||||
td.n { text-align: right; white-space: nowrap; }
|
||||
tr.due td { background: #fdf6f2; font-weight: bold; }
|
||||
.encart { border-left: 3px solid #8a1c1c; background: #fbf6f5; padding: 8px 12px; margin: 11px 0;
|
||||
font-size: 9.4pt; }
|
||||
.calme { border-left: 3px solid #b8b8b0; background: #f8f8f6; padding: 8px 12px; margin: 11px 0;
|
||||
font-size: 9.4pt; }
|
||||
.signature { margin-top: 17px; page-break-inside: avoid; }
|
||||
.signature .qui { font-family: "Optima", "Seravek", sans-serif; font-weight: 600; font-size: 11pt; }
|
||||
.signature .role { font-family: "Optima", "Seravek", sans-serif; font-size: 9pt; color: #555; }
|
||||
</style></head><body>
|
||||
|
||||
<div class="entete">
|
||||
<div><div class="nom">ARCODANGE</div>
|
||||
<div class="marque"><img src="{{ARC}}" alt=""><img src="{{LAPTOP}}" alt=""><img src="{{AILE}}" alt=""></div></div>
|
||||
<div class="mentions">
|
||||
SARL au capital de 1 000 € — SIREN 999 657 455 R.C.S. Évry<br>
|
||||
73 boulevard de l'Yerres, 91000 Évry-Courcouronnes, France<br>
|
||||
VAT FR00 999 657 455 — [email protected]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dest">
|
||||
<span class="a">To</span><br>
|
||||
<strong>{{DESTINATAIRE}}</strong> — {{FONCTION}}, {{SOCIETE}}<br>
|
||||
{{ADRESSE}}<br>
|
||||
<span class="a" style="font-size:7.4pt">cc</span> {{COPIE}}
|
||||
</div>
|
||||
|
||||
<h1>{{TITRE}}</h1>
|
||||
<p class="sous">{{DATE}}</p>
|
||||
|
||||
<p>Dear {{PRENOM}},</p>
|
||||
|
||||
<!-- ═══ CORPS ═══════════════════════════════════════════════════════════════
|
||||
Le contenu, et rien d'autre, change d'une lettre à l'autre. Ce qui suit
|
||||
est l'exemple du 24/08/2026 (dossier KissMetrics), gardé pour montrer les
|
||||
éléments disponibles. Le remplacer intégralement.
|
||||
|
||||
<h2>1 — Titre de section</h2> numérotées, c'est une lettre d'affaires
|
||||
<p>…</p>
|
||||
<div class="calme">…</div> encadré gris : une question, une demande
|
||||
<div class="encart">…</div> encadré rouge sourd : ce qui doit être vu
|
||||
<table>…</table> <thead> obligatoire : l'en-tête se répète
|
||||
<tr class="due">…</tr> ligne mise en avant dans un tableau
|
||||
════════════════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<h2>1 — Une section</h2>
|
||||
|
||||
<p>Corps de texte en Charter. Les <strong>montants</strong> et les points qui
|
||||
portent la décision se mettent en gras ; les citations en <em>italique</em>.</p>
|
||||
|
||||
<div class="calme">
|
||||
<p style="margin:0"><strong>Une question posée franchement</strong> se met dans
|
||||
un encadré gris — elle ne doit pas se noyer dans un paragraphe.</p>
|
||||
</div>
|
||||
|
||||
<div class="encart">
|
||||
<p style="margin:0">L'encadré rouge sourd est réservé à ce qui doit être lu
|
||||
même en diagonale. <strong>Un par lettre, pas deux.</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="signature">
|
||||
<p class="qui">Gabriel <img src="{{AILE}}" alt=""> Radureau</p>
|
||||
<p class="role">Gérant — Arcodange</p>
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"at": "2026-08-24T13:04:32+00:00",
|
||||
"stage": "pre",
|
||||
"runtime": "mistral",
|
||||
"model": "vibe -p (mistral)",
|
||||
"prompt_sha256": "6fc0e33db64e27fe0bb706c44256fc9203575cf731d05f6d0903f91a7787363d",
|
||||
"verdict": "BLOCK",
|
||||
"response": "VERDICT: BLOCK\nREASON: The title claims invoices are \"passing to dollars,\" but they were already labeled in USD; the change-set only rewrites notes and descriptions.\nFINDINGS:\n- Title-scope mismatch: change-set updates notes/descriptions/revalidates, but does not change currency from EUR to USD.\nRESIDUAL RISK:\n- Confirm in production that multicurrency is fully enabled so the USD fields are treated as the authoritative amounts."
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"at": "2026-08-24T13:05:26+00:00",
|
||||
"decision": "approved",
|
||||
"by": "Gabriel Radureau — arbitrage explicite en session du 2026-08-24 : « Maintenant, sur les quatre factures non réglées », après avoir demandé « on ne peut pas emettre de facture en dollars ? ». Approbation portée par l'agent sur cet arbitrage nommé. LE FINDING DU JUGE EST FONDÉ ET ACCEPTÉ : le titre déborde du contenu. Le passage en devise proprement dit est fait AVANT ce change-set par test/setInvoiceCurrency.ts, parce que l'API REST accepte multicurrency_code et répond 200 sans rien appliquer ; le change-set ne porte que la suite — notes réécrites, désignations nettoyées, revalidation, échéances reposées. Le rationale le dit déjà explicitement. Le titre n'est pas rectifié pour ne pas rompre l'empreinte du manifeste sur laquelle ce gate est scellé ; l'imprécision est consignée ici. SON RESIDUAL RISK EST HONORÉ : la multidevise est vérifiée active en production, EUR et USD déclarés au dictionnaire. La répétition démontre en outre que le taux du dictionnaire est sans incidence — il valait 1,14416 en bac à sable, le changement de code s'en est servi, et la réécriture de la ligne a ramené la contre-valeur euro à l'euro près.",
|
||||
"manifest_digest": "b34b23f6b8703758",
|
||||
"judge_verdict": "BLOCK",
|
||||
"override_of_judge": true,
|
||||
"override_of_failed_rehearsal": false
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"at": "2026-08-24T13:11:50+00:00",
|
||||
"stage": "post",
|
||||
"runtime": "mistral",
|
||||
"model": "vibe -p (mistral)",
|
||||
"prompt_sha256": "e28f05a44b61b0061c0cf4566d379338be2062cee228dc945b57997c16615615",
|
||||
"verdict": "PASS",
|
||||
"response": "VERDICT: PASS\nREASON: All substantive fields (amounts, currency, dates, states, notes, line descriptions) match exactly between sandbox and production.\nDRIFT:\n- none\nFOLLOW-UP:\n- none"
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"title": "Les quatre factures non réglées passent en dollars — le montant affiché devient le montant dû",
|
||||
"rationale": "DÉFAUT CORRIGÉ. Les factures étaient libellées en euros pour un prix contractuel en dollars, avec une contre-valeur figée au taux du jour d'émission. Leur propre note le reconnaissait : « le montant effectivement dû en euros sera arrêté au taux du jour du règlement ». Une facture dont le montant affiché n'est pas le montant dû est bancale — et coûteuse : FAC004, FAC006 et FAC009 ont dû être reprises le 24/08 parce que leur contre-valeur avait été figée au mauvais taux.\n\nLe client paie d'ailleurs EN DOLLARS : les euros reçus varient de 2 147,00 à 2 195,97 pour une prestation à prix fixe, ce qui est la signature d'un virement en devise converti à l'arrivée par Wise.\n\nDROIT. Une facture peut être libellée dans toute monnaie (CGI art. 289, II ; directive 2006/112, art. 230) ; seule la TVA à payer doit être déterminée en euros, et il n'y en a pas ici — autoliquidation par un preneur hors UE (CGI art. 259, 1° et 283-2). Les LIVRES restent tenus en euros (C. com. art. L.123-22) : la contre-valeur inscrite ne bouge pas d'un centime.\n\nPÉRIMÈTRE. Les quatre factures NON RÉGLÉES seulement — FAC005, FAC007, FAC010, FAC011. Aucune n'a été transmise au client. Les quatre réglées gardent leur montant en euros, qui est exactement ce qui a été encaissé : chaque facture dit ainsi sa propre vérité, et rien de déjà rapproché avec la banque ne bouge.\n\nLe passage en devise proprement dit a été fait au préalable par test/setInvoiceCurrency.ts — l'API REST accepte multicurrency_code et répond 200 sans rien appliquer. Ce change-set ne porte que ce qui suit : notes réécrites, désignations nettoyées, revalidation, échéances reposées.\n\nCHANGEMENT DE MÉTHODE au sens d'adc-011 : il est daté, motivé, et s'applique à toutes les factures émises à compter de ce jour.",
|
||||
"observe": [
|
||||
"/invoices/15",
|
||||
"/invoices/17",
|
||||
"/invoices/20",
|
||||
"/invoices/21"
|
||||
],
|
||||
"ops": [
|
||||
{
|
||||
"label": "FAC005 — note publique réécrite : la facture est en dollars, plus de contre-valeur indicative",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/15",
|
||||
"body": {
|
||||
"note_public": "PART DIFFÉRÉE DU CYCLE M1 — contrat cadre du 23/04/2026 (art. 6) et son avenant (art. 2 et 4).\nFACTURE LIBELLÉE EN DOLLARS DES ÉTATS-UNIS. Le montant dû est de 3 000,00 USD — le montant contractuel lui-même, et non une contre-valeur. Une facture peut être libellée dans toute monnaie (CGI art. 289, II ; directive 2006/112, art. 230) ; seule la TVA à payer devrait être exprimée en euros, et il n'y en a pas ici — autoliquidation par un preneur établi hors de l'Union européenne (CGI art. 259, 1° et 283-2).\nLa contre-valeur de 2 575,11 € portée aux livres (taux de 1,164992 USD pour 1 EUR retenu à l'émission) n'a qu'une portée comptable interne : les documents comptables de la société sont tenus en euros (C. com. art. L.123-22). L'écart entre cette contre-valeur et les euros effectivement encaissés sera constaté en écart de change.\nLe différé de trois mois est une modalité de PRIX convenue au contrat, non un délai de paiement accordé après facturation — l'art. L.441-10 I n'est donc pas en cause.\nPériode d'exécution : 23/04/2026 au 23/05/2026. Échéance : 23/08/2026.\nPÉNALITÉS DE RETARD — En cas de retard de paiement, sont automatiquement dues, sans rappel préalable : (i) des pénalités de retard calculées au taux d'intérêt appliqué par la Banque centrale européenne à son opération de refinancement la plus récente, majoré de 10 points de pourcentage (art. L.441-10 II du Code de commerce) — soit 12,15 % l'an au 1er semestre 2026 et 12,40 % l'an au 2e semestre 2026 ; (ii) une indemnité forfaitaire pour frais de recouvrement de quarante euros (40 €) par facture impayée (art. L.441-10 III du Code de commerce et décret n° 2012-1115) ; (iii) une indemnisation complémentaire sur justification documentée lorsque les frais de recouvrement exposés sont supérieurs à ce forfait. Aucun escompte n'est accordé en cas de paiement anticipé. Ces stipulations sont des minima légaux d'ordre public auxquels il ne peut être renoncé. / LATE PAYMENT — automatically due without prior reminder: (i) interest at the ECB refinancing rate plus 10 percentage points (Art. L.441-10 II French Commercial Code) — 12.15 % p.a. in H1 2026, 12.40 % p.a. in H2 2026; (ii) a fixed recovery indemnity of forty euros (€40) per unpaid invoice (Art. L.441-10 III and Decree no. 2012-1115); (iii) further indemnification on documented justification. No early-payment discount."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC005 — revalider (même numéro, même date d'émission)",
|
||||
"api": {
|
||||
"method": "POST",
|
||||
"path": "/invoices/15/validate",
|
||||
"body": {
|
||||
"notrigger": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC005 — échéance au 23/08/2026, à reposer après validation",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/15",
|
||||
"body": {
|
||||
"date_lim_reglement": 1787479200
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC007 — note publique réécrite : la facture est en dollars, plus de contre-valeur indicative",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/17",
|
||||
"body": {
|
||||
"note_public": "PART DIFFÉRÉE DU CYCLE M2 — contrat cadre du 23/04/2026 (art. 6) et son avenant (art. 2 et 4).\nFACTURE LIBELLÉE EN DOLLARS DES ÉTATS-UNIS. Le montant dû est de 3 000,00 USD — le montant contractuel lui-même, et non une contre-valeur. Une facture peut être libellée dans toute monnaie (CGI art. 289, II ; directive 2006/112, art. 230) ; seule la TVA à payer devrait être exprimée en euros, et il n'y en a pas ici — autoliquidation par un preneur établi hors de l'Union européenne (CGI art. 259, 1° et 283-2).\nLa contre-valeur de 2 575,11 € portée aux livres (taux de 1,164992 USD pour 1 EUR retenu à l'émission) n'a qu'une portée comptable interne : les documents comptables de la société sont tenus en euros (C. com. art. L.123-22). L'écart entre cette contre-valeur et les euros effectivement encaissés sera constaté en écart de change.\nLe différé de trois mois est une modalité de PRIX convenue au contrat, non un délai de paiement accordé après facturation — l'art. L.441-10 I n'est donc pas en cause.\nPériode d'exécution : 23/05/2026 au 23/06/2026. Échéance : 23/09/2026.\nPÉNALITÉS DE RETARD — En cas de retard de paiement, sont automatiquement dues, sans rappel préalable : (i) des pénalités de retard calculées au taux d'intérêt appliqué par la Banque centrale européenne à son opération de refinancement la plus récente, majoré de 10 points de pourcentage (art. L.441-10 II du Code de commerce) — soit 12,15 % l'an au 1er semestre 2026 et 12,40 % l'an au 2e semestre 2026 ; (ii) une indemnité forfaitaire pour frais de recouvrement de quarante euros (40 €) par facture impayée (art. L.441-10 III du Code de commerce et décret n° 2012-1115) ; (iii) une indemnisation complémentaire sur justification documentée lorsque les frais de recouvrement exposés sont supérieurs à ce forfait. Aucun escompte n'est accordé en cas de paiement anticipé. Ces stipulations sont des minima légaux d'ordre public auxquels il ne peut être renoncé. / LATE PAYMENT — automatically due without prior reminder: (i) interest at the ECB refinancing rate plus 10 percentage points (Art. L.441-10 II French Commercial Code) — 12.15 % p.a. in H1 2026, 12.40 % p.a. in H2 2026; (ii) a fixed recovery indemnity of forty euros (€40) per unpaid invoice (Art. L.441-10 III and Decree no. 2012-1115); (iii) further indemnification on documented justification. No early-payment discount."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC007 — revalider (même numéro, même date d'émission)",
|
||||
"api": {
|
||||
"method": "POST",
|
||||
"path": "/invoices/17/validate",
|
||||
"body": {
|
||||
"notrigger": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC007 — échéance au 23/09/2026, à reposer après validation",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/17",
|
||||
"body": {
|
||||
"date_lim_reglement": 1790157600
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC011 — note publique réécrite : la facture est en dollars, plus de contre-valeur indicative",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/21",
|
||||
"body": {
|
||||
"note_public": "PART DIFFÉRÉE DU CYCLE M3 — contrat cadre du 23/04/2026 (art. 6) et son avenant (art. 2 et 4).\nFACTURE LIBELLÉE EN DOLLARS DES ÉTATS-UNIS. Le montant dû est de 3 000,00 USD — le montant contractuel lui-même, et non une contre-valeur. Une facture peut être libellée dans toute monnaie (CGI art. 289, II ; directive 2006/112, art. 230) ; seule la TVA à payer devrait être exprimée en euros, et il n'y en a pas ici — autoliquidation par un preneur établi hors de l'Union européenne (CGI art. 259, 1° et 283-2).\nLa contre-valeur de 2 568,54 € portée aux livres (taux de 1,167978 USD pour 1 EUR retenu à l'émission) n'a qu'une portée comptable interne : les documents comptables de la société sont tenus en euros (C. com. art. L.123-22). L'écart entre cette contre-valeur et les euros effectivement encaissés sera constaté en écart de change.\nÉMISSION TARDIVE, ASSUMÉE COMME TELLE : la part différée du cycle M3 aurait dû être émise le 23/07/2026, en même temps que sa part fixe (FAC008). Elle ne l'a pas été. La facture pro forma PR2607-0001, remise au client en juillet, annonçait son émission au 24/08/2026 — c'est la date retenue, et la numérotation reste ainsi chronologique au sens de l'article 289 du CGI. L'échéance, elle, n'est pas décalée : elle reste au 23/10/2026, soit trois mois après la fin du cycle M3, comme le contrat le prévoit.\nLe différé de trois mois est une modalité de PRIX convenue au contrat, non un délai de paiement accordé après facturation — l'art. L.441-10 I n'est donc pas en cause.\nPériode d'exécution : 23/06/2026 au 23/07/2026. Échéance : 23/10/2026.\nPÉNALITÉS DE RETARD — En cas de retard de paiement, sont automatiquement dues, sans rappel préalable : (i) des pénalités de retard calculées au taux d'intérêt appliqué par la Banque centrale européenne à son opération de refinancement la plus récente, majoré de 10 points de pourcentage (art. L.441-10 II du Code de commerce) — soit 12,15 % l'an au 1er semestre 2026 et 12,40 % l'an au 2e semestre 2026 ; (ii) une indemnité forfaitaire pour frais de recouvrement de quarante euros (40 €) par facture impayée (art. L.441-10 III du Code de commerce et décret n° 2012-1115) ; (iii) une indemnisation complémentaire sur justification documentée lorsque les frais de recouvrement exposés sont supérieurs à ce forfait. Aucun escompte n'est accordé en cas de paiement anticipé. Ces stipulations sont des minima légaux d'ordre public auxquels il ne peut être renoncé. / LATE PAYMENT — automatically due without prior reminder: (i) interest at the ECB refinancing rate plus 10 percentage points (Art. L.441-10 II French Commercial Code) — 12.15 % p.a. in H1 2026, 12.40 % p.a. in H2 2026; (ii) a fixed recovery indemnity of forty euros (€40) per unpaid invoice (Art. L.441-10 III and Decree no. 2012-1115); (iii) further indemnification on documented justification. No early-payment discount."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC011 — désignation : retirer la conversion en euros, devenue sans objet",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/21/lines/21",
|
||||
"body": {
|
||||
"desc": "Conseil et accompagnement infrastructure cloud — Cycle M3 (part différée) — période d'exécution du 23/06/2026 au 23/07/2026. Échéance 23/10/2026. Montant contractuel 3 000,00 USD.\nPrestation de services au sens des articles 259, 1° et 283-2 du CGI. TVA française non applicable — preneur assujetti établi hors de l'Union européenne (États-Unis).",
|
||||
"description": "Conseil et accompagnement infrastructure cloud — Cycle M3 (part différée) — période d'exécution du 23/06/2026 au 23/07/2026. Échéance 23/10/2026. Montant contractuel 3 000,00 USD.\nPrestation de services au sens des articles 259, 1° et 283-2 du CGI. TVA française non applicable — preneur assujetti établi hors de l'Union européenne (États-Unis).",
|
||||
"subprice": "2568.54",
|
||||
"pu_ht": "2568.54",
|
||||
"multicurrency_subprice": "3000.00",
|
||||
"qty": "1",
|
||||
"product_type": "1",
|
||||
"tva_tx": "0.0000",
|
||||
"remise_percent": "0",
|
||||
"localtax1_tx": "0.0000",
|
||||
"localtax2_tx": "0.0000",
|
||||
"info_bits": "0",
|
||||
"special_code": "0",
|
||||
"rang": "0",
|
||||
"situation_percent": "100",
|
||||
"fk_warehouse": "0",
|
||||
"pa_ht": "0.00000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC011 — revalider (même numéro, même date d'émission)",
|
||||
"api": {
|
||||
"method": "POST",
|
||||
"path": "/invoices/21/validate",
|
||||
"body": {
|
||||
"notrigger": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC011 — échéance au 23/10/2026, à reposer après validation",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/21",
|
||||
"body": {
|
||||
"date_lim_reglement": 1792749600
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC010 — note publique réécrite : la facture est en dollars, plus de contre-valeur indicative",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/20",
|
||||
"body": {
|
||||
"note_public": "PART DIFFÉRÉE DU CYCLE M4 — contrat cadre du 23/04/2026 (art. 6) et son avenant (art. 2 et 4).\nFACTURE LIBELLÉE EN DOLLARS DES ÉTATS-UNIS. Le montant dû est de 3 000,00 USD — le montant contractuel lui-même, et non une contre-valeur. Une facture peut être libellée dans toute monnaie (CGI art. 289, II ; directive 2006/112, art. 230) ; seule la TVA à payer devrait être exprimée en euros, et il n'y en a pas ici — autoliquidation par un preneur établi hors de l'Union européenne (CGI art. 259, 1° et 283-2).\nLa contre-valeur de 2 568,54 € portée aux livres (taux de 1,167978 USD pour 1 EUR retenu à l'émission) n'a qu'une portée comptable interne : les documents comptables de la société sont tenus en euros (C. com. art. L.123-22). L'écart entre cette contre-valeur et les euros effectivement encaissés sera constaté en écart de change.\nLe différé de trois mois est une modalité de PRIX convenue au contrat, non un délai de paiement accordé après facturation — l'art. L.441-10 I n'est donc pas en cause.\nPériode d'exécution : 23/07/2026 au 23/08/2026. Échéance : 23/11/2026.\nPÉNALITÉS DE RETARD — En cas de retard de paiement, sont automatiquement dues, sans rappel préalable : (i) des pénalités de retard calculées au taux d'intérêt appliqué par la Banque centrale européenne à son opération de refinancement la plus récente, majoré de 10 points de pourcentage (art. L.441-10 II du Code de commerce) — soit 12,15 % l'an au 1er semestre 2026 et 12,40 % l'an au 2e semestre 2026 ; (ii) une indemnité forfaitaire pour frais de recouvrement de quarante euros (40 €) par facture impayée (art. L.441-10 III du Code de commerce et décret n° 2012-1115) ; (iii) une indemnisation complémentaire sur justification documentée lorsque les frais de recouvrement exposés sont supérieurs à ce forfait. Aucun escompte n'est accordé en cas de paiement anticipé. Ces stipulations sont des minima légaux d'ordre public auxquels il ne peut être renoncé. / LATE PAYMENT — automatically due without prior reminder: (i) interest at the ECB refinancing rate plus 10 percentage points (Art. L.441-10 II French Commercial Code) — 12.15 % p.a. in H1 2026, 12.40 % p.a. in H2 2026; (ii) a fixed recovery indemnity of forty euros (€40) per unpaid invoice (Art. L.441-10 III and Decree no. 2012-1115); (iii) further indemnification on documented justification. No early-payment discount."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC010 — désignation : retirer la conversion en euros, devenue sans objet",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/20/lines/20",
|
||||
"body": {
|
||||
"desc": "Conseil et accompagnement infrastructure cloud — Cycle M4 (part différée) — période d'exécution du 23/07/2026 au 23/08/2026. Échéance 23/11/2026. Montant contractuel 3 000,00 USD.\nPrestation de services au sens des articles 259, 1° et 283-2 du CGI. TVA française non applicable — preneur assujetti établi hors de l'Union européenne (États-Unis).",
|
||||
"description": "Conseil et accompagnement infrastructure cloud — Cycle M4 (part différée) — période d'exécution du 23/07/2026 au 23/08/2026. Échéance 23/11/2026. Montant contractuel 3 000,00 USD.\nPrestation de services au sens des articles 259, 1° et 283-2 du CGI. TVA française non applicable — preneur assujetti établi hors de l'Union européenne (États-Unis).",
|
||||
"subprice": "2568.54",
|
||||
"pu_ht": "2568.54",
|
||||
"multicurrency_subprice": "3000.00",
|
||||
"qty": "1",
|
||||
"product_type": "1",
|
||||
"tva_tx": "0.0000",
|
||||
"remise_percent": "0",
|
||||
"localtax1_tx": "0.0000",
|
||||
"localtax2_tx": "0.0000",
|
||||
"info_bits": "0",
|
||||
"special_code": "0",
|
||||
"rang": "0",
|
||||
"situation_percent": "100",
|
||||
"fk_warehouse": "0",
|
||||
"pa_ht": "0.00000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC010 — revalider (même numéro, même date d'émission)",
|
||||
"api": {
|
||||
"method": "POST",
|
||||
"path": "/invoices/20/validate",
|
||||
"body": {
|
||||
"notrigger": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "FAC010 — échéance au 23/11/2026, à reposer après validation",
|
||||
"api": {
|
||||
"method": "PUT",
|
||||
"path": "/invoices/20",
|
||||
"body": {
|
||||
"date_lim_reglement": 1795428000
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"verify": [
|
||||
{
|
||||
"path": "/invoices/15",
|
||||
"expect": "USD"
|
||||
},
|
||||
{
|
||||
"path": "/invoices/21",
|
||||
"expect": "USD"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"at": "2026-08-24T12:59:59+00:00", "stage": "rehearsal", "file": "01-rehearsal.json"}
|
||||
{"at": "2026-08-24T13:04:32+00:00", "stage": "pre", "file": "02-pre-verdict.json"}
|
||||
{"at": "2026-08-24T13:05:26+00:00", "stage": "gate", "file": "03-gate.json"}
|
||||
{"at": "2026-08-24T13:07:26+00:00", "stage": "applied", "file": "04-applied.json"}
|
||||
{"at": "2026-08-24T13:11:50+00:00", "stage": "post", "file": "05-post-verdict.json"}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"at": "2026-08-24T12:17:47+00:00",
|
||||
"stage": "pre",
|
||||
"runtime": "mistral",
|
||||
"model": "vibe -p (mistral)",
|
||||
"prompt_sha256": "55fb1db8807c6c55db9cb801abfd9f053df9a32b9de38d42da05fa2107c36b4f",
|
||||
"verdict": "BLOCK",
|
||||
"response": "VERDICT: BLOCK\nREASON: date_lim_reglement set after invoice date for a 3‑month deferred term violates the stated contract rule.\nFINDINGS:\n- date_lim_reglement 1792706400 (23/10/2026) is based on cycle end 23/07/2026, not on invoice date 24/08/2026; this creates a due‑date gap of only ~2 months from emission instead of the 3‑month deferred price modality claimed for the other cycles.\n- manifested date 1787565600 (12:00) was normalized by the API to 1787522400 (00:00), producing a date_validation before date_creation in the result set.\n- scope creep: change‑set title mentions only M3 deferred, but ops include validation and due‑date update not reflected in title.\n\nRESIDUAL RISK:\n- confirm that the 3‑month deferred term is indeed measured from cycle end (23/07) rather than invoice date (24/08), as this may diverge from contractual intent and prior deferred invoices (FAC005, FAC007, FAC010)."
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
PREUVE — l'échéance du 23/10/2026 est celle annoncée au client
|
||||
|
||||
FINDING 1 : « le différé court-il depuis l'émission ou depuis la fin du cycle ? »
|
||||
La question est légitime : pour M1, M2 et M4 les deux lectures coïncident,
|
||||
l'émission tombant le dernier jour du cycle. Pour M3, la facture est tardive,
|
||||
et les deux lectures divergent — 23/10 contre 24/11.
|
||||
|
||||
La facture PRO FORMA PR2607-0001, remise au client en juillet 2026, tranche
|
||||
en toutes lettres, dans ses deux versions :
|
||||
FR : « La facture définitive correspondante sera émise le 24/08/2026,
|
||||
à échéance du 23/10/2026. »
|
||||
EN : « The definitive invoice will be issued on 2026-08-24,
|
||||
due 2026-10-23. »
|
||||
et : « Paiement différé à 3 mois DE LA PRESTATION. »
|
||||
Le différé court donc depuis la prestation, pas depuis l'émission. La date
|
||||
retenue est celle que le client a déjà reçue par écrit ; en retenir une autre
|
||||
reviendrait à s'écarter de notre propre annonce.
|
||||
|
||||
Cohérence avec les autres parts différées :
|
||||
FAC005-CL0001005 M1 23/04→23/05 émise 23/05/2026 échéance 23/08/2026
|
||||
FAC007-CL0001007 M2 23/05→23/06 émise 23/06/2026 échéance 23/09/2026
|
||||
FAC010-CL0001010 M4 23/07→23/08 émise 23/08/2026 échéance 23/11/2026
|
||||
FAC011-CL0001011 M3 23/06→23/07 émise 24/08/2026 échéance 23/10/2026
|
||||
Pour les trois premières : fin de cycle + 3 mois = échéance. FAC011 suit la
|
||||
même règle ; seule son émission est tardive, et la note le dit.
|
||||
|
||||
FINDING 2 : « date normalisée à 00:00, date_validation avant date_creation ».
|
||||
C'est le comportement normal de Dolibarr, identique sur toutes les factures :
|
||||
FAC008-CL0001008 date=23/07/2026 00:00 creation=26/07/2026 00:49 validation=26/07/2026 00:00
|
||||
FAC010-CL0001010 date=23/08/2026 00:00 creation=24/08/2026 09:46 validation=24/08/2026 00:00
|
||||
FAC011-CL0001011 date=24/08/2026 00:00 creation=24/08/2026 14:12 validation=24/08/2026 00:00
|
||||
La date de facture est une DATE (minuit), les horodatages techniques sont
|
||||
des INSTANTS. Les comparer entre eux n'a pas de sens, et aucune facture du
|
||||
registre n'y échappe.
|
||||
|
||||
FINDING 3 : « scope creep — le titre ne mentionne pas la validation ni l'échéance ».
|
||||
Valider une facture et lui poser son échéance, c'est l'ÉMETTRE. Ce ne sont pas
|
||||
des opérations distinctes du titre, ce sont ses composantes ; l'échéance doit
|
||||
d'ailleurs être posée APRÈS validation, Dolibarr l'ignorant à la création.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"at": "2026-08-24T12:18:43+00:00",
|
||||
"decision": "approved",
|
||||
"by": "Gabriel Radureau — instruction en session du 2026-08-24 : « La priorité est de régler l'administratif (on profite du rappel sur les 3000$ différé pour en parler) », et « dans le dossier à partager via slack je veux le tout complet ». Approbation portée par l'agent. OUTREPASSEMENT MOTIVÉ, preuves en 02b-preuve-echeance.txt : le juge demandait si le différé de trois mois court depuis l'émission ou depuis la fin du cycle — question légitime, les deux lectures ne divergeant que pour M3 dont la facture est tardive. La facture pro forma PR2607-0001, REMISE AU CLIENT en juillet, tranche en toutes lettres et dans ses deux versions : « émise le 24/08/2026, à échéance du 23/10/2026 », et « paiement différé à 3 mois DE LA PRESTATION ». Retenir une autre date reviendrait à s'écarter de notre propre annonce écrite. Les findings 2 (dates normalisées à minuit) et 3 (validation et échéance présentées comme hors titre) tombent : le premier vaut pour toutes les factures du registre, le second confond l'émission avec ses composantes.",
|
||||
"manifest_digest": "caa4cedc955da004",
|
||||
"judge_verdict": "BLOCK",
|
||||
"override_of_judge": true,
|
||||
"override_of_failed_rehearsal": false
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"at": "2026-08-24T12:21:16+00:00",
|
||||
"stage": "post",
|
||||
"runtime": "mistral",
|
||||
"model": "vibe -p (mistral)",
|
||||
"prompt_sha256": "fa027965633a6505de35d0b137db032383ee94cb59449120f0a3d8b812630145",
|
||||
"verdict": "PASS",
|
||||
"response": "VERDICT: PASS\nREASON: All substantive fields match between sandbox and production.\nDRIFT:\n- none\nFOLLOW-UP:\n- none"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"title": "Part différée du cycle M3 jamais facturée — 3 000 USD, échéance 23/10/2026",
|
||||
"rationale": "LA PART DIFFÉRÉE DU CYCLE M3 N'A JAMAIS ÉTÉ FACTURÉE. La cartographie des cycles le montre sans ambiguïté : M1, M2 et M4 ont chacun leur part fixe ET leur part différée ; M3 n'a que sa part fixe (FAC008, réglée le 20/07). Il manque 3 000 USD. La facture pro forma PR2607-0001, préparée en juillet, annonçait d'ailleurs cette émission pour le 24/08/2026 — soit aujourd'hui.\n\nÉmise à cette date, la numérotation reste chronologique au sens de l'article 289 du CGI. L'échéance, elle, n'est pas décalée : elle reste au 23/10/2026, trois mois après la fin du cycle M3, comme le contrat le prévoit. Le retard est ÉCRIT DANS LA NOTE plutôt que masqué par une date d'émission rétroactive, qui elle romprait la chronologie.\n\nCE QUI A ÉTÉ RETIRÉ DE CE CHANGE-SET, ET POURQUOI. Une première version ajoutait la clause de pénalités L.441-10 à FAC001, FAC002 et FAC003, qui en sont dépourvues. L'opérateur a précisé le circuit réel : pendant la phase d'audit (janvier-février 2026), c'est WISE qui a émis les factures, en reprenant les identifiants internes de Dolibarr. Ces trois factures ONT donc été transmises au client. Modifier leur note aujourd'hui ferait diverger le registre interne du document que le client détient — l'inverse de ce qu'on cherche. Le défaut de mention est réel mais historique, sur des factures réglées : il est consigné, pas réécrit.\n\nDepuis avril 2026, en revanche, KissMetrics vire le montant sur le compte Wise SANS recevoir de facture. FAC004 et suivantes n'ont jamais été transmises — c'est ce qui a permis de les reprendre plus tôt dans la journée, et c'est ce dossier-ci qui les portera au client pour la première fois.",
|
||||
"observe": [
|
||||
"/invoices?limit=60"
|
||||
],
|
||||
"ops": [
|
||||
{
|
||||
"label": "FAC011 — part différée du cycle M3, 3 000 USD (jamais facturée)",
|
||||
"api": {
|
||||
"method": "POST",
|
||||
"path": "/invoices",
|
||||
"body": {
|
||||
"socid": 1,
|
||||
"type": 0,
|
||||
"date": 1787565600,
|
||||
"note_public": "PART DIFFÉRÉE DU CYCLE M3 — contrat cadre du 23/04/2026 (art. 6) et son avenant (art. 2 et 4).\nMontant contractuel : 3 000,00 USD, réglé en euros au taux de référence EUR/USD du jour du paiement. Conversion indicative à l'émission : 1 EUR = 1,16798 USD le 24/08/2026, soit 2 568,54 €. Le montant effectivement dû en euros sera arrêté au taux du jour du règlement.\nÉMISSION TARDIVE, ASSUMÉE COMME TELLE : la part différée du cycle M3 aurait dû être émise le 23/07/2026, en même temps que sa part fixe (FAC008). Elle ne l'a pas été. La facture pro forma PR2607-0001, remise au client en juillet, annonçait son émission au 24/08/2026 — c'est la date retenue, et la numérotation reste ainsi chronologique au sens de l'article 289 du CGI. L'échéance, elle, n'est pas décalée : elle reste au 23/10/2026, soit trois mois après la fin du cycle M3, comme le contrat le prévoit.\nLe différé de trois mois est une modalité de PRIX convenue au contrat, non un délai de paiement accordé après facturation — l'art. L.441-10 I n'est donc pas en cause.\nPériode d'exécution : 23/06/2026 au 23/07/2026.\nPÉNALITÉS DE RETARD — En cas de retard de paiement, sont automatiquement dues, sans rappel préalable : (i) des pénalités de retard calculées au taux d'intérêt appliqué par la Banque centrale européenne à son opération de refinancement la plus récente, majoré de 10 points de pourcentage (art. L.441-10 II du Code de commerce) — soit 12,15 % l'an au 1er semestre 2026 et 12,40 % l'an au 2e semestre 2026 ; (ii) une indemnité forfaitaire pour frais de recouvrement de quarante euros (40 €) par facture impayée (art. L.441-10 III du Code de commerce et décret n° 2012-1115) ; (iii) une indemnisation complémentaire sur justification documentée lorsque les frais de recouvrement exposés sont supérieurs à ce forfait. Aucun escompte n'est accordé en cas de paiement anticipé. Ces stipulations sont des minima légaux d'ordre public auxquels il ne peut être renoncé. / LATE PAYMENT — automatically due without prior reminder: (i) interest at the ECB refinancing rate plus 10 percentage points (Art. L.441-10 II French Commercial Code) — 12.15 % p.a. in H1 2026, 12.40 % p.a. in H2 2026; (ii) a fixed recovery indemnity of forty euros (€40) per unpaid invoice (Art. L.441-10 III and Decree no. 2012-1115); (iii) further indemnification on documented justification. No early-payment discount.",
|
||||
"lines": [
|
||||
{
|
||||
"desc": "Conseil et accompagnement infrastructure cloud — Cycle M3 (part différée) — période d'exécution du 23/06/2026 au 23/07/2026. Échéance 23/10/2026. Montant contractuel 3 000,00 USD, converti à titre indicatif à 2 568,54 € (taux du 24/08/2026) ; règlement au taux du jour du paiement.\nPrestation de services au sens des articles 259, 1° et 283-2 du CGI. TVA française non applicable — preneur assujetti établi hors de l'Union européenne (États-Unis).",
|
||||
"subprice": "2568.54",
|
||||
"pu_ht": "2568.54",
|
||||
"qty": "1",
|
||||
"tva_tx": "0",
|
||||
"product_type": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"then": [
|
||||
{
|
||||
"label": "valider",
|
||||
"method": "POST",
|
||||
"path": "/invoices/{id}/validate",
|
||||
"body": {
|
||||
"notrigger": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "échéance au 23/10/2026 (+3 mois après la fin du cycle M3)",
|
||||
"method": "PUT",
|
||||
"path": "/invoices/{id}",
|
||||
"body": {
|
||||
"date_lim_reglement": 1792749600
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"verify": [
|
||||
{
|
||||
"path": "/invoices?limit=60",
|
||||
"expect": "FAC011"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"at": "2026-08-24T12:04:04+00:00", "stage": "rehearsal", "file": "01-rehearsal.json"}
|
||||
{"at": "2026-08-24T12:12:33+00:00", "stage": "rehearsal", "file": "01-rehearsal.json"}
|
||||
{"at": "2026-08-24T12:17:47+00:00", "stage": "pre", "file": "02-pre-verdict.json"}
|
||||
{"at": "2026-08-24T12:18:43+00:00", "stage": "gate", "file": "03-gate.json"}
|
||||
{"at": "2026-08-24T12:18:57+00:00", "stage": "applied", "file": "04-applied.json"}
|
||||
{"at": "2026-08-24T12:21:16+00:00", "stage": "post", "file": "05-post-verdict.json"}
|
||||
@@ -1,66 +1,136 @@
|
||||
---
|
||||
id: adc-006
|
||||
title: "KM retainer currency & composition (USD multicurrency vs EUR-at-settlement; line structure)"
|
||||
status: Proposed
|
||||
decided: null
|
||||
effective_from: null
|
||||
title: "Retainer KissMetrics : factures libellées en dollars, une facture par part, écarts de change en 766/666"
|
||||
status: Accepted
|
||||
decided: 2026-08-24
|
||||
effective_from: 2026-08-24
|
||||
effective_until: null
|
||||
supersedes: null
|
||||
superseded_by: null
|
||||
---
|
||||
|
||||
# adc-006 — KM retainer currency & composition (Proposed)
|
||||
|
||||
> **Proposed — operator input needed.** This stub reserves the decision slot
|
||||
> per the PRD [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc)
|
||||
> ("blocks erp#53 wiring; operator input needed"). Agents may enrich the
|
||||
> draft with sources and alternatives; **only the operator Accepts**.
|
||||
# adc-006 — Le retainer KissMetrics se facture en dollars
|
||||
|
||||
## Context
|
||||
|
||||
The recurring template id=1 carries 5100 while the actual May/June drafts were
|
||||
2×2145.92 + 2×2575.11 EUR; the 2026-07-11 regularization surfaced a probable
|
||||
**USD-denominated retainer** ($5,500/mo = $2,500 net-30 + $3,000 deferred
|
||||
+3 months — erp#53; contract WIP `test/km_contract.json` in the trunk). FX
|
||||
deltas have already been booked twice under adc-002. The invoicing method must
|
||||
be stable before T05 automation wires it (permanence des méthodes — not
|
||||
improvised per month).
|
||||
Le contrat cadre du 23/04/2026 fixe un prix **en dollars** : 2 500 USD par mois
|
||||
en part fixe, plus 3 000 USD en part différée à trois mois. Les factures, elles,
|
||||
étaient libellées **en euros**, pour une contre-valeur figée au taux du jour de
|
||||
leur émission — le montant dû n'étant arrêté qu'au règlement.
|
||||
|
||||
Le client **paie en dollars** ; l'argent **arrive en euros**, Wise convertissant
|
||||
à la réception puisqu'il ne détient qu'un solde en euros. Les montants reçus
|
||||
varient donc de 2 147,00 à 2 195,97 € pour une prestation à prix fixe.
|
||||
|
||||
La méthode antérieure consistait à faire coïncider la facture avec l'encaissement
|
||||
— à « matcher » l'un sur l'autre. Elle représente fidèlement ce qui est entré en
|
||||
banque, et c'est sa qualité : les factures réglées portent au centime près ce qui
|
||||
a été reçu. Sa limite apparaît sur une facture NON encore réglée : le montant
|
||||
affiché n'est alors qu'une contre-valeur provisoire, et la note devait l'écrire —
|
||||
« le montant effectivement dû en euros sera arrêté au taux du jour du règlement ».
|
||||
Le 24 août 2026, trois factures ont dû être ajustées après coup pour rejoindre le
|
||||
virement reçu : FAC004 (2 145,92 € portés à 2 147,00), FAC006 (portés à 2 195,97)
|
||||
et FAC009 (2 140,45 portés à 2 164,75) — 51,13 € d'encaissement qui manquaient
|
||||
aux livres.
|
||||
|
||||
## Decision
|
||||
|
||||
**To arbitrate — no decision recorded.** Open questions (erp#53):
|
||||
**1. Les factures du retainer sont libellées en dollars des États-Unis.** Le
|
||||
montant porté sur la facture est le montant contractuel — 2 500,00 ou 3 000,00
|
||||
USD — et non une contre-valeur. La contre-valeur en euros inscrite aux livres
|
||||
est celle du taux retenu à l'émission ; elle n'a qu'une portée comptable
|
||||
interne.
|
||||
|
||||
1. USD multicurrency invoices vs EUR-at-settlement invoicing?
|
||||
2. One invoice with two lines vs two invoices per month (net-30 part /
|
||||
deferred part)?
|
||||
3. Écarts de change policy interaction with adc-002 (would a USD-multicurrency
|
||||
choice narrow or supersede it for future invoices)?
|
||||
**2. Une facture par part, pas une facture à deux lignes.** La part fixe et la
|
||||
part différée ont des échéances distinctes — net 30 pour l'une, trois mois après
|
||||
la fin du cycle pour l'autre. Deux échéances sur une même facture ne se relancent
|
||||
pas séparément et ne se soldent pas séparément. C'est la pratique suivie depuis
|
||||
M1 ; elle est ici confirmée, pas inaugurée.
|
||||
|
||||
**3. Les écarts de change restent régis par adc-002** : le règlement solde la
|
||||
facture pour son montant restant, et la différence entre la contre-valeur euro
|
||||
inscrite et les euros réellement reçus va en 766 (gain) ou 666 (perte). Ce qui
|
||||
était un cas d'exception devient le cas normal, et c'est le but.
|
||||
|
||||
**4. Le périmètre du basculement, au 24/08/2026** : les quatre factures alors
|
||||
**non réglées** — FAC005, FAC007, FAC010, FAC011 — passent en dollars ; aucune
|
||||
n'avait été transmise au client. Les quatre déjà réglées restent en euros : leur
|
||||
montant est exactement ce qui a été encaissé, et rien de rapproché avec la
|
||||
banque ne doit bouger. Chaque facture dit ainsi sa propre vérité.
|
||||
|
||||
## Base légale & doctrine
|
||||
|
||||
To gather with the full draft: CGI art. 289 (invoice currency rules), PCG FX
|
||||
treatment for multicurrency receivables, Dolibarr multicurrency module
|
||||
implications. Nothing asserted here yet.
|
||||
**Une facture peut être libellée dans toute monnaie.** CGI art. 289, II, qui
|
||||
transpose l'article 230 de la directive 2006/112/CE : seule contrainte, le
|
||||
montant de **taxe à payer** doit être déterminé en euros. Il n'y a pas de TVA
|
||||
française ici — autoliquidation par un preneur assujetti établi hors de l'Union
|
||||
européenne, CGI art. 259, 1° et 283-2, ce que pose déjà [adc-001]. La contrainte
|
||||
ne s'applique donc pas.
|
||||
|
||||
**Les livres restent tenus en euros.** C. com. art. L.123-22 : les documents
|
||||
comptables sont établis en euros et en langue française. Facture en devise et
|
||||
comptabilité en euros ne s'opposent pas — ce sont deux objets différents.
|
||||
|
||||
**Permanence des méthodes.** [adc-011] impose qu'un changement de méthode soit
|
||||
daté, motivé et appliqué uniformément ensuite. C'est le cas : le basculement
|
||||
porte une date, un motif écrit, un périmètre explicite, et vaut pour toute
|
||||
facture émise à compter du 24/08/2026.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
None yet — the alternatives above are open; each needs its why-it-loses
|
||||
written before acceptance.
|
||||
**Rester en euros avec contre-valeur au règlement** — c'est l'état antérieur, et
|
||||
il n'est pas absurde : il fait dire à la facture ce qui est réellement entré en
|
||||
banque. Rejeté pour les factures NON réglées seulement, où il n'a rien à
|
||||
représenter encore : le montant affiché y est provisoire, et l'ajustement après
|
||||
coup a coûté trois reprises en une journée.
|
||||
|
||||
**Basculer aussi les factures déjà réglées** — rejeté : leur montant en euros
|
||||
est exactement ce qui a été encaissé et rapproché avec la banque. Le modifier
|
||||
déplacerait un montant déjà vérifié, sans rien gagner.
|
||||
|
||||
**Attendre le cycle M5 pour basculer** — rejeté par l'opérateur : les quatre
|
||||
factures non réglées partaient le jour même dans le dossier client. Les envoyer
|
||||
avec un montant euro que leur propre note qualifie d'indicatif aurait figé le
|
||||
défaut dans les mains du client.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Blocks erp#53** (T05 monthly client invoice at A2): wiring waits for this
|
||||
record to be Accepted.
|
||||
- May supersede or narrow adc-002's scope for future invoices.
|
||||
- Annexe: revenue-recognition/currency method disclosure once Accepted.
|
||||
- **Débloque erp#53** (T05, facture client mensuelle) : la méthode est stable,
|
||||
l'automatisation peut la câbler.
|
||||
- **Restreint [adc-002] sans l'abroger.** adc-002 pose qu'« une facture validée
|
||||
n'est jamais ajustée pour raison de change ». Cette règle reste vraie et
|
||||
devient d'application plus simple : une facture en dollars n'a aucune raison
|
||||
d'être ajustée, puisque son montant ne dépend d'aucun taux. Il faut cependant
|
||||
consigner que le 24/08/2026 trois factures **en euros** ont bel et bien été
|
||||
reprises — ce qu'adc-002 proscrit. L'opérateur l'a arbitré au motif
|
||||
qu'aucune n'avait été transmise au client, et la présente décision supprime
|
||||
la situation qui rendait l'arbitrage nécessaire. Voir la trace complète dans
|
||||
`fleet/harness/runs/2026-08-24-reprise-fac004-fac006/`.
|
||||
- **Écarts de change plus fréquents** : chaque règlement en produira un. C'est
|
||||
attendu et sans gravité — 766 et 666 sont faits pour ça.
|
||||
- **Annexe des comptes** : la méthode de conversion et le changement daté du
|
||||
24/08/2026 sont à mentionner au titre des règles et méthodes comptables.
|
||||
|
||||
## QA & validation
|
||||
|
||||
Planned per erp#53: dry-run parity (first agent-fired invoice == its manual
|
||||
twin), then one real month through the gate — after acceptance.
|
||||
Appliqué le 24/08/2026 par la voie gated, avec répétition sur bac à sable
|
||||
rafraîchi depuis la production, juge pré-gate, gate motivé et juge post-gate.
|
||||
Artefacts : `fleet/harness/runs/2026-08-24-facturation-usd/`.
|
||||
|
||||
Contrôles passés : la contre-valeur en euros n'a bougé sur aucune des quatre
|
||||
factures ; les PDF portent « Montants exprimés en Dollars US » et un total de
|
||||
3 000,00 ; les désignations et le type de ligne (prestation de services) sont
|
||||
intacts ; juge post-gate PASS sans dérive.
|
||||
|
||||
Un piège, consigné dans `test/setInvoiceCurrency.ts` : `PUT /invoices/{id}`
|
||||
**accepte** `multicurrency_code` et répond 200 **sans rien appliquer**. Seule
|
||||
l'interface change la devise, et ce changement abîme la ligne — la contre-valeur
|
||||
euro dérive et doit être rétablie dans le même mouvement.
|
||||
|
||||
## References
|
||||
|
||||
- erp#53 (T05 client invoice; decision D3) · erp#54 (register seeding).
|
||||
- PRD compliance.md § ADC seed table (adc-006).
|
||||
- adc-002 (current FX method) · trunk WIP `test/km_contract.json`.
|
||||
- erp#53 (T05 facture client) · erp#54 (amorçage du registre).
|
||||
- [adc-001] autoliquidation CGI 259-1° · [adc-002] écarts de change 766/666 ·
|
||||
[adc-011] permanence des méthodes.
|
||||
- Contrat cadre du 23/04/2026 et son avenant ; pro forma PR2607-0001.
|
||||
- CGI art. 289, II ; directive 2006/112/CE art. 230 ; C. com. art. L.123-22.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Pilotage — le tableau de bord
|
||||
|
||||
Répond à une question : **combien de temps la société tient sans nouveau
|
||||
client**, et ce que l'opérateur peut se verser en attendant.
|
||||
|
||||
Publié comme artefact : https://claude.ai/code/artifact/a91fc16a-4531-4a9d-9226-73b8df76af92
|
||||
|
||||
## Régénérer
|
||||
|
||||
```bash
|
||||
python3 pilotage/snapshot.py # relit l'ERP + fleet/profile/*.yaml
|
||||
```
|
||||
|
||||
Puis reporter les valeurs de `snapshot.json` dans l'objet `D` en tête du script
|
||||
de `tableau.html`, et republier. **Le tableau ne se connecte pas à l'ERP** : un
|
||||
artefact est une page autonome, sans accès réseau vers le lab.
|
||||
|
||||
## Pourquoi hors de Dolibarr
|
||||
|
||||
Le tableau de bord de Dolibarr affiche l'état des objets — factures ouvertes,
|
||||
soldes. Il ne sait pas projeter, et un module maison coûterait plus à maintenir
|
||||
qu'il ne rapporterait pour une société d'une personne. La séparation est nette :
|
||||
**Dolibarr tient les écritures, ce tableau sert à décider.**
|
||||
|
||||
## Ce que la page calcule, et ce qu'elle ne calcule pas
|
||||
|
||||
Elle ne code aucun chiffre d'affaires en dur : `snapshot.py` extrait les
|
||||
factures, les charges, les soldes bancaires et la dette de compte courant de
|
||||
l'ERP de production, et relit les règles de `fleet/profile/fiscal.yaml` et les
|
||||
échéances de `calendar.yaml`.
|
||||
|
||||
Les **projections**, elles, se calculent dans la page — à partir d'hypothèses
|
||||
que l'opérateur déplace. Les figer dans le JSON les rendrait invérifiables :
|
||||
personne ne saurait plus quel taux de cotisation ou quel taux de change a
|
||||
produit tel résultat.
|
||||
|
||||
## Les jours ouvrés
|
||||
|
||||
Calculés pour de vrai — week-ends et onze jours fériés français retirés, Pâques
|
||||
comprise via l'algorithme de comput. **242 jours pour l'exercice 2026**
|
||||
(16/01 → 31/12), **254 pour 2027**.
|
||||
|
||||
Ils servent au calcul du taux d'occupation, et surtout à celui du **taux
|
||||
journalier implicite du forfait KissMetrics** : un forfait n'a pas de TJM, il en
|
||||
acquiert un a posteriori selon le temps qu'on y a passé. C'est la seule
|
||||
comparaison honnête entre un forfait et une mission en régie.
|
||||
|
||||
## Réserves portées par la page elle-même
|
||||
|
||||
- Les taux de cotisations TNS sont un ordre de grandeur : le taux réel est
|
||||
progressif et plus bas sous un PASS.
|
||||
- L'articulation avec l'allocation France Travail est modélisée par la règle des
|
||||
70 %, mais le montant des droits reste à saisir par l'opérateur.
|
||||
- **Le piège de la deuxième année** est affiché : les cotisations 2027 seront
|
||||
provisionnelles sur un revenu 2026 nul, et la régularisation tombera en 2028.
|
||||
|
||||
Ce tableau sert à décider, pas à déclarer.
|
||||
@@ -0,0 +1,508 @@
|
||||
{
|
||||
"genere_le": "2026-08-24T20:16:45+02:00",
|
||||
"exercice": {
|
||||
"debut": "2026-01-16",
|
||||
"fin": "2026-12-31"
|
||||
},
|
||||
"produits": {
|
||||
"ca_ht_facture": 27140.02,
|
||||
"factures": [
|
||||
{
|
||||
"ref": "AVC001-CL0001001",
|
||||
"ht": -510.0,
|
||||
"ttc": -510.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": -510.0,
|
||||
"emise": 1769727600,
|
||||
"echeance": 1769727600,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAC001-CL00001",
|
||||
"ht": 510.0,
|
||||
"ttc": 510.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 510.0,
|
||||
"emise": 1769727600,
|
||||
"echeance": 1770591600,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAC001-CL0001001",
|
||||
"ht": 510.0,
|
||||
"ttc": 510.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 510.0,
|
||||
"emise": 1769727600,
|
||||
"echeance": 1770591600,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC002-CL0001002",
|
||||
"ht": 5100.0,
|
||||
"ttc": 5100.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 5100.0,
|
||||
"emise": 1771887600,
|
||||
"echeance": 1774908000,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC003-CL0001003",
|
||||
"ht": 2550.0,
|
||||
"ttc": 2550.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 2550.0,
|
||||
"emise": 1771887600,
|
||||
"echeance": 1774908000,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC004-CL0001004",
|
||||
"ht": 2147.0,
|
||||
"ttc": 2147.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 2147.0,
|
||||
"emise": 1779487200,
|
||||
"echeance": 1782770400,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC005-CL0001005",
|
||||
"ht": 2575.11,
|
||||
"ttc": 2575.11,
|
||||
"devise": "USD",
|
||||
"montant_devise": 3000.0,
|
||||
"emise": 1779487200,
|
||||
"echeance": 1787436000,
|
||||
"payee": false,
|
||||
"statut": "1",
|
||||
"reste_du": 2575.11,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC006-CL0001006",
|
||||
"ht": 2195.97,
|
||||
"ttc": 2195.97,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 2195.97,
|
||||
"emise": 1782165600,
|
||||
"echeance": 1785448800,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC007-CL0001007",
|
||||
"ht": 2575.11,
|
||||
"ttc": 2575.11,
|
||||
"devise": "USD",
|
||||
"montant_devise": 3000.0,
|
||||
"emise": 1782165600,
|
||||
"echeance": 1790114400,
|
||||
"payee": false,
|
||||
"statut": "1",
|
||||
"reste_du": 2575.11,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC008-CL0001008",
|
||||
"ht": 2185.0,
|
||||
"ttc": 2185.0,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 2185.0,
|
||||
"emise": 1784757600,
|
||||
"echeance": 1787349600,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC009-CL0001009",
|
||||
"ht": 2164.75,
|
||||
"ttc": 2164.75,
|
||||
"devise": "EUR",
|
||||
"montant_devise": 2164.75,
|
||||
"emise": 1787436000,
|
||||
"echeance": 1790028000,
|
||||
"payee": true,
|
||||
"statut": "2",
|
||||
"reste_du": 0.0,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC010-CL0001010",
|
||||
"ht": 2568.54,
|
||||
"ttc": 2568.54,
|
||||
"devise": "USD",
|
||||
"montant_devise": 3000.0,
|
||||
"emise": 1787436000,
|
||||
"echeance": 1795388400,
|
||||
"payee": false,
|
||||
"statut": "1",
|
||||
"reste_du": 2568.54,
|
||||
"annulee": false
|
||||
},
|
||||
{
|
||||
"ref": "FAC011-CL0001011",
|
||||
"ht": 2568.54,
|
||||
"ttc": 2568.54,
|
||||
"devise": "USD",
|
||||
"montant_devise": 3000.0,
|
||||
"emise": 1787522400,
|
||||
"echeance": 1792706400,
|
||||
"payee": false,
|
||||
"statut": "1",
|
||||
"reste_du": 2568.54,
|
||||
"annulee": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"charges": {
|
||||
"fournisseurs_ht": 2331.13,
|
||||
"factures": [
|
||||
{
|
||||
"ref": "FAF2025001",
|
||||
"ht": 6.08,
|
||||
"date": 1761256800,
|
||||
"tiers": "OVH",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026003",
|
||||
"ht": 1.66,
|
||||
"date": 1767481200,
|
||||
"tiers": "YOLAW",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026005",
|
||||
"ht": 169.0,
|
||||
"date": 1767654000,
|
||||
"tiers": "Qonto",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026002",
|
||||
"ht": 47.69,
|
||||
"date": 1767913200,
|
||||
"tiers": "Greffe du tribunal de commerce Évry",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026004",
|
||||
"ht": 124.0,
|
||||
"date": 1767913200,
|
||||
"tiers": "Medialex",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026006",
|
||||
"ht": 8.43,
|
||||
"date": 1768172400,
|
||||
"tiers": "La Poste",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026007",
|
||||
"ht": 2.67,
|
||||
"date": 1768604400,
|
||||
"tiers": "Infogreffe",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026001",
|
||||
"ht": 50.0,
|
||||
"date": 1769382000,
|
||||
"tiers": "Wise Europe SA",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026008",
|
||||
"ht": 510.0,
|
||||
"date": 1772233200,
|
||||
"tiers": "Darnis Operations",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026009",
|
||||
"ht": 255.0,
|
||||
"date": 1772233200,
|
||||
"tiers": "Darnis Operations",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026012",
|
||||
"ht": 143.9,
|
||||
"date": 1775080800,
|
||||
"tiers": "Mistral AI SAS",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026011",
|
||||
"ht": 180.0,
|
||||
"date": 1775944800,
|
||||
"tiers": "Anthropic Ireland Limited",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026010",
|
||||
"ht": 219.5,
|
||||
"date": 1782684000,
|
||||
"tiers": "Darnis Operations",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026013",
|
||||
"ht": 214.7,
|
||||
"date": 1782770400,
|
||||
"tiers": "Darnis Operations",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026015",
|
||||
"ht": 90.0,
|
||||
"date": 1784325600,
|
||||
"tiers": "Anthropic PBC",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026016",
|
||||
"ht": 90.0,
|
||||
"date": 1787090400,
|
||||
"tiers": "Anthropic PBC",
|
||||
"payee": true
|
||||
},
|
||||
{
|
||||
"ref": "FAF2026014",
|
||||
"ht": 218.5,
|
||||
"date": 1788127200,
|
||||
"tiers": "Darnis Operations",
|
||||
"payee": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"tresorerie": {
|
||||
"total": 13877.15,
|
||||
"comptes": [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "QONTO",
|
||||
"solde": 2013.3,
|
||||
"mouvements": 15
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"label": "WISE EURO",
|
||||
"solde": 11863.85,
|
||||
"mouvements": 19
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"label": "G.RADUREAU Compte Courant Asso",
|
||||
"solde": -1912.98,
|
||||
"mouvements": 16
|
||||
}
|
||||
],
|
||||
"dette_compte_courant": 1912.98
|
||||
},
|
||||
"regles_fiscales": [
|
||||
{
|
||||
"id": "vat-regime-reel-simplifie",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": "2026-12-31",
|
||||
"decision": "adc-004"
|
||||
},
|
||||
{
|
||||
"id": "vat-regime-ca3-quarterly",
|
||||
"label": null,
|
||||
"effective_from": "2027-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-004"
|
||||
},
|
||||
{
|
||||
"id": "client-export-autoliquidation",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-001"
|
||||
},
|
||||
{
|
||||
"id": "supplier-fr-vat-deductible",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-004"
|
||||
},
|
||||
{
|
||||
"id": "supplier-intra-eu-reverse-charge",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-004"
|
||||
},
|
||||
{
|
||||
"id": "fx-deltas-766-666",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-002"
|
||||
},
|
||||
{
|
||||
"id": "saas-subscriptions-expensed",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-003"
|
||||
},
|
||||
{
|
||||
"id": "personal-card-cca-lane",
|
||||
"label": null,
|
||||
"effective_from": "2026-01-01",
|
||||
"effective_until": null,
|
||||
"decision": "adc-005"
|
||||
}
|
||||
],
|
||||
"calendrier": [
|
||||
{
|
||||
"id": "tva-acompte-2026-07",
|
||||
"label": null,
|
||||
"due": "2026-07",
|
||||
"status": "estimated",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "tva-acompte-2026-12",
|
||||
"label": null,
|
||||
"due": "2026-12-15",
|
||||
"status": "confirmed",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "tva-ca12-fy2026",
|
||||
"label": null,
|
||||
"due": "2027-05-04",
|
||||
"status": "confirmed",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "regime-simplifie-abolition",
|
||||
"label": null,
|
||||
"due": "2027-01-01",
|
||||
"status": "confirmed",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "ca3-2027-q1",
|
||||
"label": null,
|
||||
"due": "2027-04-20",
|
||||
"status": "confirmed",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "ca3-quarterly",
|
||||
"label": null,
|
||||
"due": null,
|
||||
"status": "estimated",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "cfe-annual",
|
||||
"label": null,
|
||||
"due": null,
|
||||
"status": "estimated",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "is-installments",
|
||||
"label": null,
|
||||
"due": "2027-06-30",
|
||||
"status": "conditional",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "ag-comptes-annuels-fy2026",
|
||||
"label": null,
|
||||
"due": "2027-06-30",
|
||||
"status": "confirmed",
|
||||
"authority": "Greffe / AG"
|
||||
},
|
||||
{
|
||||
"id": "einvoicing-reception",
|
||||
"label": null,
|
||||
"due": "2026-09-01",
|
||||
"status": "confirmed",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "einvoicing-emission-ereporting",
|
||||
"label": null,
|
||||
"due": "2027-09-01",
|
||||
"status": "confirmed",
|
||||
"authority": "DGFiP"
|
||||
},
|
||||
{
|
||||
"id": "urssaf-2026-t2",
|
||||
"label": null,
|
||||
"due": "2026-08-05",
|
||||
"status": "confirmed",
|
||||
"authority": "URSSAF Île-de-France"
|
||||
},
|
||||
{
|
||||
"id": "urssaf-2026-t3",
|
||||
"label": null,
|
||||
"due": "2026-11-05",
|
||||
"status": "confirmed",
|
||||
"authority": "URSSAF Île-de-France"
|
||||
},
|
||||
{
|
||||
"id": "remuneration-gerant-decision-2027",
|
||||
"label": null,
|
||||
"due": "2027-01-31",
|
||||
"status": "confirmed",
|
||||
"authority": "associé unique"
|
||||
},
|
||||
{
|
||||
"id": "km-fac005-due",
|
||||
"label": null,
|
||||
"due": "2026-08-23",
|
||||
"status": "confirmed",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"id": "km-fac007-due",
|
||||
"label": null,
|
||||
"due": "2026-09-23",
|
||||
"status": "confirmed",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"id": "km-contract-renewal-4pct-window",
|
||||
"label": null,
|
||||
"due": null,
|
||||
"status": "pending-definition",
|
||||
"authority": null
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rassemble en un seul JSON ce qu'il faut pour piloter la société.
|
||||
|
||||
POURQUOI CE SCRIPT EXISTE. Le tableau de bord ne doit RIEN coder en dur. Trois
|
||||
sources existent déjà et font autorité, chacune sur son domaine :
|
||||
|
||||
- l'ERP de production les écritures : factures, règlements, banque
|
||||
- fleet/profile/fiscal.yaml les règles fiscales, avec leurs dates d'effet
|
||||
- fleet/profile/calendar.yaml les échéances de conformité
|
||||
|
||||
Un tableau qui recopierait ces chiffres deviendrait faux le lendemain. Celui-ci
|
||||
les relit et produit un instantané daté, que la page consomme.
|
||||
|
||||
CE QU'IL NE FAIT PAS. Aucune projection, aucune hypothèse, aucun taux
|
||||
d'imposition. Les scénarios se calculent DANS la page, où l'opérateur peut en
|
||||
changer les paramètres — les figer ici les rendrait invérifiables.
|
||||
|
||||
Usage :
|
||||
python3 pilotage/snapshot.py [--out pilotage/snapshot.json]
|
||||
"""
|
||||
import argparse, json, os, pathlib, re, subprocess, sys, time
|
||||
from datetime import date, datetime
|
||||
|
||||
RACINE = pathlib.Path(__file__).resolve().parent.parent
|
||||
DOL = RACINE / ".claude/skills/dolibarr/scripts/dol-curl.sh"
|
||||
|
||||
|
||||
def erp(chemin: str):
|
||||
return json.loads(subprocess.check_output([str(DOL), chemin], text=True))
|
||||
|
||||
|
||||
def yaml_entrees(fichier: pathlib.Path, champs: tuple[str, ...]) -> list[dict]:
|
||||
"""Lit les listes `- id:` du sous-ensemble YAML utilisé par le profil.
|
||||
|
||||
On ne dépend pas de PyYAML : le profil est écrit dans un sous-ensemble
|
||||
strict (cf. son README), et le validateur du dépôt le lit déjà ainsi.
|
||||
"""
|
||||
t = fichier.read_text(encoding="utf-8")
|
||||
out = []
|
||||
for m in re.finditer(r'^\s{2}- id: "?([\w-]+)"?', t, re.M):
|
||||
seg = t[m.start(): m.start() + 900]
|
||||
e = {"id": m.group(1)}
|
||||
for c in champs:
|
||||
v = re.search(rf'{c}: "?([^"\n]+?)"?\s*(?:#.*)?$', seg, re.M)
|
||||
e[c] = None if not v or v.group(1) == "null" else v.group(1).strip()
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
a = argparse.ArgumentParser()
|
||||
a.add_argument("--out", default=str(RACINE / "pilotage/snapshot.json"))
|
||||
o = a.parse_args()
|
||||
|
||||
if not DOL.exists():
|
||||
print(f"introuvable : {DOL}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# ---- produits -----------------------------------------------------------
|
||||
factures, ca_ht = [], 0.0
|
||||
for f in sorted(erp("/invoices?limit=200"), key=lambda x: x["ref"]):
|
||||
# La paire annulée FAC001-CL00001 / AVC001 se neutralise : la compter
|
||||
# gonflerait le chiffre d'affaires de 510 EUR dans les deux sens.
|
||||
annulee = f["ref"].startswith("AVC") or f["ref"] == "FAC001-CL00001"
|
||||
ht = float(f["total_ht"])
|
||||
if not annulee:
|
||||
ca_ht += ht
|
||||
factures.append({
|
||||
"ref": f["ref"], "ht": ht, "ttc": float(f["total_ttc"]),
|
||||
"devise": f["multicurrency_code"],
|
||||
"montant_devise": float(f["multicurrency_total_ht"] or 0),
|
||||
"emise": int(f["date"]), "echeance": int(f["date_lim_reglement"] or 0) or None,
|
||||
"payee": f["paye"] == "1", "statut": f["status"],
|
||||
"reste_du": float(f.get("remaintopay") or 0),
|
||||
"annulee": annulee,
|
||||
})
|
||||
|
||||
# ---- charges ------------------------------------------------------------
|
||||
tiers = {t["id"]: t["name"] for t in erp("/thirdparties?limit=300")}
|
||||
fournisseurs, charges_ht = [], 0.0
|
||||
for f in sorted(erp("/supplierinvoices?limit=200"), key=lambda x: int(x["date"] or 0)):
|
||||
ht = float(f["total_ht"]); charges_ht += ht
|
||||
fournisseurs.append({"ref": f["ref"], "ht": ht, "date": int(f["date"] or 0),
|
||||
"tiers": tiers.get(f.get("socid"), "?"), "payee": f["paye"] == "1"})
|
||||
|
||||
# ---- banque et compte courant -------------------------------------------
|
||||
comptes = []
|
||||
for c in erp("/bankaccounts?limit=20"):
|
||||
lignes = erp(f"/bankaccounts/{c['id']}/lines?limit=300")
|
||||
solde = sum(float(x.get("amount") or 0) for x in lignes)
|
||||
comptes.append({"id": int(c["id"]), "label": c.get("label"),
|
||||
"solde": round(solde, 2), "mouvements": len(lignes)})
|
||||
|
||||
# Le compte courant d'associé porte un solde NÉGATIF quand la société doit :
|
||||
# on le rend explicite plutôt que de laisser la page deviner le signe.
|
||||
cca = next((c for c in comptes if "COURANT" in (c["label"] or "").upper()), None)
|
||||
dette_associe = round(-cca["solde"], 2) if cca and cca["solde"] < 0 else 0.0
|
||||
tresorerie = round(sum(c["solde"] for c in comptes if c is not cca), 2)
|
||||
|
||||
snap = {
|
||||
"genere_le": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"exercice": {"debut": "2026-01-16", "fin": "2026-12-31"},
|
||||
"produits": {"ca_ht_facture": round(ca_ht, 2), "factures": factures},
|
||||
"charges": {"fournisseurs_ht": round(charges_ht, 2), "factures": fournisseurs},
|
||||
"tresorerie": {"total": tresorerie, "comptes": comptes,
|
||||
"dette_compte_courant": dette_associe},
|
||||
"regles_fiscales": yaml_entrees(RACINE / "fleet/profile/fiscal.yaml",
|
||||
("label", "effective_from", "effective_until", "decision")),
|
||||
"calendrier": yaml_entrees(RACINE / "fleet/profile/calendar.yaml",
|
||||
("label", "due", "status", "authority")),
|
||||
}
|
||||
|
||||
pathlib.Path(o.out).write_text(json.dumps(snap, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
print(f"ok — {o.out}")
|
||||
print(f" CA facturé {snap['produits']['ca_ht_facture']:.2f} EUR · "
|
||||
f"charges {snap['charges']['fournisseurs_ht']:.2f} EUR · "
|
||||
f"trésorerie {tresorerie:.2f} EUR · dette CCA {dette_associe:.2f} EUR")
|
||||
print(f" {len(snap['regles_fiscales'])} règles fiscales · {len(snap['calendrier'])} échéances")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,634 @@
|
||||
<title>Tenir sans nouveau client</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,300;0,500;0,600;1,300&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--paper:#FAFAF8; --raise:#FFFFFF; --ink:#161A20; --ink-2:#4A5058; --ink-3:#7C8189;
|
||||
--rule:#DBDAD4; --rule-soft:#EBEAE5;
|
||||
--accent:#2E5A5F; --accent-soft:#E3EDED; --accent-ink:#1E3D41;
|
||||
--ok:#3F6B4A; --ok-soft:#E6EEE7;
|
||||
--warn:#A66A1F; --warn-soft:#F7EEDF;
|
||||
--crit:#9B3535; --crit-soft:#F7E7E7;
|
||||
--shadow:0 1px 2px rgba(22,26,32,.05),0 8px 24px -16px rgba(22,26,32,.28);
|
||||
--sans:"IBM Plex Sans",system-ui,-apple-system,sans-serif;
|
||||
--serif:"Spectral",Georgia,serif;
|
||||
--mono:"IBM Plex Mono",ui-monospace,Menlo,monospace;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#14171C; --raise:#1B1F26; --ink:#E9EAE6; --ink-2:#A7ACB4; --ink-3:#767C85;
|
||||
--rule:#2C3138; --rule-soft:#22262C;
|
||||
--accent:#7FB3B6; --accent-soft:#1E2C2E; --accent-ink:#A9CFD1;
|
||||
--ok:#7FB08B; --ok-soft:#1C2620;
|
||||
--warn:#D8A45E; --warn-soft:#2A2318;
|
||||
--crit:#D98585; --crit-soft:#2B1D1D;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px -16px rgba(0,0,0,.7);
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#14171C; --raise:#1B1F26; --ink:#E9EAE6; --ink-2:#A7ACB4; --ink-3:#767C85;
|
||||
--rule:#2C3138; --rule-soft:#22262C;
|
||||
--accent:#7FB3B6; --accent-soft:#1E2C2E; --accent-ink:#A9CFD1;
|
||||
--ok:#7FB08B; --ok-soft:#1C2620;
|
||||
--warn:#D8A45E; --warn-soft:#2A2318;
|
||||
--crit:#D98585; --crit-soft:#2B1D1D;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px -16px rgba(0,0,0,.7);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
|
||||
font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased;
|
||||
font-variant-numeric:tabular-nums}
|
||||
.wrap{max-width:1080px;margin:0 auto;padding:40px 24px 96px}
|
||||
h1,h2,h3{font-family:var(--serif);font-weight:500;text-wrap:balance;margin:0}
|
||||
.eyebrow{font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);
|
||||
font-weight:600}
|
||||
.num{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ── masthead ─────────────────────────────────────────────── */
|
||||
header{border-bottom:2px solid var(--ink);padding-bottom:18px;margin-bottom:8px}
|
||||
header h1{font-size:34px;line-height:1.12;letter-spacing:-.01em}
|
||||
header .sub{color:var(--ink-2);margin-top:8px;max-width:60ch;font-size:14.5px}
|
||||
.stamp{display:flex;justify-content:space-between;align-items:baseline;gap:20px;flex-wrap:wrap}
|
||||
.asof{font-size:12px;color:var(--ink-3);font-family:var(--mono)}
|
||||
|
||||
/* ── verdict ──────────────────────────────────────────────── */
|
||||
.verdict{margin:28px 0 34px;border:1px solid var(--rule);border-left:4px solid var(--accent);
|
||||
background:var(--raise);border-radius:3px;padding:22px 24px;box-shadow:var(--shadow)}
|
||||
.verdict .big{font-family:var(--serif);font-size:27px;line-height:1.25;margin:8px 0 10px}
|
||||
.verdict .big strong{color:var(--accent-ink);font-weight:600}
|
||||
.verdict p{margin:0;color:var(--ink-2);max-width:66ch;font-size:14.5px}
|
||||
|
||||
/* ── situation band ───────────────────────────────────────── */
|
||||
.band{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));
|
||||
gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:3px;
|
||||
overflow:hidden;margin-bottom:36px}
|
||||
.cell{background:var(--raise);padding:14px 16px 16px}
|
||||
.cell .k{font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3);
|
||||
font-weight:600}
|
||||
.cell .v{font-family:var(--mono);font-size:21px;font-weight:500;margin-top:7px;letter-spacing:-.02em}
|
||||
.cell .n{font-size:12px;color:var(--ink-3);margin-top:3px;line-height:1.4}
|
||||
.cell.mark .v{color:var(--accent-ink)}
|
||||
|
||||
/* ── sections ─────────────────────────────────────────────── */
|
||||
section{margin-bottom:44px}
|
||||
.shead{display:flex;align-items:baseline;gap:14px;border-bottom:1px solid var(--rule);
|
||||
padding-bottom:9px;margin-bottom:20px}
|
||||
.shead h2{font-size:20px}
|
||||
.shead .hint{font-size:13px;color:var(--ink-3);margin-left:auto;text-align:right}
|
||||
|
||||
/* ── controls ─────────────────────────────────────────────── */
|
||||
.controls{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:20px;
|
||||
background:var(--raise);border:1px solid var(--rule);border-radius:3px;
|
||||
padding:20px 22px;margin-bottom:22px}
|
||||
.ctl label{display:block;font-size:11px;letter-spacing:.1em;text-transform:uppercase;
|
||||
color:var(--ink-3);font-weight:600;margin-bottom:8px}
|
||||
.ctl .row{display:flex;align-items:center;gap:12px}
|
||||
.ctl output{font-family:var(--mono);font-size:16px;font-weight:500;min-width:82px;
|
||||
text-align:right;color:var(--accent-ink)}
|
||||
input[type=range]{flex:1;accent-color:var(--accent);height:22px}
|
||||
input[type=range]:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||
.ctl .note{font-size:12px;color:var(--ink-3);margin-top:7px;line-height:1.45}
|
||||
.seg{display:flex;border:1px solid var(--rule);border-radius:3px;overflow:hidden}
|
||||
.seg button{flex:1;border:0;background:var(--raise);color:var(--ink-2);font:inherit;
|
||||
font-size:13px;padding:7px 4px;cursor:pointer;border-right:1px solid var(--rule)}
|
||||
.seg button:last-child{border-right:0}
|
||||
.seg button[aria-pressed=true]{background:var(--accent);color:#fff;font-weight:500}
|
||||
:root[data-theme="dark"] .seg button[aria-pressed=true],
|
||||
@media (prefers-color-scheme:dark){}
|
||||
.seg button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
||||
|
||||
/* ── result grid ──────────────────────────────────────────── */
|
||||
.res{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1px;
|
||||
background:var(--rule);border:1px solid var(--rule);border-radius:3px;overflow:hidden}
|
||||
.res div{background:var(--raise);padding:13px 15px 15px}
|
||||
.res .k{font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3);
|
||||
font-weight:600}
|
||||
.res .v{font-family:var(--mono);font-size:19px;font-weight:500;margin-top:6px}
|
||||
.res .strong .v{color:var(--accent-ink);font-size:22px}
|
||||
|
||||
/* ── runway bar ───────────────────────────────────────────── */
|
||||
.runway{margin-top:20px;background:var(--raise);border:1px solid var(--rule);border-radius:3px;
|
||||
padding:20px 22px}
|
||||
.track{position:relative;height:38px;background:var(--rule-soft);border-radius:2px;
|
||||
overflow:hidden;margin:14px 0 8px}
|
||||
.fill{position:absolute;inset:0 auto 0 0;background:var(--accent);opacity:.9;
|
||||
transition:width .28s cubic-bezier(.4,0,.2,1)}
|
||||
.marks{position:relative;height:20px}
|
||||
.mark{position:absolute;top:0;font-size:11px;color:var(--ink-3);font-family:var(--mono);
|
||||
transform:translateX(-50%);white-space:nowrap}
|
||||
.mark::before{content:"";position:absolute;left:50%;top:-46px;width:1px;height:44px;
|
||||
background:var(--ink);opacity:.34}
|
||||
@media (prefers-reduced-motion:reduce){.fill{transition:none}}
|
||||
|
||||
/* ── timeline ─────────────────────────────────────────────── */
|
||||
.spine{position:relative;padding-left:26px}
|
||||
.spine::before{content:"";position:absolute;left:5px;top:6px;bottom:6px;width:2px;
|
||||
background:var(--rule)}
|
||||
.ev{position:relative;padding:11px 0 11px 4px;border-bottom:1px solid var(--rule-soft);
|
||||
display:grid;grid-template-columns:96px 1fr auto;gap:16px;align-items:baseline}
|
||||
.ev:last-child{border-bottom:0}
|
||||
.ev::before{content:"";position:absolute;left:-25px;top:19px;width:10px;height:10px;
|
||||
border-radius:50%;background:var(--paper);border:2px solid var(--ink-3)}
|
||||
.ev.is-crit::before{border-color:var(--crit);background:var(--crit)}
|
||||
.ev.is-warn::before{border-color:var(--warn);background:var(--warn)}
|
||||
.ev.is-ok::before{border-color:var(--ok)}
|
||||
.ev .d{font-family:var(--mono);font-size:12.5px;color:var(--ink-2)}
|
||||
.ev .t{font-size:14px}
|
||||
.ev .t small{display:block;color:var(--ink-3);font-size:12.5px;margin-top:2px;line-height:1.45}
|
||||
.pill{font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;font-weight:600;
|
||||
padding:3px 8px;border-radius:2px;white-space:nowrap}
|
||||
.p-crit{background:var(--crit-soft);color:var(--crit)}
|
||||
.p-warn{background:var(--warn-soft);color:var(--warn)}
|
||||
.p-ok{background:var(--ok-soft);color:var(--ok)}
|
||||
.p-mute{background:var(--rule-soft);color:var(--ink-3)}
|
||||
|
||||
/* ── table ────────────────────────────────────────────────── */
|
||||
.tw{overflow-x:auto;border:1px solid var(--rule);border-radius:3px;background:var(--raise)}
|
||||
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
||||
th{text-align:left;font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
|
||||
color:var(--ink-3);font-weight:600;padding:10px 14px;border-bottom:1px solid var(--rule);
|
||||
background:var(--rule-soft)}
|
||||
td{padding:9px 14px;border-bottom:1px solid var(--rule-soft)}
|
||||
tr:last-child td{border-bottom:0}
|
||||
td.n{text-align:right;font-family:var(--mono)}
|
||||
tr.sum td{font-weight:600;background:var(--rule-soft)}
|
||||
|
||||
/* ── notes ────────────────────────────────────────────────── */
|
||||
.caveat{margin-top:16px;font-size:13px;color:var(--ink-2);border-left:2px solid var(--rule);
|
||||
padding-left:14px;max-width:70ch}
|
||||
.caveat strong{color:var(--ink)}
|
||||
footer{margin-top:56px;padding-top:20px;border-top:1px solid var(--rule);
|
||||
font-size:12.5px;color:var(--ink-3);max-width:74ch}
|
||||
footer code{font-family:var(--mono);font-size:12px;color:var(--ink-2)}
|
||||
@media (max-width:640px){
|
||||
.ev{grid-template-columns:80px 1fr;row-gap:4px}
|
||||
.ev .pill{grid-column:2}
|
||||
header h1{font-size:27px}
|
||||
.verdict .big{font-size:22px}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div class="stamp">
|
||||
<div>
|
||||
<div class="eyebrow">Arcodange · exercice 2026</div>
|
||||
<h1>Tenir sans nouveau client</h1>
|
||||
</div>
|
||||
<div class="asof" id="asof"></div>
|
||||
</div>
|
||||
<p class="sub">Ce que la société encaisse, ce qu'elle doit, et combien de mois
|
||||
elle tient si aucun prospect ne se présente après KissMetrics.</p>
|
||||
</header>
|
||||
|
||||
<div class="verdict">
|
||||
<div class="eyebrow">Réponse</div>
|
||||
<p class="big">Sans nouveau client, tu tiens jusqu'en <strong id="v-mois">—</strong>
|
||||
en te versant <strong id="v-remu">—</strong> par mois.</p>
|
||||
<p id="v-detail">—</p>
|
||||
</div>
|
||||
|
||||
<div class="band" id="band"></div>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>Hypothèses</h2>
|
||||
<div class="hint">Tout ce qui suit se recalcule</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="ctl">
|
||||
<label for="remu">Rémunération nette mensuelle</label>
|
||||
<div class="row"><input type="range" id="remu" min="0" max="2500" step="50" value="700">
|
||||
<output id="o-remu"></output></div>
|
||||
<div class="note">Ce que tu touches avant impôt. Les cotisations TNS
|
||||
s'ajoutent au coût pour la société.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label for="are">Allocation France Travail mensuelle</label>
|
||||
<div class="row"><input type="range" id="are" min="0" max="2500" step="50" value="0">
|
||||
<output id="o-are"></output></div>
|
||||
<div class="note">À 0 si tu n'es plus indemnisé. Une rémunération réduit
|
||||
l'allocation de 70 % de son montant.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label for="perso">Charges personnelles mensuelles</label>
|
||||
<div class="row"><input type="range" id="perso" min="500" max="3000" step="50" value="1500">
|
||||
<output id="o-perso"></output></div>
|
||||
<div class="note">Le seuil que tes revenus doivent couvrir.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label for="cot">Taux de cotisations TNS</label>
|
||||
<div class="row"><input type="range" id="cot" min="25" max="50" step="1" value="43">
|
||||
<output id="o-cot"></output></div>
|
||||
<div class="note">43 % est l'ordre de grandeur courant. Le taux réel est
|
||||
progressif et plus bas sous un PASS.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label>Cycles M5 et M6 facturés</label>
|
||||
<div class="seg" role="group">
|
||||
<button type="button" data-cyc="2" aria-pressed="true">Les deux</button>
|
||||
<button type="button" data-cyc="1" aria-pressed="false">M5 seul</button>
|
||||
<button type="button" data-cyc="0" aria-pressed="false">Aucun</button>
|
||||
</div>
|
||||
<div class="note">Le contrat couvre six cycles jusqu'au 23/10/2026.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label for="fx">Taux EUR/USD retenu</label>
|
||||
<div class="row"><input type="range" id="fx" min="105" max="130" step="1" value="117">
|
||||
<output id="o-fx"></output></div>
|
||||
<div class="note">Appliqué à ce qui reste à facturer et à encaisser en
|
||||
dollars.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>Combien de temps la trésorerie tient</h2>
|
||||
<div class="hint">À partir de janvier 2027, contrat KissMetrics terminé</div>
|
||||
</div>
|
||||
<div class="runway">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;color:var(--ink-2)">
|
||||
<span>Trésorerie disponible au 1<sup>er</sup> janvier 2027 :
|
||||
<span class="num" id="r-depart" style="color:var(--ink);font-weight:500"></span></span>
|
||||
<span>Sortie mensuelle : <span class="num" id="r-burn" style="color:var(--ink);font-weight:500"></span></span>
|
||||
</div>
|
||||
<div class="track"><div class="fill" id="r-fill" style="width:0%"></div></div>
|
||||
<div class="marks" id="r-marks"></div>
|
||||
<p class="caveat" id="r-note"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>Capacité et prix</h2>
|
||||
<div class="hint">Exercice 2027 · 254 jours ouvrés</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="ctl">
|
||||
<label for="tjm">Taux journalier</label>
|
||||
<div class="row"><input type="range" id="tjm" min="200" max="900" step="25" value="450">
|
||||
<output id="o-tjm"></output></div>
|
||||
<div class="note">Ce que tu factures pour une journée de travail.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label for="jours">Jours vendus par mois</label>
|
||||
<div class="row"><input type="range" id="jours" min="0" max="21" step="1" value="10">
|
||||
<output id="o-jours"></output></div>
|
||||
<div class="note">Sur ~21 jours ouvrés mensuels. Le reste va à la
|
||||
prospection, l'administratif et les creux.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<label for="kmj">Jours passés sur KissMetrics, par mois</label>
|
||||
<div class="row"><input type="range" id="kmj" min="4" max="21" step="1" value="12">
|
||||
<output id="o-kmj"></output></div>
|
||||
<div class="note">Sert à calculer ce que le forfait KM vaut réellement à
|
||||
la journée.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="res" id="capa"></div>
|
||||
|
||||
<div class="runway" style="margin-top:20px">
|
||||
<div class="eyebrow">Ce que vaut le forfait KissMetrics</div>
|
||||
<p id="km-verdict" style="margin:10px 0 0;font-size:14.5px;color:var(--ink-2);max-width:70ch"></p>
|
||||
<div class="tw" style="margin-top:14px"><table id="km-tab"></table></div>
|
||||
</div>
|
||||
|
||||
<p class="caveat">Un forfait n'a pas de taux journalier — il en acquiert un
|
||||
<strong>a posteriori</strong>, selon le temps qu'on y a passé. C'est la seule
|
||||
façon de savoir si un contrat au forfait était bien tarifé, et la seule
|
||||
comparaison honnête avec une mission en régie.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>Ta rémunération, ce qu'il t'en reste</h2>
|
||||
<div class="hint">Gérant majoritaire — TNS, article 62 du CGI</div>
|
||||
</div>
|
||||
<div class="res" id="paie"></div>
|
||||
<p class="caveat"><strong>Le piège de la deuxième année.</strong> Tes cotisations 2027
|
||||
seront calculées provisoirement sur ton revenu 2026, qui est nul — tu ne paieras
|
||||
donc presque rien en 2027, puis la régularisation tombera en 2028 sur le revenu
|
||||
réel. Le montant affiché ci-dessus en « cotisations » est à provisionner, pas à
|
||||
dépenser.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>L'exercice 2026</h2>
|
||||
<div class="hint">Périmètre KissMetrics uniquement</div>
|
||||
</div>
|
||||
<div class="tw"><table id="pnl"></table></div>
|
||||
<p class="caveat">Le résultat paraît élevé parce qu'<strong>aucune rémunération
|
||||
n'a été versée en 2026</strong> — c'est la décision n° 2 de l'associé unique. Ce
|
||||
bénéfice reste dans la société et supporte l'impôt ; il n'est pas disponible.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>Ce qui arrive, et quand</h2>
|
||||
<div class="hint">Encaissements, échéances fiscales, bascules de régime</div>
|
||||
</div>
|
||||
<div class="spine" id="spine"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="shead">
|
||||
<h2>Les seuils qui décident</h2>
|
||||
</div>
|
||||
<div class="tw"><table id="seuils"></table></div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>Chiffres extraits de l'ERP de production par <code>pilotage/snapshot.py</code>,
|
||||
qui relit aussi <code>fleet/profile/fiscal.yaml</code> et
|
||||
<code>calendar.yaml</code>. Aucune valeur n'est saisie à la main dans cette page :
|
||||
les projections se calculent à partir des hypothèses ci-dessus, que tu peux
|
||||
changer.</p>
|
||||
<p style="margin-top:10px">Ce tableau sert à décider, pas à déclarer. Les taux de
|
||||
cotisations et le barème de l'impôt sont des ordres de grandeur ; l'articulation
|
||||
avec France Travail et le montant à provisionner pour 2028 se font trancher par
|
||||
un expert-comptable.</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const D = {
|
||||
genere: "2026-08-24T20:16:45+02:00",
|
||||
tresorerie: 13877.15, dette_cca: 1912.98,
|
||||
ca_facture: 27140.02, encaisse: 16852.72, reste_du_eur: 10287.30,
|
||||
charges_enregistrees: 2331.13,
|
||||
ouvertes_usd: 12000, // FAC005, FAC007, FAC010, FAC011
|
||||
cycles_restants_usd: 5500, // par cycle : 2 500 fixe + 3 000 différé
|
||||
urssaf_restant: 1333, indemnite_restante: 1100, claude_restant: 360,
|
||||
divers_restant: 500,
|
||||
darnis_taux: 0.10, darnis_taux_2027: 0.05,
|
||||
ouvres_2026: 242, ouvres_2027: 254,
|
||||
ouvres_mois_2027: {1:20,2:20,3:23,4:20,5:20,6:22,7:22,8:22,9:22,10:21,11:21,12:22},
|
||||
km_total_usd: 33000, km_mois: 6,
|
||||
charges_fixes_2027: 4200, // outils, banque, assurance, comptable — ordre de grandeur
|
||||
is_taux_reduit: 0.15, is_plafond: 42500,
|
||||
ir_seuil: 11497, ir_t1: 0.11, ir_seuil2: 29315, ir_t2: 0.30, ir_abattement: 0.10,
|
||||
};
|
||||
|
||||
const eur = n => new Intl.NumberFormat("fr-FR",{style:"currency",currency:"EUR",
|
||||
maximumFractionDigits:0}).format(n);
|
||||
const eur2 = n => new Intl.NumberFormat("fr-FR",{style:"currency",currency:"EUR",
|
||||
minimumFractionDigits:2,maximumFractionDigits:2}).format(n);
|
||||
const $ = s => document.querySelector(s);
|
||||
|
||||
let cycles = 2;
|
||||
|
||||
function ir(annuel){
|
||||
const b = annuel * (1 - D.ir_abattement);
|
||||
let t = 0;
|
||||
if (b > D.ir_seuil) t += (Math.min(b, D.ir_seuil2) - D.ir_seuil) * D.ir_t1;
|
||||
if (b > D.ir_seuil2) t += (b - D.ir_seuil2) * D.ir_t2;
|
||||
return t;
|
||||
}
|
||||
|
||||
function calc(){
|
||||
const remuM = +$("#remu").value, areM = +$("#are").value,
|
||||
persoM = +$("#perso").value, cot = +$("#cot").value/100,
|
||||
fx = +$("#fx").value/100;
|
||||
|
||||
// ── exercice 2026 ────────────────────────────────────────────────────────
|
||||
const reste_a_facturer = cycles * D.cycles_restants_usd / fx;
|
||||
const ca = D.ca_facture + reste_a_facturer;
|
||||
|
||||
// Encaissements 2026 restants : les 4 factures ouvertes + les parts des
|
||||
// cycles M5/M6 dont l'échéance tombe avant le 31/12.
|
||||
const enc_cycles_usd = cycles === 2 ? 8000 : cycles === 1 ? 5500 : 0;
|
||||
const enc_restants = (D.ouvertes_usd + enc_cycles_usd) / fx;
|
||||
|
||||
const darnis = (enc_restants) * D.darnis_taux;
|
||||
const charges = D.charges_enregistrees + darnis + D.urssaf_restant
|
||||
+ D.indemnite_restante + D.claude_restant + D.divers_restant;
|
||||
const resultat = ca - charges;
|
||||
const is = Math.min(resultat, D.is_plafond) * D.is_taux_reduit
|
||||
+ Math.max(resultat - D.is_plafond, 0) * 0.25;
|
||||
const net_societe = resultat - is;
|
||||
|
||||
// ── trésorerie au 1er janvier 2027 ───────────────────────────────────────
|
||||
const decaissements = darnis + D.urssaf_restant + D.indemnite_restante
|
||||
+ D.claude_restant + D.divers_restant;
|
||||
const treso2027 = D.tresorerie + enc_restants - decaissements - is;
|
||||
const dette_fin = D.dette_cca + D.indemnite_restante;
|
||||
const dispo = treso2027 - dette_fin;
|
||||
|
||||
// ── rémunération ─────────────────────────────────────────────────────────
|
||||
const remuA = remuM * 12, cotA = remuA * cot, coutA = remuA + cotA;
|
||||
// L'ARE se cumule partiellement : elle est réduite de 70 % du brut déclaré.
|
||||
const areReduite = Math.max(areM - 0.7 * remuM, 0);
|
||||
const imposable = remuA + areReduite * 12;
|
||||
const irA = ir(imposable);
|
||||
const enPocheM = remuM + areReduite - irA/12;
|
||||
|
||||
// ── autonomie ────────────────────────────────────────────────────────────
|
||||
const sortieM = coutA / 12;
|
||||
const mois = sortieM > 0 ? treso2027 / sortieM : Infinity;
|
||||
const moisDispo = sortieM > 0 ? dispo / sortieM : Infinity;
|
||||
|
||||
// ── capacité 2027 ────────────────────────────────────────────────────────
|
||||
const tjm = +$("#tjm").value, jours = +$("#jours").value, kmj = +$("#kmj").value;
|
||||
const joursAn = Math.round(jours * 12 * (D.ouvres_2027 / 252));
|
||||
const ca27 = tjm * joursAn;
|
||||
// La commission d'apport tombe à 5 % le 30/01/2027, et ne vise que les
|
||||
// clients apportés — on l'applique au seul chiffre d'affaires KissMetrics,
|
||||
// qui est nul en 2027 hors dernier encaissement. Les charges 2027 sont donc
|
||||
// les charges fixes plus les cotisations de la rémunération.
|
||||
const charges27 = D.charges_fixes_2027 + cotA;
|
||||
const res27 = ca27 - charges27 - remuA;
|
||||
const is27 = Math.max(0, Math.min(res27, D.is_plafond) * D.is_taux_reduit
|
||||
+ Math.max(res27 - D.is_plafond, 0) * 0.25);
|
||||
|
||||
// TJM implicite du forfait KissMetrics
|
||||
const kmEur = D.km_total_usd / fx;
|
||||
const kmJours = kmj * D.km_mois;
|
||||
const kmTjm = kmEur / kmJours;
|
||||
|
||||
// ── rendu ────────────────────────────────────────────────────────────────
|
||||
$("#o-remu").textContent = eur(remuM) + "/mois";
|
||||
$("#o-are").textContent = areM ? eur(areM) + "/mois" : "aucune";
|
||||
$("#o-perso").textContent= eur(persoM) + "/mois";
|
||||
$("#o-cot").textContent = cot*100 + " %";
|
||||
$("#o-fx").textContent = fx.toFixed(2);
|
||||
|
||||
const fin = new Date(2027, 0, 1);
|
||||
fin.setMonth(fin.getMonth() + Math.floor(Math.min(mois, 600)));
|
||||
const moisNom = mois > 240 ? "longtemps" :
|
||||
fin.toLocaleDateString("fr-FR",{month:"long",year:"numeric"});
|
||||
$("#v-mois").textContent = moisNom;
|
||||
$("#v-remu").textContent = eur(remuM);
|
||||
|
||||
const couvre = enPocheM >= persoM;
|
||||
$("#v-detail").innerHTML = remuM === 0
|
||||
? `Sans te verser quoi que ce soit, la trésorerie ne s'épuise pas — mais tu ne
|
||||
touches rien non plus. Monte la rémunération pour voir ce que ça coûte.`
|
||||
: `Tu touches <strong class="num">${eur(enPocheM)}</strong> par mois net d'impôt,
|
||||
${couvre ? "ce qui couvre" : "<strong>ce qui ne couvre pas</strong>"}
|
||||
tes ${eur(persoM)} de charges personnelles.
|
||||
${cycles < 2 ? "Hypothèse prudente : " + (cycles ? "seul le cycle M5" : "aucun cycle") + " est facturé. " : ""}
|
||||
La société supporte ${eur(sortieM)} par mois, cotisations comprises.`;
|
||||
|
||||
$("#band").innerHTML = [
|
||||
["Trésorerie", eur2(D.tresorerie), "Qonto + Wise, aujourd'hui", 0],
|
||||
["Reste à encaisser", eur(enc_restants), `${(D.ouvertes_usd+enc_cycles_usd).toLocaleString("fr-FR")} USD d'ici décembre`, 0],
|
||||
["Trésorerie au 1ᵉʳ janv. 2027", eur(treso2027), "après charges et impôt", 1],
|
||||
["Dont dû en compte courant", eur(dette_fin), "à toi, sans impôt ni cotisation", 0],
|
||||
["Chiffre d'affaires 2026", eur(ca), "exercice complet", 0],
|
||||
["Résultat net après IS", eur(net_societe), "sans rémunération de gérance", 0],
|
||||
].map(([k,v,n,m]) =>
|
||||
`<div class="cell${m?" mark":""}"><div class="k">${k}</div><div class="v">${v}</div>
|
||||
<div class="n">${n}</div></div>`).join("");
|
||||
|
||||
$("#r-depart").textContent = eur(treso2027);
|
||||
$("#r-burn").textContent = sortieM > 0 ? eur(sortieM) : "—";
|
||||
const pct = Math.max(0, Math.min(100, (mois/24)*100));
|
||||
$("#r-fill").style.width = (sortieM>0 ? pct : 100) + "%";
|
||||
$("#r-marks").innerHTML = [6,12,18,24].map(m =>
|
||||
`<span class="mark" style="left:${(m/24)*100}%">${m} mois</span>`).join("");
|
||||
$("#r-note").innerHTML = sortieM === 0
|
||||
? "Aucune sortie : la trésorerie est stable. Ce n'est pas une stratégie, c'est une pause."
|
||||
: `À ce rythme la trésorerie couvre <strong>${mois.toFixed(1)} mois</strong> de
|
||||
rémunération. En retranchant ce que la société te doit déjà en compte courant,
|
||||
il reste <strong>${moisDispo.toFixed(1)} mois</strong> de marge réelle.
|
||||
L'échelle s'arrête à 24 mois.`;
|
||||
|
||||
$("#o-tjm").textContent = eur(tjm) + "/jour";
|
||||
$("#o-jours").textContent = jours + " j/mois";
|
||||
$("#o-kmj").textContent = kmj + " j/mois";
|
||||
|
||||
$("#capa").innerHTML = [
|
||||
["Jours vendus dans l'année", joursAn + " j", `sur ${D.ouvres_2027} ouvrés`, 0],
|
||||
["Taux d'occupation", Math.round(joursAn/D.ouvres_2027*100) + " %", "du temps disponible", 0],
|
||||
["Chiffre d'affaires 2027", eur(ca27), "à ce prix et ce rythme", 1],
|
||||
["Charges et cotisations", eur(charges27), "fixes + TNS sur ta rémunération", 0],
|
||||
["Résultat avant impôt", eur(res27), res27 < 0 ? "déficitaire" : "après ta rémunération", 0],
|
||||
["Impôt sur les sociétés", eur(is27), "15 % sous 42 500 €", 0],
|
||||
].map(([k,v,n,s2]) =>
|
||||
`<div class="${s2?"strong":""}"><div class="k">${k}</div><div class="v">${v}</div>
|
||||
<div class="n" style="font-size:12px;color:var(--ink-3);margin-top:3px">${n}</div></div>`
|
||||
).join("");
|
||||
|
||||
const ecart = kmTjm - tjm;
|
||||
$("#km-verdict").innerHTML =
|
||||
`À <strong class="num">${kmj} jours par mois</strong>, les 33 000 USD du contrat
|
||||
— soit ${eur(kmEur)} — reviennent à un taux journalier de
|
||||
<strong class="num" style="color:${ecart<0?"var(--crit)":"var(--ok)"}">${eur(kmTjm)}</strong>.
|
||||
${Math.abs(ecart) < 25
|
||||
? "C'est exactement le prix que tu vises."
|
||||
: ecart < 0
|
||||
? `C'est <strong>${eur(-ecart)} en dessous</strong> du taux que tu vises.
|
||||
Sur ${kmJours} jours, l'écart représente ${eur(-ecart*kmJours)}.`
|
||||
: `C'est ${eur(ecart)} au-dessus du taux que tu vises — le forfait a bien
|
||||
travaillé pour toi.`}`;
|
||||
|
||||
$("#km-tab").innerHTML = `
|
||||
<thead><tr><th>Si tu y passes</th><th style="text-align:right">Jours au total</th>
|
||||
<th style="text-align:right">Taux journalier réel</th><th>Face à ${eur(tjm)}</th></tr></thead>
|
||||
<tbody>${[6,8,10,12,15,18].map(j => {
|
||||
const t = kmEur/(j*D.km_mois), d = t - tjm;
|
||||
return `<tr${j===kmj?' class="sum"':''}><td>${j} j/mois</td>
|
||||
<td class="n">${j*D.km_mois}</td><td class="n">${eur(t)}</td>
|
||||
<td style="color:${d<0?"var(--crit)":"var(--ok)"}">${d<0?"−":"+"}${eur(Math.abs(d))}</td></tr>`;
|
||||
}).join("")}</tbody>`;
|
||||
|
||||
$("#paie").innerHTML = [
|
||||
["Coût pour la société", eur(coutA), "par an", 0],
|
||||
["Cotisations TNS", eur(cotA), "à provisionner pour 2028", 0],
|
||||
["Rémunération brute", eur(remuA), "article 62 du CGI", 0],
|
||||
["Impôt sur le revenu", eur(irA), irA === 0 ? "nul à ce niveau" : "barème, 1 part", 0],
|
||||
["Net en poche", eur(enPocheM*12), "par an, ARE comprise", 1],
|
||||
].map(([k,v,n,s]) =>
|
||||
`<div class="${s?"strong":""}"><div class="k">${k}</div><div class="v">${v}</div>
|
||||
<div class="n" style="font-size:12px;color:var(--ink-3);margin-top:3px">${n}</div></div>`
|
||||
).join("");
|
||||
|
||||
$("#pnl").innerHTML = `
|
||||
<thead><tr><th>Poste</th><th style="text-align:right">Montant</th><th>Détail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Facturé à ce jour</td><td class="n">${eur2(D.ca_facture)}</td>
|
||||
<td>FAC001 à FAC011</td></tr>
|
||||
<tr><td>Reste à facturer</td><td class="n">${eur(reste_a_facturer)}</td>
|
||||
<td>${cycles} cycle${cycles>1?"s":""} × 5 500 USD</td></tr>
|
||||
<tr class="sum"><td>Chiffre d'affaires</td><td class="n">${eur(ca)}</td><td></td></tr>
|
||||
<tr><td>Charges enregistrées</td><td class="n">−${eur2(D.charges_enregistrees)}</td>
|
||||
<td>création, outils, commissions déjà réglées</td></tr>
|
||||
<tr><td>Commissions Darnis restantes</td><td class="n">−${eur(darnis)}</td>
|
||||
<td>10 % de l'encaissé</td></tr>
|
||||
<tr><td>URSSAF, indemnité, divers</td><td class="n">−${eur(D.urssaf_restant+D.indemnite_restante+D.claude_restant+D.divers_restant)}</td>
|
||||
<td>échéancier, occupation du domicile, abonnements</td></tr>
|
||||
<tr class="sum"><td>Résultat avant impôt</td><td class="n">${eur(resultat)}</td><td></td></tr>
|
||||
<tr><td>Impôt sur les sociétés</td><td class="n">−${eur(is)}</td>
|
||||
<td>15 % sous 42 500 €</td></tr>
|
||||
<tr class="sum"><td>Résultat net</td><td class="n">${eur(net_societe)}</td><td></td></tr>
|
||||
</tbody>`;
|
||||
render();
|
||||
}
|
||||
|
||||
const EV = [
|
||||
["2026-09-01","einvoicing","Recevoir des factures électroniques","Obligation de réception. Vérifier que la plateforme est choisie.","warn"],
|
||||
["2026-09-23","km","FAC007 échue — 3 000 USD","Part différée du cycle M2.","ok"],
|
||||
["2026-09-23","km","Émettre le cycle M5","Part fixe 2 500 USD + différée 3 000 USD, en dollars.","ok"],
|
||||
["2026-10-23","km","FAC011 échue — 3 000 USD","Part différée M3, émise en retard le 24/08.","ok"],
|
||||
["2026-10-23","km","Émettre le cycle M6 — dernier cycle","Fin de la période active du contrat.","warn"],
|
||||
["2026-11-05","urssaf","URSSAF — 3ᵉ échéance","1 333 € selon l'échéancier 2026.","ok"],
|
||||
["2026-11-23","km","FAC010 échue — 3 000 USD","Part différée du cycle M4.","ok"],
|
||||
["2026-12-15","tva","Acompte de TVA","Régime réel simplifié, dernier acompte.","ok"],
|
||||
["2026-12-31","exercice","Clôture de l'exercice","Dernier jour pour décider d'une rémunération 2026.","crit"],
|
||||
["2027-01-01","regime","Fin du régime réel simplifié","Bascule en CA3 trimestrielle. Le rythme déclaratif change.","crit"],
|
||||
["2027-01-23","km","Dernier encaissement KissMetrics","Part différée de M6. Après, plus rien n'entre.","crit"],
|
||||
["2027-01-30","darnis","Commission Darnis : 10 % → 5 %","Douze mois après la première facturation.","warn"],
|
||||
["2027-01-31","remu","Décision de rémunération 2027","À consigner au registre des décisions.","warn"],
|
||||
["2027-04-20","tva","Première CA3 trimestrielle","Nouveau régime.","ok"],
|
||||
["2027-05-04","tva","CA12 de l'exercice 2026","Déclaration annuelle du régime simplifié.","ok"],
|
||||
["2027-06-30","ag","Approbation des comptes 2026","Assemblée et dépôt.","ok"],
|
||||
["2027-09-01","einvoicing","Émettre en facturation électronique","Et e-reporting. Chantier à préparer bien avant.","warn"],
|
||||
];
|
||||
|
||||
function render(){
|
||||
const auj = new Date("2026-08-24");
|
||||
$("#spine").innerHTML = EV.map(([d,,titre,note,sev]) => {
|
||||
const dt = new Date(d), passe = dt < auj;
|
||||
const j = Math.round((dt-auj)/86400000);
|
||||
const quand = passe ? "passée" : j < 31 ? `dans ${j} j` : `dans ${Math.round(j/30)} mois`;
|
||||
return `<div class="ev is-${sev}">
|
||||
<span class="d">${dt.toLocaleDateString("fr-FR",{day:"2-digit",month:"short",year:"2-digit"})}</span>
|
||||
<span class="t">${titre}<small>${note}</small></span>
|
||||
<span class="pill p-${passe?"mute":sev}">${quand}</span></div>`;
|
||||
}).join("");
|
||||
|
||||
$("#seuils").innerHTML = `
|
||||
<thead><tr><th>Seuil</th><th style="text-align:right">Valeur</th><th>Ce qu'il change</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Impôt sur le revenu nul</td><td class="n">≈ 12 750 €</td>
|
||||
<td>Rémunération annuelle sous laquelle tu ne paies pas d'IR, seul, sans autre revenu</td></tr>
|
||||
<tr><td>IS au taux réduit</td><td class="n">42 500 €</td>
|
||||
<td>15 % en dessous, 25 % au-delà. Le résultat 2026 reste largement dessous</td></tr>
|
||||
<tr><td>Dividende cotisé TNS</td><td class="n">≈ 400 €</td>
|
||||
<td>10 % du capital + compte courant. Au-delà, cotisations au lieu des 17,2 %</td></tr>
|
||||
<tr><td>Compte courant d'associé</td><td class="n">3 013 €</td>
|
||||
<td>Remboursable sans impôt ni cotisation — ce n'est pas un revenu</td></tr>
|
||||
<tr><td>Abattement ARE</td><td class="n">70 %</td>
|
||||
<td>Part de la rémunération brute déduite de l'allocation</td></tr>
|
||||
</tbody>`;
|
||||
}
|
||||
|
||||
document.querySelectorAll(".seg button").forEach(b => b.addEventListener("click", () => {
|
||||
document.querySelectorAll(".seg button").forEach(x => x.setAttribute("aria-pressed","false"));
|
||||
b.setAttribute("aria-pressed","true"); cycles = +b.dataset.cyc; calc();
|
||||
}));
|
||||
["remu","are","perso","cot","fx","tjm","jours","kmj"].forEach(id =>
|
||||
$("#"+id).addEventListener("input", calc));
|
||||
|
||||
$("#asof").textContent = "Données au " +
|
||||
new Date(D.genere).toLocaleDateString("fr-FR",{day:"2-digit",month:"long",year:"numeric"});
|
||||
calc();
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
// Rend un emoji en PNG transparent via Chromium, qui sait lire Apple Color
|
||||
// Emoji là où weasyprint échoue (format bitmap sbix, non géré).
|
||||
import { chromium } from "playwright";
|
||||
const [outDir, ...emojis] = Deno.args;
|
||||
const b = await chromium.launch({ headless: true });
|
||||
const p = await (await b.newContext({ deviceScaleFactor: 8 })).newPage();
|
||||
for (const [i, e] of emojis.entries()) {
|
||||
await p.setContent(
|
||||
`<body style="margin:0"><span id="g" style="font-family:'Apple Color Emoji';` +
|
||||
`font-size:64px;line-height:1;display:inline-block">${e}</span></body>`);
|
||||
const el = p.locator("#g");
|
||||
const f = `${outDir}/emoji-${i + 1}.png`;
|
||||
await el.screenshot({ path: f, omitBackground: true });
|
||||
console.log(` ${e} -> ${f}`);
|
||||
}
|
||||
await b.close();
|
||||
+21
-3
@@ -143,8 +143,13 @@ export const SCOPES: Readonly<Record<string, Scope>> = {
|
||||
/**
|
||||
* The narrow production writer, used ONLY by the gated promote apply, and only
|
||||
* for what that step actually does: create/modify invoices, attach payments.
|
||||
* No thirdparty creation, no product creation, no proposals — those are
|
||||
* rehearsed then applied by a human when they are genuinely needed.
|
||||
* No product creation, no proposals — those are rehearsed then applied by a
|
||||
* human when they are genuinely needed.
|
||||
* `122` (thirdparties) WAS excluded on that same reasoning, and the reasoning
|
||||
* did not survive contact: a supplier invoice needs a supplier, so the scope
|
||||
* could create the document but not the party it names. The exclusion turned
|
||||
* every new supplier into a manual step in the middle of a gated flow — which
|
||||
* is where discipline erodes.
|
||||
* `162` (contracts) IS included: recording the engagement behind an invoice is
|
||||
* what the piste d'audit fiable asks for, and it recurs (Darnis, then KM).
|
||||
*
|
||||
@@ -162,7 +167,20 @@ export const SCOPES: Readonly<Record<string, Scope>> = {
|
||||
// modification de factures » — les factures fournisseur en sont, et leur
|
||||
// saisie est l'opération la plus courante du back-office. Ajouté après un
|
||||
// 403 en production sur la facture Darnis F1048.
|
||||
rights: [...READ_ONLY, 12, 14, 162, 1232, 2503],
|
||||
//
|
||||
// 122 (tiers) : ajouté sur décision de l'opérateur du 2026-08-15, après un
|
||||
// second 403 — cette fois sur Anthropic PBC, entité américaine distincte
|
||||
// d'Anthropic Ireland, dont la facture ne pouvait être saisie faute de
|
||||
// pouvoir créer le fournisseur qu'elle nomme.
|
||||
//
|
||||
// CE QUE CE DROIT COÛTE, et il faut le dire : Dolibarr le libelle
|
||||
// « Créer/MODIFIER les tiers ». Il n'existe pas de droit de création seule.
|
||||
// Le writer de production peut donc désormais altérer l'identité de
|
||||
// n'importe quel tiers existant — nom, adresse, identifiants fiscaux — et
|
||||
// non seulement en créer. C'est la contrepartie assumée : sans lui, toute
|
||||
// facture d'un fournisseur nouveau s'arrête au milieu du flux gated.
|
||||
// La suppression (125) reste exclue, comme dans tous les scopes.
|
||||
rights: [...READ_ONLY, 12, 14, 122, 162, 1232, 2503],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
Passe une facture client dans une autre devise, sans toucher à sa contre-valeur
|
||||
en euros.
|
||||
|
||||
POURQUOI CE CHEMIN EXISTE. `PUT /invoices/{id}` ACCEPTE les champs
|
||||
`multicurrency_code` et `multicurrency_tx` et répond 200 — mais ne les
|
||||
applique pas : `Facture::update()` ne les traite pas. La facture reste en EUR
|
||||
et rien ne le signale. Seule l'interface expose les actions
|
||||
`editmulticurrencycode` et `editmulticurrencyrate`.
|
||||
|
||||
POURQUOI LE SCRIPT REMET AUSSI LA LIGNE. Changer la devise ABÎME la ligne :
|
||||
Dolibarr recalcule `montant_devise = euros × taux_du_dictionnaire`, puis le
|
||||
changement de taux fige la devise et recalcule les euros. La contre-valeur
|
||||
euro se met donc à dériver. Le seul moyen de la ramener est de réécrire la
|
||||
ligne en donnant les DEUX prix — `subprice` en euros, `multicurrency_subprice`
|
||||
en devise. Les deux opérations sont inséparables ; les séparer laisserait la
|
||||
facture dans un état faux entre les deux.
|
||||
|
||||
Et `PUT /invoices/{id}/lines/{lid}` n'est pas un PATCH : les champs absents du
|
||||
corps sont remis à zéro — `desc` effacé, `product_type` ramené de service à
|
||||
produit. Le script relit donc la ligne et la renvoie ENTIÈRE.
|
||||
|
||||
CE QUI NE DOIT PAS BOUGER : la contre-valeur en euros. Les livres sont tenus
|
||||
en euros (C. com. art. L.123-22) ; libeller la facture en devise ne doit pas y
|
||||
déplacer un centime. Le script le vérifie et refuse si l'euro a dérivé.
|
||||
|
||||
RAPPEL DE DROIT. Une facture peut être libellée dans toute monnaie
|
||||
(CGI art. 289, II — directive 2006/112, art. 230) ; seule la TVA à payer doit
|
||||
être déterminée en euros. Sans TVA française — autoliquidation par un preneur
|
||||
hors UE, CGI art. 259-1° et 283-2 — la contrainte ne s'applique pas.
|
||||
|
||||
Usage :
|
||||
deno run -A test/setInvoiceCurrency.ts --id 15 --code USD \
|
||||
--rate 1.164992 --mc-amount 3000.00 [--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 code = pick("--code", "USD");
|
||||
const rate = pick("--rate");
|
||||
const mcAmount = pick("--mc-amount"); // prix unitaire dans la devise
|
||||
const dryRun = argv.includes("--dry-run");
|
||||
|
||||
if (!id || !rate || !mcAmount) {
|
||||
console.error("--id, --rate et --mc-amount sont requis");
|
||||
Deno.exit(2);
|
||||
}
|
||||
|
||||
const dolibarrAddress = assertSandbox();
|
||||
const apiUrl = Deno.env.get("DOLIBARR_URL") || dolibarrAddress;
|
||||
const apiKey = Deno.env.get("DOLIBARR_API_KEY") || "";
|
||||
|
||||
console.log(`cible : ${dolibarrAddress}`);
|
||||
console.log(`facture : id=${id} → ${mcAmount} ${code} au taux ${rate}`);
|
||||
|
||||
/** Relit la facture par l'API : la page de retour ne prouve rien. */
|
||||
async function etat(): Promise<Record<string, unknown>> {
|
||||
const r = await fetch(`${apiUrl}/api/index.php/invoices/${id}`, {
|
||||
headers: { DOLAPIKEY: apiKey, Accept: "application/json" },
|
||||
});
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ locale: "fr-FR" });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
const avant = await etat();
|
||||
console.log(`avant : ${avant.multicurrency_code} taux ${avant.multicurrency_tx} — ` +
|
||||
`HT ${avant.total_ht} EUR, ${avant.multicurrency_total_ht} en devise`);
|
||||
if (avant.multicurrency_code === code && Number(avant.multicurrency_tx) === Number(rate)) {
|
||||
console.log("déjà dans cette devise à ce taux, rien à faire.");
|
||||
Deno.exit(0);
|
||||
}
|
||||
// La devise ne se change que sur un brouillon. On y passe nous-même plutôt
|
||||
// que d'exiger de l'appelant une étape séparée : la remise en brouillon fait
|
||||
// partie de l'opération, pas de sa préparation.
|
||||
if (String(avant.status) !== "0") {
|
||||
if (String(avant.paye) === "1") {
|
||||
console.error("facture RÉGLÉE — refus. Changer la devise d'une facture encaissée " +
|
||||
"déplacerait un montant déjà rapproché de la banque.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
console.log(`statut ${avant.status} → remise en brouillon`);
|
||||
if (!dryRun) {
|
||||
const r = await fetch(`${apiUrl}/api/index.php/invoices/${id}/settodraft`, {
|
||||
method: "POST",
|
||||
headers: { DOLAPIKEY: apiKey, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ idwarehouse: 0 }),
|
||||
});
|
||||
if (!r.ok) { console.error(`settodraft a échoué : HTTP ${r.status}`); Deno.exit(1); }
|
||||
}
|
||||
}
|
||||
if (dryRun) {
|
||||
console.log("\n--dry-run : rien n'est soumis.");
|
||||
Deno.exit(0);
|
||||
}
|
||||
|
||||
await login.doAdminLogin({
|
||||
page,
|
||||
dolibarrAddress,
|
||||
adminCredentials: {
|
||||
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
||||
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
||||
},
|
||||
});
|
||||
|
||||
/** Ouvre une action d'édition en ligne et soumet le formulaire qui la porte. */
|
||||
async function editer(action: string, remplir: () => Promise<void>): Promise<void> {
|
||||
await page.goto(`${dolibarrAddress}/compta/facture/card.php?facid=${id}`);
|
||||
const href = await page.locator(`a[href*="${action}"]`).first().getAttribute("href");
|
||||
if (!href) throw new Error(`action ${action} absente de la fiche — droits, ou module inactif`);
|
||||
await page.goto(new URL(href, dolibarrAddress).toString());
|
||||
await remplir();
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
await editer("editmulticurrencycode", async () => {
|
||||
// Le select n'a pas de nom stable d'une version à l'autre : on le trouve
|
||||
// par l'option qu'il contient.
|
||||
const sel = page.locator(`select:has(option[value="${code}"])`).first();
|
||||
await sel.selectOption(code);
|
||||
await sel.locator("xpath=ancestor::form").locator('input[type="submit"]').first().click();
|
||||
});
|
||||
|
||||
await editer("editmulticurrencyrate", async () => {
|
||||
const inp = page.locator('input[name="multicurrency_tx"], input[name="rate"]').first();
|
||||
await inp.fill(rate);
|
||||
await inp.locator("xpath=ancestor::form").locator('input[type="submit"]').first().click();
|
||||
});
|
||||
|
||||
// Réparation de la ligne, indissociable de ce qui précède. On relit d'abord :
|
||||
// le PUT n'est pas un PATCH, tout champ omis serait remis à zéro.
|
||||
const ligne = ((await etat()).lines as Record<string, string>[])[0];
|
||||
const eurAvant = String(avant.total_ht);
|
||||
const puEur = (Number(eurAvant) / Number(ligne.qty || 1)).toFixed(2);
|
||||
const rep = await fetch(`${apiUrl}/api/index.php/invoices/${id}/lines/${ligne.id}`, {
|
||||
method: "PUT",
|
||||
headers: { DOLAPIKEY: apiKey, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
desc: ligne.desc, description: ligne.desc,
|
||||
subprice: puEur, pu_ht: puEur, multicurrency_subprice: mcAmount,
|
||||
qty: ligne.qty, product_type: "1", tva_tx: ligne.tva_tx,
|
||||
remise_percent: ligne.remise_percent,
|
||||
localtax1_tx: ligne.localtax1_tx, localtax2_tx: ligne.localtax2_tx,
|
||||
info_bits: ligne.info_bits, special_code: ligne.special_code, rang: ligne.rang,
|
||||
situation_percent: ligne.situation_percent ?? "100",
|
||||
fk_warehouse: ligne.fk_warehouse ?? "0", pa_ht: ligne.pa_ht ?? "0",
|
||||
}),
|
||||
});
|
||||
if (!rep.ok) { console.error(`réécriture de la ligne : HTTP ${rep.status}`); Deno.exit(1); }
|
||||
|
||||
const apres = await etat();
|
||||
console.log(`après : ${apres.multicurrency_code} taux ${apres.multicurrency_tx} — ` +
|
||||
`HT ${apres.total_ht} EUR, ${apres.multicurrency_total_ht} en devise`);
|
||||
|
||||
if (apres.multicurrency_code !== code) {
|
||||
console.error("ÉCHEC — la devise relue n'est pas celle demandée.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
if (Number(apres.total_ht) !== Number(avant.total_ht)) {
|
||||
console.error(`ÉCHEC — la contre-valeur euro a bougé : ${avant.total_ht} -> ${apres.total_ht}. ` +
|
||||
"Le passage en devise ne doit RIEN déplacer dans les livres.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
if (Math.abs(Number(apres.multicurrency_total_ht) - Number(mcAmount)) > 0.01) {
|
||||
console.error(`ÉCHEC — montant en devise ${apres.multicurrency_total_ht}, attendu ${mcAmount}.`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
const lf = (apres.lines as Record<string, string>[])[0];
|
||||
if (!(lf.desc || "").trim() || lf.product_type !== "1") {
|
||||
console.error(`ÉCHEC — la ligne a été abîmée : desc=${lf.desc ? "ok" : "VIDE"}, ` +
|
||||
`product_type=${lf.product_type} (attendu 1).`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
console.log("ok — devise posée, contre-valeur euro inchangée, ligne intacte, relu par l'API.");
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user