feat(r3.1): socle backend gestion — stock dérivé, BC, coûts figés sur OT

- migration r3_gestion (7 tables) : Partner, Part (SANS colonne de
  quantité), StockMovement (signé, tracé, PU figé), PurchaseOrder/Line,
  LaborTime (taux figé), Document (R3.2) ; User.hourlyRate administrable
- API (66 opérations) : tiers ; pièces (stock = Σ mouvements, alerte sous
  seuil) ; entrée/ajustement (motif requis, stock jamais négatif, en
  transaction) ; BC Brouillon→Envoyé→Reçu (la réception crée les RECEIPT
  et met à jour lastUnitPrice) ; consommation sur OT (stock suffisant,
  PRIX FIGÉ) ; main-d'œuvre (TAUX FIGÉ, refus si taux non défini) ;
  WorkOrderDetail.costs ; coûts verrouillés après clôture (409)
- seed : tiers/pièces/BC/taux de la maquette — OT-0341 = 505 MAD (testé)
- durcissement : références OT/DEM/BC/P par SÉQUENCES Postgres (nextval)
  — fin des courses « max+1 » (500 sporadiques sous charge parallèle) ;
  3 runs Jest complets consécutifs verts
- 55 tests (92 % stmts / 74,9 % branches) dont la recette officielle :
  consommer sous seuil → BC → réception → réappro, prix/taux figés prouvés

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 20:42:03 +01:00
parent d5041c6f05
commit dfdf8f7c18
33 changed files with 5361 additions and 717 deletions

View File

@@ -56,6 +56,12 @@ model User {
requests Request[]
checklistDone ChecklistItem[]
meterReadings MeterReading[]
// R3 — gestion : taux horaire COURANT (le taux d'une saisie est figé dans LaborTime)
hourlyRate Decimal? @db.Decimal(8, 2)
laborTimes LaborTime[]
stockMovements StockMovement[]
purchaseOrders PurchaseOrder[]
documents Document[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -127,6 +133,7 @@ model Asset {
workOrders WorkOrder[]
requests Request[]
meters Meter[]
documents Document[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -225,6 +232,9 @@ model WorkOrder {
checklist ChecklistItem[]
report InterventionReport?
request Request?
stockMovements StockMovement[]
laborTimes LaborTime[]
documents Document[]
// Grille du mois : « AAAA-MM » — l'unicité [assetId, periodKey] EST
// l'idempotence de la génération (les OT correctifs restent à null).
periodKey String?
@@ -358,3 +368,147 @@ model MeterReading {
@@index([meterId])
}
// ————— R3 — Gestion (docs/03-architecture/modele-donnees.md §R3) —————
enum PartnerKind {
SUPPLIER
CLIENT // syndic / propriétaire
}
enum PurchaseOrderStatus {
DRAFT
SENT
RECEIVED
CANCELLED
}
enum StockMovementKind {
RECEIPT // réception de BC (+, PU figé)
ENTRY // entrée manuelle (+)
CONSUMPTION // consommation d'OT (, PU figé)
ADJUSTMENT // inventaire (±, motif REQUIS)
}
enum DocumentKind {
NOTICE
CERTIFICATE
PHOTO
OTHER
}
model Partner {
id String @id @default(uuid()) @db.Uuid
name String @unique
kind PartnerKind
contactName String?
phone String?
email String?
city String?
isActive Boolean @default(true)
parts Part[]
purchaseOrders PurchaseOrder[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Part {
id String @id @default(uuid()) @db.Uuid
reference String @unique // P-0113
designation String
threshold Int @default(0) // seuil d'alerte
// Dernier prix d'achat — figé sur chaque mouvement au moment T.
lastUnitPrice Decimal? @db.Decimal(10, 2)
compatible String?
supplierId String? @db.Uuid
supplier Partner? @relation(fields: [supplierId], references: [id])
isActive Boolean @default(true)
movements StockMovement[]
orderLines PurchaseOrderLine[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Le stock EST la somme des mouvements — aucune colonne de quantité.
model StockMovement {
id String @id @default(uuid()) @db.Uuid
partId String @db.Uuid
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
kind StockMovementKind
quantity Int // signé : + entrée, sortie
unitPrice Decimal? @db.Decimal(10, 2) // figé (réception, consommation)
reason String? // ajustement : motif requis (service)
workOrderId String? @db.Uuid
workOrder WorkOrder? @relation(fields: [workOrderId], references: [id])
purchaseOrderId String? @db.Uuid
purchaseOrder PurchaseOrder? @relation(fields: [purchaseOrderId], references: [id])
byId String? @db.Uuid
by User? @relation(fields: [byId], references: [id])
createdAt DateTime @default(now())
@@index([partId])
@@index([workOrderId])
}
model PurchaseOrder {
id String @id @default(uuid()) @db.Uuid
reference String @unique // BC-2026-0024
status PurchaseOrderStatus @default(DRAFT)
supplierId String @db.Uuid
supplier Partner @relation(fields: [supplierId], references: [id])
lines PurchaseOrderLine[]
movements StockMovement[]
sentAt DateTime?
receivedAt DateTime?
cancelledAt DateTime?
createdById String? @db.Uuid
createdBy User? @relation(fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PurchaseOrderLine {
id String @id @default(uuid()) @db.Uuid
purchaseOrderId String @db.Uuid
purchaseOrder PurchaseOrder @relation(fields: [purchaseOrderId], references: [id], onDelete: Cascade)
partId String @db.Uuid
part Part @relation(fields: [partId], references: [id])
quantity Int
unitPrice Decimal @db.Decimal(10, 2)
@@index([purchaseOrderId])
}
// Main-d'œuvre : le taux est FIGÉ à la saisie (le coût d'un OT ne bouge plus).
model LaborTime {
id String @id @default(uuid()) @db.Uuid
workOrderId String @db.Uuid
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
userId String @db.Uuid
user User @relation(fields: [userId], references: [id])
minutes Int
hourlyRate Decimal @db.Decimal(8, 2)
note String?
createdAt DateTime @default(now())
@@index([workOrderId])
}
model Document {
id String @id @default(uuid()) @db.Uuid
kind DocumentKind
fileName String
storageKey String @unique // clé MinIO (FileStorage)
size Int
contentType String
assetId String? @db.Uuid
asset Asset? @relation(fields: [assetId], references: [id])
workOrderId String? @db.Uuid
workOrder WorkOrder? @relation(fields: [workOrderId], references: [id])
uploadedById String? @db.Uuid
uploadedBy User? @relation(fields: [uploadedById], references: [id])
createdAt DateTime @default(now())
@@index([assetId])
@@index([workOrderId])
}