# Module — Achats / Fournisseurs

> Fiche de conception. Voir le [catalogue des modules](README.md), les [conventions](../notes/conventions.md) et les [décisions (ADR)](../notes/decisions.md).

## 0. État d'implémentation — incrément 1 (2026-07-24) 🚧

Fournisseurs + **réceptions qui réapprovisionnent le stock**, livré et testé.

| Livré ✅ | Cible non encore faite ⬜ |
|---|---|
| `Supplier` (company-scopée, soft-delete : nom, tél, email, note) + API `/api/v1/suppliers` (liste/recherche, création, mise à jour) | fiche fournisseur, historique d'achats par fournisseur |
| `PurchaseOrder` / `PurchaseLine` (append-only, référence `A-AAAA-000000`, coût total, statut `received`) | commande en attente (draft → reçu partiel), retours fournisseur |
| API `POST /api/v1/purchase-orders` (réception) → **entrées de stock `in` automatiques** (via `StockService`) ; `GET` liste + détail | réception partielle, mise à jour du coût moyen pondéré |
| Gating : lecture membre (`COMPANY_VIEW`), réception/gestion manager (`COMPANY_EDIT`) | permission dédiée `purchase.*` (bascule RBAC phase 2) |
| PWA : **réapprovisionnement** depuis la fiche produit (quantité, coût unitaire, fournisseur optionnel + création rapide, **montant payé**) → stock mis à jour | écran achats dédié, multi-lignes, bons de commande |
| **Dettes fournisseurs** (B2) : réception à crédit (`amountPaidCents` < total → dette), `SupplierPayment` + `Supplier.balanceCents` + `PurchaseOrder.paidCents/dueCents` ; `GET /suppliers/creditors`, `GET/POST /{uuid}/payments` (règlement FIFO). Impact **trésorerie** (sorties). PWA : écran **Dettes fournisseurs** (créditeurs, compte, règlement) | échéances, comptabilité avancée |
| Tests `tests/Functional/Api/PurchaseApiTest.php` + `SupplierDebtApiTest.php` (réception à crédit, dette, règlement) | tests coût moyen/retours |

Fichiers : `src/Entity/{Supplier,PurchaseOrder,PurchaseLine}.php`,
`src/Controller/Api/V1/{SupplierController,PurchaseOrderController}.php`,
`yima-shop/src/app/features/catalogue/products.page` (section réappro), `core/services/suppliers.service.ts`.

## Objectif

Gérer l'approvisionnement de la boutique : **fournisseurs**, **commandes d'achat** et **réceptions** qui alimentent le stock. Suivre le **coût d'achat** pour la valorisation et la marge.

- **Manager** : gère les fournisseurs, crée les commandes d'achat, valide les réceptions.
- **Employé** : selon permission, saisit les réceptions à l'arrivée des marchandises (aucun accès par défaut).

## Périmètre

**In (v1)**
- CRUD `Supplier` scoped `Company`.
- Commande d'achat `PurchaseOrder` + lignes `PurchaseLine` (produit, quantité, coût unitaire).
- Cycle : **commande → réception partielle/totale → génération de `StockMovement` d'entrée**.
- Coût d'achat unitaire enregistré par ligne (base valorisation/marge).

**Out (v1)**
- Paiements fournisseurs / comptabilité fournisseur (échéances, soldes).
- Retours fournisseur, avoirs d'achat.
- Multi-dépôt de réception (dépend de [ADR-OPEN-E](../notes/decisions.md)).
- Réappro automatique / suggestions de commande.

## Entités

Rattachées à `Company` ; `TimestampableEntity` ; **UUID public** (exposées API).

- **`Supplier`** — *effaçable* : soft-delete Gedmo + index `idx_supplier_deleted`.
  - `id`, `uuid`, `company` (FK), `name`, `phone?`, `email?`, `address?`, `notes?`, `deletedAt`, timestamps.
- **`PurchaseOrder`** — cycle d'état, **non soft-deletable** (traçabilité achats ; annulation = statut).
  - `id`, `uuid`, `company` (FK), `supplier` (FK), `reference`, `status` (`draft` → `ordered` → `partially_received` → `received` / `cancelled`), `expectedAt?`, `totalCostCents` (int), `createdBy`, timestamps.
- **`PurchaseLine`**
  - `id`, `purchaseOrder` (FK), `product` (FK catalogue), `quantityOrdered` (int), `quantityReceived` (int, défaut 0), `unitCostCents` (int).
  - Ligne de coût en **centimes XOF**.
- **Réception** → produit des `StockMovement` d'entrée (`type=in`, référence `purchaseOrder`) portés par [Stock & inventaire](stock-inventaire.md). Pas d'entité « réception » lourde en v1 : la réception incrémente `quantityReceived` et émet les mouvements (traçables, append-only).

Index utiles : `PurchaseOrder(company, status)`, `PurchaseLine(purchaseOrder)`, `PurchaseLine(product)`.

## Permissions

| Code | Description | Rôles par défaut |
|---|---|---|
| `purchase.view` | Consulter fournisseurs et commandes | Manager |
| `purchase.create` | Créer/éditer fournisseurs et commandes | Manager |
| `purchase.receive` | Enregistrer une réception (→ stock) | Manager (option Employé) |

Gating : `denyAccessUnlessGranted('purchase.create', $company)` / `is_granted('purchase.receive', company)`. Scope `Company` **imposé serveur**.

## API

Base `/api/v1`, enveloppe `ApiResponseFormatter`. Créations sensibles : `Idempotency-Key`.

| Méthode | Route | Corps | Permission |
|---|---|---|---|
| GET | `/api/v1/suppliers?filter[q]=&page=&limit=` | — | `purchase.view` |
| POST | `/api/v1/suppliers` | `{ name, phone?, email?, address?, notes? }` | `purchase.create` |
| PUT | `/api/v1/suppliers/{uuid}` | `{ name?, phone?, email?, address?, notes? }` | `purchase.create` |
| DELETE | `/api/v1/suppliers/{uuid}` | — | `purchase.create` |
| GET | `/api/v1/purchase-orders?filter[status]=&page=&limit=` | — | `purchase.view` |
| POST | `/api/v1/purchase-orders` | `{ supplierUuid, expectedAt?, lines:[{ productUuid, quantityOrdered, unitCostCents }] }` | `purchase.create` |
| GET | `/api/v1/purchase-orders/{uuid}` | — | `purchase.view` |
| PUT | `/api/v1/purchase-orders/{uuid}` | `{ ...lignes/entête (si draft/ordered) }` | `purchase.create` |
| POST | `/api/v1/purchase-orders/{uuid}/receive` | `{ lines:[{ lineUuid, quantity }] }` | `purchase.receive` |
| POST | `/api/v1/purchase-orders/{uuid}/cancel` | — | `purchase.create` |

- Montants en **centimes XOF**. Validation → 422. Réception rejouable (idempotence).

## Écrans

**Web (Twig, espace Manager)**
- Liste fournisseurs + fiche (coordonnées, historique de commandes).
- Liste commandes d'achat (filtre par statut), fiche commande (lignes, coûts, total, avancement réception).
- Formulaire nouvelle commande (sélection produits, quantités, coûts).
- Écran de **réception** : saisie des quantités reçues par ligne (partielle/totale) → confirme et génère les mouvements de stock.

**PWA**
- Écran **réception** mobile (scan produit, saisie quantité reçue) pour le personnel en réserve, si `purchase.receive`. Le reste (création de commandes) est prioritairement web.

## Règles métier

- Fournisseur et commande appartiennent à **une** boutique ; jamais partagés.
- Transitions d'état : `draft` → `ordered` → `partially_received` → `received` ; `cancelled` possible tant que non totalement reçue. Édition des lignes bloquée après premier mouvement de réception.
- **Réception** : `quantityReceived ≤ quantityOrdered` par ligne ; chaque réception émet des `StockMovement` d'entrée (append-only, [ADR-006](../notes/decisions.md)) — jamais de suppression, une correction se fait par mouvement d'ajustement côté stock.
- Statut `received` quand toutes les lignes sont soldées ; sinon `partially_received`.
- **Coût d'achat** : `unitCostCents` alimente le coût moyen pondéré du produit (utilisé par valorisation/marge dans [Rapports & analytics](rapports-analytics.md)). Calcul du coût moyen à la réception.
- `totalCostCents` = Σ(`quantityOrdered` × `unitCostCents`) ; montants en **centimes XOF**.
- Suppression fournisseur = soft-delete ; interdite s'il porte des commandes non clôturées (ou conserver le lien historique).

## Projection e-commerce

**Non répliqué.** Achats et fournisseurs sont **internes** ; aucune donnée n'est exposée à la base client ni au site e-commerce ([ADR-001](../notes/decisions.md), [projection e-commerce](ecommerce-projection.md)). Seul l'effet indirect (stock disponible) transparaît via le module stock.

## Tests

- **Unitaires** : transitions d'état ; contrainte `quantityReceived ≤ quantityOrdered` ; calcul `totalCostCents` ; mise à jour du coût moyen pondéré à la réception.
- **Fonctionnels** : CRUD fournisseurs ; création commande ; réception partielle puis totale → **mouvements de stock générés** et quantités mises à jour ; idempotence de `receive` ; annulation.
- **Accès** : `purchase.*` refusées hors permission (403) ; **isolation boutique** ; un Employé sans `purchase.receive` ne peut pas réceptionner (403).

## Dépendances

- Socle : RBAC/permissions, `EntityManagerService`, `FileLogger`, `ApiResponseFormatter`, scope `Company`.
- [Catalogue produits](catalogue-produits.md) : produits référencés par les lignes.
- [Stock & inventaire](stock-inventaire.md) : génération des `StockMovement` d'entrée à la réception.
- Fournit le coût d'achat à [Rapports & analytics](rapports-analytics.md) (valorisation, marge).

## Notes / ADR liés

- [ADR-004](../notes/decisions.md) — Montants en centimes XOF (coûts d'achat).
- [ADR-006](../notes/decisions.md) — Mouvements de stock append-only.
- [ADR-001](../notes/decisions.md) — Interne, non projeté.
- [ADR-OPEN-E](../notes/decisions.md) — Multi-dépôt de réception (à trancher).
- Voir aussi : [modèle de données](../02-modele-de-donnees.md), [sécurité](../03-securite.md), [API](../04-api.md), [README projet](../README.md).
