Skip to main content

stateset_core/models/
cost_accounting.rs

1//! Cost Accounting domain models
2//!
3//! Models for inventory costing, cost variance tracking, and cost layer management.
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use stateset_primitives::CurrencyCode;
9use strum::{Display, EnumString};
10use uuid::Uuid;
11
12// ============================================================================
13// Core Cost Types
14// ============================================================================
15
16/// Cost record for an inventory item (standard cost master).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ItemCost {
19    pub id: Uuid,
20    pub sku: String,
21    pub cost_method: CostMethod,
22    pub standard_cost: Decimal,
23    pub average_cost: Decimal,
24    pub last_cost: Decimal,
25    pub material_cost: Decimal,
26    pub labor_cost: Decimal,
27    pub overhead_cost: Decimal,
28    pub currency: CurrencyCode,
29    pub effective_date: DateTime<Utc>,
30    pub created_at: DateTime<Utc>,
31    pub updated_at: DateTime<Utc>,
32}
33
34/// A cost layer for FIFO/LIFO costing.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CostLayer {
37    pub id: Uuid,
38    pub sku: String,
39    pub layer_date: DateTime<Utc>,
40    pub quantity: Decimal,
41    pub remaining_quantity: Decimal,
42    pub unit_cost: Decimal,
43    pub total_cost: Decimal,
44    pub source_type: CostLayerSource,
45    pub source_id: Option<Uuid>,
46    pub lot_id: Option<Uuid>,
47    pub location_id: Option<i32>,
48    pub created_at: DateTime<Utc>,
49}
50
51/// A cost transaction (records cost movements).
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct CostTransaction {
54    pub id: Uuid,
55    pub sku: String,
56    pub transaction_type: CostTransactionType,
57    pub quantity: Decimal,
58    pub unit_cost: Decimal,
59    pub total_cost: Decimal,
60    pub layer_id: Option<Uuid>,
61    pub reference_type: Option<String>,
62    pub reference_id: Option<Uuid>,
63    pub notes: Option<String>,
64    pub created_at: DateTime<Utc>,
65}
66
67/// Cost variance record.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CostVariance {
70    pub id: Uuid,
71    pub sku: String,
72    pub variance_type: VarianceType,
73    pub variance_date: DateTime<Utc>,
74    pub standard_cost: Decimal,
75    pub actual_cost: Decimal,
76    pub variance_amount: Decimal,
77    pub variance_percent: Decimal,
78    pub quantity: Decimal,
79    pub total_variance: Decimal,
80    pub reference_type: Option<String>,
81    pub reference_id: Option<Uuid>,
82    pub notes: Option<String>,
83    pub created_at: DateTime<Utc>,
84}
85
86/// Cost adjustment record.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct CostAdjustment {
89    pub id: Uuid,
90    pub adjustment_number: String,
91    pub sku: String,
92    pub adjustment_type: CostAdjustmentType,
93    pub previous_cost: Decimal,
94    pub new_cost: Decimal,
95    pub adjustment_amount: Decimal,
96    pub reason: String,
97    pub approved_by: Option<String>,
98    pub approved_at: Option<DateTime<Utc>>,
99    pub status: CostAdjustmentStatus,
100    pub created_by: Option<String>,
101    pub created_at: DateTime<Utc>,
102}
103
104/// Standard cost roll-up for manufactured items.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct CostRollup {
107    pub id: Uuid,
108    pub sku: String,
109    pub bom_id: Option<Uuid>,
110    pub rollup_date: DateTime<Utc>,
111    pub material_cost: Decimal,
112    pub labor_cost: Decimal,
113    pub overhead_cost: Decimal,
114    pub total_cost: Decimal,
115    pub previous_cost: Decimal,
116    pub cost_change: Decimal,
117    pub created_at: DateTime<Utc>,
118}
119
120// ============================================================================
121// Enums
122// ============================================================================
123
124/// Inventory costing method.
125#[derive(
126    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
127)]
128#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
129#[serde(rename_all = "snake_case")]
130#[non_exhaustive]
131pub enum CostMethod {
132    /// Weighted average cost recalculated on each receipt.
133    #[default]
134    #[strum(serialize = "average", serialize = "avg")]
135    Average,
136    /// First-in, first-out: oldest cost layers are consumed first.
137    Fifo,
138    /// Last-in, first-out: newest cost layers are consumed first.
139    Lifo,
140    /// Pre-determined standard cost used for all transactions.
141    #[strum(serialize = "standard", serialize = "std")]
142    Standard,
143    /// Each unit is tracked with its own specific cost.
144    Specific,
145}
146
147/// Source of a cost layer.
148#[derive(
149    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
150)]
151#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
152#[serde(rename_all = "snake_case")]
153#[non_exhaustive]
154pub enum CostLayerSource {
155    /// Layer created from a supplier purchase receipt.
156    #[default]
157    Purchase,
158    /// Layer created from a completed manufacturing work order.
159    Production,
160    /// Layer created by transferring inventory between locations.
161    Transfer,
162    /// Layer created by a manual cost adjustment.
163    Adjustment,
164    /// Layer representing inventory on hand at system go-live.
165    #[strum(serialize = "opening", serialize = "opening_balance")]
166    Opening,
167}
168
169/// Cost transaction type.
170#[derive(
171    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
172)]
173#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
174#[serde(rename_all = "snake_case")]
175#[non_exhaustive]
176pub enum CostTransactionType {
177    /// Goods received into inventory; cost layer is added.
178    #[default]
179    Receipt,
180    /// Goods consumed or sold; cost is relieved from a layer.
181    Issue,
182    /// Manual change to cost without a physical movement.
183    Adjustment,
184    /// Physical movement between locations; cost moves with inventory.
185    Transfer,
186    /// Cost is updated to reflect a new standard or market value.
187    Revaluation,
188}
189
190/// Variance type.
191#[derive(
192    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
193)]
194#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
195#[serde(rename_all = "snake_case")]
196#[non_exhaustive]
197pub enum VarianceType {
198    /// Difference between purchase price and standard cost.
199    #[default]
200    Purchase,
201    /// Difference in raw material usage versus the standard bill of materials.
202    Material,
203    /// Difference in direct labor hours or rates versus standard.
204    Labor,
205    /// Difference in applied overhead versus actual overhead incurred.
206    Overhead,
207    /// Difference due to operating at a different efficiency than standard.
208    Efficiency,
209    /// Difference due to producing a different volume than the planned level.
210    Volume,
211}
212
213/// Cost adjustment type.
214#[derive(
215    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
216)]
217#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
218#[serde(rename_all = "snake_case")]
219#[non_exhaustive]
220pub enum CostAdjustmentType {
221    /// Periodic update to the standard cost for a SKU.
222    #[default]
223    #[strum(serialize = "standard_cost_update", serialize = "standardcostupdate")]
224    StandardCostUpdate,
225    /// Restate inventory value to reflect current market or replacement cost.
226    Revaluation,
227    /// Remove obsolete or damaged inventory value from the books.
228    #[strum(serialize = "write_off", serialize = "writeoff")]
229    WriteOff,
230    /// Fix a data entry or calculation error in recorded cost.
231    Correction,
232}
233
234/// Cost adjustment status.
235#[derive(
236    Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
237)]
238#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
239#[serde(rename_all = "snake_case")]
240#[non_exhaustive]
241pub enum CostAdjustmentStatus {
242    /// Adjustment has been submitted and is awaiting review.
243    #[default]
244    Pending,
245    /// Adjustment has been reviewed and approved; ready to apply.
246    Approved,
247    /// Adjustment has been applied to inventory cost records.
248    Applied,
249    /// Adjustment was reviewed and denied.
250    Rejected,
251}
252
253// ============================================================================
254// Input Types
255// ============================================================================
256
257/// Input for setting item cost.
258#[derive(Debug, Clone, Serialize, Deserialize, Default)]
259pub struct SetItemCost {
260    pub sku: String,
261    pub cost_method: Option<CostMethod>,
262    pub standard_cost: Option<Decimal>,
263    pub material_cost: Option<Decimal>,
264    pub labor_cost: Option<Decimal>,
265    pub overhead_cost: Option<Decimal>,
266    pub currency: Option<CurrencyCode>,
267}
268
269/// Input for creating a cost layer.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct CreateCostLayer {
272    pub sku: String,
273    pub quantity: Decimal,
274    pub unit_cost: Decimal,
275    pub source_type: CostLayerSource,
276    pub source_id: Option<Uuid>,
277    pub lot_id: Option<Uuid>,
278    pub location_id: Option<i32>,
279}
280
281/// Input for issuing from cost layers (FIFO/LIFO).
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct IssueCostLayers {
284    pub sku: String,
285    pub quantity: Decimal,
286    pub reference_type: Option<String>,
287    pub reference_id: Option<Uuid>,
288    pub notes: Option<String>,
289}
290
291/// Input for creating a cost adjustment.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct CreateCostAdjustment {
294    pub sku: String,
295    pub adjustment_type: CostAdjustmentType,
296    pub new_cost: Decimal,
297    pub reason: String,
298    pub created_by: Option<String>,
299}
300
301/// Input for recording a cost variance.
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct RecordCostVariance {
304    pub sku: String,
305    pub variance_type: VarianceType,
306    pub standard_cost: Decimal,
307    pub actual_cost: Decimal,
308    pub quantity: Decimal,
309    pub reference_type: Option<String>,
310    pub reference_id: Option<Uuid>,
311    pub notes: Option<String>,
312}
313
314// ============================================================================
315// Filter Types
316// ============================================================================
317
318/// Filter for listing item costs.
319#[derive(Debug, Clone, Serialize, Deserialize, Default)]
320pub struct ItemCostFilter {
321    pub sku: Option<String>,
322    pub cost_method: Option<CostMethod>,
323    pub limit: Option<u32>,
324    pub offset: Option<u32>,
325}
326
327/// Filter for listing cost layers.
328#[derive(Debug, Clone, Serialize, Deserialize, Default)]
329pub struct CostLayerFilter {
330    pub sku: Option<String>,
331    pub source_type: Option<CostLayerSource>,
332    pub has_remaining: Option<bool>,
333    pub from_date: Option<DateTime<Utc>>,
334    pub to_date: Option<DateTime<Utc>>,
335    pub limit: Option<u32>,
336    pub offset: Option<u32>,
337}
338
339/// Filter for listing cost transactions.
340#[derive(Debug, Clone, Serialize, Deserialize, Default)]
341pub struct CostTransactionFilter {
342    pub sku: Option<String>,
343    pub transaction_type: Option<CostTransactionType>,
344    pub from_date: Option<DateTime<Utc>>,
345    pub to_date: Option<DateTime<Utc>>,
346    pub limit: Option<u32>,
347    pub offset: Option<u32>,
348}
349
350/// Filter for listing cost variances.
351#[derive(Debug, Clone, Serialize, Deserialize, Default)]
352pub struct CostVarianceFilter {
353    pub sku: Option<String>,
354    pub variance_type: Option<VarianceType>,
355    pub from_date: Option<DateTime<Utc>>,
356    pub to_date: Option<DateTime<Utc>>,
357    pub limit: Option<u32>,
358    pub offset: Option<u32>,
359}
360
361/// Filter for listing cost adjustments.
362#[derive(Debug, Clone, Serialize, Deserialize, Default)]
363pub struct CostAdjustmentFilter {
364    pub sku: Option<String>,
365    pub status: Option<CostAdjustmentStatus>,
366    pub adjustment_type: Option<CostAdjustmentType>,
367    pub from_date: Option<DateTime<Utc>>,
368    pub to_date: Option<DateTime<Utc>>,
369    pub limit: Option<u32>,
370    pub offset: Option<u32>,
371}
372
373// ============================================================================
374// Summary Types
375// ============================================================================
376
377/// Inventory valuation summary.
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct InventoryValuation {
380    pub total_quantity: Decimal,
381    pub total_value: Decimal,
382    pub average_unit_cost: Decimal,
383    pub valuation_method: CostMethod,
384    pub as_of_date: DateTime<Utc>,
385}
386
387/// Cost summary by SKU.
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct SkuCostSummary {
390    pub sku: String,
391    pub quantity_on_hand: Decimal,
392    pub standard_cost: Decimal,
393    pub average_cost: Decimal,
394    pub total_value: Decimal,
395    pub variance_ytd: Decimal,
396}
397
398// ============================================================================
399// Helper Functions
400// ============================================================================
401
402/// Generate a cost adjustment number.
403#[must_use]
404pub fn generate_cost_adjustment_number() -> String {
405    let timestamp = chrono::Utc::now().format("%Y%m%d%H%M").to_string();
406    let random = &uuid::Uuid::new_v4().to_string()[..4].to_uppercase();
407    format!("CADJ-{timestamp}-{random}")
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use std::str::FromStr;
414
415    #[test]
416    fn test_cost_method_from_str() {
417        assert_eq!(CostMethod::from_str("avg").unwrap(), CostMethod::Average);
418        assert_eq!(CostMethod::from_str("standard").unwrap(), CostMethod::Standard);
419        assert!(CostMethod::from_str("nope").is_err());
420    }
421
422    #[test]
423    fn test_cost_layer_source_from_str() {
424        assert_eq!(CostLayerSource::from_str("opening_balance").unwrap(), CostLayerSource::Opening);
425        assert_eq!(CostLayerSource::from_str("transfer").unwrap(), CostLayerSource::Transfer);
426        assert!(CostLayerSource::from_str("nope").is_err());
427    }
428
429    #[test]
430    fn test_cost_transaction_type_from_str() {
431        assert_eq!(CostTransactionType::from_str("receipt").unwrap(), CostTransactionType::Receipt);
432        assert_eq!(
433            CostTransactionType::from_str("revaluation").unwrap(),
434            CostTransactionType::Revaluation
435        );
436        assert!(CostTransactionType::from_str("nope").is_err());
437    }
438
439    #[test]
440    fn test_variance_type_from_str() {
441        assert_eq!(VarianceType::from_str("material").unwrap(), VarianceType::Material);
442        assert_eq!(VarianceType::from_str("volume").unwrap(), VarianceType::Volume);
443        assert!(VarianceType::from_str("nope").is_err());
444    }
445
446    #[test]
447    fn test_cost_adjustment_type_from_str() {
448        assert_eq!(
449            CostAdjustmentType::from_str("standardcostupdate").unwrap(),
450            CostAdjustmentType::StandardCostUpdate
451        );
452        assert_eq!(CostAdjustmentType::from_str("writeoff").unwrap(), CostAdjustmentType::WriteOff);
453        assert!(CostAdjustmentType::from_str("nope").is_err());
454    }
455
456    #[test]
457    fn test_cost_adjustment_status_from_str() {
458        assert_eq!(
459            CostAdjustmentStatus::from_str("approved").unwrap(),
460            CostAdjustmentStatus::Approved
461        );
462        assert!(CostAdjustmentStatus::from_str("nope").is_err());
463    }
464}