Author SHA1 Message Date
arcodangeandClaude Opus 5 68cc5eefb9 feat(pilotage): un tableau de bord pour décider — autonomie, rémunération, capacité
L'opérateur : « est-ce possible d'avoir un dashboard pour suivre la santé et
pérennité fiscale de mon entreprise ainsi que ma rémunération selon différentes
hypothèses ». Puis : « en tenant compte des jours ouvrés de l'exercice, pouvoir
simuler avec un tjm/forfait donné ».

LA QUESTION SOUS LA QUESTION. Elle était dans le message précédent :
« l'indemnisation 2026 + la rémunération minimale devrait suffire en cas de
carence de prospect ». Le tableau répond donc à ça, et le dit dès le titre :
combien de temps on tient sans nouveau client, et à quelle rémunération.

AUCUN CHIFFRE EN DUR. pilotage/snapshot.py relit l'ERP de production — factures,
charges, soldes bancaires, dette de compte courant — et va chercher les règles
dans fleet/profile/fiscal.yaml et les échéances dans calendar.yaml, qui font
déjà autorité. Un tableau qui recopierait ces valeurs serait faux le lendemain.

Les PROJECTIONS, elles, se calculent dans la page, à partir d'hypothèses que
l'opérateur déplace : rémunération, allocation France Travail, charges
personnelles, taux de cotisations, taux de change, cycles facturés. Les figer
dans le JSON les rendrait invérifiables — personne ne saurait plus quel taux a
produit quel résultat.

LES JOURS OUVRÉS sont calculés pour de vrai : week-ends et onze fériés français
retirés, Pâques par l'algorithme de comput. 242 jours pour l'exercice 2026,
254 pour 2027.

Ils servent surtout à une chose que l'opérateur n'avait pas demandée mais qui
tombe de sa question : LE 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é — 33 000 USD sur six mois font 589 EUR/jour à 8 jours par mois, et
236 EUR/jour à 20. C'est la seule comparaison honnête avec une mission en régie,
et elle dit si le contrat était bien tarifé.

HORS DE DOLIBARR, délibérément. Son tableau de bord affiche l'état des objets ;
il ne sait pas projeter, et un module maison coûterait plus à maintenir qu'il ne
rapporterait pour une société d'une personne. Dolibarr tient les écritures, ce
tableau sert à décider.

La page porte ses propres réserves, dont le piège de la deuxième année : les
cotisations 2027 seront provisionnelles sur un revenu 2026 nul, et la
régularisation tombera en 2028. Le montant affiché est à provisionner, pas à
dépenser.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-24 20:22:03 +02:00
4 changed files with 1322 additions and 0 deletions
+57
View File
@@ -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.
+508
View File
@@ -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
}
]
}
+123
View File
@@ -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())
+634
View File
@@ -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>