inventory.prisma 5.4 KB

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