use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManufacturingSettings {
pub bom_depth: u32,
pub just_in_time: bool,
pub production_order_types: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quality_framework: Option<String>,
pub supplier_tiers: u32,
pub standard_cost_frequency: String,
pub target_yield_rate: f64,
pub scrap_alert_threshold: f64,
}
impl Default for ManufacturingSettings {
fn default() -> Self {
Self {
bom_depth: 4,
just_in_time: false,
production_order_types: vec![
"standard".to_string(),
"rework".to_string(),
"prototype".to_string(),
],
quality_framework: Some("ISO_9001".to_string()),
supplier_tiers: 2,
standard_cost_frequency: "quarterly".to_string(),
target_yield_rate: 0.97,
scrap_alert_threshold: 0.03,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BillOfMaterials {
pub product_id: String,
pub product_name: String,
pub components: Vec<BomComponent>,
pub levels: u32,
pub yield_rate: f64,
pub scrap_factor: f64,
pub effective_date: String,
pub version: u32,
pub is_active: bool,
}
impl BillOfMaterials {
pub fn new(product_id: impl Into<String>, product_name: impl Into<String>) -> Self {
Self {
product_id: product_id.into(),
product_name: product_name.into(),
components: Vec::new(),
levels: 1,
yield_rate: 0.97,
scrap_factor: 0.02,
effective_date: String::new(),
version: 1,
is_active: true,
}
}
pub fn add_component(&mut self, component: BomComponent) {
if component.bom_level >= self.levels {
self.levels = component.bom_level + 1;
}
self.components.push(component);
}
pub fn total_material_cost(&self) -> Decimal {
self.components
.iter()
.map(|c| c.standard_cost * Decimal::from_f64_retain(c.quantity).unwrap_or(Decimal::ONE))
.sum()
}
pub fn component_count(&self) -> usize {
self.components.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BomComponent {
pub material_id: String,
pub material_name: String,
pub quantity: f64,
pub unit_of_measure: String,
pub bom_level: u32,
pub standard_cost: Decimal,
pub is_phantom: bool,
pub scrap_percentage: f64,
pub lead_time_days: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_number: Option<u32>,
}
impl BomComponent {
pub fn new(
material_id: impl Into<String>,
material_name: impl Into<String>,
quantity: f64,
unit_of_measure: impl Into<String>,
) -> Self {
Self {
material_id: material_id.into(),
material_name: material_name.into(),
quantity,
unit_of_measure: unit_of_measure.into(),
bom_level: 0,
standard_cost: Decimal::ZERO,
is_phantom: false,
scrap_percentage: 0.02,
lead_time_days: 5,
operation_number: None,
}
}
pub fn with_standard_cost(mut self, cost: Decimal) -> Self {
self.standard_cost = cost;
self
}
pub fn at_level(mut self, level: u32) -> Self {
self.bom_level = level;
self
}
pub fn as_phantom(mut self) -> Self {
self.is_phantom = true;
self
}
pub fn at_operation(mut self, op: u32) -> Self {
self.operation_number = Some(op);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Routing {
pub product_id: String,
pub name: String,
pub operations: Vec<RoutingOperation>,
pub effective_date: String,
pub version: u32,
pub is_active: bool,
}
impl Routing {
pub fn new(product_id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
product_id: product_id.into(),
name: name.into(),
operations: Vec::new(),
effective_date: String::new(),
version: 1,
is_active: true,
}
}
pub fn add_operation(&mut self, operation: RoutingOperation) {
self.operations.push(operation);
}
pub fn total_labor_time(&self) -> Decimal {
self.operations
.iter()
.map(|o| o.setup_time_minutes + o.run_time_per_unit)
.sum()
}
pub fn total_standard_cost(&self) -> Decimal {
self.operations
.iter()
.map(|o| {
let setup_cost = o.setup_time_minutes / Decimal::new(60, 0) * o.labor_rate;
let run_cost = o.run_time_per_unit / Decimal::new(60, 0) * o.labor_rate;
let machine_cost = o.run_time_per_unit / Decimal::new(60, 0) * o.machine_rate;
setup_cost + run_cost + machine_cost
})
.sum()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingOperation {
pub operation_number: u32,
pub description: String,
pub work_center: String,
pub setup_time_minutes: Decimal,
pub run_time_per_unit: Decimal,
pub labor_rate: Decimal,
pub machine_rate: Decimal,
pub overlap_percent: f64,
pub move_time_minutes: Decimal,
pub queue_time_minutes: Decimal,
}
impl RoutingOperation {
pub fn new(
operation_number: u32,
description: impl Into<String>,
work_center: impl Into<String>,
) -> Self {
Self {
operation_number,
description: description.into(),
work_center: work_center.into(),
setup_time_minutes: Decimal::new(30, 0),
run_time_per_unit: Decimal::new(10, 0),
labor_rate: Decimal::new(25, 0),
machine_rate: Decimal::new(15, 0),
overlap_percent: 0.0,
move_time_minutes: Decimal::new(5, 0),
queue_time_minutes: Decimal::new(60, 0),
}
}
pub fn with_run_time(mut self, minutes: Decimal) -> Self {
self.run_time_per_unit = minutes;
self
}
pub fn with_labor_rate(mut self, rate: Decimal) -> Self {
self.labor_rate = rate;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkCenter {
pub work_center_id: String,
pub name: String,
pub department: String,
pub capacity_hours: Decimal,
pub resource_count: u32,
pub efficiency: f64,
pub labor_rate: Decimal,
pub machine_rate: Decimal,
pub overhead_rate: Decimal,
pub cost_center: String,
}
impl WorkCenter {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
department: impl Into<String>,
) -> Self {
Self {
work_center_id: id.into(),
name: name.into(),
department: department.into(),
capacity_hours: Decimal::new(8, 0),
resource_count: 1,
efficiency: 85.0,
labor_rate: Decimal::new(25, 0),
machine_rate: Decimal::new(15, 0),
overhead_rate: Decimal::new(10, 0),
cost_center: String::new(),
}
}
pub fn with_cost_center(mut self, cc: impl Into<String>) -> Self {
self.cost_center = cc.into();
self
}
pub fn total_rate(&self) -> Decimal {
self.labor_rate + self.machine_rate + self.overhead_rate
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_bom() {
let mut bom = BillOfMaterials::new("FG001", "Finished Good 1");
bom.add_component(
BomComponent::new("RM001", "Raw Material 1", 2.0, "EA")
.with_standard_cost(Decimal::new(10, 0))
.at_level(0),
);
bom.add_component(
BomComponent::new("RM002", "Raw Material 2", 1.5, "KG")
.with_standard_cost(Decimal::new(5, 0))
.at_level(0),
);
assert_eq!(bom.component_count(), 2);
assert_eq!(bom.total_material_cost(), Decimal::new(275, 1)); }
#[test]
fn test_routing() {
let mut routing = Routing::new("FG001", "Standard Routing");
routing.add_operation(
RoutingOperation::new(10, "Cutting", "WC-CUT")
.with_run_time(Decimal::new(5, 0))
.with_labor_rate(Decimal::new(30, 0)),
);
routing.add_operation(RoutingOperation::new(20, "Assembly", "WC-ASM"));
assert_eq!(routing.operations.len(), 2);
assert!(routing.total_standard_cost() > Decimal::ZERO);
}
#[test]
fn test_work_center() {
let wc =
WorkCenter::new("WC-001", "Assembly Line 1", "Production").with_cost_center("CC-PROD");
assert_eq!(wc.total_rate(), Decimal::new(50, 0)); }
#[test]
fn test_manufacturing_settings() {
let settings = ManufacturingSettings::default();
assert_eq!(settings.bom_depth, 4);
assert!(!settings.just_in_time);
assert!(settings.target_yield_rate > 0.9);
}
}