use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::models::subledger::GLReference;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InventoryMovement {
pub document_number: String,
pub item_number: u32,
pub company_code: String,
pub movement_date: NaiveDate,
pub posting_date: NaiveDate,
pub movement_type: MovementType,
pub material_id: String,
pub description: String,
pub plant: String,
pub storage_location: String,
pub quantity: Decimal,
pub unit: String,
pub value: Decimal,
pub currency: String,
pub unit_cost: Decimal,
pub batch_number: Option<String>,
pub serial_numbers: Vec<String>,
pub reference_doc_type: Option<ReferenceDocType>,
pub reference_doc_number: Option<String>,
pub reference_item: Option<u32>,
pub vendor_id: Option<String>,
pub customer_id: Option<String>,
pub cost_center: Option<String>,
pub gl_account: String,
pub offset_account: String,
pub gl_reference: Option<GLReference>,
pub special_stock: Option<SpecialStockType>,
pub reason_code: Option<String>,
pub created_by: String,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
pub is_reversed: bool,
pub reversal_doc: Option<String>,
pub notes: Option<String>,
}
impl InventoryMovement {
#[allow(clippy::too_many_arguments)]
pub fn new(
document_number: String,
item_number: u32,
company_code: String,
movement_date: NaiveDate,
movement_type: MovementType,
material_id: String,
description: String,
plant: String,
storage_location: String,
quantity: Decimal,
unit: String,
unit_cost: Decimal,
currency: String,
created_by: String,
) -> Self {
let value = quantity * unit_cost;
let (gl_account, offset_account) = movement_type.default_accounts();
Self {
document_number,
item_number,
company_code,
movement_date,
posting_date: movement_date,
movement_type,
material_id,
description,
plant,
storage_location,
quantity,
unit,
value,
currency,
unit_cost,
batch_number: None,
serial_numbers: Vec::new(),
reference_doc_type: None,
reference_doc_number: None,
reference_item: None,
vendor_id: None,
customer_id: None,
cost_center: None,
gl_account,
offset_account,
gl_reference: None,
special_stock: None,
reason_code: None,
created_by,
created_at: Utc::now(),
is_reversed: false,
reversal_doc: None,
notes: None,
}
}
#[allow(clippy::too_many_arguments)]
pub fn goods_receipt_po(
document_number: String,
item_number: u32,
company_code: String,
movement_date: NaiveDate,
material_id: String,
description: String,
plant: String,
storage_location: String,
quantity: Decimal,
unit: String,
unit_cost: Decimal,
currency: String,
po_number: String,
po_item: u32,
vendor_id: String,
created_by: String,
) -> Self {
let mut movement = Self::new(
document_number,
item_number,
company_code,
movement_date,
MovementType::GoodsReceiptPO,
material_id,
description,
plant,
storage_location,
quantity,
unit,
unit_cost,
currency,
created_by,
);
movement.reference_doc_type = Some(ReferenceDocType::PurchaseOrder);
movement.reference_doc_number = Some(po_number);
movement.reference_item = Some(po_item);
movement.vendor_id = Some(vendor_id);
movement
}
#[allow(clippy::too_many_arguments)]
pub fn goods_issue_sales(
document_number: String,
item_number: u32,
company_code: String,
movement_date: NaiveDate,
material_id: String,
description: String,
plant: String,
storage_location: String,
quantity: Decimal,
unit: String,
unit_cost: Decimal,
currency: String,
sales_order: String,
sales_item: u32,
customer_id: String,
created_by: String,
) -> Self {
let mut movement = Self::new(
document_number,
item_number,
company_code,
movement_date,
MovementType::GoodsIssueSales,
material_id,
description,
plant,
storage_location,
quantity,
unit,
unit_cost,
currency,
created_by,
);
movement.reference_doc_type = Some(ReferenceDocType::SalesOrder);
movement.reference_doc_number = Some(sales_order);
movement.reference_item = Some(sales_item);
movement.customer_id = Some(customer_id);
movement
}
pub fn with_batch(mut self, batch_number: String) -> Self {
self.batch_number = Some(batch_number);
self
}
pub fn with_serials(mut self, serial_numbers: Vec<String>) -> Self {
self.serial_numbers = serial_numbers;
self
}
pub fn with_cost_center(mut self, cost_center: String) -> Self {
self.cost_center = Some(cost_center);
self
}
pub fn with_reason(mut self, reason_code: String) -> Self {
self.reason_code = Some(reason_code);
self
}
pub fn with_gl_reference(mut self, reference: GLReference) -> Self {
self.gl_reference = Some(reference);
self
}
pub fn reverse(&mut self, reversal_doc: String) {
self.is_reversed = true;
self.reversal_doc = Some(reversal_doc);
}
pub fn create_reversal(&self, reversal_doc_number: String, created_by: String) -> Self {
let mut reversal = Self::new(
reversal_doc_number,
self.item_number,
self.company_code.clone(),
chrono::Local::now().date_naive(),
self.movement_type.reversal_type(),
self.material_id.clone(),
self.description.clone(),
self.plant.clone(),
self.storage_location.clone(),
self.quantity,
self.unit.clone(),
self.unit_cost,
self.currency.clone(),
created_by,
);
reversal.reference_doc_type = Some(ReferenceDocType::MaterialDocument);
reversal.reference_doc_number = Some(self.document_number.clone());
reversal.reference_item = Some(self.item_number);
reversal.batch_number = self.batch_number.clone();
reversal.notes = Some(format!(
"Reversal of {}/{}",
self.document_number, self.item_number
));
reversal
}
pub fn quantity_sign(&self) -> i8 {
self.movement_type.quantity_sign()
}
pub fn signed_quantity(&self) -> Decimal {
self.quantity * Decimal::from(self.quantity_sign())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MovementType {
GoodsReceiptPO,
GoodsReceiptProduction,
GoodsReceiptOther,
GoodsReceipt,
ReturnToVendor,
GoodsIssueSales,
GoodsIssueProduction,
GoodsIssueCostCenter,
GoodsIssueScrapping,
GoodsIssue,
Scrap,
TransferPlant,
TransferStorageLocation,
TransferIn,
TransferOut,
TransferToInspection,
TransferFromInspection,
PhysicalInventory,
InventoryAdjustmentIn,
InventoryAdjustmentOut,
InitialStock,
ReversalGoodsReceipt,
ReversalGoodsIssue,
}
impl MovementType {
pub fn quantity_sign(&self) -> i8 {
match self {
MovementType::GoodsReceiptPO
| MovementType::GoodsReceiptProduction
| MovementType::GoodsReceiptOther
| MovementType::GoodsReceipt
| MovementType::TransferFromInspection
| MovementType::TransferIn
| MovementType::InventoryAdjustmentIn
| MovementType::InitialStock
| MovementType::ReversalGoodsIssue => 1,
MovementType::ReturnToVendor
| MovementType::GoodsIssueSales
| MovementType::GoodsIssueProduction
| MovementType::GoodsIssueCostCenter
| MovementType::GoodsIssueScrapping
| MovementType::GoodsIssue
| MovementType::Scrap
| MovementType::TransferOut
| MovementType::InventoryAdjustmentOut
| MovementType::TransferToInspection
| MovementType::ReversalGoodsReceipt => -1,
MovementType::TransferPlant
| MovementType::TransferStorageLocation
| MovementType::PhysicalInventory => 0, }
}
pub fn default_accounts(&self) -> (String, String) {
match self {
MovementType::GoodsReceiptPO => ("1200".to_string(), "2100".to_string()), MovementType::GoodsReceiptProduction => ("1200".to_string(), "1300".to_string()), MovementType::GoodsReceiptOther => ("1200".to_string(), "1299".to_string()), MovementType::GoodsReceipt => ("1200".to_string(), "1299".to_string()), MovementType::ReturnToVendor => ("2100".to_string(), "1200".to_string()), MovementType::GoodsIssueSales => ("5000".to_string(), "1200".to_string()), MovementType::GoodsIssueProduction => ("1300".to_string(), "1200".to_string()), MovementType::GoodsIssueCostCenter => ("7000".to_string(), "1200".to_string()), MovementType::GoodsIssueScrapping => ("7900".to_string(), "1200".to_string()), MovementType::GoodsIssue => ("7000".to_string(), "1200".to_string()), MovementType::Scrap => ("7900".to_string(), "1200".to_string()), MovementType::TransferPlant => ("1200".to_string(), "1200".to_string()), MovementType::TransferStorageLocation => ("1200".to_string(), "1200".to_string()),
MovementType::TransferIn => ("1200".to_string(), "1299".to_string()), MovementType::TransferOut => ("1299".to_string(), "1200".to_string()), MovementType::TransferToInspection => ("1210".to_string(), "1200".to_string()),
MovementType::TransferFromInspection => ("1200".to_string(), "1210".to_string()),
MovementType::PhysicalInventory => ("7910".to_string(), "1200".to_string()), MovementType::InventoryAdjustmentIn => ("1200".to_string(), "7910".to_string()), MovementType::InventoryAdjustmentOut => ("7910".to_string(), "1200".to_string()), MovementType::InitialStock => ("1200".to_string(), "3000".to_string()), MovementType::ReversalGoodsReceipt => ("2100".to_string(), "1200".to_string()),
MovementType::ReversalGoodsIssue => ("1200".to_string(), "5000".to_string()),
}
}
pub fn reversal_type(&self) -> MovementType {
match self {
MovementType::GoodsReceiptPO
| MovementType::GoodsReceiptProduction
| MovementType::GoodsReceiptOther
| MovementType::GoodsReceipt
| MovementType::TransferIn
| MovementType::InventoryAdjustmentIn => MovementType::ReversalGoodsReceipt,
MovementType::GoodsIssueSales
| MovementType::GoodsIssueProduction
| MovementType::GoodsIssueCostCenter
| MovementType::GoodsIssue
| MovementType::Scrap
| MovementType::TransferOut
| MovementType::InventoryAdjustmentOut
| MovementType::GoodsIssueScrapping => MovementType::ReversalGoodsIssue,
_ => *self, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReferenceDocType {
PurchaseOrder,
SalesOrder,
ProductionOrder,
Delivery,
MaterialDocument,
Reservation,
PhysicalInventoryDoc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpecialStockType {
VendorConsignment,
CustomerConsignment,
ProjectStock,
SalesOrderStock,
Subcontracting,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockTransfer {
pub document_number: String,
pub company_code: String,
pub transfer_date: NaiveDate,
pub from_plant: String,
pub from_storage_location: String,
pub to_plant: String,
pub to_storage_location: String,
pub items: Vec<TransferItem>,
pub status: TransferStatus,
pub created_by: String,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferItem {
pub item_number: u32,
pub material_id: String,
pub description: String,
pub quantity: Decimal,
pub unit: String,
pub batch_number: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransferStatus {
Draft,
InTransit,
PartiallyReceived,
Completed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhysicalInventoryDoc {
pub document_number: String,
pub company_code: String,
pub plant: String,
pub storage_location: String,
pub planned_date: NaiveDate,
pub count_date: Option<NaiveDate>,
pub status: PIStatus,
pub items: Vec<PIItem>,
pub created_by: String,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
pub posted: bool,
#[serde(default, with = "crate::serde_timestamp::utc::option")]
pub posted_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PIStatus {
Created,
Active,
Counted,
Posted,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PIItem {
pub item_number: u32,
pub material_id: String,
pub description: String,
pub book_quantity: Decimal,
pub counted_quantity: Option<Decimal>,
pub difference: Option<Decimal>,
pub unit: String,
pub batch_number: Option<String>,
pub zero_count: bool,
pub difference_reason: Option<String>,
}
impl PIItem {
pub fn calculate_difference(&mut self) {
if let Some(counted) = self.counted_quantity {
self.difference = Some(counted - self.book_quantity);
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_goods_receipt_po() {
let movement = InventoryMovement::goods_receipt_po(
"MBLNR001".to_string(),
1,
"1000".to_string(),
NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
"MAT001".to_string(),
"Test Material".to_string(),
"PLANT01".to_string(),
"SLOC01".to_string(),
dec!(100),
"EA".to_string(),
dec!(10),
"USD".to_string(),
"PO001".to_string(),
10,
"VEND001".to_string(),
"USER1".to_string(),
);
assert_eq!(movement.movement_type, MovementType::GoodsReceiptPO);
assert_eq!(movement.quantity, dec!(100));
assert_eq!(movement.value, dec!(1000));
assert_eq!(movement.quantity_sign(), 1);
}
#[test]
fn test_goods_issue_sales() {
let movement = InventoryMovement::goods_issue_sales(
"MBLNR002".to_string(),
1,
"1000".to_string(),
NaiveDate::from_ymd_opt(2024, 1, 20).unwrap(),
"MAT001".to_string(),
"Test Material".to_string(),
"PLANT01".to_string(),
"SLOC01".to_string(),
dec!(50),
"EA".to_string(),
dec!(10),
"USD".to_string(),
"SO001".to_string(),
10,
"CUST001".to_string(),
"USER1".to_string(),
);
assert_eq!(movement.movement_type, MovementType::GoodsIssueSales);
assert_eq!(movement.quantity_sign(), -1);
assert_eq!(movement.signed_quantity(), dec!(-50));
}
#[test]
fn test_create_reversal() {
let original = InventoryMovement::goods_receipt_po(
"MBLNR001".to_string(),
1,
"1000".to_string(),
NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
"MAT001".to_string(),
"Test Material".to_string(),
"PLANT01".to_string(),
"SLOC01".to_string(),
dec!(100),
"EA".to_string(),
dec!(10),
"USD".to_string(),
"PO001".to_string(),
10,
"VEND001".to_string(),
"USER1".to_string(),
);
let reversal = original.create_reversal("MBLNR002".to_string(), "USER2".to_string());
assert_eq!(reversal.movement_type, MovementType::ReversalGoodsReceipt);
assert_eq!(reversal.reference_doc_number, Some("MBLNR001".to_string()));
}
}