Skip to main content

datasynth_generators/industry/manufacturing/
transactions.rs

1//! Manufacturing transaction types.
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use super::super::common::{IndustryGlAccount, IndustryJournalLine, IndustryTransaction};
9
10/// Production order type.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum ProductionOrderType {
13    /// Standard production order.
14    Standard,
15    /// Rework order for defective products.
16    Rework,
17    /// Prototype/engineering order.
18    Prototype,
19    /// Repair order for customer returns.
20    Repair,
21    /// Refurbishment order.
22    Refurbishment,
23}
24
25impl ProductionOrderType {
26    /// Returns the order type code.
27    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/// Scrap reason codes.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40pub enum ScrapReason {
41    /// Material defect.
42    MaterialDefect,
43    /// Machine malfunction.
44    MachineMalfunction,
45    /// Operator error.
46    OperatorError,
47    /// Design issue.
48    DesignIssue,
49    /// Quality spec failure.
50    QualityFailure,
51    /// Contamination.
52    Contamination,
53    /// Obsolescence.
54    Obsolescence,
55    /// Damage in handling.
56    HandlingDamage,
57}
58
59impl ScrapReason {
60    /// Returns the reason code.
61    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/// Variance type for production cost analysis.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub enum VarianceType {
78    /// Material price variance (actual vs standard price).
79    MaterialPrice,
80    /// Material usage variance (actual vs standard quantity).
81    MaterialUsage,
82    /// Labor rate variance.
83    LaborRate,
84    /// Labor efficiency variance.
85    LaborEfficiency,
86    /// Variable overhead spending variance.
87    VariableOverheadSpending,
88    /// Variable overhead efficiency variance.
89    VariableOverheadEfficiency,
90    /// Fixed overhead budget variance.
91    FixedOverheadBudget,
92    /// Fixed overhead volume variance.
93    FixedOverheadVolume,
94}
95
96impl VarianceType {
97    /// Returns the variance type code.
98    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    /// Returns the GL account suffix for this variance.
112    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/// Manufacturing transaction types.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub enum ManufacturingTransaction {
129    // ========== Production Transactions ==========
130    /// Creation of a production order.
131    WorkOrderIssuance {
132        order_id: String,
133        product_id: String,
134        quantity: u32,
135        order_type: ProductionOrderType,
136        date: NaiveDate,
137    },
138    /// Issuance of materials to production.
139    MaterialRequisition {
140        order_id: String,
141        materials: Vec<MaterialLine>,
142        date: NaiveDate,
143    },
144    /// Posting of labor hours to production.
145    LaborBooking {
146        order_id: String,
147        work_center: String,
148        hours: Decimal,
149        labor_rate: Decimal,
150        date: NaiveDate,
151    },
152    /// Overhead absorption posting.
153    OverheadAbsorption {
154        order_id: String,
155        absorption_rate: Decimal,
156        base_amount: Decimal,
157        date: NaiveDate,
158    },
159    /// Scrap reporting.
160    ScrapReporting {
161        order_id: String,
162        material_id: String,
163        quantity: u32,
164        reason: ScrapReason,
165        scrap_value: Decimal,
166        date: NaiveDate,
167    },
168    /// Rework order creation.
169    ReworkOrder {
170        original_order_id: String,
171        rework_order_id: String,
172        quantity: u32,
173        estimated_cost: Decimal,
174        date: NaiveDate,
175    },
176    /// Production variance posting.
177    ProductionVariance {
178        order_id: String,
179        variance_type: VarianceType,
180        amount: Decimal,
181        date: NaiveDate,
182    },
183    /// Completion of production order.
184    ProductionCompletion {
185        order_id: String,
186        product_id: String,
187        quantity_completed: u32,
188        total_cost: Decimal,
189        date: NaiveDate,
190    },
191
192    // ========== Inventory Transactions ==========
193    /// Receipt of raw materials.
194    RawMaterialReceipt {
195        po_id: String,
196        material_id: String,
197        quantity: u32,
198        unit_cost: Decimal,
199        date: NaiveDate,
200    },
201    /// Transfer between production stages.
202    WipTransfer {
203        from_center: String,
204        to_center: String,
205        order_id: String,
206        quantity: u32,
207        value: Decimal,
208        date: NaiveDate,
209    },
210    /// Transfer of finished goods to inventory.
211    FinishedGoodsTransfer {
212        order_id: String,
213        product_id: String,
214        quantity: u32,
215        location: String,
216        unit_cost: Decimal,
217        date: NaiveDate,
218    },
219    /// Cycle count adjustment.
220    CycleCountAdjustment {
221        material_id: String,
222        location: String,
223        variance_quantity: i32,
224        unit_cost: Decimal,
225        date: NaiveDate,
226    },
227
228    // ========== Costing Transactions ==========
229    /// Standard cost revaluation.
230    StandardCostRevaluation {
231        material_id: String,
232        old_cost: Decimal,
233        new_cost: Decimal,
234        inventory_quantity: u32,
235        date: NaiveDate,
236    },
237    /// Purchase price variance.
238    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/// Material line in a requisition.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct MaterialLine {
251    /// Material ID.
252    pub material_id: String,
253    /// Quantity issued.
254    pub quantity: f64,
255    /// Unit of measure.
256    pub unit_of_measure: String,
257    /// Standard cost per unit.
258    pub standard_cost: Decimal,
259    /// Storage location.
260    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()] // WIP, Raw Materials
346            }
347            ManufacturingTransaction::LaborBooking { .. } => {
348                vec!["1400".to_string(), "2100".to_string()] // WIP, Wages Payable
349            }
350            ManufacturingTransaction::OverheadAbsorption { .. } => {
351                vec!["1400".to_string(), "5400".to_string()] // WIP, Overhead Applied
352            }
353            ManufacturingTransaction::ScrapReporting { .. } => {
354                vec!["5200".to_string(), "1400".to_string()] // Scrap Loss, WIP
355            }
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()] // FG Inventory, WIP
364            }
365            ManufacturingTransaction::RawMaterialReceipt { .. } => {
366                vec!["1300".to_string(), "2000".to_string()] // Raw Materials, AP
367            }
368            ManufacturingTransaction::FinishedGoodsTransfer { .. } => {
369                vec!["1500".to_string(), "1400".to_string()] // FG Inventory, WIP
370            }
371            ManufacturingTransaction::CycleCountAdjustment { .. } => {
372                vec!["5300".to_string(), "1300".to_string()] // Inventory Adjustment, Inventory
373            }
374            ManufacturingTransaction::StandardCostRevaluation { .. } => {
375                vec!["1300".to_string(), "5510".to_string()] // Inventory, Revaluation
376            }
377            ManufacturingTransaction::PurchasePriceVariance { .. } => {
378                vec!["5510".to_string(), "2000".to_string()] // PPV, AP
379            }
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/// Generator for manufacturing transactions.
463#[derive(Debug, Clone)]
464pub struct ManufacturingTransactionGenerator {
465    /// Production order types to generate.
466    pub order_types: Vec<ProductionOrderType>,
467    /// Average orders per day.
468    pub avg_orders_per_day: f64,
469    /// Average materials per order.
470    pub avg_materials_per_order: u32,
471    /// Scrap rate (0.0-1.0).
472    pub scrap_rate: f64,
473    /// Variance rate (0.0-1.0).
474    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    /// Returns manufacturing-specific GL accounts.
491    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}