Skip to main content

datasynth_generators/industry/manufacturing/
master_data.rs

1//! Manufacturing master data structures.
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6/// Manufacturing industry settings.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ManufacturingSettings {
9    /// Bill of Materials depth (typical: 3-7).
10    pub bom_depth: u32,
11    /// Whether just-in-time inventory is used.
12    pub just_in_time: bool,
13    /// Production order types to generate.
14    pub production_order_types: Vec<String>,
15    /// Quality framework (ISO 9001, Six Sigma, etc.).
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub quality_framework: Option<String>,
18    /// Number of supplier tiers to model.
19    pub supplier_tiers: u32,
20    /// Standard cost update frequency (monthly, quarterly, annual).
21    pub standard_cost_frequency: String,
22    /// Target yield rate (0.95-0.99 typical).
23    pub target_yield_rate: f64,
24    /// Scrap percentage threshold for alerts.
25    pub scrap_alert_threshold: f64,
26}
27
28impl Default for ManufacturingSettings {
29    fn default() -> Self {
30        Self {
31            bom_depth: 4,
32            just_in_time: false,
33            production_order_types: vec![
34                "standard".to_string(),
35                "rework".to_string(),
36                "prototype".to_string(),
37            ],
38            quality_framework: Some("ISO_9001".to_string()),
39            supplier_tiers: 2,
40            standard_cost_frequency: "quarterly".to_string(),
41            target_yield_rate: 0.97,
42            scrap_alert_threshold: 0.03,
43        }
44    }
45}
46
47/// Bill of Materials for a product.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct BillOfMaterials {
50    /// Product/finished goods ID.
51    pub product_id: String,
52    /// Product name.
53    pub product_name: String,
54    /// BOM components.
55    pub components: Vec<BomComponent>,
56    /// Number of levels in the BOM.
57    pub levels: u32,
58    /// Expected yield rate (0.95-0.99).
59    pub yield_rate: f64,
60    /// Scrap factor (0.01-0.05).
61    pub scrap_factor: f64,
62    /// Effective date.
63    pub effective_date: String,
64    /// Version number.
65    pub version: u32,
66    /// Whether this is the active BOM.
67    pub is_active: bool,
68}
69
70impl BillOfMaterials {
71    /// Creates a new BOM.
72    pub fn new(product_id: impl Into<String>, product_name: impl Into<String>) -> Self {
73        Self {
74            product_id: product_id.into(),
75            product_name: product_name.into(),
76            components: Vec::new(),
77            levels: 1,
78            yield_rate: 0.97,
79            scrap_factor: 0.02,
80            effective_date: String::new(),
81            version: 1,
82            is_active: true,
83        }
84    }
85
86    /// Adds a component.
87    pub fn add_component(&mut self, component: BomComponent) {
88        // Update levels if this component has a deeper BOM
89        if component.bom_level >= self.levels {
90            self.levels = component.bom_level + 1;
91        }
92        self.components.push(component);
93    }
94
95    /// Calculates total material cost at standard.
96    pub fn total_material_cost(&self) -> Decimal {
97        self.components
98            .iter()
99            .map(|c| c.standard_cost * Decimal::from_f64_retain(c.quantity).unwrap_or(Decimal::ONE))
100            .sum()
101    }
102
103    /// Returns component count.
104    pub fn component_count(&self) -> usize {
105        self.components.len()
106    }
107}
108
109/// A component in a Bill of Materials.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct BomComponent {
112    /// Component material ID.
113    pub material_id: String,
114    /// Component name.
115    pub material_name: String,
116    /// Quantity required per unit of parent.
117    pub quantity: f64,
118    /// Unit of measure.
119    pub unit_of_measure: String,
120    /// BOM level (0 = direct component).
121    pub bom_level: u32,
122    /// Standard cost per unit.
123    pub standard_cost: Decimal,
124    /// Whether this is a phantom item (not stocked).
125    pub is_phantom: bool,
126    /// Scrap percentage for this component.
127    pub scrap_percentage: f64,
128    /// Lead time in days.
129    pub lead_time_days: u32,
130    /// Operation at which this is consumed (if routing-linked).
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub operation_number: Option<u32>,
133}
134
135impl BomComponent {
136    /// Creates a new BOM component.
137    pub fn new(
138        material_id: impl Into<String>,
139        material_name: impl Into<String>,
140        quantity: f64,
141        unit_of_measure: impl Into<String>,
142    ) -> Self {
143        Self {
144            material_id: material_id.into(),
145            material_name: material_name.into(),
146            quantity,
147            unit_of_measure: unit_of_measure.into(),
148            bom_level: 0,
149            standard_cost: Decimal::ZERO,
150            is_phantom: false,
151            scrap_percentage: 0.02,
152            lead_time_days: 5,
153            operation_number: None,
154        }
155    }
156
157    /// Sets the standard cost.
158    pub fn with_standard_cost(mut self, cost: Decimal) -> Self {
159        self.standard_cost = cost;
160        self
161    }
162
163    /// Sets the BOM level.
164    pub fn at_level(mut self, level: u32) -> Self {
165        self.bom_level = level;
166        self
167    }
168
169    /// Marks as phantom item.
170    pub fn as_phantom(mut self) -> Self {
171        self.is_phantom = true;
172        self
173    }
174
175    /// Sets the operation number.
176    pub fn at_operation(mut self, op: u32) -> Self {
177        self.operation_number = Some(op);
178        self
179    }
180}
181
182/// Manufacturing routing for a product.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct Routing {
185    /// Product ID this routing is for.
186    pub product_id: String,
187    /// Routing name/description.
188    pub name: String,
189    /// Routing operations.
190    pub operations: Vec<RoutingOperation>,
191    /// Effective date.
192    pub effective_date: String,
193    /// Version number.
194    pub version: u32,
195    /// Whether this is the active routing.
196    pub is_active: bool,
197}
198
199impl Routing {
200    /// Creates a new routing.
201    pub fn new(product_id: impl Into<String>, name: impl Into<String>) -> Self {
202        Self {
203            product_id: product_id.into(),
204            name: name.into(),
205            operations: Vec::new(),
206            effective_date: String::new(),
207            version: 1,
208            is_active: true,
209        }
210    }
211
212    /// Adds an operation.
213    pub fn add_operation(&mut self, operation: RoutingOperation) {
214        self.operations.push(operation);
215    }
216
217    /// Returns total standard labor time.
218    pub fn total_labor_time(&self) -> Decimal {
219        self.operations
220            .iter()
221            .map(|o| o.setup_time_minutes + o.run_time_per_unit)
222            .sum()
223    }
224
225    /// Returns total standard cost.
226    pub fn total_standard_cost(&self) -> Decimal {
227        self.operations
228            .iter()
229            .map(|o| {
230                let setup_cost = o.setup_time_minutes / Decimal::new(60, 0) * o.labor_rate;
231                let run_cost = o.run_time_per_unit / Decimal::new(60, 0) * o.labor_rate;
232                let machine_cost = o.run_time_per_unit / Decimal::new(60, 0) * o.machine_rate;
233                setup_cost + run_cost + machine_cost
234            })
235            .sum()
236    }
237}
238
239/// A routing operation.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct RoutingOperation {
242    /// Operation number (10, 20, 30, etc.).
243    pub operation_number: u32,
244    /// Operation description.
245    pub description: String,
246    /// Work center ID.
247    pub work_center: String,
248    /// Setup time in minutes.
249    pub setup_time_minutes: Decimal,
250    /// Run time per unit in minutes.
251    pub run_time_per_unit: Decimal,
252    /// Labor rate per hour.
253    pub labor_rate: Decimal,
254    /// Machine rate per hour.
255    pub machine_rate: Decimal,
256    /// Overlap percentage with previous operation (0-100).
257    pub overlap_percent: f64,
258    /// Move time to next operation in minutes.
259    pub move_time_minutes: Decimal,
260    /// Queue time before operation in minutes.
261    pub queue_time_minutes: Decimal,
262}
263
264impl RoutingOperation {
265    /// Creates a new routing operation.
266    pub fn new(
267        operation_number: u32,
268        description: impl Into<String>,
269        work_center: impl Into<String>,
270    ) -> Self {
271        Self {
272            operation_number,
273            description: description.into(),
274            work_center: work_center.into(),
275            setup_time_minutes: Decimal::new(30, 0),
276            run_time_per_unit: Decimal::new(10, 0),
277            labor_rate: Decimal::new(25, 0),
278            machine_rate: Decimal::new(15, 0),
279            overlap_percent: 0.0,
280            move_time_minutes: Decimal::new(5, 0),
281            queue_time_minutes: Decimal::new(60, 0),
282        }
283    }
284
285    /// Sets run time per unit.
286    pub fn with_run_time(mut self, minutes: Decimal) -> Self {
287        self.run_time_per_unit = minutes;
288        self
289    }
290
291    /// Sets labor rate.
292    pub fn with_labor_rate(mut self, rate: Decimal) -> Self {
293        self.labor_rate = rate;
294        self
295    }
296}
297
298/// Work center definition.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct WorkCenter {
301    /// Work center ID.
302    pub work_center_id: String,
303    /// Work center name.
304    pub name: String,
305    /// Department.
306    pub department: String,
307    /// Capacity in hours per day.
308    pub capacity_hours: Decimal,
309    /// Number of machines/resources.
310    pub resource_count: u32,
311    /// Efficiency percentage (0-100).
312    pub efficiency: f64,
313    /// Standard labor rate per hour.
314    pub labor_rate: Decimal,
315    /// Standard machine rate per hour.
316    pub machine_rate: Decimal,
317    /// Overhead rate per hour.
318    pub overhead_rate: Decimal,
319    /// Cost center for allocation.
320    pub cost_center: String,
321}
322
323impl WorkCenter {
324    /// Creates a new work center.
325    pub fn new(
326        id: impl Into<String>,
327        name: impl Into<String>,
328        department: impl Into<String>,
329    ) -> Self {
330        Self {
331            work_center_id: id.into(),
332            name: name.into(),
333            department: department.into(),
334            capacity_hours: Decimal::new(8, 0),
335            resource_count: 1,
336            efficiency: 85.0,
337            labor_rate: Decimal::new(25, 0),
338            machine_rate: Decimal::new(15, 0),
339            overhead_rate: Decimal::new(10, 0),
340            cost_center: String::new(),
341        }
342    }
343
344    /// Sets the cost center.
345    pub fn with_cost_center(mut self, cc: impl Into<String>) -> Self {
346        self.cost_center = cc.into();
347        self
348    }
349
350    /// Calculates total rate per hour.
351    pub fn total_rate(&self) -> Decimal {
352        self.labor_rate + self.machine_rate + self.overhead_rate
353    }
354}
355
356#[cfg(test)]
357#[allow(clippy::unwrap_used)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_bom() {
363        let mut bom = BillOfMaterials::new("FG001", "Finished Good 1");
364
365        bom.add_component(
366            BomComponent::new("RM001", "Raw Material 1", 2.0, "EA")
367                .with_standard_cost(Decimal::new(10, 0))
368                .at_level(0),
369        );
370        bom.add_component(
371            BomComponent::new("RM002", "Raw Material 2", 1.5, "KG")
372                .with_standard_cost(Decimal::new(5, 0))
373                .at_level(0),
374        );
375
376        assert_eq!(bom.component_count(), 2);
377        assert_eq!(bom.total_material_cost(), Decimal::new(275, 1)); // 2*10 + 1.5*5 = 27.5
378    }
379
380    #[test]
381    fn test_routing() {
382        let mut routing = Routing::new("FG001", "Standard Routing");
383
384        routing.add_operation(
385            RoutingOperation::new(10, "Cutting", "WC-CUT")
386                .with_run_time(Decimal::new(5, 0))
387                .with_labor_rate(Decimal::new(30, 0)),
388        );
389        routing.add_operation(RoutingOperation::new(20, "Assembly", "WC-ASM"));
390
391        assert_eq!(routing.operations.len(), 2);
392        assert!(routing.total_standard_cost() > Decimal::ZERO);
393    }
394
395    #[test]
396    fn test_work_center() {
397        let wc =
398            WorkCenter::new("WC-001", "Assembly Line 1", "Production").with_cost_center("CC-PROD");
399
400        assert_eq!(wc.total_rate(), Decimal::new(50, 0)); // 25 + 15 + 10
401    }
402
403    #[test]
404    fn test_manufacturing_settings() {
405        let settings = ManufacturingSettings::default();
406
407        assert_eq!(settings.bom_depth, 4);
408        assert!(!settings.just_in_time);
409        assert!(settings.target_yield_rate > 0.9);
410    }
411}