# ADR-014 : Coût moyen unitaire pondéré (CMUP)

**Statut :** Accepté
**Date :** 2026-10-15
**Contexte :** Lot 8 de la feuille de route restauration. Amende l'ADR-008 §3, qui valorisait au dernier prix d'achat « en v1, CMUP différé »

---

## Contexte

L'ADR-008 valorise les mouvements (pertes, écarts d'inventaire, consommations de recette) et le coût des ventes au **dernier prix d'achat**. C'était suffisant pour le pilote, mais faux dès que les prix bougent : 10 casiers achetés à 11 000 FCFA puis 1 casier à 13 000 FCFA font valoir tout le stock à 13 000 FCFA. Les écarts d'inventaire et la marge sont alors surévalués.

Le coût moyen unitaire pondéré est la méthode admise par le SYSCOHADA et la plus simple à expliquer à une gérante : « ce que me coûte en moyenne une bouteille en stock ».

## Décision

### 1. Calcul

Le CMUP d'un produit dans un entrepôt évolue **à chaque réception** :

> CMUP après réception = (stock avant × CMUP avant + quantité reçue × coût unitaire reçu) / (stock avant + quantité reçue)

- Le coût unitaire reçu est le prix unitaire **hors taxe, remise déduite** de la ligne d'achat (`single_unit_price`), ramené à l'unité de stock (ADR-011).
- Une réception compte à sa date de stock et pour sa quantité reçue (ADR-012).
- Les sorties (ventes, pertes, retours, consommations) ne changent pas le CMUP, seulement le stock.
- Si le stock avant réception est nul ou négatif, le CMUP repart du coût de la réception : on ne fait pas de moyenne avec une quantité négative.
- Le stock d'ouverture est valorisé au coût de la première réception, ou au prix d'achat catalogue s'il n'y en a pas.

### 2. Recalcul par rejeu, à chaque recalcul de stock

`AverageCostService::compute(entrepôt, produit)` rejoue en mémoire l'historique du produit : stock d'ouverture (fiche déjà chargée), lignes de commande et mouvements, triés par date de stock. Il lit 2 requêtes, quel que soit le volume ; `recalculateOrderStock` écrit le résultat dans `product_details.average_cost`.

Il est appelé à la fin de `Common::recalculateOrderStock`, donc **après tout changement de stock**. Modifier ou supprimer un achat ancien, ou une vente antérieure à une réception, corrige le CMUP de la même manière que le stock, sans chemin particulier à maintenir. Le résultat ne dépend que de l'historique et reste reproductible.

### 3. Utilisation

| Usage | Avant (ADR-008) | Maintenant |
|---|---|---|
| Coût figé des mouvements (`unit_cost`) : pertes, écarts, consommations | dernier prix d'achat HT | **CMUP**, à défaut dernier prix HT |
| Coût des ventes (`order_items.cost_price`) | prix d'achat catalogue | **CMUP**, à défaut prix catalogue |
| Coût matière des fiches techniques (ADR-013) | dernier prix HT | **CMUP**, à défaut dernier prix HT |
| Valorisation du stock | inexistante | stock × CMUP, nouvel écran |

Les coûts déjà figés (mouvements passés, `cost_price` des ventes passées) **ne changent pas** : le principe de l'ADR-008 §3 reste entier.

Le prix d'achat catalogue (`purchase_price`) garde son rôle de **prix proposé** à la saisie d'un achat (ADR-008 §2).

### 4. Valorisation du stock

Un écran « Valorisation » dans le menu Inventaire liste, par produit stocké, le stock, le CMUP et la valeur, avec le total de l'entrepôt. Il est réservé à `inventory_counts_view` : il révèle le stock théorique, que le personnel ne voit pas (ADR-006 §4).

## Alternatives Considérées

### FIFO (premier entré, premier sorti)
Rejeté pour l'instant. Il faut suivre des couches de coût par lot et les consommer dans l'ordre, ce qui alourdit chaque sortie. Pour des boissons et des denrées à rotation rapide, l'écart avec le CMUP est faible.

### Mise à jour incrémentale à chaque réception
Rejeté. Plus rapide, mais une modification ou une suppression d'achat ancien, ou une vente saisie à une date antérieure, laisserait un CMUP faux sans possibilité de le retrouver. Le rejeu coûte 2 requêtes et reste juste.

### Méthode de valorisation au choix du tenant
Rejeté. Deux méthodes, ce sont deux jeux de chiffres à expliquer et à tester. Le CMUP s'applique à tous. Sans historique de réception, il retombe sur le prix catalogue, comme avant.

## Conséquences

- Nouvelle colonne nullable `product_details.average_cost`. La migration la calcule pour les produits qui ont au moins une réception ; les autres restent à `NULL`, donc au prix catalogue.
- `recalculateOrderStock` lit 2 requêtes de plus par produit recalculé.
- La marge et les écarts valorisés deviennent plus justes quand les prix d'achat varient.
- Nouvel écran « Valorisation du stock ».

## Quand Réviser

- Si le rejeu devient lent pour des produits à très long historique : point de reprise (CMUP et stock figés à chaque inventaire validé, rejeu à partir du dernier).
- Si un client exige le FIFO pour sa comptabilité.
