Skip to main content

datasynth_core/models/
cycle_count.rs

1//! Cycle count models for warehouse inventory management.
2//!
3//! These models represent cycle counting activities used to verify
4//! inventory accuracy without performing a full physical inventory.
5
6use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use super::graph_properties::{GraphPropertyValue, ToNodeProperties};
12
13/// Status of a cycle count through the counting lifecycle.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum CycleCountStatus {
17    /// Count has been planned but not yet started
18    #[default]
19    Planned,
20    /// Count is currently in progress
21    InProgress,
22    /// Physical count has been completed
23    Counted,
24    /// Variances have been investigated and reconciled
25    Reconciled,
26    /// Count has been closed and adjustments posted
27    Closed,
28}
29
30/// Classification of count variance severity.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum CountVarianceType {
34    /// No variance between book and counted quantities
35    #[default]
36    None,
37    /// Minor variance within acceptable tolerance
38    Minor,
39    /// Major variance requiring investigation
40    Major,
41    /// Critical variance requiring immediate action
42    Critical,
43}
44
45/// A cycle count event covering one or more inventory items.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct CycleCount {
48    /// Unique cycle count identifier
49    pub count_id: String,
50    /// Company code this count belongs to
51    pub company_code: String,
52    /// Warehouse where the count takes place
53    pub warehouse_id: String,
54    /// Date the count is performed
55    pub count_date: NaiveDate,
56    /// Current status of the cycle count
57    pub status: CycleCountStatus,
58    /// Employee performing the count
59    pub counter_id: Option<String>,
60    /// Supervisor overseeing the count
61    pub supervisor_id: Option<String>,
62    /// Individual items counted
63    pub items: Vec<CycleCountItem>,
64    /// Total number of items counted
65    pub total_items_counted: u32,
66    /// Total number of items with variances
67    pub total_variances: u32,
68    /// Overall variance rate (total_variances / total_items_counted)
69    pub variance_rate: f64,
70}
71
72impl ToNodeProperties for CycleCount {
73    fn node_type_name(&self) -> &'static str {
74        "cycle_count"
75    }
76    fn node_type_code(&self) -> u16 {
77        342
78    }
79    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
80        let mut p = HashMap::new();
81        p.insert(
82            "countId".into(),
83            GraphPropertyValue::String(self.count_id.clone()),
84        );
85        p.insert(
86            "entityCode".into(),
87            GraphPropertyValue::String(self.company_code.clone()),
88        );
89        p.insert(
90            "warehouseId".into(),
91            GraphPropertyValue::String(self.warehouse_id.clone()),
92        );
93        p.insert(
94            "countDate".into(),
95            GraphPropertyValue::Date(self.count_date),
96        );
97        p.insert(
98            "status".into(),
99            GraphPropertyValue::String(format!("{:?}", self.status)),
100        );
101        p.insert(
102            "totalItemsCounted".into(),
103            GraphPropertyValue::Int(self.total_items_counted as i64),
104        );
105        p.insert(
106            "totalVariances".into(),
107            GraphPropertyValue::Int(self.total_variances as i64),
108        );
109        p.insert(
110            "varianceRate".into(),
111            GraphPropertyValue::Float(self.variance_rate),
112        );
113        p.insert(
114            "isReconciled".into(),
115            GraphPropertyValue::Bool(matches!(
116                self.status,
117                CycleCountStatus::Reconciled | CycleCountStatus::Closed
118            )),
119        );
120        p
121    }
122}
123
124/// A single item within a cycle count.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct CycleCountItem {
127    /// Material being counted
128    pub material_id: String,
129    /// Material description (denormalized, DS-011)
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub material_description: Option<String>,
132    /// Storage location within the warehouse
133    pub storage_location: String,
134    /// Quantity recorded in the system
135    #[serde(with = "rust_decimal::serde::str")]
136    pub book_quantity: Decimal,
137    /// Quantity physically counted
138    #[serde(with = "rust_decimal::serde::str")]
139    pub counted_quantity: Decimal,
140    /// Difference between counted and book quantities
141    #[serde(with = "rust_decimal::serde::str")]
142    pub variance_quantity: Decimal,
143    /// Unit cost of the material
144    #[serde(with = "rust_decimal::serde::str")]
145    pub unit_cost: Decimal,
146    /// Monetary value of the variance
147    #[serde(with = "rust_decimal::serde::str")]
148    pub variance_value: Decimal,
149    /// Classification of variance severity
150    pub variance_type: CountVarianceType,
151    /// Whether an inventory adjustment has been posted
152    pub adjusted: bool,
153    /// Reason for the adjustment (if adjusted)
154    pub adjustment_reason: Option<String>,
155}