# ADR-010 : Inventaire en aveugle, pertes expliquées et rapport d'écarts

**Statut :** Accepté
**Date :** 2026-10-14
**Contexte :** Lots 2, 3 et 4 de la feuille de route restauration — premiers écrans rendus actifs par le droit de plan `has_inventory` (ADR-006), sur le socle de l'ADR-007

---

## Contexte

Le lot 0 a posé le socle : mouvements typés et immuables, verrouillage de période, stock théorique daté (ADR-007), droit de plan et masquage du théorique pour le personnel (ADR-006). Le droit « Inventaire et écarts » n'avait encore aucun effet visible pour les admins ni pour le personnel.

La note produit fixe trois besoins :
1. **déclarer les pertes connues** (casse, périmé, offert, repas du personnel), pour les distinguer de la perte inexpliquée ;
2. **compter en aveugle**, par emplacement et sur un périmètre réduit si besoin (inventaire tournant) ;
3. **lire l'écart valorisé** en FCFA, produit par produit, du plus coûteux au moins coûteux.

L'analyse a ajouté une contrainte : une déclaration de perte sans contrôle devient elle-même une porte de fuite.

## Décision

### 1. Pertes : déclaration, puis validation

- Table `stock_losses` : produit, entrepôt, quantité, motif (`loss_breakage`, `loss_expired`, `loss_offered`, `loss_staff_meal`, `loss_other`), notes, statut (`pending`, `approved`, `rejected`), déclarant, date de déclaration, validateur.
- **Un utilisateur sans `stock_losses_approve`** (le personnel) crée une déclaration `pending`. Elle **ne touche pas le stock** tant qu'elle n'est pas validée.
- **Un utilisateur avec `stock_losses_approve`** (admin, gérante) valide ou rejette. Ses propres déclarations sont validées d'office.
- La validation écrit un mouvement typé (`StockMovementService::record`, ADR-007) avec la perte comme source. Il est daté de la déclaration, ou d'aujourd'hui si cette période a été clôturée entre-temps par un inventaire. La contrainte unique `(source_type, source_id, reason)` empêche toute double application.

### 2. Inventaire : comptage, saisies, résultats

| Table | Rôle |
|---|---|
| `inventory_counts` | Un inventaire : entrepôt, référence, **instant du comptage** (`count_at`), périmètre (`full` ou `partial`), statut (`counting`, `submitted`, `validated`, `cancelled`), auteurs et horodatages |
| `inventory_count_items` | Un produit du périmètre. À la validation, il reçoit le compté, le théorique à `count_at`, l'écart, le coût unitaire figé et la valeur de l'écart |
| `inventory_count_entries` | Une saisie : produit × emplacement × quantité × compteur. Unique par `(inventaire, produit, emplacement)` : une nouvelle saisie remplace la précédente |
| `storage_locations` | Emplacements de comptage (bar, chambre froide, réserve). **Aide au comptage uniquement** : le stock reste tenu par entrepôt (ADR-006) |

- **Création** (gérante, `inventory_counts_validate`) : choix de l'instant du comptage (par défaut maintenant, jamais dans le futur ni avant le dernier verrou) et du périmètre. `partial` correspond à l'inventaire tournant sur une liste de produits.
- **Comptage en aveugle** (personnel, `inventory_counts_create`) : l'API de comptage renvoie les produits du périmètre et les saisies déjà faites, **jamais** le stock théorique ni l'écart.
- **Validation** (`inventory_counts_validate`), en une transaction, idempotente :
  1. ligne de l'inventaire verrouillée (`lockForUpdate`) ; un inventaire déjà validé est renvoyé tel quel ;
  2. pour chaque produit **effectivement compté**, compté = somme des saisies, théorique = `StockLedgerService::theoreticalStocksAt(count_at)`, écart = compté − théorique ;
  3. chaque écart non nul devient un mouvement `inventory_variance` daté de `count_at`, source = la ligne de résultat ;
  4. verrou de période posé à `count_at` (ADR-007).
- **Un produit du périmètre non compté** est signalé et laissé de côté. Il n'est pas compté à zéro : un zéro implicite fabriquerait une fausse perte.

### 3. Rapport d'écarts

Établi pour un inventaire validé, sur la période qui le sépare de l'inventaire validé précédent du même entrepôt :
- lignes triées par valeur d'écart absolue décroissante, avec la part cumulée du manque (lecture de Pareto : « deux tiers sur deux références ») ;
- totaux en FCFA du manque, de l'excédent et de l'écart net ;
- pertes expliquées validées sur la période, par motif, en quantité et en valeur (coût figé des mouvements) ;
- noms des compteurs.

### 4. Accès

| Action | Permission | Rôle par défaut |
|---|---|---|
| Déclarer une perte, compter | `stock_losses_create`, `inventory_counts_create` | personnel, admin |
| Valider une perte | `stock_losses_approve` | admin |
| Créer, valider, annuler un inventaire ; gérer les emplacements | `inventory_counts_validate` | admin |
| Lire le rapport d'écarts | `inventory_counts_view` | admin |

Toutes les routes portent en plus `check.feature:inventory`. Dans la SPA, le menu « Inventaire » n'apparaît que si `planFeatures.inventory_enabled` est vrai et que l'utilisateur a l'une de ces permissions.

## Alternatives Considérées

### Appliquer les pertes au stock dès leur déclaration
Rejeté. C'est la porte de fuite identifiée par l'analyse : un vol se déguise en casse sans que personne ne le voie. La validation coûte une action à la gérante, mais c'est précisément ce contrôle qui donne sa valeur à la distinction entre perte expliquée et inexpliquée.

### Compter les produits non saisis à zéro
Rejeté. Un oubli de saisie deviendrait une perte égale à tout le stock du produit, et le rapport accuserait l'équipe à tort.

### Tenir le stock par emplacement
Rejeté, comme dans l'ADR-006. Il faudrait saisir chaque transfert entre réserve et bar, à l'opposé de « réduire la saisie au strict minimum ».

### Calculer le théorique au moment de la validation plutôt qu'à l'instant du comptage
Rejeté (ADR-007 §4). Les ventes enregistrées entre le comptage et la validation produiraient de faux écarts.

## Conséquences

- Le droit `has_inventory` devient visible : menu « Inventaire » (Comptages, Pertes, Emplacements) pour les admins et pour le personnel, selon leurs permissions.
- Une validation d'inventaire clôture la période jusqu'à `count_at`. Les factures antérieures ne se corrigent plus que par un mouvement du jour (ADR-007), ce qui doit être annoncé aux utilisateurs.
- Le personnel dépend de la gérante pour que ses déclarations de pertes comptent : les pertes en attente doivent être visibles, avec un compteur dans le menu.
- Pas encore couverts : conditionnements à la saisie (lot 1), réceptionnaire dans le rapport (lot 7), export CSV du rapport.

## Quand Réviser

- Si les gérantes jugent la validation de chaque perte trop lourde : envisager un seuil de valeur sous lequel la perte s'applique d'office (réglage par tenant).
- Si les comptages durent plusieurs heures pendant le service : passer d'un instant unique `count_at` à un instant par saisie.
