Pour une PME, la finance est un pilotage quotidien — TVA à déclarer, relances clients, trésorerie à 13 semaines, marges par produit, suivi budget vs réalisé. Voici un kit minimaliste en Python (pandas) pour automatiser 80 % de ces tâches sans usine à gaz. Tout est pensé “PME française”, fichiers Excel/CSV, et résultats exportables en un clic.
Prérequis (une fois)
python -m pip install --upgrade pip pip install pandas numpy openpyxl
/fin_pme/
data/
ventes.xlsx # factures de vente
achats.xlsx # factures d'achat
banque.csv # relevé bancaire
stock.csv # valorisation stock (optionnel)
budget_mensuel.csv # budget par mois et compte
out/
...
Colonnes suggérées (ventes/achats)
Date, Echeance, Statut (Emise, Payée, Partielle)Client / FournisseurRef, Famille, Quantite, PU_HT, Montant_HT, Taux_TVA, Montant_TVA, Montant_TTCDevise (optionnel), TxChange (optionnel)import pandas as pd
from pathlib import Path
BASE = Path("fin_pme")
D = BASE/"data"
def _parse_date(s): return pd.to_datetime(s, dayfirst=True, errors="coerce")
def load_data():
ventes = pd.read_excel(D/"ventes.xlsx")
achats = pd.read_excel(D/"achats.xlsx")
banque = pd.read_csv(D/"banque.csv", sep=";", encoding="utf-8")
for df, cols in [(ventes, ["Date","Echeance"]), (achats, ["Date","Echeance"])]:
for c in cols: df[c] = _parse_date(df[c])
for c in ["Montant_HT","Montant_TVA","Montant_TTC","Taux_TVA","Quantite","PU_HT"]:
if c in df.columns: df[c] = pd.to_numeric(df[c], errors="coerce")
banque["Date"] = _parse_date(banque["Date"])
return ventes, achats, banque
ventes, achats, banque = load_data()
def tva_par_taux(df, sens="collectee"):
# sens: 'collectee' pour ventes, 'deductible' pour achats
grp = df.groupby("Taux_TVA", as_index=False)["Montant_TVA"].sum()
grp = grp.rename(columns={"Montant_TVA": f"TVA_{sens}"})
total = grp[f"TVA_{sens}"].sum()
return grp, total
tva_v, total_collectee = tva_par_taux(ventes, "collectee")
tva_a, total_deduct = tva_par_taux(achats, "deductible")
tva = pd.merge(tva_v, tva_a, on="Taux_TVA", how="outer").fillna(0.0)
tva["Solde_par_taux"] = tva["TVA_collectee"] - tva["TVA_deductible"]
solde_global = total_collectee - total_deduct
# Garde-fous (exemples) :
assert (ventes["Montant_TVA"]>=0).all(), "TVA ventes négative détectée"
assert (achats["Montant_TVA"]>=0).all(), "TVA achats négative détectée"
Astuce : si vous êtes en pro-rata, ajoutez une colonne
ProRata(0–1) aux factures concernées et appliquezTVA_deductible_effective = Montant_TVA * ProRataavant l’agrégation.
import numpy as np
aujourdhui = pd.Timestamp.today().normalize()
def open_amount(df, sens="client"):
# Montant ouvert = TTC non payé (simplifié)
# Suppose une colonne 'Payé' (TTC encaissé/décaissé), sinon adaptez.
if "Paye" in df.columns:
return df["Montant_TTC"].fillna(0) - df["Paye"].fillna(0)
# à défaut, on considère les statuts
m = df["Statut"].str.lower().fillna("")
return np.where(m.eq("payée")|m.eq("payee")|m.eq("payé"), 0.0, df["Montant_TTC"].fillna(0))
def aging_buckets(d):
delta = (aujourdhui - d).days
if pd.isna(delta): return "NA"
if delta <= 0: return "Non échu"
if delta <= 30: return "1–30 j"
if delta <= 60: return "31–60 j"
if delta <= 90: return "61–90 j"
return ">90 j"
def balance_agee(df, tiers_col):
df = df.copy()
df["Ouvert"] = open_amount(df)
df["Tranche"] = df["Echeance"].apply(aging_buckets)
piv = df.pivot_table(index=tiers_col, columns="Tranche", values="Ouvert", aggfunc="sum", fill_value=0.0)
piv["Total"] = piv.sum(axis=1)
return piv.sort_values("Total", ascending=False)
ba_clients = balance_agee(ventes, "Client")
ba_fourn = balance_agee(achats, "Fournisseur")
def dso(ventes):
ca_jour = ventes.groupby(ventes["Date"].dt.to_period("M"))["Montant_TTC"].sum().mean()/30
encours = open_amount(ventes).sum()
return encours / ca_jour if ca_jour>0 else np.nan
def dpo(achats):
achats_jour = achats.groupby(achats["Date"].dt.to_period("M"))["Montant_TTC"].sum().mean()/30
dettes = open_amount(achats).sum()
return dettes / achats_jour if achats_jour>0 else np.nan
DSO = round(dso(ventes), 1)
DPO = round(dpo(achats), 1)
Lecture : DSO (Days Sales Outstanding) = jours de crédit client, DPO = jours de crédit fournisseur. Avec un stock moyen en jours, vous calculez le Cash Conversion Cycle = DSO + Jours de stock − DPO.
def echeancier(df, tiers_col):
df = df.copy()
df["Ouvert"] = open_amount(df)
df = df[df["Ouvert"]>0]
df["Semaine"] = df["Echeance"].dt.to_period("W").apply(lambda p: p.start_time.date())
return df.groupby(["Semaine", tiers_col], as_index=False)["Ouvert"].sum()
ech_clients = echeancier(ventes, "Client") # encaissements attendus
ech_fourn = echeancier(achats, "Fournisseur") # décaissements attendus
cal = pd.DataFrame({"Semaine": pd.date_range(aujourdhui, periods=13, freq="W-MON").date})
entrees = ech_clients.groupby("Semaine")["Ouvert"].sum().reindex(cal["Semaine"], fill_value=0.0).values
sorties = ech_fourn.groupby("Semaine")["Ouvert"].sum().reindex(cal["Semaine"], fill_value=0.0).values
cal["Entrées_prevues"] = entrees
cal["Sorties_prevues"] = sorties
cal["Net"] = cal["Entrées_prevues"] - cal["Sorties_prevues"]
# Ajoutez solde d'ouverture banque si vous l'avez :
solde_ouverture = 25_000.0
cal["Trésorerie_proj"] = solde_ouverture + cal["Net"].cumsum()
Astuce : ajoutez un coefficient de fiabilité par client (1 = toujours à l’heure, 0,7 = retards) et multipliez
Ouvertpar ce coefficient pour une prévision conservatrice.
# Marge brute par facture/ligne
if {"Quantite","PU_HT","Montant_HT"}.issubset(ventes.columns):
ventes["CA_HT"] = ventes["Montant_HT"]
else:
ventes["CA_HT"] = ventes["Quantite"]*ventes["PU_HT"]
# Coûts variables unitaires par 'Famille' (fichier simple ou dictionnaire)
couts_var = {
"Café": 0.35, "Snack": 1.10, "Service": 0.0
}
ventes["CV_unit"] = ventes["Famille"].map(couts_var).fillna(0.0)
ventes["CV_total"] = ventes["CV_unit"]*ventes["Quantite"].fillna(1.0)
ventes["Marge_brute"] = ventes["CA_HT"] - ventes["CV_total"]
marge_par_famille = ventes.groupby("Famille", as_index=False)["CA_HT","Marge_brute"].sum()
marge_par_famille["Taux_marge"] = marge_par_famille["Marge_brute"]/marge_par_famille["CA_HT"]
ABC light : si vous avez 2–3 inducteurs (heures machine, km, palettes), ventilez un coût fixe proportionnellement à ces inducteurs pour approcher un coût complet sans ERP.
# banque: Date, Libelle, Montant (positif = entrée, négatif = sortie)
# ventes payées: constituez un 'encaissements.csv' avec Date, Ref, Montant
# achats payés: 'decaissements.csv'
enc = pd.DataFrame({"Date":ventes["Date"], "Montant":ventes["Montant_TTC"]})
dec = pd.DataFrame({"Date":achats["Date"], "Montant":-achats["Montant_TTC"]})
mouvements = pd.concat([enc, dec], ignore_index=True).dropna(subset=["Montant"])
# Matching exact par montant et journée
bk = banque.copy()
bk["Date"] = bk["Date"].dt.date
mouvements["Date"] = mouvements["Date"].dt.date
matched = pd.merge(bk, mouvements, on=["Date","Montant"], how="inner")
bk_restes = pd.merge(bk, matched[["Date","Montant"]], on=["Date","Montant"], how="left", indicator=True)
bk_restes = bk_restes[bk_restes["_merge"].eq("left_only")].drop(columns=["_merge"])
# 'bk_restes' = lignes non rapprochées (écarts, agrégats, frais)
Niveau 2 : faites un matching par fenêtre de date (±3 jours) et montants proches (frais) ; attention aux faux positifs → conservez un journal des appariements.
# budget_mensuel.csv: Mois (YYYY-MM), Compte, Budget
budget = pd.read_csv(D/"budget_mensuel.csv")
budget["Mois"] = pd.PeriodIndex(budget["Mois"], freq="M")
ventes["Mois"] = ventes["Date"].dt.to_period("M")
achats["Mois"] = achats["Date"].dt.to_period("M")
ca_mensuel = ventes.groupby("Mois")["Montant_TTC"].sum().rename("CA_reel")
ach_mensuel = achats.groupby("Mois")["Montant_TTC"].sum().rename("Achats_reels")
real = pd.concat([ca_mensuel, ach_mensuel], axis=1).fillna(0)
bv = budget.pivot_table(index="Mois", columns="Compte", values="Budget", aggfunc="sum").fillna(0)
# Suppose deux comptes: 'CA' et 'Achats'
bv = bv.rename(columns={"CA":"CA_budget","Achats":"Achats_budget"})
report = pd.concat([real, bv], axis=1)
report["Ecart_CA"] = report["CA_reel"] - report["CA_budget"]
report["Ecart_Achats"] = report["Achats_reels"] - report["Achats_budget"]
# Alertes simples
ALERTE = []
if report["Ecart_CA"].iloc[-1] < -0.05 * report["CA_budget"].iloc[-1]:
ALERTE.append("CA du mois en retard >5% vs budget.")
if report["Ecart_Achats"].iloc[-1] > 0.05 * report["Achats_budget"].iloc[-1]:
ALERTE.append("Achats du mois dépassent le budget >5%.")
OUT = BASE/"out"; OUT.mkdir(parents=True, exist_ok=True)
with pd.ExcelWriter(OUT/"PME_Rapports_Financiers.xlsx", engine="openpyxl") as xw:
tva.to_excel(xw, sheet_name="TVA", index=False)
ba_clients.to_excel(xw, sheet_name="Balance_agee_clients")
ba_fourn.to_excel(xw, sheet_name="Balance_agee_fourn")
cal.to_excel(xw, sheet_name="Tresorerie_13_sem", index=False)
marge_par_famille.to_excel(xw, sheet_name="Marge_par_famille", index=False)
report.reset_index().to_excel(xw, sheet_name="Budget_vs_Reel", index=False)
print("Export OK ->", OUT/"PME_Rapports_Financiers.xlsx")
data/ et export unique out/PME_Rapports_Financiers.xlsx.out/ par semaine/mois pour la traçabilité.Ce kit Python remplace des manipulations manuelles par des scripts courts et lisibles. Vous gagnez du temps sur la TVA, les relances, la trésorerie et les marges, avec des exports Excel prêts à envoyer.
Deux outils concrets pour piloter la qualité sans alourdir vos équipes Un système qualité n’avance…
Un chantier se gagne souvent avant même l’arrivée des équipes. Quand tout est clair dès…
Le mariage a du sens quand il repose sur une décision libre, mûrie et partagée.…
Une étude de cas réussie commence par une structure sûre. Ce modèle Word vous guide…
Les soft skills se repèrent vite sur une fiche, mais elles ne pèsent vraiment que…
Outil de comparaison et repérage des offres étudiantes Choisir des verres progressifs ressemble rarement à…
This website uses cookies.