use std::str::FromStr;
use chrono::{Datelike, NaiveDate};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
const MACRS_GDS_3_YEAR: &[&str] = &["33.33", "44.45", "14.81", "7.41"];
const MACRS_GDS_5_YEAR: &[&str] = &["20.00", "32.00", "19.20", "11.52", "11.52", "5.76"];
const MACRS_GDS_7_YEAR: &[&str] = &[
"14.29", "24.49", "17.49", "12.49", "8.93", "8.92", "8.93", "4.46",
];
const MACRS_GDS_10_YEAR: &[&str] = &[
"10.00", "18.00", "14.40", "11.52", "9.22", "7.37", "6.55", "6.55", "6.56", "6.55", "3.28",
];
const MACRS_GDS_15_YEAR: &[&str] = &[
"5.00", "9.50", "8.55", "7.70", "6.93", "6.23", "5.90", "5.90", "5.91", "5.90", "5.91", "5.90",
"5.91", "5.90", "5.91", "2.95",
];
const MACRS_GDS_20_YEAR: &[&str] = &[
"3.750", "7.219", "6.677", "6.177", "5.713", "5.285", "4.888", "4.522", "4.462", "4.461",
"4.462", "4.461", "4.462", "4.461", "4.462", "4.461", "4.462", "4.461", "4.462", "4.461",
"2.231",
];
fn macrs_table_for_life(useful_life_years: u32) -> Option<&'static [&'static str]> {
match useful_life_years {
1..=3 => Some(MACRS_GDS_3_YEAR),
4..=5 => Some(MACRS_GDS_5_YEAR),
6..=7 => Some(MACRS_GDS_7_YEAR),
8..=10 => Some(MACRS_GDS_10_YEAR),
11..=15 => Some(MACRS_GDS_15_YEAR),
16..=20 => Some(MACRS_GDS_20_YEAR),
_ => None,
}
}
fn macrs_pct(s: &str) -> Decimal {
Decimal::from_str(s).unwrap_or(Decimal::ZERO)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AssetClass {
Buildings,
BuildingImprovements,
Land,
#[default]
MachineryEquipment,
Machinery,
ComputerHardware,
ItEquipment,
FurnitureFixtures,
Furniture,
Vehicles,
LeaseholdImprovements,
Intangibles,
Software,
ConstructionInProgress,
LowValueAssets,
}
impl AssetClass {
pub fn default_useful_life_months(&self) -> u32 {
match self {
Self::Buildings | Self::BuildingImprovements => 480, Self::Land => 0, Self::MachineryEquipment | Self::Machinery => 120, Self::ComputerHardware | Self::ItEquipment => 36, Self::FurnitureFixtures | Self::Furniture => 84, Self::Vehicles => 60, Self::LeaseholdImprovements => 120, Self::Intangibles | Self::Software => 60, Self::ConstructionInProgress => 0, Self::LowValueAssets => 12, }
}
pub fn is_depreciable(&self) -> bool {
!matches!(self, Self::Land | Self::ConstructionInProgress)
}
pub fn default_depreciation_method(&self) -> DepreciationMethod {
match self {
Self::Buildings | Self::BuildingImprovements | Self::LeaseholdImprovements => {
DepreciationMethod::StraightLine
}
Self::MachineryEquipment | Self::Machinery => DepreciationMethod::StraightLine,
Self::ComputerHardware | Self::ItEquipment => {
DepreciationMethod::DoubleDecliningBalance
}
Self::FurnitureFixtures | Self::Furniture => DepreciationMethod::StraightLine,
Self::Vehicles => DepreciationMethod::DoubleDecliningBalance,
Self::Intangibles | Self::Software => DepreciationMethod::StraightLine,
Self::LowValueAssets => DepreciationMethod::ImmediateExpense,
Self::Land | Self::ConstructionInProgress => DepreciationMethod::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DepreciationMethod {
#[default]
StraightLine,
DoubleDecliningBalance,
SumOfYearsDigits,
UnitsOfProduction,
Macrs,
ImmediateExpense,
Degressiv,
None,
}
impl DepreciationMethod {
pub fn calculate_monthly_depreciation(
&self,
acquisition_cost: Decimal,
salvage_value: Decimal,
useful_life_months: u32,
months_elapsed: u32,
accumulated_depreciation: Decimal,
) -> Decimal {
if useful_life_months == 0 {
return Decimal::ZERO;
}
let depreciable_base = acquisition_cost - salvage_value;
let net_book_value = acquisition_cost - accumulated_depreciation;
if net_book_value <= salvage_value {
return Decimal::ZERO;
}
match self {
Self::StraightLine => {
let monthly_amount = depreciable_base / Decimal::from(useful_life_months);
monthly_amount.min(net_book_value - salvage_value)
}
Self::DoubleDecliningBalance => {
let annual_rate = Decimal::from(2) / Decimal::from(useful_life_months) * dec!(12);
let monthly_rate = annual_rate / dec!(12);
let depreciation = net_book_value * monthly_rate;
depreciation.min(net_book_value - salvage_value)
}
Self::SumOfYearsDigits => {
let years_total = useful_life_months / 12;
let sum_of_years: u32 = (1..=years_total).sum();
let current_year = (months_elapsed / 12) + 1;
let remaining_years = years_total.saturating_sub(current_year) + 1;
if sum_of_years == 0 || remaining_years == 0 {
return Decimal::ZERO;
}
let year_fraction = Decimal::from(remaining_years) / Decimal::from(sum_of_years);
let annual_depreciation = depreciable_base * year_fraction;
let monthly_amount = annual_depreciation / dec!(12);
monthly_amount.min(net_book_value - salvage_value)
}
Self::UnitsOfProduction => {
let monthly_amount = depreciable_base / Decimal::from(useful_life_months);
monthly_amount.min(net_book_value - salvage_value)
}
Self::Macrs => {
let useful_life_years = useful_life_months / 12;
let current_year = (months_elapsed / 12) as usize;
if let Some(table) = macrs_table_for_life(useful_life_years) {
if current_year < table.len() {
let pct = macrs_pct(table[current_year]);
let annual_depreciation = acquisition_cost * pct / dec!(100);
let monthly_amount = annual_depreciation / dec!(12);
monthly_amount.min(net_book_value)
} else {
Decimal::ZERO
}
} else {
let annual_rate =
Decimal::from(2) / Decimal::from(useful_life_months) * dec!(12);
let monthly_rate = annual_rate / dec!(12);
let depreciation = net_book_value * monthly_rate;
depreciation.min(net_book_value - salvage_value)
}
}
Self::ImmediateExpense => {
if months_elapsed == 0 {
depreciable_base
} else {
Decimal::ZERO
}
}
Self::Degressiv => {
let useful_life_years = useful_life_months / 12;
if useful_life_years == 0 {
return Decimal::ZERO;
}
let sl_annual_rate = Decimal::ONE / Decimal::from(useful_life_years);
let degressiv_rate = (sl_annual_rate * Decimal::from(3)).min(dec!(0.30));
let degressiv_monthly = net_book_value * degressiv_rate / dec!(12);
let remaining_months = useful_life_months.saturating_sub(months_elapsed);
let sl_monthly = if remaining_months > 0 {
(net_book_value - salvage_value) / Decimal::from(remaining_months)
} else {
Decimal::ZERO
};
let monthly_amount = degressiv_monthly.max(sl_monthly);
monthly_amount
.min(net_book_value - salvage_value)
.max(Decimal::ZERO)
}
Self::None => Decimal::ZERO,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetAccountDetermination {
pub asset_account: String,
pub accumulated_depreciation_account: String,
pub depreciation_expense_account: String,
pub gain_on_disposal_account: String,
pub loss_on_disposal_account: String,
pub acquisition_clearing_account: String,
pub gain_loss_account: String,
}
impl Default for AssetAccountDetermination {
fn default() -> Self {
Self {
asset_account: "160000".to_string(),
accumulated_depreciation_account: "169000".to_string(),
depreciation_expense_account: "640000".to_string(),
gain_on_disposal_account: "810000".to_string(),
loss_on_disposal_account: "840000".to_string(),
acquisition_clearing_account: "299000".to_string(),
gain_loss_account: "810000".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AcquisitionType {
#[default]
Purchase,
SelfConstructed,
Transfer,
BusinessCombination,
FinanceLease,
Donation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AssetStatus {
UnderConstruction,
#[default]
Active,
Inactive,
FullyDepreciated,
PendingDisposal,
Disposed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixedAsset {
pub asset_id: String,
pub sub_number: u16,
pub description: String,
pub asset_class: AssetClass,
pub company_code: String,
pub cost_center: Option<String>,
pub location: Option<String>,
pub acquisition_date: NaiveDate,
pub acquisition_type: AcquisitionType,
pub acquisition_cost: Decimal,
pub capitalized_date: Option<NaiveDate>,
pub depreciation_method: DepreciationMethod,
pub useful_life_months: u32,
pub salvage_value: Decimal,
pub accumulated_depreciation: Decimal,
pub net_book_value: Decimal,
pub account_determination: AssetAccountDetermination,
pub status: AssetStatus,
pub disposal_date: Option<NaiveDate>,
pub disposal_proceeds: Option<Decimal>,
pub serial_number: Option<String>,
pub manufacturer: Option<String>,
pub model: Option<String>,
pub warranty_expiration: Option<NaiveDate>,
pub insurance_policy: Option<String>,
pub purchase_order: Option<String>,
pub vendor_id: Option<String>,
pub invoice_reference: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_gwg: Option<bool>,
}
impl FixedAsset {
pub fn new(
asset_id: impl Into<String>,
description: impl Into<String>,
asset_class: AssetClass,
company_code: impl Into<String>,
acquisition_date: NaiveDate,
acquisition_cost: Decimal,
) -> Self {
let useful_life_months = asset_class.default_useful_life_months();
let depreciation_method = asset_class.default_depreciation_method();
Self {
asset_id: asset_id.into(),
sub_number: 0,
description: description.into(),
asset_class,
company_code: company_code.into(),
cost_center: None,
location: None,
acquisition_date,
acquisition_type: AcquisitionType::Purchase,
acquisition_cost,
capitalized_date: Some(acquisition_date),
depreciation_method,
useful_life_months,
salvage_value: Decimal::ZERO,
accumulated_depreciation: Decimal::ZERO,
net_book_value: acquisition_cost,
account_determination: AssetAccountDetermination::default(),
status: AssetStatus::Active,
disposal_date: None,
disposal_proceeds: None,
serial_number: None,
manufacturer: None,
model: None,
warranty_expiration: None,
insurance_policy: None,
purchase_order: None,
vendor_id: None,
invoice_reference: None,
is_gwg: None,
}
}
pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
self.cost_center = Some(cost_center.into());
self
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
pub fn with_salvage_value(mut self, salvage_value: Decimal) -> Self {
self.salvage_value = salvage_value;
self
}
pub fn with_depreciation_method(mut self, method: DepreciationMethod) -> Self {
self.depreciation_method = method;
self
}
pub fn with_useful_life_months(mut self, months: u32) -> Self {
self.useful_life_months = months;
self
}
pub fn with_vendor(mut self, vendor_id: impl Into<String>) -> Self {
self.vendor_id = Some(vendor_id.into());
self
}
pub fn months_since_capitalization(&self, as_of_date: NaiveDate) -> u32 {
let cap_date = self.capitalized_date.unwrap_or(self.acquisition_date);
if as_of_date < cap_date {
return 0;
}
let years = as_of_date.year() - cap_date.year();
let months = as_of_date.month() as i32 - cap_date.month() as i32;
((years * 12) + months).max(0) as u32
}
pub fn calculate_monthly_depreciation(&self, as_of_date: NaiveDate) -> Decimal {
if !self.asset_class.is_depreciable() {
return Decimal::ZERO;
}
if self.status == AssetStatus::Disposed {
return Decimal::ZERO;
}
let months_elapsed = self.months_since_capitalization(as_of_date);
self.depreciation_method.calculate_monthly_depreciation(
self.acquisition_cost,
self.salvage_value,
self.useful_life_months,
months_elapsed,
self.accumulated_depreciation,
)
}
pub fn apply_depreciation(&mut self, depreciation_amount: Decimal) {
self.accumulated_depreciation += depreciation_amount;
self.net_book_value = self.acquisition_cost - self.accumulated_depreciation;
if self.net_book_value <= self.salvage_value && self.status == AssetStatus::Active {
self.status = AssetStatus::FullyDepreciated;
}
}
pub fn calculate_disposal_gain_loss(&self, proceeds: Decimal) -> Decimal {
proceeds - self.net_book_value
}
pub fn dispose(&mut self, disposal_date: NaiveDate, proceeds: Decimal) {
self.disposal_date = Some(disposal_date);
self.disposal_proceeds = Some(proceeds);
self.status = AssetStatus::Disposed;
}
pub fn is_fully_depreciated(&self) -> bool {
self.net_book_value <= self.salvage_value
}
pub fn remaining_useful_life_months(&self, as_of_date: NaiveDate) -> u32 {
let months_elapsed = self.months_since_capitalization(as_of_date);
self.useful_life_months.saturating_sub(months_elapsed)
}
pub fn annual_depreciation_rate(&self) -> Decimal {
if self.useful_life_months == 0 {
return Decimal::ZERO;
}
match self.depreciation_method {
DepreciationMethod::StraightLine => {
Decimal::from(12) / Decimal::from(self.useful_life_months) * dec!(100)
}
DepreciationMethod::DoubleDecliningBalance => {
Decimal::from(24) / Decimal::from(self.useful_life_months) * dec!(100)
}
_ => Decimal::from(12) / Decimal::from(self.useful_life_months) * dec!(100),
}
}
pub fn macrs_depreciation(&self, year: u32) -> Decimal {
if year == 0 {
return Decimal::ZERO;
}
let useful_life_years = self.useful_life_months / 12;
let table_index = (year - 1) as usize;
match macrs_table_for_life(useful_life_years) {
Some(table) if table_index < table.len() => {
let pct = macrs_pct(table[table_index]);
self.acquisition_cost * pct / dec!(100)
}
_ => Decimal::ZERO,
}
}
pub fn ddb_depreciation(&self) -> Decimal {
if self.useful_life_months == 0 {
return Decimal::ZERO;
}
let net_book_value = self.acquisition_cost - self.accumulated_depreciation;
if net_book_value <= self.salvage_value {
return Decimal::ZERO;
}
let annual_rate = Decimal::from(2) / Decimal::from(self.useful_life_months) * dec!(12);
let monthly_rate = annual_rate / dec!(12);
let depreciation = (net_book_value * monthly_rate).round_dp(2);
depreciation.min(net_book_value - self.salvage_value)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FixedAssetPool {
pub assets: Vec<FixedAsset>,
#[serde(skip)]
class_index: std::collections::HashMap<AssetClass, Vec<usize>>,
#[serde(skip)]
company_index: std::collections::HashMap<String, Vec<usize>>,
}
impl FixedAssetPool {
pub fn new() -> Self {
Self::default()
}
pub fn add_asset(&mut self, asset: FixedAsset) {
let idx = self.assets.len();
let asset_class = asset.asset_class;
let company_code = asset.company_code.clone();
self.assets.push(asset);
self.class_index.entry(asset_class).or_default().push(idx);
self.company_index
.entry(company_code)
.or_default()
.push(idx);
}
pub fn get_depreciable_assets(&self) -> Vec<&FixedAsset> {
self.assets
.iter()
.filter(|a| {
a.asset_class.is_depreciable()
&& a.status == AssetStatus::Active
&& !a.is_fully_depreciated()
})
.collect()
}
pub fn get_depreciable_assets_mut(&mut self) -> Vec<&mut FixedAsset> {
self.assets
.iter_mut()
.filter(|a| {
a.asset_class.is_depreciable()
&& a.status == AssetStatus::Active
&& !a.is_fully_depreciated()
})
.collect()
}
pub fn get_by_company(&self, company_code: &str) -> Vec<&FixedAsset> {
self.company_index
.get(company_code)
.map(|indices| indices.iter().map(|&i| &self.assets[i]).collect())
.unwrap_or_default()
}
pub fn get_by_class(&self, asset_class: AssetClass) -> Vec<&FixedAsset> {
self.class_index
.get(&asset_class)
.map(|indices| indices.iter().map(|&i| &self.assets[i]).collect())
.unwrap_or_default()
}
pub fn get_by_id(&self, asset_id: &str) -> Option<&FixedAsset> {
self.assets.iter().find(|a| a.asset_id == asset_id)
}
pub fn get_by_id_mut(&mut self, asset_id: &str) -> Option<&mut FixedAsset> {
self.assets.iter_mut().find(|a| a.asset_id == asset_id)
}
pub fn calculate_period_depreciation(&self, as_of_date: NaiveDate) -> Decimal {
self.get_depreciable_assets()
.iter()
.map(|a| a.calculate_monthly_depreciation(as_of_date))
.sum()
}
pub fn total_net_book_value(&self) -> Decimal {
self.assets
.iter()
.filter(|a| a.status != AssetStatus::Disposed)
.map(|a| a.net_book_value)
.sum()
}
pub fn len(&self) -> usize {
self.assets.len()
}
pub fn is_empty(&self) -> bool {
self.assets.is_empty()
}
pub fn rebuild_indices(&mut self) {
self.class_index.clear();
self.company_index.clear();
for (idx, asset) in self.assets.iter().enumerate() {
self.class_index
.entry(asset.asset_class)
.or_default()
.push(idx);
self.company_index
.entry(asset.company_code.clone())
.or_default()
.push(idx);
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn test_date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).unwrap()
}
#[test]
fn test_asset_creation() {
let asset = FixedAsset::new(
"FA-001",
"Office Computer",
AssetClass::ComputerHardware,
"1000",
test_date(2024, 1, 1),
Decimal::from(2000),
);
assert_eq!(asset.asset_id, "FA-001");
assert_eq!(asset.acquisition_cost, Decimal::from(2000));
assert_eq!(asset.useful_life_months, 36); }
#[test]
fn test_straight_line_depreciation() {
let asset = FixedAsset::new(
"FA-001",
"Office Equipment",
AssetClass::FurnitureFixtures,
"1000",
test_date(2024, 1, 1),
Decimal::from(8400),
)
.with_useful_life_months(84) .with_depreciation_method(DepreciationMethod::StraightLine);
let monthly_dep = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
assert_eq!(monthly_dep, Decimal::from(100)); }
#[test]
fn test_salvage_value_limit() {
let mut asset = FixedAsset::new(
"FA-001",
"Test Asset",
AssetClass::MachineryEquipment,
"1000",
test_date(2024, 1, 1),
Decimal::from(1200),
)
.with_useful_life_months(12)
.with_salvage_value(Decimal::from(200));
for _ in 0..11 {
let dep = Decimal::from(83);
asset.apply_depreciation(dep);
}
let final_dep = asset.calculate_monthly_depreciation(test_date(2024, 12, 1));
asset.apply_depreciation(final_dep);
assert!(asset.net_book_value >= asset.salvage_value);
}
#[test]
fn test_disposal() {
let mut asset = FixedAsset::new(
"FA-001",
"Old Equipment",
AssetClass::MachineryEquipment,
"1000",
test_date(2020, 1, 1),
Decimal::from(10000),
);
asset.apply_depreciation(Decimal::from(5000));
let gain_loss = asset.calculate_disposal_gain_loss(Decimal::from(6000));
assert_eq!(gain_loss, Decimal::from(1000));
asset.dispose(test_date(2024, 1, 1), Decimal::from(6000));
assert_eq!(asset.status, AssetStatus::Disposed);
}
#[test]
fn test_land_not_depreciable() {
let asset = FixedAsset::new(
"FA-001",
"Land Parcel",
AssetClass::Land,
"1000",
test_date(2024, 1, 1),
Decimal::from(500000),
);
let dep = asset.calculate_monthly_depreciation(test_date(2024, 6, 1));
assert_eq!(dep, Decimal::ZERO);
}
#[test]
fn test_asset_pool() {
let mut pool = FixedAssetPool::new();
pool.add_asset(FixedAsset::new(
"FA-001",
"Computer 1",
AssetClass::ComputerHardware,
"1000",
test_date(2024, 1, 1),
Decimal::from(2000),
));
pool.add_asset(FixedAsset::new(
"FA-002",
"Desk",
AssetClass::FurnitureFixtures,
"1000",
test_date(2024, 1, 1),
Decimal::from(500),
));
assert_eq!(pool.len(), 2);
assert_eq!(pool.get_by_class(AssetClass::ComputerHardware).len(), 1);
assert_eq!(pool.get_by_company("1000").len(), 2);
}
#[test]
fn test_months_since_capitalization() {
let asset = FixedAsset::new(
"FA-001",
"Test",
AssetClass::MachineryEquipment,
"1000",
test_date(2024, 3, 15),
Decimal::from(10000),
);
assert_eq!(asset.months_since_capitalization(test_date(2024, 3, 1)), 0);
assert_eq!(asset.months_since_capitalization(test_date(2024, 6, 1)), 3);
assert_eq!(asset.months_since_capitalization(test_date(2025, 3, 1)), 12);
}
#[test]
fn test_macrs_tables_sum_to_100() {
let tables: &[(&str, &[&str])] = &[
("3-year", MACRS_GDS_3_YEAR),
("5-year", MACRS_GDS_5_YEAR),
("7-year", MACRS_GDS_7_YEAR),
("10-year", MACRS_GDS_10_YEAR),
("15-year", MACRS_GDS_15_YEAR),
("20-year", MACRS_GDS_20_YEAR),
];
let tolerance = dec!(0.02);
let hundred = dec!(100);
for (label, table) in tables {
let sum: Decimal = table.iter().map(|s| macrs_pct(s)).sum();
let diff = (sum - hundred).abs();
assert!(
diff < tolerance,
"MACRS GDS {label} table sums to {sum}, expected ~100.0"
);
}
}
#[test]
fn test_macrs_table_for_life_mapping() {
assert_eq!(macrs_table_for_life(1).unwrap().len(), 4);
assert_eq!(macrs_table_for_life(3).unwrap().len(), 4);
assert_eq!(macrs_table_for_life(4).unwrap().len(), 6);
assert_eq!(macrs_table_for_life(5).unwrap().len(), 6);
assert_eq!(macrs_table_for_life(6).unwrap().len(), 8);
assert_eq!(macrs_table_for_life(7).unwrap().len(), 8);
assert_eq!(macrs_table_for_life(8).unwrap().len(), 11);
assert_eq!(macrs_table_for_life(10).unwrap().len(), 11);
assert_eq!(macrs_table_for_life(11).unwrap().len(), 16);
assert_eq!(macrs_table_for_life(15).unwrap().len(), 16);
assert_eq!(macrs_table_for_life(16).unwrap().len(), 21);
assert_eq!(macrs_table_for_life(20).unwrap().len(), 21);
assert!(macrs_table_for_life(0).is_none());
assert!(macrs_table_for_life(21).is_none());
assert!(macrs_table_for_life(100).is_none());
}
#[test]
fn test_macrs_depreciation_5_year_asset() {
let asset = FixedAsset::new(
"FA-MACRS",
"Vehicle",
AssetClass::Vehicles,
"1000",
test_date(2024, 1, 1),
Decimal::from(10000),
)
.with_useful_life_months(60) .with_depreciation_method(DepreciationMethod::Macrs);
assert_eq!(asset.macrs_depreciation(1), Decimal::from(2000));
assert_eq!(asset.macrs_depreciation(2), Decimal::from(3200));
assert_eq!(asset.macrs_depreciation(3), Decimal::from(1920));
assert_eq!(asset.macrs_depreciation(4), Decimal::from(1152));
assert_eq!(asset.macrs_depreciation(5), Decimal::from(1152));
assert_eq!(asset.macrs_depreciation(6), Decimal::from(576));
assert_eq!(asset.macrs_depreciation(7), Decimal::ZERO);
assert_eq!(asset.macrs_depreciation(0), Decimal::ZERO);
}
#[test]
fn test_macrs_calculate_monthly_depreciation_uses_tables() {
let asset = FixedAsset::new(
"FA-MACRS-M",
"Vehicle",
AssetClass::Vehicles,
"1000",
test_date(2024, 1, 1),
Decimal::from(12000),
)
.with_useful_life_months(60) .with_depreciation_method(DepreciationMethod::Macrs);
let monthly_year1 = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
assert_eq!(monthly_year1, Decimal::from(200));
let monthly_year2 = asset.calculate_monthly_depreciation(test_date(2025, 2, 1));
assert_eq!(monthly_year2, Decimal::from(320));
}
#[test]
fn test_ddb_depreciation() {
let asset = FixedAsset::new(
"FA-DDB",
"Server",
AssetClass::ComputerHardware,
"1000",
test_date(2024, 1, 1),
Decimal::from(3600),
)
.with_useful_life_months(36) .with_depreciation_method(DepreciationMethod::DoubleDecliningBalance);
let monthly = asset.ddb_depreciation();
assert_eq!(monthly, Decimal::from(200));
}
#[test]
fn test_ddb_depreciation_with_accumulated() {
let mut asset = FixedAsset::new(
"FA-DDB2",
"Laptop",
AssetClass::ComputerHardware,
"1000",
test_date(2024, 1, 1),
Decimal::from(1800),
)
.with_useful_life_months(36);
asset.apply_depreciation(Decimal::from(900));
let monthly = asset.ddb_depreciation();
assert_eq!(monthly, Decimal::from(50));
}
#[test]
fn test_ddb_depreciation_respects_salvage() {
let mut asset = FixedAsset::new(
"FA-DDB3",
"Printer",
AssetClass::ComputerHardware,
"1000",
test_date(2024, 1, 1),
Decimal::from(1800),
)
.with_useful_life_months(36)
.with_salvage_value(Decimal::from(200));
asset.apply_depreciation(Decimal::from(1590));
let monthly = asset.ddb_depreciation();
assert_eq!(monthly, Decimal::from(10));
}
#[test]
fn test_degressiv_depreciation_initial() {
let asset = FixedAsset::new(
"FA-DEG",
"Maschine",
AssetClass::MachineryEquipment,
"DE01",
test_date(2024, 1, 1),
Decimal::from(120000),
)
.with_useful_life_months(120)
.with_depreciation_method(DepreciationMethod::Degressiv);
let monthly = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
assert_eq!(monthly, Decimal::from(3000));
}
#[test]
fn test_degressiv_rate_capped_at_30_percent() {
let asset = FixedAsset::new(
"FA-DEG2",
"Fahrzeug",
AssetClass::Vehicles,
"DE01",
test_date(2024, 1, 1),
Decimal::from(6000),
)
.with_useful_life_months(60)
.with_depreciation_method(DepreciationMethod::Degressiv);
let monthly = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
assert_eq!(monthly, Decimal::from(150));
let short_asset = FixedAsset::new(
"FA-DEG2S",
"Server",
AssetClass::ComputerHardware,
"DE01",
test_date(2024, 1, 1),
Decimal::from(3600),
)
.with_useful_life_months(36)
.with_depreciation_method(DepreciationMethod::Degressiv);
let monthly_short = short_asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
assert!(
monthly_short > Decimal::from(100),
"SL should win for 3-year asset"
);
}
#[test]
fn test_degressiv_switches_to_straight_line() {
let mut asset = FixedAsset::new(
"FA-DEG3",
"Fahrzeug",
AssetClass::Vehicles,
"DE01",
test_date(2024, 1, 1),
Decimal::from(10000),
)
.with_useful_life_months(120)
.with_depreciation_method(DepreciationMethod::Degressiv);
asset.apply_depreciation(Decimal::from(9000));
let dep = asset.calculate_monthly_depreciation(test_date(2032, 1, 1));
assert!(
dep > Decimal::from(25),
"Should switch to SL when it exceeds Degressiv"
);
assert!(dep < Decimal::from(42), "SL should be ~41.67");
}
#[test]
fn test_gwg_field_default() {
let asset = FixedAsset::new(
"FA-GWG",
"Keyboard",
AssetClass::ComputerHardware,
"DE01",
test_date(2024, 1, 1),
Decimal::from(200),
);
assert_eq!(asset.is_gwg, None, "is_gwg should default to None");
}
}