# ADR-011 : Conditionnements par produit

**Statut :** Accepté
**Date :** 2026-10-14
**Contexte :** Lot 1 de la feuille de route restauration — capacité générique (ADR-006), sur le socle des quantités `decimal(14,3)` (ADR-007 §5)

---

## Contexte

Un maquis achète la bière **au casier** et la vend **à la bouteille**. La gérante compte la réserve en casiers entamés : « 3 casiers + 5 bouteilles ». Aujourd'hui, GestionStock ne connaît qu'une unité par produit : il faut convertir de tête à chaque achat et à chaque comptage. Chaque conversion mentale est une source d'erreur, et une erreur de saisie devient un faux écart d'inventaire.

Le schéma contient déjà une ébauche de conversion : `units.parent_id`, `operator` et `operator_value`, ainsi que `order_items.unit_id`. Elle n'est **jamais appliquée** : les quantités sont additionnées brutes. Surtout, une unité est **globale au tenant**, alors qu'un casier vaut 12 bouteilles pour une marque et 24 pour une autre.

## Décision

### 1. Un conditionnement appartient à un produit

Nouvelle table `product_packagings` : produit, libellé (« Casier 24 »), **facteur** = nombre d'unités de stock contenues (24). Libellé unique par produit, facteur strictement positif, jusqu'à trois décimales (« Sac 25 kg » pour un produit tenu au kilo : facteur 25).

L'**unité de stock reste celle du produit**. Le stock, le recalcul `Common::recalculateOrderStock`, le stock théorique daté et les rapports ne voient que des unités de stock : **aucun calcul existant ne change**.

### 2. Achat en conditionnement : le serveur convertit et fige

Une ligne d'achat ou de retour d'achat peut porter `packaging_id` et `packaging_quantity` :
- le serveur vérifie que le conditionnement appartient au produit de la ligne (sinon 422) ;
- il calcule `quantity = packaging_quantity × facteur` et **ignore la quantité envoyée par le client** ;
- il fige sur la ligne `conversion_factor` et `packaging_label` : modifier ou supprimer le conditionnement plus tard ne réécrit pas l'historique (même principe que le coût figé de l'ADR-008) ;
- le prix saisi peut être un **prix par conditionnement** (`packaging_price`, sous `purchases_price_edit`, ADR-008). Le prix unitaire de stock vaut alors `packaging_price / facteur`, arrondi à 4 décimales pour que le total de la ligne retombe sur le montant facturé.

Les **ventes restent à l'unité** dans cette version : un conditionnement envoyé sur une vente est refusé (422). Vendre au carton demanderait un prix de vente par conditionnement, qui n'est pas demandé par le pilote.

### 3. Comptage en conditionnement

Une saisie d'inventaire peut porter `packaging_id`, `packaging_quantity` et `loose_quantity` (« 3 casiers + 5 ») :
- le serveur calcule `quantity = packaging_quantity × facteur + loose_quantity` et fige le facteur sur la saisie ;
- le détail est conservé pour la relecture ; seule `quantity` entre dans le calcul de l'écart (ADR-010 §2) ;
- la vue de comptage expose les conditionnements du produit : ce n'est pas une donnée de stock, l'aveugle est préservé.

### 4. Accès

| Action | Permission |
|---|---|
| Lister les conditionnements d'un produit | `products_view` |
| Créer, modifier, supprimer un conditionnement | `products_edit` |
| Acheter ou compter en conditionnement | permissions existantes de l'achat et du comptage |

Pas de droit de plan : c'est une capacité générique (ADR-006), utile à tout commerce qui achète en gros.

## Alternatives Considérées

### Réactiver `units.parent_id` / `operator_value`
Rejeté. Une unité « Casier » globale ne peut pas valoir 12 pour une marque et 24 pour une autre. Il faudrait créer « Casier 12 », « Casier 24 », « Pack 6 »… et rattacher chaque produit à la bonne unité, sans pouvoir garantir qu'une unité est compatible avec le produit. La structure reste en place, inutilisée, comme avant.

### Stocker la quantité en conditionnement et convertir au calcul
Rejeté. Le recalcul de stock, le théorique daté, les ventes et tous les rapports devraient convertir à la volée, et un facteur modifié réécrirait tout l'historique. Convertir une fois, à la saisie, et figer le facteur limite le changement aux points d'entrée.

### Conditionnements sur les ventes dès maintenant
Différé. Aucun besoin exprimé pour le pilote, et il faudrait un prix de vente par conditionnement ainsi que des règles de remise. L'ajout restera compatible : mêmes colonnes sur `order_items`.

## Conséquences

- Nouvelles colonnes nullables sur `order_items` (`packaging_id`, `packaging_label`, `packaging_quantity`, `conversion_factor`) et sur `inventory_count_entries` (`packaging_id`, `packaging_quantity`, `loose_quantity`, `conversion_factor`). Les lignes existantes restent à l'unité, sans migration de données.
- Le formulaire d'achat propose un choix « Unité / Casier 24 » par ligne et affiche l'équivalent en unités de stock.
- L'écran de comptage propose deux champs, conditionnement et unités, pour les produits qui ont des conditionnements.
- Un achat de 5 casiers à 12 000 FCFA donne 120 unités à 500 FCFA : le prix catalogue mis à jour à la réception (ADR-008) reste un prix **par unité de stock**.

## Quand Réviser

- Si des clients vendent au carton : ouvrir les conditionnements aux ventes, avec un prix de vente par conditionnement.
- Si un même produit est acheté sous plusieurs conditionnements chez des fournisseurs différents : rattacher le conditionnement au fournisseur.
