Achats
Achats fournisseurs
Périmètre : devis fournisseurs (avec lignes de détail), inbox d'ingestion IA (OCR PDF → relecture humaine → commit), comparateur multi-devis, commandes fournisseurs (PO), réception → création d'assets, factures fournisseurs et rapprochement 3 voies, plus la boucle achats↔leasing (capital financé depuis devis retenu, commande auto à la signature de contrat, indicateur coût réel vs financé).
/workspace/purchasing/*, /workspace/supplier-* et /workspace/purchasing-overview. Routes API sous /api/workspace/purchasing/*, /api/workspace/supplier-quotes/*, /api/workspace/supplier-orders/*, /api/workspace/supplier-invoices/*, /api/workspace/lease-affairs/* et /api/workspace/lease-financings/*. Source de cette page : docs/specs/13-achats-fournisseurs-ia-leasing.md.Vocabulaire & entités
| Terme | Entité | Définition |
|---|---|---|
| Devis fournisseur | SupplierQuote | Offre tarifaire d'un fournisseur avec lignes de détail. Peut être rattaché à une affaire de leasing (N:N). |
| Ligne de devis | SupplierQuoteLine | Désignation, quantité, prix unitaire HT, TVA, remise, éco-participation, total calculé, lien catalogue optionnel. |
| Comparateur | LeaseAffairSupplierQuote | Table de jointure N:N affaire↔devis avec flag isRetained (au plus 1 par affaire). |
| Commande fournisseur (PO) | SupplierOrder | Commande d'achat avec lignes (SupplierOrderLine). Peut être générée automatiquement à la signature d'un contrat LLD. |
| Réception | — | Action sur SupplierOrder : saisie des quantités reçues par ligne → création d'Asset. |
| Facture fournisseur | SupplierInvoice | Facture reçue avec lignes (SupplierInvoiceLine). Rapprochement 3 voies : facture↔commande↔bon de réception. |
| Allocation contrat | SupplierInvoiceAllocation | Imputation d'une facture fournisseur vers un LeaseContract. |
| Import IA | SupplierDocumentImport | PDF déposé dans l'inbox ; passe par EXTRACTING → NEEDS_REVIEW → COMMITTED (ou ERROR/DISCARDED). |
| Capital depuis devis | LeaseFinancing.capitalHt | Capital LLD alimenté via Σ lignes HT du devis retenu (source tracée : capitalSource=SUPPLIER_QUOTE). |
Les totaux (
totalHt,totalVat,totalTtc) sont toujours recalculés serveur à partir des lignes (RG-13-01). Les champs stockés sont dénormalisés pour le tri/perf, jamais source de vérité.
Écrans
| Écran | Route | Contenu |
|---|---|---|
| Vue d'ensemble achats | /workspace/purchasing-overview | KPIs globaux achats/commandes/factures. |
| Inbox OCR | /workspace/purchasing/import | Dropzone multi-PDF + liste des imports avec statut et actions. |
| Relecture import | /workspace/purchasing/import/[id] | Form pré-rempli par l'IA (en-tête + grille de lignes éditable) ; commit ou rejet. |
| Liste devis fournisseurs | /workspace/supplier-quotes | DataTable avec filtres statut, fournisseur, montant. |
| Fiche devis | /workspace/supplier-quotes/[id] | Détail + grille SupplierLineGrid éditable. |
| Liste commandes | /workspace/supplier-orders | DataTable + filtres statut/contrat lié. |
| Fiche commande | /workspace/supplier-orders/[id] | Lignes + réception par ligne + lien contrat LLD. |
| Liste factures fournisseurs | /workspace/supplier-invoices | DataTable avec rapprochement et statut 3 voies. |
| Fiche facture | /workspace/supplier-invoices/[id] | Lignes + vue rapprochement 3 voies + allocation contrat. |
Modèle de données (points clés)
- Lignes de détail : 3 modèles iso-convention côté client —
SupplierQuoteLine,SupplierOrderLine,SupplierInvoiceLine. Chaque ligne porteposition,designation,quantity,unitPriceHt,vatRatePct,discountPct,ecoTaxHt,lineTotalHt(calculé),assetModelId?. - Traçabilité :
SupplierOrderLine.sourceQuoteLineId→ ligne de devis source ;SupplierInvoiceLine.sourceOrderLineId→ ligne de commande source (base du rapprochement 3 voies RG-13-12). - Inbox IA :
SupplierDocumentImportavecfileId(PDF),detectedType(enumQUOTE|INVOICE|CREDIT_NOTE|ORDER|UNKNOWN),status(enumEXTRACTING|NEEDS_REVIEW|COMMITTED|DISCARDED|ERROR),rawExtraction: Json?,matchedSupplierId?. Unicité par[organizationId, fileId](idempotence RG-13-04). - Boucle leasing :
LeaseAffairSupplierQuote(N:N,isRetained).LeaseFinancingétendu aveccapitalSource: CapitalSource(MANUAL|SUPPLIER_QUOTE) etsourceSupplierQuoteId?.SupplierOrderétendu avecleaseContractId?etleaseAffairId?. - Migration zero-downtime : colonnes nullables ajoutées en migration additive ; backfill des anciens documents sans lignes via
prisma/backfill/13-supplier-lines-reprise.ts(ligne uniquedesignation="Reprise — montant global").
API
Toutes les routes exigent les permissions purchasing.* ou les permissions existantes du domaine concerné.
| Méthode | Route | Permission |
|---|---|---|
POST | /api/workspace/purchasing/imports | purchasing.import:create |
GET | /api/workspace/purchasing/imports | purchasing.import:read |
GET | /api/workspace/purchasing/imports/[id] | purchasing.import:read |
POST | /api/workspace/purchasing/imports/[id]/reextract | purchasing.import:create |
POST | /api/workspace/purchasing/imports/[id]/commit | purchasing.import:commit |
DELETE | /api/workspace/purchasing/imports/[id] | purchasing.import:delete |
POST | /api/workspace/purchasing/imports/[id]/create-supplier-pappers | supplier:create |
PUT | /api/workspace/supplier-quotes/[id]/lines | supplier-quote:update |
PUT | /api/workspace/supplier-orders/[id]/lines | supplier-order:update |
PUT | /api/workspace/supplier-invoices/[id]/lines | supplier-invoice:update |
GET/POST | /api/workspace/lease-affairs/[id]/quotes | lease.affair:read / :update |
POST | /api/workspace/lease-affairs/[id]/quotes/[quoteId]/retain | lease.affair:update |
GET | /api/workspace/lease-affairs/[id]/quote-comparison | lease.affair:read |
POST | /api/workspace/lease-financings/[id]/capital-from-quote | lease.financing:update |
POST | /api/workspace/supplier-orders/[id]/receive | supplier-order:update |
GET | /api/workspace/supplier-invoices/[id]/match-lines | supplier-invoice:read |
POST | /api/workspace/supplier-invoices/[id]/allocate-contract | supplier-invoice:update |
Les lignes utilisent un PUT « set complet » (remplacement transactionnel de toutes les lignes + recalcul des totaux) plutôt qu'un CRUD ligne à ligne — cohérent avec une grille éditable UI, évite les désynchros de totaux.
Exemple de référence d'endpoint :
/api/workspace/lease-financings/[id]/capital-from-quoteAuth Prévisualise ou applique le remplissage du capital LLD depuis le devis retenu.
Corps (JSON)
::field{name="mode" type=""preview" | "apply"" required}
preview calcule sans persister ; apply persiste, pose capitalSource=SUPPLIER_QUOTE et audit-logge.
::
Requête
curl -s -X POST "$API/api/workspace/lease-financings/$FIN_ID/capital-from-quote" \
-H "Content-Type: application/json" -H "Cookie: $SESSION" \
-d '{"supplierQuoteId":"sqt_...","mode":"preview"}'
Réponse
{
"capitalHt": 18500,
"previousCapitalHt": 15000,
"periodicPmtHt": 412.50,
"previousPeriodicPmtHt": 338.20,
"totalInterestHt": 1450,
"deltaPmtPct": 22.0
}
Workflows
Inbox OCR IA (ingestion Scaleway pixtral-12b)
Dépôt PDF (1..N)
→ upload S3 + hash SHA-256 (idempotence RG-13-04)
→ SupplierDocumentImport { status: EXTRACTING }
→ job IA (purchase-document-extract) :
détection type + extraction en-tête + lignes
match SIRET → Supplier existant ou "Créer via Pappers"
→ status: NEEDS_REVIEW (ou ERROR si budget IA épuisé RG-13-05)
→ relecture humaine (form pré-rempli)
→ POST /commit { type, supplierId, documentNumber, lines[] }
→ SupplierQuote | SupplierInvoice créé + lignes
→ status: COMMITTED
L'IA propose, l'humain valide — aucune création automatique sans passage par
NEEDS_REVIEW(RG-13-03). Si le budget mensuel est épuisé, l'import passeERRORet la saisie manuelle reste disponible.
Comparateur multi-devis et capital LLD
LeaseAffair
→ POST /quotes (rattacher ≥ 1 devis)
→ GET /quote-comparison (tableau lignes comparées, moins-disant surligné)
→ POST /quotes/[id]/retain (isRetained=true, dé-retient l'ancien RG-13-07)
→ POST /lease-financings/[id]/capital-from-quote { mode:"preview" }
↓ aperçu PMT recalculé
→ POST ...capital-from-quote { mode:"apply" }
↓ capitalHt = Σ lignes HT, capitalSource=SUPPLIER_QUOTE
Commande automatique à la signature de contrat
LeaseContract → signature
→ onContractSigned() (hook service)
→ generateFromRetainedQuote(orgId, contractId)
↓ copie lignes du devis retenu
↓ SupplierOrder { status: DRAFT, leaseContractId, leaseAffairId }
[idempotent : 1 seul PO auto/contrat, re-signature sans effet RG-13-09]
→ Confirmation manuelle + envoi fournisseur (action "validate-and-send")
Réception → création d'assets
SupplierOrder { status: CONFIRMED }
→ POST /receive { lines: [{ orderLineId, receivedQuantity, serialNumbers? }] }
→ pour chaque ligne reçue :
Asset { acquisitionAmountHt=unitPriceHt, leaseContractId, modelId=assetModelId }
→ SupplierOrderLine.receivedQuantity cumulé (≤ quantity commandée RG-13-11)
→ SupplierOrder.status → RECEIVED | PARTIALLY_RECEIVED
Rapprochement 3 voies facture↔commande↔réception
SupplierInvoice + lignes (via inbox OCR ou saisie)
→ GET /match-lines
↓ écart par ligne via sourceOrderLineId → sourceQuoteLineId
tolérance 5 % → OK | MISMATCH
→ MISMATCH → blocage validation (override ADMIN avec motif obligatoire, audit)
→ POST /allocate-contract { leaseContractId }
↓ SupplierInvoiceAllocation (≤ totalHt facture RG-13-13)
↓ indicateur "coût réel vs financé" mis à jour sur le contrat
Règles métier
- RG-13-01 — Totaux
totalHt/totalVat/totalTtctoujours recalculés serveur à partir des lignes. Jamais saisis. - RG-13-02 —
lineTotalHt = round2(quantity × unitPriceHt × (1 − discountPct/100)) + ecoTaxHt. ArrondiROUND_HALF_UP2 décimales (cohérentserver/lib/finance.ts:roundCents). - RG-13-03 — Aucune création d'entité réelle sans validation humaine explicite (passage
NEEDS_REVIEW→commit). L'IA ne crée jamais en aveugle. - RG-13-04 —
SupplierDocumentImportidempotent par hash fichier (File.checksumSha256) par organisation : redéposer le même PDF réutilise l'import existant non-DISCARDED. - RG-13-05 — Extraction IA bloquée si
isAiBudgetExhausted()→ importERROR, message explicite, saisie manuelle disponible. - RG-13-06 — Un devis n'est rattachable qu'à une affaire de la même organisation.
- RG-13-07 — Au plus 1 devis
isRetained=trueparLeaseAffair. Retenir un nouveau devis dé-retient l'ancien (transaction). - RG-13-08 — Capital depuis devis exige
capitalSource=SUPPLIER_QUOTE. Modification manuelle ultérieure repassecapitalSource=MANUAL(avecsourceSupplierQuoteIdconservé pour audit). - RG-13-09/10 — Génération de commande à la signature : idempotente (1 seul PO auto par contrat), statut
DRAFT, envoi au fournisseur toujours manuel. - RG-13-11 — Assets créés à la réception (jamais à la signature). Réception partielle cumulable, jamais au-delà de la quantité commandée.
- RG-13-12 — Rapprochement 3 voies au niveau ligne via
sourceOrderLineId. Écart > 5 % →MISMATCH, bloquant sauf overrideADMIN(motif + audit). - RG-13-13 — Allocation facture → contrat ne dépasse jamais le
totalHtde la facture. - RG-13-14 — Type
UNKNOWNdétecté : l'écran de relecture s'ouvre mais l'utilisateur doit choisir le type avant le commit.
Codes d'erreur métier
| Code | HTTP | Description |
|---|---|---|
AI_BUDGET_EXHAUSTED | 402 | Budget IA mensuel épuisé — saisie manuelle disponible. |
IMPORT_ALREADY_COMMITTED | 409 | L'import a déjà été validé ; ré-extraction impossible. |
QUOTE_ALREADY_ATTACHED | 409 | Ce devis est déjà rattaché à l'affaire. |
QUOTE_NO_LINES | 422 | Le devis retenu n'a aucune ligne (bloque capital-from-quote). |
ORDER_ALREADY_GENERATED | 409 | Un PO a déjà été généré pour ce contrat. |
RECEIVE_OVER_QUANTITY | 422 | Quantité reçue supérieure à la quantité commandée. |
IA
Référence technique du domaine IA — providers (Scaleway/Gemini), workflows OCR/enrichissement, observabilité coûts et budget mensuel.
Ventes & Facturation
Référence technique du domaine Ventes & Facturation — devis, commandes, factures, encaissements, achats fournisseur, facturation électronique B2B (réforme FR 2026).