Skip to main content

datasynth_core/models/
manufacturing_models.rs

1//! Manufacturing model structs for inventory movements.
2//!
3//! Complements the existing `production_order.rs`, `quality_inspection.rs`,
4//! and `cycle_count.rs` models with stock movement tracking.
5//! BOM components live in `material.rs` alongside the existing `BomComponent`.
6
7use chrono::NaiveDate;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use super::graph_properties::{GraphPropertyValue, ToNodeProperties};
13
14/// Type of inventory movement.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "snake_case")]
17pub enum MovementType {
18    /// Receipt from purchase order
19    #[default]
20    GoodsReceipt,
21    /// Issue to production order or cost center
22    GoodsIssue,
23    /// Transfer between storage locations
24    Transfer,
25    /// Return to vendor
26    Return,
27    /// Scrap / write-off
28    Scrap,
29    /// Inventory adjustment (cycle count, revaluation)
30    Adjustment,
31}
32
33/// A stock movement record (goods receipt, issue, transfer, etc.).
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct InventoryMovement {
36    /// Unique movement document ID
37    pub id: String,
38    /// Company / entity code
39    pub entity_code: String,
40    /// Material ID
41    pub material_code: String,
42    /// Material description
43    pub material_description: String,
44    /// Date of the movement
45    pub movement_date: NaiveDate,
46    /// Fiscal period (e.g. "2024-06")
47    pub period: String,
48    /// Movement type
49    pub movement_type: MovementType,
50    /// Quantity moved
51    #[serde(with = "rust_decimal::serde::str")]
52    pub quantity: Decimal,
53    /// Unit of measure
54    pub unit: String,
55    /// Total value of the movement
56    #[serde(with = "rust_decimal::serde::str")]
57    pub value: Decimal,
58    /// Currency code
59    pub currency: String,
60    /// Storage location
61    pub storage_location: String,
62    /// Reference document (PO, production order, etc.)
63    pub reference_doc: String,
64}
65
66impl InventoryMovement {
67    /// Create a new inventory movement.
68    #[allow(clippy::too_many_arguments)]
69    pub fn new(
70        id: impl Into<String>,
71        entity_code: impl Into<String>,
72        material_code: impl Into<String>,
73        material_description: impl Into<String>,
74        movement_date: NaiveDate,
75        period: impl Into<String>,
76        movement_type: MovementType,
77        quantity: Decimal,
78        unit: impl Into<String>,
79        value: Decimal,
80        currency: impl Into<String>,
81        storage_location: impl Into<String>,
82        reference_doc: impl Into<String>,
83    ) -> Self {
84        Self {
85            id: id.into(),
86            entity_code: entity_code.into(),
87            material_code: material_code.into(),
88            material_description: material_description.into(),
89            movement_date,
90            period: period.into(),
91            movement_type,
92            quantity,
93            unit: unit.into(),
94            value,
95            currency: currency.into(),
96            storage_location: storage_location.into(),
97            reference_doc: reference_doc.into(),
98        }
99    }
100}
101
102impl ToNodeProperties for InventoryMovement {
103    fn node_type_name(&self) -> &'static str {
104        "inventory_movement"
105    }
106    fn node_type_code(&self) -> u16 {
107        105
108    }
109    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
110        let mut p = HashMap::new();
111        p.insert(
112            "entityCode".into(),
113            GraphPropertyValue::String(self.entity_code.clone()),
114        );
115        p.insert(
116            "materialCode".into(),
117            GraphPropertyValue::String(self.material_code.clone()),
118        );
119        p.insert(
120            "materialDescription".into(),
121            GraphPropertyValue::String(self.material_description.clone()),
122        );
123        p.insert(
124            "movementDate".into(),
125            GraphPropertyValue::Date(self.movement_date),
126        );
127        p.insert(
128            "period".into(),
129            GraphPropertyValue::String(self.period.clone()),
130        );
131        p.insert(
132            "movementType".into(),
133            GraphPropertyValue::String(format!("{:?}", self.movement_type)),
134        );
135        p.insert(
136            "quantity".into(),
137            GraphPropertyValue::Decimal(self.quantity),
138        );
139        p.insert("unit".into(), GraphPropertyValue::String(self.unit.clone()));
140        p.insert("value".into(), GraphPropertyValue::Decimal(self.value));
141        p.insert(
142            "currency".into(),
143            GraphPropertyValue::String(self.currency.clone()),
144        );
145        p.insert(
146            "storageLocation".into(),
147            GraphPropertyValue::String(self.storage_location.clone()),
148        );
149        p.insert(
150            "referenceDoc".into(),
151            GraphPropertyValue::String(self.reference_doc.clone()),
152        );
153        p
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_inventory_movement_properties() {
163        let mv = InventoryMovement::new(
164            "MV-001",
165            "C001",
166            "MAT-100",
167            "Widget A",
168            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
169            "2024-06",
170            MovementType::GoodsReceipt,
171            Decimal::new(100, 0),
172            "EA",
173            Decimal::new(5000, 0),
174            "USD",
175            "WH01",
176            "PO-12345",
177        );
178        let props = mv.to_node_properties();
179        assert_eq!(mv.node_type_name(), "inventory_movement");
180        assert_eq!(mv.node_type_code(), 105);
181        assert!(props.contains_key("movementType"));
182        assert!(props.contains_key("storageLocation"));
183        assert_eq!(
184            props["quantity"],
185            GraphPropertyValue::Decimal(Decimal::new(100, 0))
186        );
187    }
188}