# ADR-018 : Lots et dates de péremption

**Statut :** Accepté
**Date :** 2026-10-23
**Contexte :** Capacité générique d'inventaire (ADR-006), sur le socle du stock déduit des documents (ADR-007), de la réception contrôlée (ADR-012), du CMUP rejoué (ADR-014) et des transferts (ADR-015)

---

## Contexte

Les pharmacies, l'agroalimentaire et la restauration achètent des produits qui périment. Gestionstock ne connaît qu'une quantité par produit et par entrepôt : impossible de savoir ce qui expire bientôt, ce qui est déjà périmé en rayon, ni de vendre d'abord ce qui expire en premier.

Le stock de gestionstock n'est pas un compteur : `Common::recalculateOrderStock` le recalcule à partir des documents (achats reçus, ventes, retours, mouvements typés, stock d'ouverture), et `AverageCostService` rejoue l'historique pour le CMUP. Une dizaine de circuits produisent des sorties : caisse, factures, retours d'achat, pertes, inventaires, consommations de recette, imports de caisse, transferts, contre-passations.

## Décision

### 1. Activation

- Capacité générique, accordée par le droit de plan `inventory` (`check.feature:inventory`).
- Le suivi s'active **produit par produit** (`products.track_lots`), avec un délai d'alerte avant péremption (`products.expiry_alert_days`, 30 jours par défaut). Un produit composé (ADR-013) ne se suit pas par lot.

### 2. Les quantités par lot sont déduites, pas comptées

Comme le stock et le CMUP, la quantité restante de chaque lot se **déduit en rejouant l'historique** du produit dans l'entrepôt (`StockLotService`). Seules les écritures qui désignent un lot précis sont mémorisées, dans `stock_lot_assignments` :

| Écriture | Sens | Source |
|---|---|---|
| Réception d'un achat, lot par lot | entrée | ligne d'achat |
| Déclaration d'un lot déjà en stock (stock d'avant le suivi, achat saisi directement « Reçu ») | reclassement depuis « sans lot » | aucune |
| Perte sur un lot choisi (« Périmé ») | sortie | mouvement de perte |
| Transfert entre entrepôts | sortie à l'origine, entrée à la destination | mouvements de transfert |
| Contre-passation d'un mouvement ciblant un lot | inverse de l'original | mouvement de contre-passation |

Le rejeu, en ordre chronologique (entrées avant sorties à date égale, comme le CMUP) :
- une **entrée** va aux lots qui lui sont affectés, le reste dans le compartiment « sans lot » ;
- une **sortie** prend d'abord sur les lots qui lui sont affectés ; le reste est réparti automatiquement : « sans lot » d'abord (c'est le stock le plus ancien, antérieur au suivi), puis les lots **non périmés à la date de la sortie**, du premier qui expire au dernier, puis les lots périmés, enfin « sans lot » en négatif.

Par construction, **la somme des lots et du « sans lot » égale le stock du produit** : un test le vérifie.

Le résultat est mis en cache dans `stock_lots.remaining_quantity` à chaque recalcul de stock d'un produit suivi (`Common::recalculateOrderStock`), pour les alertes et les écrans.

### 3. Lots périmés

- Exclus de la répartition automatique tant qu'il reste autre chose : un lot périmé reste en stock, et donc visible, jusqu'à sa déclaration en perte « Périmé ».
- Réglage par tenant `block_expired_sales` (réglages du module `inventory`) : la **caisse** refuse alors une vente si le stock non périmé du produit ne la couvre pas. Pensé pour les pharmacies. Les factures et la caisse hors ligne (ADR-019) ne sont pas bloquées.

### 4. Lots et entrepôts

- Un lot appartient à un entrepôt et à un produit ; son numéro y est unique. Recevoir le même numéro avec une autre date de péremption est refusé.
- Un transfert fige, au moment du transfert, les lots sortis de l'origine (répartition automatique) et crée ou complète les mêmes lots dans la destination.

### 5. Alertes et écrans

- Alertes : lots qui expirent dans le délai du produit et lots périmés encore en stock, valorisés au coût du produit.
- Écran « Lots et péremptions » : lots en stock, compartiment « sans lot » des produits suivis, déclaration d'un lot existant, activation du suivi d'un produit, réglage du blocage en caisse.
- Réception : numéro, date de péremption et quantité par lot pour les produits suivis.
- Perte : choix du lot pour un produit suivi.

### 6. Accès

| Action | Permission | Garde |
|---|---|---|
| Voir les lots | `stock_lots_view` | `check.feature:inventory` |
| Suivre un produit, déclarer un lot, régler le blocage | `stock_lots_manage` | `check.feature:inventory` |

Les deux permissions vont au rôle `admin`. Les quantités restent soumises à `stock_quantities_view` (ADR-006 §4) dans les alertes.

## Alternatives Considérées

### Affecter chaque sortie à un lot au moment où elle est écrite
C'était le plan initial : chaque circuit de sortie écrit sa propre affectation. Rejeté pour cette version : une dizaine de circuits à modifier, dont la caisse et les factures, chacun avec ses corrections et contre-passations ; un oubli fausserait les lots sans erreur visible. Le rejeu donne le même résultat pour la question posée (que reste-t-il de chaque lot ?) et ne peut pas diverger du stock.

### Un stock par lot comme unité de stock
Rejeté : tous les calculs (stock, CMUP, inventaire, écarts) seraient à refaire par lot.

## Conséquences

- On sait ce qui reste de chaque lot, mais pas **à quel client** un lot a été vendu : un rappel de lot dit s'il en reste en stock, pas qui l'a acheté.
- Si une vente prend en réalité un lot récent alors qu'un lot plus ancien non périmé reste en rayon, l'application croit l'inverse jusqu'au prochain inventaire. L'écart se corrige par une perte ciblée ou une déclaration.
- Un document saisi avec une date passée peut modifier la répartition des lots après coup, comme il modifie déjà le CMUP.
- Le recalcul d'un produit suivi rejoue son historique (3 requêtes) à chaque mouvement, comme le CMUP.
- Nouvelles colonnes `products.track_lots`, `products.expiry_alert_days`, `stock_losses.stock_lot_id`, nouvelles tables `stock_lots` et `stock_lot_assignments`, nouvelles permissions `stock_lots_view` et `stock_lots_manage`.

## Quand Réviser

- Si des clients doivent rappeler un lot auprès des acheteurs (pharmacie, agroalimentaire réglementé) : affecter les ventes à un lot au moment de la vente, au moins en caisse.
- Si le rejeu devient lent sur des produits à très long historique : rejouer à partir d'un instantané daté (clôture de période).
