Skip to main content

stateset_core/models/
receiving.rs

1//! Receiving domain models
2//!
3//! Models for managing goods receipt, ASN processing, and put-away operations.
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9use strum::{Display, EnumString};
10use uuid::Uuid;
11
12// ============================================================================
13// Core Receiving Types
14// ============================================================================
15
16/// A goods receipt document.
17#[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    /// Reference to source document (PO, transfer, return)
24    pub reference_type: Option<String>,
25    pub reference_id: Option<Uuid>,
26    pub supplier_id: Option<Uuid>,
27    pub warehouse_id: i32,
28    /// Carrier info from ASN
29    pub carrier: Option<String>,
30    pub tracking_number: Option<String>,
31    /// Expected arrival from ASN
32    pub expected_date: Option<DateTime<Utc>>,
33    pub received_date: Option<DateTime<Utc>>,
34    pub completed_date: Option<DateTime<Utc>>,
35    /// Total items expected
36    pub expected_quantity: Decimal,
37    /// Total items received
38    pub received_quantity: Decimal,
39    /// Items requiring inspection
40    pub pending_inspection_quantity: Decimal,
41    /// Items put away to locations
42    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/// A line item on a receipt.
50#[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    /// Reference to PO line if applicable
58    pub po_line_id: Option<Uuid>,
59    /// Expected from ASN/PO
60    pub expected_quantity: Decimal,
61    pub received_quantity: Decimal,
62    pub rejected_quantity: Decimal,
63    pub unit_cost: Option<Decimal>,
64    /// Lot number assigned on receipt
65    pub lot_number: Option<String>,
66    /// Serial numbers received (comma-separated)
67    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/// Put-away record for received items.
76#[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// ============================================================================
95// Enums
96// ============================================================================
97
98/// Type of receipt.
99#[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/// Status of a receipt.
118#[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    /// ASN received, awaiting goods
124    #[default]
125    Expected,
126    /// Goods arrived, receiving in progress
127    InProgress,
128    /// All items received, pending put-away
129    Received,
130    /// Quality inspection in progress
131    Inspecting,
132    /// Put-away in progress
133    PuttingAway,
134    /// All items put away
135    Completed,
136    /// Receipt cancelled
137    Cancelled,
138}
139
140impl ReceiptStatus {
141    /// Whether a receipt in this status may still be cancelled.
142    ///
143    /// A receipt can only be cancelled before its goods are received — once it
144    /// reaches `Received` (or a later inspecting/put-away/completed state, or is
145    /// already cancelled) the goods are physically on hand, so it must not be
146    /// cancelled. Both backends gate `cancel_receipt` on this so they agree.
147    #[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/// Status of a receipt line item.
170#[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/// Status of a put-away task.
189#[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// ============================================================================
207// Input Types
208// ============================================================================
209
210/// Input for creating a receipt.
211#[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/// Input for creating a receipt item.
228#[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/// Input for receiving items.
241#[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/// A line in a receive operation.
250#[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/// Input for creating a put-away task.
263#[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/// Input for completing a put-away task.
277#[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/// Input for updating a receipt.
286#[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// ============================================================================
295// Filter Types
296// ============================================================================
297
298/// Filter for listing receipts.
299#[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/// Filter for listing put-aways.
313#[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
323// ============================================================================
324// Type Aliases for API compatibility
325// ============================================================================
326
327/// Alias for `CreateReceiptItem` for API convenience
328pub type CreateReceiptLine = CreateReceiptItem;
329
330// ============================================================================
331// Helper Functions
332// ============================================================================
333
334/// Generate a receipt number.
335#[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}