1use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9use strum::{Display, EnumString};
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Receipt {
19 pub id: Uuid,
20 pub receipt_number: String,
21 pub receipt_type: ReceiptType,
22 pub status: ReceiptStatus,
23 pub reference_type: Option<String>,
25 pub reference_id: Option<Uuid>,
26 pub supplier_id: Option<Uuid>,
27 pub warehouse_id: i32,
28 pub carrier: Option<String>,
30 pub tracking_number: Option<String>,
31 pub expected_date: Option<DateTime<Utc>>,
33 pub received_date: Option<DateTime<Utc>>,
34 pub completed_date: Option<DateTime<Utc>>,
35 pub expected_quantity: Decimal,
37 pub received_quantity: Decimal,
39 pub pending_inspection_quantity: Decimal,
41 pub put_away_quantity: Decimal,
43 pub notes: Option<String>,
44 pub created_by: Option<String>,
45 pub created_at: DateTime<Utc>,
46 pub updated_at: DateTime<Utc>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ReceiptItem {
52 pub id: Uuid,
53 pub receipt_id: Uuid,
54 pub line_number: i32,
55 pub sku: String,
56 pub description: Option<String>,
57 pub po_line_id: Option<Uuid>,
59 pub expected_quantity: Decimal,
61 pub received_quantity: Decimal,
62 pub rejected_quantity: Decimal,
63 pub unit_cost: Option<Decimal>,
64 pub lot_number: Option<String>,
66 pub serial_numbers: Option<String>,
68 pub expiration_date: Option<DateTime<Utc>>,
69 pub status: ReceiptItemStatus,
70 pub notes: Option<String>,
71 pub created_at: DateTime<Utc>,
72 pub updated_at: DateTime<Utc>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct PutAway {
78 pub id: Uuid,
79 pub receipt_id: Uuid,
80 pub receipt_item_id: Uuid,
81 pub sku: String,
82 pub from_location_id: Option<i32>,
83 pub to_location_id: i32,
84 pub quantity: Decimal,
85 pub lot_id: Option<Uuid>,
86 pub status: PutAwayStatus,
87 pub assigned_to: Option<String>,
88 pub started_at: Option<DateTime<Utc>>,
89 pub completed_at: Option<DateTime<Utc>>,
90 pub notes: Option<String>,
91 pub created_at: DateTime<Utc>,
92}
93
94#[derive(
100 Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
101)]
102#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
103#[serde(rename_all = "snake_case")]
104#[non_exhaustive]
105pub enum ReceiptType {
106 #[default]
107 #[strum(serialize = "purchase_order", serialize = "purchaseorder", serialize = "po")]
108 PurchaseOrder,
109 Transfer,
110 #[strum(serialize = "return", serialize = "returns")]
111 Return,
112 Adjustment,
113 Production,
114 Other,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
119#[strum(serialize_all = "snake_case")]
120#[serde(rename_all = "snake_case")]
121#[non_exhaustive]
122pub enum ReceiptStatus {
123 #[default]
125 Expected,
126 InProgress,
128 Received,
130 Inspecting,
132 PuttingAway,
134 Completed,
136 Cancelled,
138}
139
140impl ReceiptStatus {
141 #[must_use]
148 pub const fn can_cancel(self) -> bool {
149 matches!(self, Self::Expected | Self::InProgress)
150 }
151}
152
153impl FromStr for ReceiptStatus {
154 type Err = String;
155 fn from_str(s: &str) -> Result<Self, Self::Err> {
156 match s.trim().to_ascii_lowercase().as_str() {
157 "expected" => Ok(Self::Expected),
158 "in_progress" | "inprogress" => Ok(Self::InProgress),
159 "received" => Ok(Self::Received),
160 "inspecting" => Ok(Self::Inspecting),
161 "putting_away" | "puttingaway" => Ok(Self::PuttingAway),
162 "completed" => Ok(Self::Completed),
163 "cancelled" | "canceled" => Ok(Self::Cancelled),
164 _ => Err(format!("Unknown receipt status: {s}")),
165 }
166 }
167}
168
169#[derive(
171 Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
172)]
173#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
174#[serde(rename_all = "snake_case")]
175#[non_exhaustive]
176pub enum ReceiptItemStatus {
177 #[default]
178 Pending,
179 #[strum(serialize = "partially_received", serialize = "partiallyreceived")]
180 PartiallyReceived,
181 Received,
182 Inspecting,
183 Rejected,
184 #[strum(serialize = "put_away", serialize = "putaway")]
185 PutAway,
186}
187
188#[derive(
190 Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
191)]
192#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
193#[serde(rename_all = "snake_case")]
194#[non_exhaustive]
195pub enum PutAwayStatus {
196 #[default]
197 Pending,
198 Assigned,
199 #[strum(serialize = "in_progress", serialize = "inprogress")]
200 InProgress,
201 Completed,
202 #[strum(serialize = "cancelled", serialize = "canceled")]
203 Cancelled,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
212pub struct CreateReceipt {
213 pub receipt_number: Option<String>,
214 pub receipt_type: ReceiptType,
215 pub reference_type: Option<String>,
216 pub reference_id: Option<Uuid>,
217 pub supplier_id: Option<Uuid>,
218 pub warehouse_id: i32,
219 pub carrier: Option<String>,
220 pub tracking_number: Option<String>,
221 pub expected_date: Option<DateTime<Utc>>,
222 pub notes: Option<String>,
223 pub created_by: Option<String>,
224 pub items: Vec<CreateReceiptItem>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, Default)]
229pub struct CreateReceiptItem {
230 pub sku: String,
231 pub description: Option<String>,
232 pub po_line_id: Option<Uuid>,
233 pub expected_quantity: Decimal,
234 pub unit_cost: Option<Decimal>,
235 pub lot_number: Option<String>,
236 pub expiration_date: Option<DateTime<Utc>>,
237 pub notes: Option<String>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct ReceiveItems {
243 pub receipt_id: Uuid,
244 pub items: Vec<ReceiveItemLine>,
245 pub receiving_location_id: Option<i32>,
246 pub received_by: Option<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ReceiveItemLine {
252 pub receipt_item_id: Uuid,
253 pub quantity_received: Decimal,
254 pub quantity_rejected: Option<Decimal>,
255 pub rejection_reason: Option<String>,
256 pub lot_number: Option<String>,
257 pub serial_numbers: Option<Vec<String>>,
258 pub expiration_date: Option<DateTime<Utc>>,
259 pub notes: Option<String>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct CreatePutAway {
265 pub receipt_id: Uuid,
266 pub receipt_item_id: Uuid,
267 pub sku: String,
268 pub from_location_id: Option<i32>,
269 pub to_location_id: i32,
270 pub quantity: Decimal,
271 pub lot_id: Option<Uuid>,
272 pub assigned_to: Option<String>,
273 pub notes: Option<String>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CompletePutAway {
279 pub put_away_id: Uuid,
280 pub actual_location_id: Option<i32>,
281 pub completed_by: Option<String>,
282 pub notes: Option<String>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, Default)]
287pub struct UpdateReceipt {
288 pub carrier: Option<String>,
289 pub tracking_number: Option<String>,
290 pub expected_date: Option<DateTime<Utc>>,
291 pub notes: Option<String>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, Default)]
300pub struct ReceiptFilter {
301 pub warehouse_id: Option<i32>,
302 pub receipt_type: Option<ReceiptType>,
303 pub status: Option<ReceiptStatus>,
304 pub supplier_id: Option<Uuid>,
305 pub reference_id: Option<Uuid>,
306 pub from_date: Option<DateTime<Utc>>,
307 pub to_date: Option<DateTime<Utc>>,
308 pub limit: Option<u32>,
309 pub offset: Option<u32>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, Default)]
314pub struct PutAwayFilter {
315 pub receipt_id: Option<Uuid>,
316 pub warehouse_id: Option<i32>,
317 pub status: Option<PutAwayStatus>,
318 pub assigned_to: Option<String>,
319 pub limit: Option<u32>,
320 pub offset: Option<u32>,
321}
322
323pub type CreateReceiptLine = CreateReceiptItem;
329
330#[must_use]
336pub fn generate_receipt_number() -> String {
337 let timestamp = chrono::Utc::now().format("%Y%m%d").to_string();
338 let random = &uuid::Uuid::new_v4().to_string()[..6].to_uppercase();
339 format!("RCV-{timestamp}-{random}")
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use std::str::FromStr;
346
347 #[test]
348 fn receipt_type_from_str() {
349 assert_eq!(ReceiptType::from_str("po").unwrap(), ReceiptType::PurchaseOrder);
350 assert!(ReceiptType::from_str("unknown").is_err());
351 }
352
353 #[test]
354 fn receipt_status_from_str() {
355 assert_eq!(ReceiptStatus::from_str("inprogress").unwrap(), ReceiptStatus::InProgress);
356 assert!(ReceiptStatus::from_str("unknown").is_err());
357 }
358
359 #[test]
360 fn receipt_item_status_from_str() {
361 assert_eq!(
362 ReceiptItemStatus::from_str("partiallyreceived").unwrap(),
363 ReceiptItemStatus::PartiallyReceived
364 );
365 assert!(ReceiptItemStatus::from_str("unknown").is_err());
366 }
367
368 #[test]
369 fn put_away_status_from_str() {
370 assert_eq!(PutAwayStatus::from_str("inprogress").unwrap(), PutAwayStatus::InProgress);
371 assert_eq!(PutAwayStatus::from_str("canceled").unwrap(), PutAwayStatus::Cancelled);
372 assert!(PutAwayStatus::from_str("unknown").is_err());
373 }
374}