quotes.prisma 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. model Quote {
  2. id String @id @default(cuid())
  3. quoteNumber String?
  4. title String
  5. description String?
  6. status String @default("draft")
  7. validUntil DateTime?
  8. subtotal Float @default(0)
  9. taxRate Float @default(0)
  10. taxAmount Float @default(0)
  11. taxInclusive Boolean @default(false)
  12. /// See ServiceRecord.taxComponents.
  13. taxComponents Json?
  14. /// See ServiceRecord.taxableSubtotal.
  15. taxableSubtotal Float?
  16. discountType String?
  17. discountValue Float @default(0)
  18. discountAmount Float @default(0)
  19. totalAmount Float @default(0)
  20. notes String?
  21. /// The warranty the customer is being offered, stated before they accept.
  22. /// Same meaning as on ServiceRecord, and copied there on conversion. No
  23. /// expiry: the period starts with the work, which a quote has no date for.
  24. warrantyStatus String?
  25. warrantyMonths Int?
  26. warrantyMileage Int?
  27. warrantyNotes String?
  28. publicToken String?
  29. sharedAt DateTime?
  30. viewCount Int @default(0)
  31. lastViewedAt DateTime?
  32. customerMessage String?
  33. convertedToId String?
  34. createdAt DateTime @default(now())
  35. updatedAt DateTime @updatedAt
  36. /// Most recent time this quote reached the customer, by email or share
  37. /// link. Exists so an unlock is spent when the quote is issued again; the
  38. /// status alone cannot show that, because re-sending an accepted quote does
  39. /// not change it.
  40. sentAt DateTime?
  41. /// When the workshop dismissed the customer's answer from its list of
  42. /// responses. An acceptance keeps its status when dismissed, because it is
  43. /// the customer's agreement and may be what holds the quote locked, so the
  44. /// list needs its own record of it. Cleared whenever the customer answers
  45. /// again.
  46. responseDismissedAt DateTime?
  47. /// See ServiceRecord.editUnlockedAt.
  48. editUnlockedAt DateTime?
  49. editUnlockedById String?
  50. userId String
  51. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  52. organizationId String?
  53. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  54. customerId String?
  55. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  56. /// The stored set this quote is about, when it came out of the tire hotel.
  57. /// SetNull rather than Cascade: a quote already sent is a document the
  58. /// customer holds, and deleting the set must not erase it.
  59. tireSetId String?
  60. tireSet TireSet? @relation(fields: [tireSetId], references: [id], onDelete: SetNull)
  61. vehicleId String?
  62. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  63. inspectionId String?
  64. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  65. partItems QuotePart[]
  66. laborItems QuoteLabor[]
  67. attachments QuoteAttachment[]
  68. @@index([organizationId])
  69. @@map("quotes")
  70. }
  71. model QuoteAttachment {
  72. id String @id @default(cuid())
  73. fileName String
  74. fileUrl String
  75. fileType String
  76. fileSize Int
  77. category String @default("image")
  78. description String?
  79. includeInInvoice Boolean @default(true)
  80. createdAt DateTime @default(now())
  81. quoteId String
  82. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  83. @@index([quoteId])
  84. @@map("quote_attachments")
  85. }
  86. model QuotePart {
  87. id String @id @default(cuid())
  88. partNumber String?
  89. name String
  90. quantity Float @default(1)
  91. /// Unit of measure snapshotted from InventoryPart at pick time; null for
  92. /// free-text lines and legacy rows.
  93. unit String?
  94. /// Category snapshotted from InventoryPart at pick time, or typed on a
  95. /// free-text line; carried into ServicePart on conversion. See there.
  96. category String?
  97. /// Internal purchase cost — never shown to customers (quote PDF/share
  98. /// render unitPrice only)
  99. unitCost Float @default(0)
  100. markupPercent Float @default(0)
  101. unitPrice Float @default(0)
  102. total Float @default(0)
  103. excluded Boolean @default(false)
  104. /// Carried into ServicePart on conversion. See there.
  105. taxable Boolean @default(true)
  106. /// Optional link to the stocked part this line was picked from. Carried into
  107. /// ServicePart on conversion so the job deducts the right inventory item.
  108. inventoryPartId String?
  109. quoteId String
  110. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  111. @@index([quoteId])
  112. @@map("quote_parts")
  113. }
  114. model QuoteLabor {
  115. id String @id @default(cuid())
  116. description String
  117. hours Float @default(0)
  118. rate Float @default(0)
  119. total Float @default(0)
  120. pricingType String @default("hourly")
  121. excluded Boolean @default(false)
  122. /// Carried into ServiceLabor on conversion. See there.
  123. taxable Boolean @default(true)
  124. quoteId String
  125. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  126. @@index([quoteId])
  127. @@map("quote_labor")
  128. }