datasynth-core 2.3.0

Core domain models, traits, and distributions for synthetic enterprise data generation
Documentation
//! Production order models for manufacturing processes.
//!
//! These models represent production orders and their routing operations,
//! supporting the full manufacturing lifecycle from planning through completion.

use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::graph_properties::{GraphPropertyValue, ToNodeProperties};

/// Status of a production order through the manufacturing lifecycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ProductionOrderStatus {
    /// Order has been planned but not yet released
    Planned,
    /// Order has been released for execution
    Released,
    /// Order is currently being processed
    #[default]
    InProcess,
    /// Order production is completed
    Completed,
    /// Order has been closed and settled
    Closed,
    /// Order has been cancelled
    Cancelled,
}

/// Type of production order.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ProductionOrderType {
    /// Standard production run
    #[default]
    Standard,
    /// Rework of defective materials
    Rework,
    /// Prototype or trial production
    Prototype,
    /// Production triggered by a specific customer order
    MakeToOrder,
    /// Production for inventory replenishment
    MakeToStock,
}

/// A production order representing a manufacturing run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionOrder {
    /// Unique production order identifier
    pub order_id: String,
    /// Company code this order belongs to
    pub company_code: String,
    /// Material being produced
    pub material_id: String,
    /// Description of the material being produced
    pub material_description: String,
    /// Type of production order
    pub order_type: ProductionOrderType,
    /// Current status of the production order
    pub status: ProductionOrderStatus,
    /// Planned production quantity
    #[serde(with = "crate::serde_decimal")]
    pub planned_quantity: Decimal,
    /// Actual quantity produced
    #[serde(with = "crate::serde_decimal")]
    pub actual_quantity: Decimal,
    /// Quantity scrapped during production
    #[serde(with = "crate::serde_decimal")]
    pub scrap_quantity: Decimal,
    /// Planned start date
    pub planned_start: NaiveDate,
    /// Planned end date
    pub planned_end: NaiveDate,
    /// Actual start date (set when production begins)
    pub actual_start: Option<NaiveDate>,
    /// Actual end date (set when production completes)
    pub actual_end: Option<NaiveDate>,
    /// Work center responsible for production
    pub work_center: String,
    /// Optional routing identifier
    pub routing_id: Option<String>,
    /// Planned cost of production
    #[serde(with = "crate::serde_decimal")]
    pub planned_cost: Decimal,
    /// Actual cost incurred
    #[serde(with = "crate::serde_decimal")]
    pub actual_cost: Decimal,
    /// Detailed cost breakdown by component (material, labor, overhead).
    /// When present, `actual_cost` equals `cost_breakdown.total_actual()`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost_breakdown: Option<CostBreakdown>,
    /// Total labor hours consumed
    pub labor_hours: f64,
    /// Total machine hours consumed
    pub machine_hours: f64,
    /// Production yield rate (0.0 to 1.0)
    pub yield_rate: f64,
    /// Optional batch number for traceability
    pub batch_number: Option<String>,
    /// Routing operations for this production order
    pub operations: Vec<RoutingOperation>,
}

/// A single operation within a production routing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingOperation {
    /// Sequential operation number
    pub operation_number: u32,
    /// Description of the operation
    pub operation_description: String,
    /// Work center where the operation is performed
    pub work_center: String,
    /// Setup time in hours
    pub setup_time_hours: f64,
    /// Run time in hours
    pub run_time_hours: f64,
    /// Planned quantity for this operation
    #[serde(with = "crate::serde_decimal")]
    pub planned_quantity: Decimal,
    /// Actual quantity processed in this operation
    #[serde(with = "crate::serde_decimal")]
    pub actual_quantity: Decimal,
    /// Current status of the operation
    pub status: OperationStatus,
    /// Date the operation started
    pub started_at: Option<NaiveDate>,
    /// Date the operation completed
    pub completed_at: Option<NaiveDate>,
}

/// Breakdown of production order costs into constituent components.
///
/// Enables variance analysis: actual vs standard for each component.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CostBreakdown {
    /// Material cost (raw materials consumed)
    #[serde(with = "crate::serde_decimal")]
    pub material_cost: Decimal,
    /// Direct labor cost (hours × rate)
    #[serde(with = "crate::serde_decimal")]
    pub labor_cost: Decimal,
    /// Applied overhead (labor cost × overhead rate)
    #[serde(with = "crate::serde_decimal")]
    pub overhead_cost: Decimal,
    /// Standard material cost for variance calculation
    #[serde(with = "crate::serde_decimal")]
    pub standard_material_cost: Decimal,
    /// Standard labor cost for variance calculation
    #[serde(with = "crate::serde_decimal")]
    pub standard_labor_cost: Decimal,
    /// Standard overhead cost for variance calculation
    #[serde(with = "crate::serde_decimal")]
    pub standard_overhead_cost: Decimal,
    /// Standard cost per unit of output
    #[serde(with = "crate::serde_decimal")]
    pub standard_unit_cost: Decimal,
}

impl CostBreakdown {
    /// Total actual cost (sum of material + labor + overhead).
    pub fn total_actual(&self) -> Decimal {
        self.material_cost + self.labor_cost + self.overhead_cost
    }

    /// Total standard cost.
    pub fn total_standard(&self) -> Decimal {
        self.standard_material_cost + self.standard_labor_cost + self.standard_overhead_cost
    }

    /// Total variance (actual - standard).
    pub fn total_variance(&self) -> Decimal {
        self.total_actual() - self.total_standard()
    }
}

impl ToNodeProperties for ProductionOrder {
    fn node_type_name(&self) -> &'static str {
        "production_order"
    }
    fn node_type_code(&self) -> u16 {
        340
    }
    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
        let mut p = HashMap::new();
        p.insert(
            "orderNumber".into(),
            GraphPropertyValue::String(self.order_id.clone()),
        );
        p.insert(
            "entityCode".into(),
            GraphPropertyValue::String(self.company_code.clone()),
        );
        p.insert(
            "materialId".into(),
            GraphPropertyValue::String(self.material_id.clone()),
        );
        p.insert(
            "materialDescription".into(),
            GraphPropertyValue::String(self.material_description.clone()),
        );
        p.insert(
            "orderType".into(),
            GraphPropertyValue::String(format!("{:?}", self.order_type)),
        );
        p.insert(
            "status".into(),
            GraphPropertyValue::String(format!("{:?}", self.status)),
        );
        p.insert(
            "plannedQuantity".into(),
            GraphPropertyValue::Decimal(self.planned_quantity),
        );
        p.insert(
            "actualQuantity".into(),
            GraphPropertyValue::Decimal(self.actual_quantity),
        );
        p.insert(
            "scrapQuantity".into(),
            GraphPropertyValue::Decimal(self.scrap_quantity),
        );
        p.insert(
            "plannedStart".into(),
            GraphPropertyValue::Date(self.planned_start),
        );
        p.insert(
            "plannedEnd".into(),
            GraphPropertyValue::Date(self.planned_end),
        );
        p.insert(
            "plannedCost".into(),
            GraphPropertyValue::Decimal(self.planned_cost),
        );
        p.insert(
            "actualCost".into(),
            GraphPropertyValue::Decimal(self.actual_cost),
        );
        p.insert(
            "yieldRate".into(),
            GraphPropertyValue::Float(self.yield_rate),
        );
        p.insert(
            "operationCount".into(),
            GraphPropertyValue::Int(self.operations.len() as i64),
        );
        p.insert(
            "isComplete".into(),
            GraphPropertyValue::Bool(matches!(
                self.status,
                ProductionOrderStatus::Completed | ProductionOrderStatus::Closed
            )),
        );
        p
    }
}

/// Status of a routing operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OperationStatus {
    /// Operation has not yet started
    #[default]
    Pending,
    /// Operation is currently being executed
    InProcess,
    /// Operation has been completed
    Completed,
    /// Operation has been cancelled
    Cancelled,
}