1use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use stateset_primitives::CurrencyCode;
9use strum::{Display, EnumString};
10use uuid::Uuid;
11
12#[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#[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#[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#[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#[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#[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#[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 #[default]
134 #[strum(serialize = "average", serialize = "avg")]
135 Average,
136 Fifo,
138 Lifo,
140 #[strum(serialize = "standard", serialize = "std")]
142 Standard,
143 Specific,
145}
146
147#[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 #[default]
157 Purchase,
158 Production,
160 Transfer,
162 Adjustment,
164 #[strum(serialize = "opening", serialize = "opening_balance")]
166 Opening,
167}
168
169#[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 #[default]
179 Receipt,
180 Issue,
182 Adjustment,
184 Transfer,
186 Revaluation,
188}
189
190#[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 #[default]
200 Purchase,
201 Material,
203 Labor,
205 Overhead,
207 Efficiency,
209 Volume,
211}
212
213#[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 #[default]
223 #[strum(serialize = "standard_cost_update", serialize = "standardcostupdate")]
224 StandardCostUpdate,
225 Revaluation,
227 #[strum(serialize = "write_off", serialize = "writeoff")]
229 WriteOff,
230 Correction,
232}
233
234#[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 #[default]
244 Pending,
245 Approved,
247 Applied,
249 Rejected,
251}
252
253#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}