mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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:
@@ -0,0 +1,191 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PartnerKind" AS ENUM ('SUPPLIER', 'CLIENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PurchaseOrderStatus" AS ENUM ('DRAFT', 'SENT', 'RECEIVED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StockMovementKind" AS ENUM ('RECEIPT', 'ENTRY', 'CONSUMPTION', 'ADJUSTMENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentKind" AS ENUM ('NOTICE', 'CERTIFICATE', 'PHOTO', 'OTHER');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "hourlyRate" DECIMAL(8,2);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Partner" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"kind" "PartnerKind" NOT NULL,
|
||||
"contactName" TEXT,
|
||||
"phone" TEXT,
|
||||
"email" TEXT,
|
||||
"city" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Partner_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Part" (
|
||||
"id" UUID NOT NULL,
|
||||
"reference" TEXT NOT NULL,
|
||||
"designation" TEXT NOT NULL,
|
||||
"threshold" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastUnitPrice" DECIMAL(10,2),
|
||||
"compatible" TEXT,
|
||||
"supplierId" UUID,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Part_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StockMovement" (
|
||||
"id" UUID NOT NULL,
|
||||
"partId" UUID NOT NULL,
|
||||
"kind" "StockMovementKind" NOT NULL,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"unitPrice" DECIMAL(10,2),
|
||||
"reason" TEXT,
|
||||
"workOrderId" UUID,
|
||||
"purchaseOrderId" UUID,
|
||||
"byId" UUID,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "StockMovement_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PurchaseOrder" (
|
||||
"id" UUID NOT NULL,
|
||||
"reference" TEXT NOT NULL,
|
||||
"status" "PurchaseOrderStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"supplierId" UUID NOT NULL,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"receivedAt" TIMESTAMP(3),
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"createdById" UUID,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PurchaseOrder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PurchaseOrderLine" (
|
||||
"id" UUID NOT NULL,
|
||||
"purchaseOrderId" UUID NOT NULL,
|
||||
"partId" UUID NOT NULL,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"unitPrice" DECIMAL(10,2) NOT NULL,
|
||||
|
||||
CONSTRAINT "PurchaseOrderLine_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LaborTime" (
|
||||
"id" UUID NOT NULL,
|
||||
"workOrderId" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"minutes" INTEGER NOT NULL,
|
||||
"hourlyRate" DECIMAL(8,2) NOT NULL,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "LaborTime_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Document" (
|
||||
"id" UUID NOT NULL,
|
||||
"kind" "DocumentKind" NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"storageKey" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"contentType" TEXT NOT NULL,
|
||||
"assetId" UUID,
|
||||
"workOrderId" UUID,
|
||||
"uploadedById" UUID,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Partner_name_key" ON "Partner"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Part_reference_key" ON "Part"("reference");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockMovement_partId_idx" ON "StockMovement"("partId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StockMovement_workOrderId_idx" ON "StockMovement"("workOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PurchaseOrder_reference_key" ON "PurchaseOrder"("reference");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PurchaseOrderLine_purchaseOrderId_idx" ON "PurchaseOrderLine"("purchaseOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LaborTime_workOrderId_idx" ON "LaborTime"("workOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Document_storageKey_key" ON "Document"("storageKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Document_assetId_idx" ON "Document"("assetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Document_workOrderId_idx" ON "Document"("workOrderId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Part" ADD CONSTRAINT "Part_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "Partner"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_purchaseOrderId_fkey" FOREIGN KEY ("purchaseOrderId") REFERENCES "PurchaseOrder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StockMovement" ADD CONSTRAINT "StockMovement_byId_fkey" FOREIGN KEY ("byId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PurchaseOrder" ADD CONSTRAINT "PurchaseOrder_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PurchaseOrderLine" ADD CONSTRAINT "PurchaseOrderLine_purchaseOrderId_fkey" FOREIGN KEY ("purchaseOrderId") REFERENCES "PurchaseOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PurchaseOrderLine" ADD CONSTRAINT "PurchaseOrderLine_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LaborTime" ADD CONSTRAINT "LaborTime_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LaborTime" ADD CONSTRAINT "LaborTime_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_workOrderId_fkey" FOREIGN KEY ("workOrderId") REFERENCES "WorkOrder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Références OT/DEM/BC/P : séquences Postgres — la concurrence ne peut plus
|
||||
-- produire de collision (leçon des générations parallèles).
|
||||
-- La numérotation ne se remet pas à zéro chaque année : l'unicité prime.
|
||||
CREATE SEQUENCE IF NOT EXISTS "work_order_ref_seq";
|
||||
CREATE SEQUENCE IF NOT EXISTS "request_ref_seq";
|
||||
CREATE SEQUENCE IF NOT EXISTS "purchase_order_ref_seq";
|
||||
CREATE SEQUENCE IF NOT EXISTS "part_ref_seq";
|
||||
|
||||
SELECT setval('work_order_ref_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(SUBSTRING("reference" FROM '\d+$')::int), 0) FROM "WorkOrder"), 1000));
|
||||
SELECT setval('request_ref_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(SUBSTRING("reference" FROM '\d+$')::int), 0) FROM "Request"), 1000));
|
||||
SELECT setval('purchase_order_ref_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(SUBSTRING("reference" FROM '\d+$')::int), 0) FROM "PurchaseOrder"), 1000));
|
||||
SELECT setval('part_ref_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(SUBSTRING("reference" FROM '\d+$')::int), 0) FROM "Part"), 1000));
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ export async function seed(prisma: PrismaClient): Promise<void> {
|
||||
await seedUsers(prisma, roleIds, passwordHash);
|
||||
await seedReferentiel(prisma);
|
||||
await seedExploitation(prisma);
|
||||
await seedGestion(prisma);
|
||||
}
|
||||
|
||||
async function seedUsers(
|
||||
@@ -580,6 +581,177 @@ async function seedExploitation(prisma: PrismaClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ————— R3 — Gestion (tiers, pièces, mouvements, BC, taux — données maquette) —————
|
||||
|
||||
const PARTNERS: {
|
||||
name: string; kind: 'SUPPLIER' | 'CLIENT'; contactName?: string;
|
||||
phone?: string; email?: string; city?: string;
|
||||
}[] = [
|
||||
{ name: 'Ascentech Maroc', kind: 'SUPPLIER', contactName: 'M. Berrini', phone: '05 22 34 56 78', city: 'Casablanca' },
|
||||
{ name: 'SchindlerParts', kind: 'SUPPLIER', email: 'commandes@schindlerparts.ma' },
|
||||
{ name: 'Lubmaroc', kind: 'SUPPLIER', phone: '05 22 11 22 33' },
|
||||
{ name: 'Atlas Property Management', kind: 'CLIENT', contactName: 'Mme Zerhouni', phone: '06 61 98 76 54' },
|
||||
{ name: 'Syndic Al Manar', kind: 'CLIENT', contactName: 'M. Alami (gardien référent)' },
|
||||
];
|
||||
|
||||
const PARTS: {
|
||||
reference: string; designation: string; threshold: number;
|
||||
lastUnitPrice?: number; supplier?: string; compatible?: string;
|
||||
}[] = [
|
||||
{ reference: 'P-0019', designation: 'Cellule barrière porte (paire)', threshold: 4, lastUnitPrice: 640, supplier: 'SchindlerParts' },
|
||||
{ reference: 'P-0031', designation: 'Bouton palier lumineux Ø22', threshold: 10, lastUnitPrice: 45, supplier: 'Ascentech Maroc' },
|
||||
{ reference: 'P-0042', designation: 'Graisse guide (cartouche 400 g)', threshold: 8, lastUnitPrice: 85, supplier: 'Lubmaroc' },
|
||||
{ reference: 'P-0087', designation: 'Coulisseau de guide 16 mm', threshold: 6, lastUnitPrice: 120, supplier: 'Ascentech Maroc' },
|
||||
{ reference: 'P-0113', designation: 'Contact de porte NC-31', threshold: 5, lastUnitPrice: 85, supplier: 'Ascentech Maroc', compatible: 'Otis Gen2 · Schindler 3300' },
|
||||
];
|
||||
|
||||
async function seedGestion(prisma: PrismaClient): Promise<void> {
|
||||
const partnerIds = new Map<string, string>();
|
||||
for (const p of PARTNERS) {
|
||||
const partner = await prisma.partner.upsert({
|
||||
where: { name: p.name },
|
||||
update: {},
|
||||
create: p,
|
||||
});
|
||||
partnerIds.set(p.name, partner.id);
|
||||
}
|
||||
|
||||
const partIds = new Map<string, string>();
|
||||
for (const p of PARTS) {
|
||||
const part = await prisma.part.upsert({
|
||||
where: { reference: p.reference },
|
||||
update: {},
|
||||
create: {
|
||||
reference: p.reference,
|
||||
designation: p.designation,
|
||||
threshold: p.threshold,
|
||||
lastUnitPrice: p.lastUnitPrice,
|
||||
compatible: p.compatible,
|
||||
supplierId: p.supplier ? partnerIds.get(p.supplier) : undefined,
|
||||
},
|
||||
});
|
||||
partIds.set(p.reference, part.id);
|
||||
}
|
||||
|
||||
// Taux horaires courants (les saisies figent leur propre taux)
|
||||
for (const [email, taux] of [
|
||||
['technicien@demo.siop.ma', 120],
|
||||
['technicien-limite@demo.siop.ma', 90],
|
||||
] as const) {
|
||||
await prisma.user.update({ where: { email }, data: { hourlyRate: taux } });
|
||||
}
|
||||
|
||||
// Idempotence : si des mouvements existent déjà, l'histoire est en place
|
||||
if ((await prisma.stockMovement.count()) > 0) return;
|
||||
|
||||
const annee = new Date().getFullYear();
|
||||
const ahmed = await prisma.user.findUniqueOrThrow({ where: { email: 'technicien@demo.siop.ma' } });
|
||||
const nadia = await prisma.user.findUniqueOrThrow({ where: { email: 'gestionnaire@demo.siop.ma' } });
|
||||
|
||||
// BC reçus (historique) et BC en cours — comme la maquette
|
||||
const bc21 = await prisma.purchaseOrder.create({
|
||||
data: {
|
||||
reference: `BC-${annee}-0021`,
|
||||
status: 'RECEIVED',
|
||||
supplierId: partnerIds.get('Ascentech Maroc')!,
|
||||
createdById: nadia.id,
|
||||
sentAt: new Date(Date.now() - 40 * 86400e3),
|
||||
receivedAt: new Date(Date.now() - 34 * 86400e3),
|
||||
lines: {
|
||||
create: [
|
||||
{ partId: partIds.get('P-0113')!, quantity: 5, unitPrice: 85 },
|
||||
{ partId: partIds.get('P-0087')!, quantity: 6, unitPrice: 120 },
|
||||
{ partId: partIds.get('P-0031')!, quantity: 20, unitPrice: 45 },
|
||||
],
|
||||
},
|
||||
},
|
||||
include: { lines: true },
|
||||
});
|
||||
await prisma.purchaseOrder.create({
|
||||
data: {
|
||||
reference: `BC-${annee}-0024`,
|
||||
status: 'SENT',
|
||||
supplierId: partnerIds.get('Ascentech Maroc')!,
|
||||
createdById: nadia.id,
|
||||
sentAt: new Date(),
|
||||
lines: {
|
||||
create: [
|
||||
{ partId: partIds.get('P-0113')!, quantity: 10, unitPrice: 85 },
|
||||
{ partId: partIds.get('P-0087')!, quantity: 2, unitPrice: 127.5 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mouvements : réceptions du BC-0021 + entrées initiales + consommations
|
||||
for (const line of bc21.lines) {
|
||||
await prisma.stockMovement.create({
|
||||
data: {
|
||||
partId: line.partId,
|
||||
kind: 'RECEIPT',
|
||||
quantity: line.quantity,
|
||||
unitPrice: line.unitPrice,
|
||||
purchaseOrderId: bc21.id,
|
||||
byId: nadia.id,
|
||||
createdAt: new Date(Date.now() - 34 * 86400e3),
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const [ref, qte] of [
|
||||
['P-0042', 19],
|
||||
['P-0019', 7],
|
||||
['P-0031', 6],
|
||||
] as const) {
|
||||
await prisma.stockMovement.create({
|
||||
data: {
|
||||
partId: partIds.get(ref)!,
|
||||
kind: 'ENTRY',
|
||||
quantity: qte,
|
||||
reason: 'Reprise de l’inventaire initial',
|
||||
byId: nadia.id,
|
||||
createdAt: new Date(Date.now() - 60 * 86400e3),
|
||||
},
|
||||
});
|
||||
}
|
||||
// P-0113 : consommations + ajustement → stock 2 (sous le seuil, maquette)
|
||||
const ot341 = await prisma.workOrder.findUnique({
|
||||
where: { reference: `OT-${annee}-0341` },
|
||||
});
|
||||
await prisma.stockMovement.createMany({
|
||||
data: [
|
||||
{ partId: partIds.get('P-0113')!, kind: 'ADJUSTMENT', quantity: -1, reason: 'Inventaire — pièce endommagée', byId: nadia.id },
|
||||
{ partId: partIds.get('P-0113')!, kind: 'CONSUMPTION', quantity: -2, unitPrice: 85, byId: ahmed.id },
|
||||
],
|
||||
});
|
||||
// P-0087 : consommation sur OT-0341 (carte maquette : 2 × 120 = 240 MAD)
|
||||
if (ot341) {
|
||||
await prisma.stockMovement.create({
|
||||
data: {
|
||||
partId: partIds.get('P-0087')!,
|
||||
kind: 'CONSUMPTION',
|
||||
quantity: -2,
|
||||
unitPrice: 120,
|
||||
workOrderId: ot341.id,
|
||||
byId: ahmed.id,
|
||||
},
|
||||
});
|
||||
await prisma.stockMovement.create({
|
||||
data: {
|
||||
partId: partIds.get('P-0042')!,
|
||||
kind: 'CONSUMPTION',
|
||||
quantity: -1,
|
||||
unitPrice: 85,
|
||||
workOrderId: ot341.id,
|
||||
byId: ahmed.id,
|
||||
},
|
||||
});
|
||||
// main-d'œuvre : 1 h 30 × 120 MAD/h = 180 (total maquette : 505 MAD)
|
||||
await prisma.laborTime.create({
|
||||
data: { workOrderId: ot341.id, userId: ahmed.id, minutes: 90, hourlyRate: 120 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* c8 ignore start — wrapper CLI */
|
||||
if (require.main === module) {
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
Reference in New Issue
Block a user