1use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use super::super::common::{IndustryGlAccount, IndustryJournalLine, IndustryTransaction};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum ProductionOrderType {
13 Standard,
15 Rework,
17 Prototype,
19 Repair,
21 Refurbishment,
23}
24
25impl ProductionOrderType {
26 pub fn code(&self) -> &'static str {
28 match self {
29 ProductionOrderType::Standard => "STD",
30 ProductionOrderType::Rework => "RWK",
31 ProductionOrderType::Prototype => "PRT",
32 ProductionOrderType::Repair => "REP",
33 ProductionOrderType::Refurbishment => "RFB",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40pub enum ScrapReason {
41 MaterialDefect,
43 MachineMalfunction,
45 OperatorError,
47 DesignIssue,
49 QualityFailure,
51 Contamination,
53 Obsolescence,
55 HandlingDamage,
57}
58
59impl ScrapReason {
60 pub fn code(&self) -> &'static str {
62 match self {
63 ScrapReason::MaterialDefect => "MAT",
64 ScrapReason::MachineMalfunction => "MCH",
65 ScrapReason::OperatorError => "OPR",
66 ScrapReason::DesignIssue => "DES",
67 ScrapReason::QualityFailure => "QUA",
68 ScrapReason::Contamination => "CON",
69 ScrapReason::Obsolescence => "OBS",
70 ScrapReason::HandlingDamage => "HND",
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub enum VarianceType {
78 MaterialPrice,
80 MaterialUsage,
82 LaborRate,
84 LaborEfficiency,
86 VariableOverheadSpending,
88 VariableOverheadEfficiency,
90 FixedOverheadBudget,
92 FixedOverheadVolume,
94}
95
96impl VarianceType {
97 pub fn code(&self) -> &'static str {
99 match self {
100 VarianceType::MaterialPrice => "MPV",
101 VarianceType::MaterialUsage => "MUV",
102 VarianceType::LaborRate => "LRV",
103 VarianceType::LaborEfficiency => "LEV",
104 VarianceType::VariableOverheadSpending => "VOSV",
105 VarianceType::VariableOverheadEfficiency => "VOEV",
106 VarianceType::FixedOverheadBudget => "FOBV",
107 VarianceType::FixedOverheadVolume => "FOVV",
108 }
109 }
110
111 pub fn account_suffix(&self) -> &'static str {
113 match self {
114 VarianceType::MaterialPrice => "510",
115 VarianceType::MaterialUsage => "520",
116 VarianceType::LaborRate => "530",
117 VarianceType::LaborEfficiency => "540",
118 VarianceType::VariableOverheadSpending => "550",
119 VarianceType::VariableOverheadEfficiency => "560",
120 VarianceType::FixedOverheadBudget => "570",
121 VarianceType::FixedOverheadVolume => "580",
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub enum ManufacturingTransaction {
129 WorkOrderIssuance {
132 order_id: String,
133 product_id: String,
134 quantity: u32,
135 order_type: ProductionOrderType,
136 date: NaiveDate,
137 },
138 MaterialRequisition {
140 order_id: String,
141 materials: Vec<MaterialLine>,
142 date: NaiveDate,
143 },
144 LaborBooking {
146 order_id: String,
147 work_center: String,
148 hours: Decimal,
149 labor_rate: Decimal,
150 date: NaiveDate,
151 },
152 OverheadAbsorption {
154 order_id: String,
155 absorption_rate: Decimal,
156 base_amount: Decimal,
157 date: NaiveDate,
158 },
159 ScrapReporting {
161 order_id: String,
162 material_id: String,
163 quantity: u32,
164 reason: ScrapReason,
165 scrap_value: Decimal,
166 date: NaiveDate,
167 },
168 ReworkOrder {
170 original_order_id: String,
171 rework_order_id: String,
172 quantity: u32,
173 estimated_cost: Decimal,
174 date: NaiveDate,
175 },
176 ProductionVariance {
178 order_id: String,
179 variance_type: VarianceType,
180 amount: Decimal,
181 date: NaiveDate,
182 },
183 ProductionCompletion {
185 order_id: String,
186 product_id: String,
187 quantity_completed: u32,
188 total_cost: Decimal,
189 date: NaiveDate,
190 },
191
192 RawMaterialReceipt {
195 po_id: String,
196 material_id: String,
197 quantity: u32,
198 unit_cost: Decimal,
199 date: NaiveDate,
200 },
201 WipTransfer {
203 from_center: String,
204 to_center: String,
205 order_id: String,
206 quantity: u32,
207 value: Decimal,
208 date: NaiveDate,
209 },
210 FinishedGoodsTransfer {
212 order_id: String,
213 product_id: String,
214 quantity: u32,
215 location: String,
216 unit_cost: Decimal,
217 date: NaiveDate,
218 },
219 CycleCountAdjustment {
221 material_id: String,
222 location: String,
223 variance_quantity: i32,
224 unit_cost: Decimal,
225 date: NaiveDate,
226 },
227
228 StandardCostRevaluation {
231 material_id: String,
232 old_cost: Decimal,
233 new_cost: Decimal,
234 inventory_quantity: u32,
235 date: NaiveDate,
236 },
237 PurchasePriceVariance {
239 material_id: String,
240 po_id: String,
241 standard_cost: Decimal,
242 actual_cost: Decimal,
243 quantity: u32,
244 date: NaiveDate,
245 },
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct MaterialLine {
251 pub material_id: String,
253 pub quantity: f64,
255 pub unit_of_measure: String,
257 pub standard_cost: Decimal,
259 pub location: String,
261}
262
263impl IndustryTransaction for ManufacturingTransaction {
264 fn transaction_type(&self) -> &str {
265 match self {
266 ManufacturingTransaction::WorkOrderIssuance { .. } => "work_order_issuance",
267 ManufacturingTransaction::MaterialRequisition { .. } => "material_requisition",
268 ManufacturingTransaction::LaborBooking { .. } => "labor_booking",
269 ManufacturingTransaction::OverheadAbsorption { .. } => "overhead_absorption",
270 ManufacturingTransaction::ScrapReporting { .. } => "scrap_reporting",
271 ManufacturingTransaction::ReworkOrder { .. } => "rework_order",
272 ManufacturingTransaction::ProductionVariance { .. } => "production_variance",
273 ManufacturingTransaction::ProductionCompletion { .. } => "production_completion",
274 ManufacturingTransaction::RawMaterialReceipt { .. } => "raw_material_receipt",
275 ManufacturingTransaction::WipTransfer { .. } => "wip_transfer",
276 ManufacturingTransaction::FinishedGoodsTransfer { .. } => "finished_goods_transfer",
277 ManufacturingTransaction::CycleCountAdjustment { .. } => "cycle_count_adjustment",
278 ManufacturingTransaction::StandardCostRevaluation { .. } => "standard_cost_revaluation",
279 ManufacturingTransaction::PurchasePriceVariance { .. } => "purchase_price_variance",
280 }
281 }
282
283 fn date(&self) -> NaiveDate {
284 match self {
285 ManufacturingTransaction::WorkOrderIssuance { date, .. }
286 | ManufacturingTransaction::MaterialRequisition { date, .. }
287 | ManufacturingTransaction::LaborBooking { date, .. }
288 | ManufacturingTransaction::OverheadAbsorption { date, .. }
289 | ManufacturingTransaction::ScrapReporting { date, .. }
290 | ManufacturingTransaction::ReworkOrder { date, .. }
291 | ManufacturingTransaction::ProductionVariance { date, .. }
292 | ManufacturingTransaction::ProductionCompletion { date, .. }
293 | ManufacturingTransaction::RawMaterialReceipt { date, .. }
294 | ManufacturingTransaction::WipTransfer { date, .. }
295 | ManufacturingTransaction::FinishedGoodsTransfer { date, .. }
296 | ManufacturingTransaction::CycleCountAdjustment { date, .. }
297 | ManufacturingTransaction::StandardCostRevaluation { date, .. }
298 | ManufacturingTransaction::PurchasePriceVariance { date, .. } => *date,
299 }
300 }
301
302 fn amount(&self) -> Option<Decimal> {
303 match self {
304 ManufacturingTransaction::LaborBooking {
305 hours, labor_rate, ..
306 } => Some(*hours * *labor_rate),
307 ManufacturingTransaction::ScrapReporting { scrap_value, .. } => Some(*scrap_value),
308 ManufacturingTransaction::ProductionVariance { amount, .. } => Some(*amount),
309 ManufacturingTransaction::ProductionCompletion { total_cost, .. } => Some(*total_cost),
310 ManufacturingTransaction::RawMaterialReceipt {
311 quantity,
312 unit_cost,
313 ..
314 } => Some(Decimal::from(*quantity) * *unit_cost),
315 ManufacturingTransaction::WipTransfer { value, .. } => Some(*value),
316 ManufacturingTransaction::FinishedGoodsTransfer {
317 quantity,
318 unit_cost,
319 ..
320 } => Some(Decimal::from(*quantity) * *unit_cost),
321 ManufacturingTransaction::CycleCountAdjustment {
322 variance_quantity,
323 unit_cost,
324 ..
325 } => Some(Decimal::from(*variance_quantity) * *unit_cost),
326 ManufacturingTransaction::StandardCostRevaluation {
327 old_cost,
328 new_cost,
329 inventory_quantity,
330 ..
331 } => Some((*new_cost - *old_cost) * Decimal::from(*inventory_quantity)),
332 ManufacturingTransaction::PurchasePriceVariance {
333 standard_cost,
334 actual_cost,
335 quantity,
336 ..
337 } => Some((*actual_cost - *standard_cost) * Decimal::from(*quantity)),
338 _ => None,
339 }
340 }
341
342 fn accounts(&self) -> Vec<String> {
343 match self {
344 ManufacturingTransaction::MaterialRequisition { .. } => {
345 vec!["1400".to_string(), "1300".to_string()] }
347 ManufacturingTransaction::LaborBooking { .. } => {
348 vec!["1400".to_string(), "2100".to_string()] }
350 ManufacturingTransaction::OverheadAbsorption { .. } => {
351 vec!["1400".to_string(), "5400".to_string()] }
353 ManufacturingTransaction::ScrapReporting { .. } => {
354 vec!["5200".to_string(), "1400".to_string()] }
356 ManufacturingTransaction::ProductionVariance { variance_type, .. } => {
357 vec![
358 format!("5{}", variance_type.account_suffix()),
359 "1400".to_string(),
360 ]
361 }
362 ManufacturingTransaction::ProductionCompletion { .. } => {
363 vec!["1500".to_string(), "1400".to_string()] }
365 ManufacturingTransaction::RawMaterialReceipt { .. } => {
366 vec!["1300".to_string(), "2000".to_string()] }
368 ManufacturingTransaction::FinishedGoodsTransfer { .. } => {
369 vec!["1500".to_string(), "1400".to_string()] }
371 ManufacturingTransaction::CycleCountAdjustment { .. } => {
372 vec!["5300".to_string(), "1300".to_string()] }
374 ManufacturingTransaction::StandardCostRevaluation { .. } => {
375 vec!["1300".to_string(), "5510".to_string()] }
377 ManufacturingTransaction::PurchasePriceVariance { .. } => {
378 vec!["5510".to_string(), "2000".to_string()] }
380 _ => Vec::new(),
381 }
382 }
383
384 fn to_journal_lines(&self) -> Vec<IndustryJournalLine> {
385 match self {
386 ManufacturingTransaction::MaterialRequisition { materials, .. } => {
387 let total: Decimal = materials
388 .iter()
389 .map(|m| {
390 m.standard_cost
391 * Decimal::from_f64_retain(m.quantity).unwrap_or(Decimal::ONE)
392 })
393 .sum();
394
395 vec![
396 IndustryJournalLine::debit("1400", total, "WIP - Material Issue"),
397 IndustryJournalLine::credit("1300", total, "Raw Materials Inventory"),
398 ]
399 }
400 ManufacturingTransaction::LaborBooking {
401 hours,
402 labor_rate,
403 work_center,
404 ..
405 } => {
406 let amount = *hours * *labor_rate;
407 vec![
408 IndustryJournalLine::debit("1400", amount, "WIP - Direct Labor")
409 .with_cost_center(work_center),
410 IndustryJournalLine::credit("2100", amount, "Wages Payable"),
411 ]
412 }
413 ManufacturingTransaction::ProductionCompletion {
414 total_cost,
415 product_id,
416 quantity_completed,
417 ..
418 } => {
419 vec![
420 IndustryJournalLine::debit("1500", *total_cost, "Finished Goods Inventory")
421 .with_dimension("product", product_id)
422 .with_dimension("quantity", quantity_completed.to_string()),
423 IndustryJournalLine::credit("1400", *total_cost, "WIP - Completion"),
424 ]
425 }
426 ManufacturingTransaction::ProductionVariance {
427 variance_type,
428 amount,
429 order_id,
430 ..
431 } => {
432 let account = format!("5{}", variance_type.account_suffix());
433 let desc = format!("{:?} - Order {}", variance_type, order_id);
434
435 if *amount >= Decimal::ZERO {
436 vec![
437 IndustryJournalLine::debit(&account, *amount, &desc),
438 IndustryJournalLine::credit("1400", *amount, "WIP - Variance"),
439 ]
440 } else {
441 vec![
442 IndustryJournalLine::debit("1400", amount.abs(), "WIP - Variance"),
443 IndustryJournalLine::credit(&account, amount.abs(), &desc),
444 ]
445 }
446 }
447 _ => Vec::new(),
448 }
449 }
450
451 fn metadata(&self) -> HashMap<String, String> {
452 let mut meta = HashMap::new();
453 meta.insert("industry".to_string(), "manufacturing".to_string());
454 meta.insert(
455 "transaction_type".to_string(),
456 self.transaction_type().to_string(),
457 );
458 meta
459 }
460}
461
462#[derive(Debug, Clone)]
464pub struct ManufacturingTransactionGenerator {
465 pub order_types: Vec<ProductionOrderType>,
467 pub avg_orders_per_day: f64,
469 pub avg_materials_per_order: u32,
471 pub scrap_rate: f64,
473 pub variance_rate: f64,
475}
476
477impl Default for ManufacturingTransactionGenerator {
478 fn default() -> Self {
479 Self {
480 order_types: vec![ProductionOrderType::Standard, ProductionOrderType::Rework],
481 avg_orders_per_day: 5.0,
482 avg_materials_per_order: 4,
483 scrap_rate: 0.02,
484 variance_rate: 0.15,
485 }
486 }
487}
488
489impl ManufacturingTransactionGenerator {
490 pub fn gl_accounts() -> Vec<IndustryGlAccount> {
492 vec![
493 IndustryGlAccount::new("1300", "Raw Materials Inventory", "Asset", "Inventory")
494 .into_control(),
495 IndustryGlAccount::new("1400", "Work in Process", "Asset", "Inventory").into_control(),
496 IndustryGlAccount::new("1500", "Finished Goods Inventory", "Asset", "Inventory")
497 .into_control(),
498 IndustryGlAccount::new("5100", "Cost of Goods Sold", "Expense", "COGS"),
499 IndustryGlAccount::new("5200", "Scrap and Spoilage", "Expense", "Manufacturing"),
500 IndustryGlAccount::new("5300", "Inventory Adjustments", "Expense", "Manufacturing"),
501 IndustryGlAccount::new(
502 "5400",
503 "Manufacturing Overhead Applied",
504 "Expense",
505 "Overhead",
506 )
507 .with_normal_balance("Credit"),
508 IndustryGlAccount::new("5510", "Material Price Variance", "Expense", "Variance"),
509 IndustryGlAccount::new("5520", "Material Usage Variance", "Expense", "Variance"),
510 IndustryGlAccount::new("5530", "Labor Rate Variance", "Expense", "Variance"),
511 IndustryGlAccount::new("5540", "Labor Efficiency Variance", "Expense", "Variance"),
512 IndustryGlAccount::new("5550", "Variable OH Spending Var", "Expense", "Variance"),
513 IndustryGlAccount::new("5560", "Variable OH Efficiency Var", "Expense", "Variance"),
514 IndustryGlAccount::new("5570", "Fixed OH Budget Variance", "Expense", "Variance"),
515 IndustryGlAccount::new("5580", "Fixed OH Volume Variance", "Expense", "Variance"),
516 ]
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_production_order_type() {
526 let std = ProductionOrderType::Standard;
527 assert_eq!(std.code(), "STD");
528
529 let rework = ProductionOrderType::Rework;
530 assert_eq!(rework.code(), "RWK");
531 }
532
533 #[test]
534 fn test_variance_type() {
535 let mpv = VarianceType::MaterialPrice;
536 assert_eq!(mpv.code(), "MPV");
537 assert_eq!(mpv.account_suffix(), "510");
538 }
539
540 #[test]
541 fn test_manufacturing_transaction() {
542 let tx = ManufacturingTransaction::ProductionCompletion {
543 order_id: "PO001".to_string(),
544 product_id: "FG001".to_string(),
545 quantity_completed: 100,
546 total_cost: Decimal::new(5000, 0),
547 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
548 };
549
550 assert_eq!(tx.transaction_type(), "production_completion");
551 assert_eq!(tx.amount(), Some(Decimal::new(5000, 0)));
552 assert_eq!(tx.accounts().len(), 2);
553 }
554
555 #[test]
556 fn test_journal_lines() {
557 let tx = ManufacturingTransaction::ProductionVariance {
558 order_id: "PO001".to_string(),
559 variance_type: VarianceType::MaterialPrice,
560 amount: Decimal::new(500, 0),
561 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
562 };
563
564 let lines = tx.to_journal_lines();
565 assert_eq!(lines.len(), 2);
566 assert_eq!(lines[0].debit, Decimal::new(500, 0));
567 assert_eq!(lines[1].credit, Decimal::new(500, 0));
568 }
569
570 #[test]
571 fn test_gl_accounts() {
572 let accounts = ManufacturingTransactionGenerator::gl_accounts();
573 assert!(accounts.len() >= 10);
574
575 let wip = accounts.iter().find(|a| a.account_number == "1400");
576 assert!(wip.is_some());
577 assert!(wip.unwrap().is_control);
578 }
579}