Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68cc5eefb9 |
@@ -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>
|
||||
Reference in New Issue
Block a user