datasynth_generators/period_close/
depreciation.rs1use rust_decimal::Decimal;
4use rust_decimal_macros::dec;
5
6use datasynth_core::models::subledger::fa::{
7 AssetStatus, DepreciationAreaType, DepreciationEntry, DepreciationRun, DepreciationRunStatus,
8 FixedAssetRecord,
9};
10use datasynth_core::models::{FiscalPeriod, JournalEntry, JournalEntryLine};
11
12#[derive(Debug, Clone)]
14pub struct DepreciationRunConfig {
15 pub default_expense_account: String,
17 pub default_accum_depr_account: String,
19 pub post_zero_entries: bool,
21 pub minimum_amount: Decimal,
23}
24
25impl Default for DepreciationRunConfig {
26 fn default() -> Self {
27 Self {
28 default_expense_account: "6100".to_string(),
29 default_accum_depr_account: "1510".to_string(),
30 post_zero_entries: false,
31 minimum_amount: dec!(0.01),
32 }
33 }
34}
35
36pub struct DepreciationRunGenerator {
38 config: DepreciationRunConfig,
39 run_counter: u64,
40}
41
42impl DepreciationRunGenerator {
43 pub fn new(config: DepreciationRunConfig) -> Self {
45 Self {
46 config,
47 run_counter: 0,
48 }
49 }
50
51 pub fn execute_run(
53 &mut self,
54 company_code: &str,
55 assets: &mut [FixedAssetRecord],
56 fiscal_period: &FiscalPeriod,
57 ) -> DepreciationRunResult {
58 self.run_counter += 1;
59 let run_id = format!("DEPR-{}-{:08}", company_code, self.run_counter);
60
61 let mut run = DepreciationRun::new(
62 run_id.clone(),
63 company_code.to_string(),
64 fiscal_period.year,
65 fiscal_period.period as u32,
66 DepreciationAreaType::Book,
67 fiscal_period.end_date,
68 "SYSTEM".to_string(),
69 );
70
71 run.start();
72
73 let mut journal_entries = Vec::new();
74 let errors = Vec::new();
75
76 for asset in assets.iter_mut() {
77 if asset.status != AssetStatus::Active {
79 continue;
80 }
81
82 if asset.company_code != company_code {
84 continue;
85 }
86
87 if let Some(entry) = DepreciationEntry::from_asset(asset, DepreciationAreaType::Book) {
89 if entry.depreciation_amount < self.config.minimum_amount
90 && !self.config.post_zero_entries
91 {
92 continue;
93 }
94
95 let je = self.generate_depreciation_je(asset, &entry, fiscal_period);
97
98 asset.record_depreciation(entry.depreciation_amount, DepreciationAreaType::Book);
100
101 if asset.is_fully_depreciated() {
103 asset.status = AssetStatus::FullyDepreciated;
104 }
105
106 run.add_entry(entry);
107 journal_entries.push(je);
108 }
109 }
110
111 run.complete();
112
113 DepreciationRunResult {
114 run,
115 journal_entries,
116 errors,
117 }
118 }
119
120 fn generate_depreciation_je(
122 &self,
123 asset: &FixedAssetRecord,
124 entry: &DepreciationEntry,
125 period: &FiscalPeriod,
126 ) -> JournalEntry {
127 let expense_account = if entry.expense_account.is_empty() {
128 &self.config.default_expense_account
129 } else {
130 &entry.expense_account
131 };
132
133 let accum_account = if entry.accum_depr_account.is_empty() {
134 &self.config.default_accum_depr_account
135 } else {
136 &entry.accum_depr_account
137 };
138
139 let mut je = JournalEntry::new_simple(
140 format!("DEPR-{}", asset.asset_number),
141 asset.company_code.clone(),
142 period.end_date,
143 format!(
144 "Depreciation {} P{}/{}",
145 asset.asset_number, period.year, period.period
146 ),
147 );
148
149 je.add_line(JournalEntryLine {
151 line_number: 1,
152 gl_account: expense_account.to_string(),
153 debit_amount: entry.depreciation_amount,
154 cost_center: asset.cost_center.clone(),
155 profit_center: asset.profit_center.clone(),
156 reference: Some(asset.asset_number.clone()),
157 assignment: Some(format!("{:?}", asset.asset_class)),
158 text: Some(asset.description.clone()),
159 ..Default::default()
160 });
161
162 je.add_line(JournalEntryLine {
164 line_number: 2,
165 gl_account: accum_account.to_string(),
166 credit_amount: entry.depreciation_amount,
167 reference: Some(asset.asset_number.clone()),
168 assignment: Some(format!("{:?}", asset.asset_class)),
169 ..Default::default()
170 });
171
172 je
173 }
174
175 pub fn forecast_depreciation(
177 &self,
178 assets: &[FixedAssetRecord],
179 start_period: &FiscalPeriod,
180 months: u32,
181 ) -> Vec<DepreciationForecastEntry> {
182 let mut forecast = Vec::new();
183
184 let mut simulated_assets: Vec<SimulatedAsset> = assets
186 .iter()
187 .filter(|a| a.status == AssetStatus::Active)
188 .map(|a| {
189 let monthly_depr = a
190 .depreciation_areas
191 .first()
192 .map(|area| area.calculate_monthly_depreciation())
193 .unwrap_or(Decimal::ZERO);
194 SimulatedAsset {
195 asset_number: a.asset_number.clone(),
196 net_book_value: a.net_book_value,
197 salvage_value: a.salvage_value(),
198 monthly_depreciation: monthly_depr,
199 }
200 })
201 .collect();
202
203 let mut current_year = start_period.year;
204 let mut current_month = start_period.period;
205
206 for _ in 0..months {
207 let period_key = format!("{}-{:02}", current_year, current_month);
208 let mut period_total = Decimal::ZERO;
209
210 for sim_asset in &mut simulated_assets {
211 let remaining = sim_asset.net_book_value - sim_asset.salvage_value;
212 if remaining > Decimal::ZERO {
213 let depr = sim_asset.monthly_depreciation.min(remaining);
214 sim_asset.net_book_value -= depr;
215 period_total += depr;
216 }
217 }
218
219 forecast.push(DepreciationForecastEntry {
220 period_key,
221 fiscal_year: current_year,
222 fiscal_period: current_month,
223 forecasted_depreciation: period_total,
224 });
225
226 if current_month == 12 {
228 current_month = 1;
229 current_year += 1;
230 } else {
231 current_month += 1;
232 }
233 }
234
235 forecast
236 }
237}
238
239struct SimulatedAsset {
241 asset_number: String,
242 net_book_value: Decimal,
243 salvage_value: Decimal,
244 monthly_depreciation: Decimal,
245}
246
247#[derive(Debug, Clone)]
249pub struct DepreciationRunResult {
250 pub run: DepreciationRun,
252 pub journal_entries: Vec<JournalEntry>,
254 pub errors: Vec<DepreciationError>,
256}
257
258impl DepreciationRunResult {
259 pub fn is_success(&self) -> bool {
261 matches!(
262 self.run.status,
263 DepreciationRunStatus::Completed | DepreciationRunStatus::CompletedWithErrors
264 )
265 }
266
267 pub fn total_depreciation(&self) -> Decimal {
269 self.run.total_depreciation
270 }
271}
272
273#[derive(Debug, Clone)]
275pub struct DepreciationError {
276 pub asset_number: String,
278 pub error: String,
280}
281
282#[derive(Debug, Clone)]
284pub struct DepreciationForecastEntry {
285 pub period_key: String,
287 pub fiscal_year: i32,
289 pub fiscal_period: u8,
291 pub forecasted_depreciation: Decimal,
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use chrono::NaiveDate;
299 use datasynth_core::models::subledger::fa::{AssetClass, DepreciationArea, DepreciationMethod};
300 use rust_decimal_macros::dec;
301
302 fn create_test_asset() -> FixedAssetRecord {
303 let mut asset = FixedAssetRecord::new(
304 "FA00001".to_string(),
305 "1000".to_string(),
306 AssetClass::MachineryEquipment,
307 "Test Machine".to_string(),
308 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
309 dec!(120000),
310 "USD".to_string(),
311 );
312
313 let area = DepreciationArea::new(
315 DepreciationAreaType::Book,
316 DepreciationMethod::StraightLine,
317 60, dec!(120000),
319 )
320 .with_salvage_value(dec!(12000));
321
322 asset.add_depreciation_area(area);
323 asset.cost_center = Some("CC100".to_string());
324
325 asset
326 }
327
328 #[test]
329 fn test_depreciation_run() {
330 let mut generator = DepreciationRunGenerator::new(DepreciationRunConfig::default());
331 let mut assets = vec![create_test_asset()];
332 let period = FiscalPeriod::monthly(2024, 1);
333
334 let result = generator.execute_run("1000", &mut assets, &period);
335
336 assert!(result.is_success());
337 assert!(result.journal_entries.iter().all(|je| je.is_balanced()));
338
339 assert_eq!(result.total_depreciation(), dec!(1800));
341 }
342
343 #[test]
344 fn test_depreciation_forecast() {
345 let generator = DepreciationRunGenerator::new(DepreciationRunConfig::default());
346 let assets = vec![create_test_asset()];
347 let period = FiscalPeriod::monthly(2024, 1);
348
349 let forecast = generator.forecast_depreciation(&assets, &period, 12);
350
351 assert_eq!(forecast.len(), 12);
352 assert!(forecast
353 .iter()
354 .all(|f| f.forecasted_depreciation == dec!(1800)));
355 }
356}