1use chrono::{DateTime, NaiveDate, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use crate::models::subledger::{CurrencyAmount, GLReference, SubledgerDocumentStatus, TaxInfo};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ARCreditMemo {
12 pub credit_memo_number: String,
14 pub company_code: String,
16 pub customer_id: String,
18 pub customer_name: String,
20 pub memo_date: NaiveDate,
22 pub posting_date: NaiveDate,
24 pub memo_type: ARCreditMemoType,
26 pub status: SubledgerDocumentStatus,
28 pub reason_code: CreditMemoReason,
30 pub reason_description: String,
32 pub lines: Vec<ARCreditMemoLine>,
34 pub net_amount: CurrencyAmount,
36 pub tax_amount: CurrencyAmount,
38 pub gross_amount: CurrencyAmount,
40 pub amount_applied: Decimal,
42 pub amount_remaining: Decimal,
44 pub tax_details: Vec<TaxInfo>,
46 pub reference_invoice: Option<String>,
48 pub reference_return: Option<String>,
50 pub applied_invoices: Vec<CreditMemoApplication>,
52 pub gl_reference: Option<GLReference>,
54 pub approval_status: ApprovalStatus,
56 pub approved_by: Option<String>,
58 pub approved_date: Option<NaiveDate>,
60 #[serde(with = "crate::serde_timestamp::utc")]
62 pub created_at: DateTime<Utc>,
63 pub created_by: Option<String>,
65 pub notes: Option<String>,
67}
68
69impl ARCreditMemo {
70 #[allow(clippy::too_many_arguments)]
72 pub fn new(
73 credit_memo_number: String,
74 company_code: String,
75 customer_id: String,
76 customer_name: String,
77 memo_date: NaiveDate,
78 reason_code: CreditMemoReason,
79 reason_description: String,
80 currency: String,
81 ) -> Self {
82 Self {
83 credit_memo_number,
84 company_code,
85 customer_id,
86 customer_name,
87 memo_date,
88 posting_date: memo_date,
89 memo_type: ARCreditMemoType::Standard,
90 status: SubledgerDocumentStatus::Open,
91 reason_code,
92 reason_description,
93 lines: Vec::new(),
94 net_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
95 tax_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
96 gross_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency),
97 amount_applied: Decimal::ZERO,
98 amount_remaining: Decimal::ZERO,
99 tax_details: Vec::new(),
100 reference_invoice: None,
101 reference_return: None,
102 applied_invoices: Vec::new(),
103 gl_reference: None,
104 approval_status: ApprovalStatus::Pending,
105 approved_by: None,
106 approved_date: None,
107 created_at: Utc::now(),
108 created_by: None,
109 notes: None,
110 }
111 }
112
113 #[allow(clippy::too_many_arguments)]
115 pub fn for_invoice(
116 credit_memo_number: String,
117 company_code: String,
118 customer_id: String,
119 customer_name: String,
120 memo_date: NaiveDate,
121 invoice_number: String,
122 reason_code: CreditMemoReason,
123 reason_description: String,
124 currency: String,
125 ) -> Self {
126 let mut memo = Self::new(
127 credit_memo_number,
128 company_code,
129 customer_id,
130 customer_name,
131 memo_date,
132 reason_code,
133 reason_description,
134 currency,
135 );
136 memo.reference_invoice = Some(invoice_number);
137 memo
138 }
139
140 pub fn add_line(&mut self, line: ARCreditMemoLine) {
142 self.lines.push(line);
143 self.recalculate_totals();
144 }
145
146 pub fn recalculate_totals(&mut self) {
148 let net_total: Decimal = self.lines.iter().map(|l| l.net_amount).sum();
149 let tax_total: Decimal = self.lines.iter().map(|l| l.tax_amount).sum();
150 let gross_total = net_total + tax_total;
151
152 self.net_amount.document_amount = net_total;
153 self.net_amount.local_amount = net_total * self.net_amount.exchange_rate;
154 self.tax_amount.document_amount = tax_total;
155 self.tax_amount.local_amount = tax_total * self.tax_amount.exchange_rate;
156 self.gross_amount.document_amount = gross_total;
157 self.gross_amount.local_amount = gross_total * self.gross_amount.exchange_rate;
158 self.amount_remaining = gross_total - self.amount_applied;
159 }
160
161 pub fn apply_to_invoice(&mut self, invoice_number: String, amount: Decimal) {
163 let application = CreditMemoApplication {
164 invoice_number,
165 amount_applied: amount,
166 application_date: chrono::Local::now().date_naive(),
167 };
168
169 self.applied_invoices.push(application);
170 self.amount_applied += amount;
171 self.amount_remaining = self.gross_amount.document_amount - self.amount_applied;
172
173 if self.amount_remaining <= Decimal::ZERO {
174 self.status = SubledgerDocumentStatus::Cleared;
175 } else {
176 self.status = SubledgerDocumentStatus::PartiallyCleared;
177 }
178 }
179
180 pub fn approve(&mut self, approver: String, approval_date: NaiveDate) {
182 self.approval_status = ApprovalStatus::Approved;
183 self.approved_by = Some(approver);
184 self.approved_date = Some(approval_date);
185 }
186
187 pub fn reject(&mut self, reason: String) {
189 self.approval_status = ApprovalStatus::Rejected;
190 self.notes = Some(format!(
191 "{}Rejected: {}",
192 self.notes
193 .as_ref()
194 .map(|n| format!("{n}. "))
195 .unwrap_or_default(),
196 reason
197 ));
198 }
199
200 pub fn set_gl_reference(&mut self, reference: GLReference) {
202 self.gl_reference = Some(reference);
203 }
204
205 pub fn with_return_order(mut self, return_order: String) -> Self {
207 self.reference_return = Some(return_order);
208 self.memo_type = ARCreditMemoType::Return;
209 self
210 }
211
212 pub fn requires_approval(&self, threshold: Decimal) -> bool {
214 self.gross_amount.document_amount > threshold
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
220pub enum ARCreditMemoType {
221 #[default]
223 Standard,
224 Return,
226 PriceAdjustment,
228 QuantityAdjustment,
230 Rebate,
232 Promotional,
234 Cancellation,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
240pub enum CreditMemoReason {
241 Return,
243 Damaged,
245 WrongItem,
247 PriceError,
249 QuantityError,
251 QualityIssue,
253 LateDelivery,
255 Promotional,
257 VolumeRebate,
259 Goodwill,
261 BillingError,
263 ContractAdjustment,
265 #[default]
267 Other,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ARCreditMemoLine {
273 pub line_number: u32,
275 pub material_id: Option<String>,
277 pub description: String,
279 pub quantity: Decimal,
281 pub unit: String,
283 pub unit_price: Decimal,
285 pub net_amount: Decimal,
287 pub tax_code: Option<String>,
289 pub tax_rate: Decimal,
291 pub tax_amount: Decimal,
293 pub gross_amount: Decimal,
295 pub revenue_account: String,
297 pub reference_invoice_line: Option<u32>,
299 pub cost_center: Option<String>,
301 pub profit_center: Option<String>,
303}
304
305impl ARCreditMemoLine {
306 pub fn new(
308 line_number: u32,
309 description: String,
310 quantity: Decimal,
311 unit: String,
312 unit_price: Decimal,
313 revenue_account: String,
314 ) -> Self {
315 let net_amount = (quantity * unit_price).round_dp(2);
316 Self {
317 line_number,
318 material_id: None,
319 description,
320 quantity,
321 unit,
322 unit_price,
323 net_amount,
324 tax_code: None,
325 tax_rate: Decimal::ZERO,
326 tax_amount: Decimal::ZERO,
327 gross_amount: net_amount,
328 revenue_account,
329 reference_invoice_line: None,
330 cost_center: None,
331 profit_center: None,
332 }
333 }
334
335 pub fn with_tax(mut self, tax_code: String, tax_rate: Decimal) -> Self {
337 self.tax_code = Some(tax_code);
338 self.tax_rate = tax_rate;
339 self.tax_amount = (self.net_amount * tax_rate / rust_decimal_macros::dec!(100)).round_dp(2);
340 self.gross_amount = self.net_amount + self.tax_amount;
341 self
342 }
343
344 pub fn with_invoice_reference(mut self, line_number: u32) -> Self {
346 self.reference_invoice_line = Some(line_number);
347 self
348 }
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct CreditMemoApplication {
354 pub invoice_number: String,
356 pub amount_applied: Decimal,
358 pub application_date: NaiveDate,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
364pub enum ApprovalStatus {
365 #[default]
367 Pending,
368 Approved,
370 Rejected,
372 NotRequired,
374}
375
376#[cfg(test)]
377#[allow(clippy::unwrap_used)]
378mod tests {
379 use super::*;
380 use rust_decimal_macros::dec;
381
382 #[test]
383 fn test_credit_memo_creation() {
384 let memo = ARCreditMemo::new(
385 "CM001".to_string(),
386 "1000".to_string(),
387 "CUST001".to_string(),
388 "Test Customer".to_string(),
389 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
390 CreditMemoReason::Return,
391 "Goods returned".to_string(),
392 "USD".to_string(),
393 );
394
395 assert_eq!(memo.status, SubledgerDocumentStatus::Open);
396 assert_eq!(memo.approval_status, ApprovalStatus::Pending);
397 }
398
399 #[test]
400 fn test_credit_memo_totals() {
401 let mut memo = ARCreditMemo::new(
402 "CM001".to_string(),
403 "1000".to_string(),
404 "CUST001".to_string(),
405 "Test Customer".to_string(),
406 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
407 CreditMemoReason::PriceError,
408 "Price correction".to_string(),
409 "USD".to_string(),
410 );
411
412 let line = ARCreditMemoLine::new(
413 1,
414 "Product A".to_string(),
415 dec!(5),
416 "EA".to_string(),
417 dec!(100),
418 "4000".to_string(),
419 )
420 .with_tax("VAT".to_string(), dec!(20));
421
422 memo.add_line(line);
423
424 assert_eq!(memo.net_amount.document_amount, dec!(500));
425 assert_eq!(memo.tax_amount.document_amount, dec!(100));
426 assert_eq!(memo.gross_amount.document_amount, dec!(600));
427 }
428
429 #[test]
430 fn test_apply_to_invoice() {
431 let mut memo = ARCreditMemo::new(
432 "CM001".to_string(),
433 "1000".to_string(),
434 "CUST001".to_string(),
435 "Test Customer".to_string(),
436 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
437 CreditMemoReason::Return,
438 "Goods returned".to_string(),
439 "USD".to_string(),
440 );
441
442 let line = ARCreditMemoLine::new(
443 1,
444 "Product A".to_string(),
445 dec!(10),
446 "EA".to_string(),
447 dec!(50),
448 "4000".to_string(),
449 );
450 memo.add_line(line);
451
452 memo.apply_to_invoice("INV001".to_string(), dec!(300));
453
454 assert_eq!(memo.amount_applied, dec!(300));
455 assert_eq!(memo.amount_remaining, dec!(200));
456 assert_eq!(memo.status, SubledgerDocumentStatus::PartiallyCleared);
457 }
458
459 #[test]
460 fn test_approval_workflow() {
461 let mut memo = ARCreditMemo::new(
462 "CM001".to_string(),
463 "1000".to_string(),
464 "CUST001".to_string(),
465 "Test Customer".to_string(),
466 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
467 CreditMemoReason::Return,
468 "Goods returned".to_string(),
469 "USD".to_string(),
470 );
471
472 memo.approve(
473 "MANAGER1".to_string(),
474 NaiveDate::from_ymd_opt(2024, 2, 16).unwrap(),
475 );
476
477 assert_eq!(memo.approval_status, ApprovalStatus::Approved);
478 assert_eq!(memo.approved_by, Some("MANAGER1".to_string()));
479 }
480}