1use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ManufacturingSettings {
9 pub bom_depth: u32,
11 pub just_in_time: bool,
13 pub production_order_types: Vec<String>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub quality_framework: Option<String>,
18 pub supplier_tiers: u32,
20 pub standard_cost_frequency: String,
22 pub target_yield_rate: f64,
24 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#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct BillOfMaterials {
50 pub product_id: String,
52 pub product_name: String,
54 pub components: Vec<BomComponent>,
56 pub levels: u32,
58 pub yield_rate: f64,
60 pub scrap_factor: f64,
62 pub effective_date: String,
64 pub version: u32,
66 pub is_active: bool,
68}
69
70impl BillOfMaterials {
71 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 pub fn add_component(&mut self, component: BomComponent) {
88 if component.bom_level >= self.levels {
90 self.levels = component.bom_level + 1;
91 }
92 self.components.push(component);
93 }
94
95 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 pub fn component_count(&self) -> usize {
105 self.components.len()
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct BomComponent {
112 pub material_id: String,
114 pub material_name: String,
116 pub quantity: f64,
118 pub unit_of_measure: String,
120 pub bom_level: u32,
122 pub standard_cost: Decimal,
124 pub is_phantom: bool,
126 pub scrap_percentage: f64,
128 pub lead_time_days: u32,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub operation_number: Option<u32>,
133}
134
135impl BomComponent {
136 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 pub fn with_standard_cost(mut self, cost: Decimal) -> Self {
159 self.standard_cost = cost;
160 self
161 }
162
163 pub fn at_level(mut self, level: u32) -> Self {
165 self.bom_level = level;
166 self
167 }
168
169 pub fn as_phantom(mut self) -> Self {
171 self.is_phantom = true;
172 self
173 }
174
175 pub fn at_operation(mut self, op: u32) -> Self {
177 self.operation_number = Some(op);
178 self
179 }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct Routing {
185 pub product_id: String,
187 pub name: String,
189 pub operations: Vec<RoutingOperation>,
191 pub effective_date: String,
193 pub version: u32,
195 pub is_active: bool,
197}
198
199impl Routing {
200 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 pub fn add_operation(&mut self, operation: RoutingOperation) {
214 self.operations.push(operation);
215 }
216
217 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct RoutingOperation {
242 pub operation_number: u32,
244 pub description: String,
246 pub work_center: String,
248 pub setup_time_minutes: Decimal,
250 pub run_time_per_unit: Decimal,
252 pub labor_rate: Decimal,
254 pub machine_rate: Decimal,
256 pub overlap_percent: f64,
258 pub move_time_minutes: Decimal,
260 pub queue_time_minutes: Decimal,
262}
263
264impl RoutingOperation {
265 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 pub fn with_run_time(mut self, minutes: Decimal) -> Self {
287 self.run_time_per_unit = minutes;
288 self
289 }
290
291 pub fn with_labor_rate(mut self, rate: Decimal) -> Self {
293 self.labor_rate = rate;
294 self
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct WorkCenter {
301 pub work_center_id: String,
303 pub name: String,
305 pub department: String,
307 pub capacity_hours: Decimal,
309 pub resource_count: u32,
311 pub efficiency: f64,
313 pub labor_rate: Decimal,
315 pub machine_rate: Decimal,
317 pub overhead_rate: Decimal,
319 pub cost_center: String,
321}
322
323impl WorkCenter {
324 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 pub fn with_cost_center(mut self, cc: impl Into<String>) -> Self {
346 self.cost_center = cc.into();
347 self
348 }
349
350 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)); }
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)); }
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}