Chaque semaine, je vois des PME où des gens compétents passent des heures sur des tâches qu'un script Python de 200 lignes pourrait faire en 30 secondes. Pas parce qu'ils sont mauvais — parce que personne ne leur a montré que c'était automatisable.
Voici 5 cas réels (anonymisés) avec les gains mesurés. Pas de théorie, pas de "et si" — du concret, du chiffré.
Cas 1 : Facturation automatique à partir d'un ERP
Le problème
Cabinet de conseil, 25 salariés. Chaque mois, la comptable passe 3 jours à créer les factures : elle ouvre l'ERP, note les heures par projet et par consultant, calcule les montants, crée la facture dans un template Word, génère le PDF, l'envoie par email. 120 factures par mois.
La solution
import openpyxl
from datetime import datetime
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
def generer_facture(client, lignes, numero):
# Genere un PDF de facture a partir des donnees projet.
filename = f"facture_{numero}_{client['code']}.pdf"
c = canvas.Canvas(filename, pagesize=A4)
# En-tête
c.setFont("Helvetica-Bold", 16)
c.drawString(50, 780, f"FACTURE N° {numero}")
c.setFont("Helvetica", 10)
c.drawString(50, 760, f"Date : {datetime.now().strftime('%d/%m/%Y')}")
c.drawString(50, 745, f"Client : {client['nom']}")
c.drawString(50, 730, f"SIRET : {client['siret']}")
# Lignes de facturation
y = 680
total_ht = 0
for ligne in lignes:
montant = ligne['heures'] * ligne['taux_horaire']
c.drawString(50, y, f"{ligne['consultant']} — {ligne['projet']}")
c.drawString(350, y, f"{ligne['heures']}h × {ligne['taux_horaire']}€")
c.drawString(480, y, f"{montant:.2f} €")
total_ht += montant
y -= 20
# Totaux
tva = total_ht * 0.20
c.setFont("Helvetica-Bold", 11)
c.drawString(400, y - 30, f"Total HT : {total_ht:.2f} €")
c.drawString(400, y - 50, f"TVA 20% : {tva:.2f} €")
c.drawString(400, y - 70, f"Total TTC : {total_ht + tva:.2f} €")
c.save()
return filename
Le script récupère les timesheet depuis l'API de l'ERP, calcule les montants par client, génère les PDF et les envoie par email avec les bonnes pièces jointes. Le tout tourne en Celery task le 1er de chaque mois.
Le plus dur n'a pas été le code — c'est la gestion des cas particuliers. Factures d'acompte, clients en devise étrangère, projets au forfait vs en régie, TVA intracommunautaire... Chaque exception a nécessité un if/elif supplémentaire. Au bout du compte, le module de facturation fait 800 lignes de Python, dont 500 lignes de gestion d'exceptions métier. Mais il tourne sans intervention humaine depuis 14 mois.
Un point qu'on oublie souvent : l'archivage. Chaque facture générée est stockée en PDF sur un bucket S3 avec un naming convention strict (YYYY/MM/FACT-XXXXX_CLIENT.pdf) et un index en base. La comptable peut retrouver n'importe quelle facture en 10 secondes.
Résultat
- Avant : 3 jours/mois (24h de travail)
- Après : 2h/mois (vérification + cas particuliers)
- Gain annuel : 264 heures, soit ~13 200 € (au coût chargé de la comptable)
- Coût du développement : 4 500 €
- ROI : 4 mois
Cas 2 : Relances clients automatiques
Le problème
Société de négoce BtoB, 180 clients actifs. La gestionnaire passait chaque lundi matin à faire le tour des factures impayées, rédiger des emails de relance personnalisés, et mettre à jour un tableau Excel de suivi. Temps : 4h/semaine minimum, plus quand ça s'accumule.
La solution
from datetime import date, timedelta
from django.core.mail import send_mail
from django.template.loader import render_to_string
PALIERS_RELANCE = [
(7, 'relance_1', 'Rappel de paiement'),
(21, 'relance_2', 'Relance — facture en retard'),
(45, 'relance_3', 'Mise en demeure amiable'),
]
def relancer_impayes():
factures = Facture.objects.filter(
statut='envoyee',
date_echeance__lt=date.today(),
).select_related('client')
nb_relances = 0
for facture in factures:
jours_retard = (date.today() - facture.date_echeance).days
for seuil, template, objet in PALIERS_RELANCE:
if jours_retard >= seuil and not facture.relances.filter(palier=template).exists():
html = render_to_string(f'emails/{template}.html', {
'facture': facture,
'client': facture.client,
'jours_retard': jours_retard,
})
send_mail(
subject=f"{objet} — Facture {facture.numero}",
message='',
html_message=html,
from_email='comptabilite@exemple.fr',
recipient_list=[facture.client.email_compta],
)
facture.relances.create(palier=template, date=date.today())
nb_relances += 1
break # Un seul palier par exécution
return nb_relances
Le script tourne tous les lundis à 9h via un cron Celery Beat. Les relances sont graduées (rappel amical → relance ferme → mise en demeure). Un récap hebdomadaire est envoyé à la gestionnaire.
Résultat
- Avant : 4h/semaine + oublis fréquents (30% des factures n'étaient pas relancées à temps)
- Après : 30 min/semaine (revue du récap + cas spéciaux)
- Bonus inattendu : le délai moyen de paiement est passé de 52 à 38 jours. Impact trésorerie massif.
- Coût du développement : 3 200 €
Le détail qui change tout ici : les emails sont personnalisés. La relance niveau 1 dit "Bonjour M. Dupont, nous nous permettons de vous rappeler que la facture N-2024-0142 d'un montant de 4 500 € HT arrive à échéance le...". Pas un email générique. Le client sent que c'est une personne qui écrit, pas un robot. Le template utilise les données de la facture pour produire un message naturel. Résultat : le taux de réponse aux relances est passé de 15% à 42%.
Autre subtilité : on n'envoie pas de relance si le client a un litige ouvert ou un avoir en cours de traitement. Le script vérifie automatiquement ces conditions avant d'envoyer. Sans ça, on risque d'envoyer une relance pour une facture que le client conteste déjà — très mauvais pour la relation commerciale.
Cas 3 : Rapports hebdomadaires automatiques
Le problème
Directeur commercial d'une PME industrielle. Chaque vendredi, il passait 2h à compiler des données depuis 3 sources (ERP, CRM, fichier Excel partagé) pour produire un dashboard de suivi commercial envoyé au CODIR. Copier-coller, mise en forme, vérification des chiffres...
La solution
import pandas as pd
from openpyxl import Workbook
from openpyxl.chart import BarChart, Reference
from io import BytesIO
def generer_rapport_commercial(semaine):
# Récupération depuis les différentes sources
commandes = pd.read_sql("SELECT * FROM commandes WHERE semaine = %s", conn_erp, params=[semaine])
prospects = pd.read_sql("SELECT * FROM pipeline WHERE updated_at >= %s", conn_crm, params=[debut_semaine])
# Calculs
ca_semaine = commandes['montant_ht'].sum()
ca_cumul = commandes_annee['montant_ht'].sum()
taux_conversion = len(commandes) / max(len(prospects), 1) * 100
top_clients = commandes.groupby('client')['montant_ht'].sum().nlargest(5)
# Génération Excel avec graphiques
wb = Workbook()
ws = wb.active
ws.title = f"S{semaine}"
ws.append(["Indicateur", "Valeur", "Objectif", "Écart"])
ws.append(["CA semaine", f"{ca_semaine:,.0f} €", "50 000 €", f"{ca_semaine - 50000:+,.0f} €"])
ws.append(["CA cumulé YTD", f"{ca_cumul:,.0f} €", f"{objectif_ytd:,.0f} €", ""])
ws.append(["Taux conversion", f"{taux_conversion:.1f}%", "25%", ""])
ws.append(["Nbre commandes", len(commandes), "", ""])
# Graphique barres — top 5 clients
for i, (client, montant) in enumerate(top_clients.items(), start=7):
ws.append([client, montant])
chart = BarChart()
chart.title = "Top 5 clients de la semaine"
data = Reference(ws, min_col=2, min_row=7, max_row=7 + len(top_clients) - 1)
cats = Reference(ws, min_col=1, min_row=7, max_row=7 + len(top_clients) - 1)
chart.add_data(data)
chart.set_categories(cats)
ws.add_chart(chart, "D7")
buffer = BytesIO()
wb.save(buffer)
return buffer.getvalue()
Résultat
- Avant : 2h chaque vendredi, rapport parfois en retard ou avec des erreurs de copier-coller
- Après : rapport généré et envoyé automatiquement le vendredi à 8h. Le directeur commercial consacre 15 min à l'analyser au lieu de 2h à le produire.
- Coût du développement : 2 800 €
Cas 4 : Veille prix fournisseurs par scraping
Le problème
Distributeur de fournitures industrielles. L'acheteur comparait manuellement les prix de 300 références chez 4 fournisseurs, via leurs catalogues en ligne. Un travail de titan fait une fois par trimestre (il aurait fallu le faire chaque semaine).
La solution
import requests
from bs4 import BeautifulSoup
from decimal import Decimal
import logging
logger = logging.getLogger(__name__)
def scraper_prix_fournisseur(fournisseur, references):
# Scrape les prix pour une liste de references chez un fournisseur.
resultats = []
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (compatible; PriceBot/1.0)',
})
for ref in references:
try:
url = fournisseur.url_template.format(ref=ref.code_fournisseur)
resp = session.get(url, timeout=15)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, 'html.parser')
prix_elem = soup.select_one(fournisseur.selecteur_prix)
if prix_elem:
prix_texte = prix_elem.text.strip().replace('€', '').replace(',', '.').strip()
prix = Decimal(prix_texte)
resultats.append({
'reference': ref.code,
'fournisseur': fournisseur.nom,
'prix': prix,
'date': date.today(),
'url': url,
})
logger.info("Prix trouvé: %s @ %s = %s€", ref.code, fournisseur.nom, prix)
else:
logger.warning("Prix non trouvé: %s @ %s", ref.code, fournisseur.nom)
except Exception as e:
logger.error("Erreur scraping %s @ %s: %s", ref.code, fournisseur.nom, e)
return resultats
Le script tourne chaque nuit, compare les prix et génère un rapport d'alerte quand un écart significatif est détecté (>5% de variation, nouveau meilleur prix, rupture de stock). L'acheteur reçoit un email avec les opportunités d'achat.
Résultat
- Avant : comparaison trimestrielle (12h de travail, souvent repoussée)
- Après : veille quotidienne automatique, alertes en temps réel
- Économie mesurée : 8% sur le panier d'achat moyen (meilleur timing d'achat + mise en concurrence systématique). Sur un volume d'achat de 800 000 €/an, ça fait 64 000 €/an.
- Coût du développement : 5 500 €
Cas 5 : Pré-tri de candidatures (parsing CV)
Le problème
Cabinet de recrutement spécialisé. 80 à 150 CV reçus par offre, traitement manuel par les chargés de recrutement. Temps moyen par offre pour le premier tri : 3h. Et ils ont 15 offres actives en permanence.
La solution
import pdfplumber
import re
from dataclasses import dataclass
@dataclass
class CVAnalyse:
nom: str
email: str
telephone: str
competences: list
experience_annees: int
formation: str
score: float
def extraire_cv(pdf_path):
# Extrait et structure les informations d un CV PDF.
with pdfplumber.open(pdf_path) as pdf:
texte = "\n".join(page.extract_text() or "" for page in pdf.pages)
# Extraction email
emails = re.findall(r'[\w.+-]+@[\w-]+\.[\w.]+', texte)
email = emails[0] if emails else ""
# Extraction téléphone (formats FR)
tels = re.findall(r'(?:0|\+33)[\s.-]?[1-9](?:[\s.-]?\d{2}){4}', texte)
tel = tels[0] if tels else ""
# Extraction années d'expérience (heuristique)
exp_match = re.findall(r'(\d+)\s*(?:ans?|années?)\s*(?:d.expérience)?', texte, re.I)
experience = max((int(x) for x in exp_match), default=0)
return CVAnalyse(
nom=extraire_nom(texte),
email=email,
telephone=tel,
competences=extraire_competences(texte, referentiel),
experience_annees=experience,
formation=extraire_formation(texte),
score=0.0,
)
def scorer_cv(cv_analyse, criteres_offre):
# Score un CV par rapport aux criteres de l offre (0-100).
score = 0
# Compétences requises (50 points max)
competences_matchees = set(cv_analyse.competences) & set(criteres_offre['competences_requises'])
score += (len(competences_matchees) / len(criteres_offre['competences_requises'])) * 50
# Expérience (30 points max)
exp_min = criteres_offre.get('experience_min', 0)
if cv_analyse.experience_annees >= exp_min:
score += 30
elif cv_analyse.experience_annees >= exp_min - 2:
score += 15
# Formation (20 points max)
if any(f in cv_analyse.formation for f in criteres_offre.get('formations', [])):
score += 20
cv_analyse.score = score
return cv_analyse
Les CV arrivent par email, sont parsés automatiquement, scorés selon les critères de l'offre et classés. Le recruteur reçoit un tableau trié par score avec le top 20. Il ne regarde en détail que ceux-là.
Résultat
- Avant : 3h de tri par offre × 15 offres actives = 45h/mois
- Après : 30 min de revue par offre × 15 = 7.5h/mois
- Gain : 37.5h/mois, soit 450h/an
- Coût du développement : 6 800 €
Un point technique important : le parsing de CV est un problème plus dur qu'il n'y paraît. Les gens mettent leur expérience dans des ordres différents, avec des mises en page différentes, en PDF ou en Word. Le module utilise pdfplumber pour les PDF, python-docx pour les Word, et une série de regex combinées à des heuristiques de position dans le document. C'est pas parfait — aucune solution ne l'est — mais avec un taux de recall de 87% sur les compétences clés et 93% sur les coordonnées, c'est suffisant pour un premier tri.
L'amélioration la plus efficace a été d'ajouter une couche Ollama par-dessus pour les cas que les regex ne captent pas. Le LLM local analyse le texte brut du CV et extrait les informations manquantes. C'est plus lent (2-3 secondes par CV au lieu de 100ms) mais ça fait passer le taux de recall à 94%. On le lance en batch la nuit, donc le temps n'est pas un problème.
Le pattern commun
Vous remarquerez que ces 5 cas partagent les mêmes caractéristiques :
- Tâche répétitive (quotidienne, hebdomadaire ou mensuelle)
- Règles claires et documentables (même si personne ne les avait documentées)
- Données déjà numériques (dans un ERP, un CRM, des fichiers)
- ROI mesurable et rapide (3-12 mois)
Comment identifier les processus automatisables
La question que me posent le plus souvent mes clients : "par où commencer ?". Voici ma méthode en 3 étapes :
Étape 1 : l'inventaire. Passez une journée avec chaque service (comptabilité, commercial, production, RH). Demandez à chaque personne : "quelle tâche vous prend le plus de temps et vous ennuie le plus ?". Notez tout. Vous allez récolter entre 10 et 30 tâches candidates.
Étape 2 : le scoring. Pour chaque tâche, évaluez sur 5 :
- Fréquence (1 = annuelle, 5 = quotidienne)
- Temps par occurrence (1 = 5 min, 5 = 1 journée)
- Complexité d'automatisation (1 = nécessite de l'IA, 5 = règles simples)
- Volume de données disponibles (1 = papier, 5 = tout en numérique)
Multipliez les scores. Les tâches avec le score le plus élevé sont les meilleures candidates.
Étape 3 : le POC. Prenez la tâche numéro 1 du classement et faites un prototype en 1-2 semaines. Pas un produit fini — un script qui marche sur 80% des cas et qu'on peut montrer aux utilisateurs. Si le POC convainc, on passe à l'industrialisation. Sinon, on passe à la tâche suivante.
En général, les 3 premières tâches identifiées représentent 60% du temps gagnable. C'est le principe de Pareto appliqué à l'automatisation : 20% des processus consomment 80% du temps humain. Concentrez-vous dessus.
Si vous avez ce type de processus dans votre entreprise — et je suis à peu près sûr que oui — ça vaut le coup d'en discuter. Mes prestations d'automatisation Python sont détaillées ici.