1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
//! 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,
}