Tout système de stock digne de confiance partage une propriété : sa source de vérité est un journal de ce qui s'est passé, et non un total courant. Les banques le font. Les logiciels comptables le font. La plupart des ERP le font — et le cachent ensuite derrière une interface qui prétend le contraire.
Le modèle du total courant est séduisant parce qu'il est rapide. Une seule ligne par produit, un seul entier à mettre à jour, une seule lecture pour répondre à « combien en avons-nous ? ». Il est aussi fragile, car cet unique entier est une valeur dérivée sans provenance. Quand il est faux — et il finira par l'être — impossible de savoir pourquoi, quand ou par qui. On ne peut que le corriger en l'écrasant, ce qui détruit la preuve de l'erreur.
Pourquoi append-only Si chaque modification écrit une ligne et qu'aucune ligne n'est jamais modifiée, la piste d'audit devient triviale. Des questions comme « quel était le stock de la SKU AB-001 le 14 mars ? » ne sont qu'une somme SQL. Des questions comme « qui a déplacé cela ? » ne sont qu'une simple jointure.
La différence opérationnelle est bien réelle. Dans un système mutable, répondre à une question historique exige de la confiance — confiance que personne n'a modifié l'historique, que la sauvegarde est exacte, que la personne à l'origine du changement a pensé à le consigner ailleurs. Dans un système append-only, répondre à une question historique exige une requête. La réponse est déterministe, reproductible et recevable en cas de litige avec un client ou un auditeur.
Pas de bouton de suppression
L'admin de StockMovement désactive explicitement les permissions add/change/delete — même pour les superutilisateurs. Cette mesure de sécurité implique que les corrections passent par un point de terminaison dédié POST /api/stock/{id}/adjust/ qui écrit une ligne ADJUSTMENT accompagnée d'une note obligatoire. Le registre reste honnête.
C'est cette discipline qui rend append-only opérationnel. S'il existe une porte de derrière — un administrateur de base de données capable de UPDATE stock_movement SET quantity = 5 WHERE id = 1234 — alors le registre n'est qu'un journal assorti d'un astérisque. En désactivant toutes les opérations de mutation au niveau de l'admin Django et en proposant un chemin de correction unique et auditable, nous garantissons que chaque nombre du registre dispose d'une trace écrite. Même les corrections en ont une — la note d'ajustement est obligatoire, horodatée et attribuée à l'admin qui l'a rédigée.
Cache, pas vérité
StockItem.on_hand et StockItem.reserved sont un cache, mis à jour dans la même transaction que l'écriture du registre. S'ils venaient à diverger, le registre les reconstruit.
Le cache existe pour des raisons de performance — on ne veut pas faire la somme de six années de mouvements de stock à chaque affichage de la page d'inventaire. Mais le cache est jetable par conception. Une commande de management peut remettre à zéro chaque StockItem et le reconstruire à partir du registre en une seule passe. Ce modèle de conception — le cache comme optimisation, le registre comme vérité — est emprunté aux systèmes comptables et appliqué au stock. Ce n'est pas une nouveauté, mais c'est correct, et en matière de stock, la justesse vaut mieux que la nouveauté.
