inventory.prisma 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. model LaborPreset {
  2. id String @id @default(cuid())
  3. name String
  4. description String?
  5. isArchived Boolean @default(false)
  6. createdAt DateTime @default(now())
  7. updatedAt DateTime @updatedAt
  8. userId String
  9. organizationId String
  10. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  11. items LaborPresetItem[]
  12. parts LaborPresetPart[]
  13. @@index([organizationId])
  14. @@map("labor_presets")
  15. }
  16. model LaborPresetItem {
  17. id String @id @default(cuid())
  18. description String
  19. hours Float @default(0)
  20. rate Float @default(0)
  21. pricingType String @default("hourly")
  22. sortOrder Int @default(0)
  23. presetId String
  24. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  25. @@index([presetId])
  26. @@map("labor_preset_items")
  27. }
  28. model LaborPresetPart {
  29. id String @id @default(cuid())
  30. name String
  31. partNumber String?
  32. quantity Float @default(1)
  33. /// Unit of measure snapshotted from InventoryPart at pick time.
  34. unit String?
  35. /// Category snapshotted from InventoryPart at pick time; copied onto the
  36. /// part line when the preset is applied.
  37. category String?
  38. unitPrice Float @default(0)
  39. inventoryPartId String?
  40. sortOrder Int @default(0)
  41. presetId String
  42. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  43. @@index([presetId])
  44. @@map("labor_preset_parts")
  45. }
  46. model InventoryPart {
  47. id String @id @default(cuid())
  48. partNumber String?
  49. barcode String?
  50. name String
  51. description String?
  52. category String?
  53. quantity Float @default(0)
  54. minQuantity Float @default(0)
  55. /// Unit of measure for `quantity` (e.g. "pcs", "l", "kg"). Free text,
  56. /// snapshotted onto part lines at pick time so invoices keep the unit the
  57. /// part had when it was billed.
  58. unit String?
  59. unitCost Float @default(0)
  60. sellPrice Float @default(0)
  61. supplier String?
  62. supplierPhone String?
  63. supplierEmail String?
  64. supplierUrl String?
  65. imageUrl String?
  66. location String?
  67. isArchived Boolean @default(false)
  68. /// Free-text labels the workshop gives a part, such as the makes and models
  69. /// it fits ("Toyota", "Hilux 2015-2020"). Searched wherever parts are, and
  70. /// matched against the job's vehicle to list fitting parts first.
  71. tags String[] @default([])
  72. /// Set when a low-stock alert has been sent for the current dip below
  73. /// `minQuantity`, and cleared once stock climbs back above it. This
  74. /// hysteresis is what stops a part that sits at zero from re-alerting on
  75. /// every check — it alerts once per descent, not once per run.
  76. lowStockAlertedAt DateTime?
  77. createdAt DateTime @default(now())
  78. updatedAt DateTime @updatedAt
  79. userId String
  80. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  81. organizationId String?
  82. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  83. gallery StoredImage[] @relation("InventoryPartImages")
  84. movements StockMovement[]
  85. @@unique([organizationId, barcode])
  86. @@index([organizationId])
  87. @@map("inventory_parts")
  88. }
  89. /// Append-only audit trail of every change to InventoryPart.quantity.
  90. ///
  91. /// Written inside the same transaction as the stock mutation itself, so the
  92. /// ledger can never drift from the balance it describes. `quantityAfter` is
  93. /// captured from the atomic `UPDATE ... RETURNING`, giving an exact running
  94. /// balance even under concurrent writes.
  95. model StockMovement {
  96. id String @id @default(cuid())
  97. /// Signed change in stock units (fractional allowed, e.g. 0.5 l).
  98. /// Negative = consumed, positive = restocked.
  99. delta Float
  100. /// Part balance immediately after this movement was applied.
  101. quantityAfter Float
  102. /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
  103. reason String
  104. note String?
  105. inventoryPartId String
  106. inventoryPart InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
  107. /// The job the part was used on. Kept on SetNull so history survives
  108. /// deletion of the service record — "where was this used" outlives the job.
  109. serviceRecordId String?
  110. serviceRecord ServiceRecord? @relation(fields: [serviceRecordId], references: [id], onDelete: SetNull)
  111. /// Denormalised so history still reads correctly after a service record is
  112. /// deleted and serviceRecordId goes null.
  113. serviceRecordLabel String?
  114. userId String?
  115. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  116. organizationId String
  117. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  118. createdAt DateTime @default(now())
  119. @@index([inventoryPartId, createdAt(sort: Desc)])
  120. @@index([organizationId, createdAt(sort: Desc)])
  121. @@index([serviceRecordId])
  122. @@map("stock_movements")
  123. }
  124. model StoredImage {
  125. id String @id @default(cuid())
  126. url String
  127. fileName String?
  128. description String?
  129. sortOrder Int @default(0)
  130. createdAt DateTime @default(now())
  131. inventoryPartId String?
  132. inventoryPart InventoryPart? @relation("InventoryPartImages", fields: [inventoryPartId], references: [id], onDelete: Cascade)
  133. /// Condition photos taken when a tire is measured. Second parent on this
  134. /// model — an image belongs to exactly one of them.
  135. tireMeasurementId String?
  136. tireMeasurement TireMeasurement? @relation("TireMeasurementImages", fields: [tireMeasurementId], references: [id], onDelete: Cascade)
  137. @@index([inventoryPartId])
  138. @@index([tireMeasurementId])
  139. @@map("stored_images")
  140. }