Étude de cas · En production

Inventaire d'atelier industriel

Les opérateurs d'un atelier de rectification consultaient l'état de 446 outils dans un ERP lourd, installé sur un seul poste. J'ai construit l'interface web qui expose ces données à tous les postes, et automatise les commandes de réapprovisionnement.

Rôle
Conception, développement et déploiement — seul
Durée
Juillet 2026 — en service depuis
Volume
8 750 lignes applicatives, 899 lignes de tests
Données
446 outils, 113 clients, synchronisés depuis l'ERP
Stack
PHP 8 · Python 3 · JavaScript · Tailwind · ReportLab · SMTP
Contrainte
Windows Server hors ligne, aucune base de données installable

Le problème

L'atelier gère son outillage dans Clipper, un ERP industriel propriétaire. Consulter l'emplacement physique d'une meule, savoir si un outil client est disponible ou au rebut, demander un rachat : tout passait par un poste unique, sous licence, avec une interface conçue pour la saisie comptable et non pour un opérateur devant sa machine.

En pratique, les opérateurs se déplaçaient, ou demandaient à un collègue. Les demandes de réapprovisionnement partaient par e-mail manuel, une par une, sans trace consolidée.

Trois contraintes non négociables encadraient la solution :

  • Aucune modification de Clipper. L'ERP est le système de référence comptable ; y écrire depuis un outil tiers était exclu. Le seul point d'entrée disponible est un export XML déposé sur un partage réseau.
  • Aucune base de données. Le serveur est un poste Windows de l'atelier, sans SGBD installé et sans budget ni administrateur pour en maintenir un.
  • Aucun accès Internet entrant. Le service ne devait être joignable que depuis le réseau local.

L'architecture

Le flux est volontairement unidirectionnel : Clipper reste la source de vérité, l'application n'écrit jamais en retour. Elle transforme, affiche, et notifie par e-mail.

Architecture de l'application d'inventaire Les exports XML de l'ERP Clipper sont lus par un script Python qui produit des fichiers JSON. Une API PHP lit et écrit ces fichiers sous verrou, et les sert aux navigateurs de l'atelier. Une tâche planifiée quotidienne envoie les commandes groupées par e-mail. SERVEUR ATELIER — PHP intégré + Python Partage GPAO Export_*.xml parser.py détection encodage export/*.json état applicatif api/*.php 11 endpoints batch 17h30 tâche planifiée Postes atelier navigateur SMTP fournisseurs lit flock fetch() JSON PDF + e-mail XML
Flux unidirectionnel : l'ERP alimente l'application, jamais l'inverse.
Tableau de l'inventaire : colonnes moyen, type, désignation, dimensions, fournisseur et gisement, avec filtres par catégorie et compteurs.
Vue principale — 446 références, recherche instantanée côté client. Données anonymisées pour cette capture.

Décisions techniques

Fichiers JSON verrouillés par flock(), plutôt qu'un SGBD

SQLite / MySQL

Le serveur ne pouvait pas héberger de base de données. J'ai retenu des fichiers JSON dont chaque écriture passe par un verrou exclusif : ouverture en c+, LOCK_EX, lecture par fread() à l'intérieur du verrou, ftruncate(), réécriture, fflush() avant déverrouillage. L'ordre compte : déverrouiller avant le vidage du tampon romprait l'atomicité, et le bug serait invisible jusqu'à la première collision réelle.

Trois points d'entrée manipulent le même fichier de commandes. Plutôt que de répéter la séquence trois fois — et de risquer qu'une copie dérive — je l'ai factorisée dans un helper qui prend une fonction de mutation : l'appelant décrit la transformation, le helper garantit le verrou.

Un seul langage écrit les fichiers d'état

Écriture directe depuis Python

Le batch quotidien est écrit en Python et doit marquer des commandes comme envoyées. Le réflexe serait d'écrire le JSON directement. Je ne l'ai pas fait : le module fcntl de Python n'existe pas sous Windows, et rien ne garantit qu'un verrou posé côté Python s'entende avec le flock() de PHP sur la même plateforme.

Le script Python appelle donc un point d'entrée PHP en ligne de commande pour toute écriture. Un seul langage écrit, avec un seul mécanisme de verrouillage. Le coût est un processus supplémentaire par exécution ; le gain est l'absence de corruption silencieuse.

Chemins de l'ERP surchargeables par variables d'environnement

Chemins codés en dur

Les exports vivent sur un partage Windows au chemin figé. Les coder en dur rendait le projet intestable ailleurs que sur le serveur de production. Deux variables d'environnement les surchargent, avec repli sur les valeurs réelles : le comportement en production est inchangé, mais le développement et les tests deviennent possibles sur une autre machine.

Difficultés rencontrées

Des fichiers XML qui mentent sur leur encodage

Les exports déclarent encoding="ISO-8859-1" dans leur en-tête. L'un d'eux respecte cette déclaration, un autre non. Résultat : selon le fichier, soit les accents s'affichaient correctement, soit ils devenaient des caractères parasites — et faire confiance à l'en-tête cassait systématiquement l'un des deux cas.

J'ai écrit une fonction de chargement qui détecte l'encodage réel au lieu de croire la déclaration, puis l'ai couverte par des tests qui construisent les octets bruts directement en mémoire. Utiliser des fichiers de test aurait réintroduit le problème : l'outil qui les écrit choisit son propre encodage, et le test aurait validé autre chose que le cas réel.

Un regroupement qui s'effondrait sur une catégorie

L'envoi groupé range les commandes par fournisseur. Les meules portent un nom de fournisseur ; les diamants, dans l'export Clipper, n'en ont pas — le champ est toujours vide. Sans traitement particulier, toutes les commandes de diamants tombaient dans un unique groupe générique et partaient au mauvais destinataire.

Le correctif est un repli sur le code court du fournisseur quand le nom complet manque. Le cas est couvert par un test dédié, parce que c'est exactement le genre de régression qui repasse inaperçue après une refonte.

Un « valeur par défaut » qui s'affichait dans les PDF

Les bons de commande affichaient littéralement --- à la place des observations vides. La fonction de formatage traitait une chaîne vide passée explicitement comme une absence de valeur, et substituait son marqueur de remplissage. Bug mineur, mais visible par le fournisseur sur un document officiel.

Un incident de sécurité, et sa correction

La première version stockait les identifiants SMTP dans un fichier .env placé à la racine du site. C'est une faute : le serveur intégré de PHP sert n'importe quel fichier de son dossier, fichiers cachés compris. Toute personne du réseau connaissant l'URL pouvait télécharger le mot de passe en clair.

Corrections apportées :

  • Le fichier de secrets a été déplacé un niveau au-dessus de la racine web, hors de portée du serveur.
  • Le script d'installation le crée avec une saisie masquée, et ne le réécrit jamais s'il existe déjà.
  • Le mot de passe précédent a été considéré comme compromis et révoqué : la documentation impose d'en générer un nouveau plutôt que de réutiliser l'ancien.
  • L'authentification utilise un mot de passe d'application dédié, jamais le mot de passe principal du compte.

Je documente cet incident plutôt que de le taire : la faute initiale est réelle, la question qui compte est de savoir si elle a été comprise et traitée correctement. Considérer l'ancien secret comme compromis plutôt que de simplement le déplacer est la partie non-évidente — et la seule qui protège réellement.

Résultats

446

outils consultables depuis tous les postes

113

fiches clients gérées

1/jour

envoi groupé au lieu d'e-mails manuels

899

lignes de tests (Python + PHP)

L'application tourne en continu depuis juillet 2026, relancée automatiquement au démarrage du serveur par une tâche planifiée. Les demandes de rachat de la journée partent groupées par fournisseur à 17h30, accompagnées d'un PDF généré avec l'en-tête et le tampon de l'entreprise. En cas d'échec, les commandes restent en attente et sont reprises au passage suivant : aucune demande n'est perdue.

Page des outils clients : fiches par client avec nombre d'outils enregistrés, statut disponible ou rebut, et création de nouveau client.
Second module — 113 fiches clients, statut par outil et création à la volée. Noms de clients fictifs.

Ce que je referais autrement

SQLite plutôt que des fichiers JSON. J'avais écarté toute base de données à cause de la contrainte d'installation — à tort : SQLite n'est pas un serveur, c'est un fichier, et l'extension est fournie avec PHP. J'aurais eu les transactions et le verrouillage sans écrire une ligne de gestion de concurrence. C'est la décision que je défends le moins aujourd'hui.

Le serveur intégré de PHP traite une requête à la fois. Cela suffit pour quelques postes, mais ne tient pas si l'usage s'étend. La documentation signale la limite et pointe vers Apache ; ce n'est pas encore fait.

Les journaux ne sont pas purgés. Un fichier par jour s'accumule indéfiniment. Une rotation aurait coûté quelques lignes.