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 pub created_at: DateTime<Utc>,
62 pub created_by: Option<String>,
64 pub notes: Option<String>,
66}
67
68impl ARCreditMemo {
69 #[allow(clippy::too_many_arguments)]
71 pub fn new(
72 credit_memo_number: String,
73 company_code: String,
74 customer_id: String,
75 customer_name: String,
76 memo_date: NaiveDate,
77 reason_code: CreditMemoReason,
78 reason_description: String,
79 currency: String,
80 ) -> Self {
81 Self {
82 credit_memo_number,
83 company_code,
84 customer_id,
85 customer_name,
86 memo_date,
87 posting_date: memo_date,
88 memo_type: ARCreditMemoType::Standard,
89 status: SubledgerDocumentStatus::Open,
90 reason_code,
91 reason_description,
92 lines: Vec::new(),
93 net_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
94 tax_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
95 gross_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency),
96 amount_applied: Decimal::ZERO,
97 amount_remaining: Decimal::ZERO,
98 tax_details: Vec::new(),
99 reference_invoice: None,
100 reference_return: None,
101 applied_invoices: Vec::new(),
102 gl_reference: None,
103 approval_status: ApprovalStatus::Pending,
104 approved_by: None,
105 approved_date: None,
106 created_at: Utc::now(),
107 created_by: None,
108 notes: None,
109 }
110 }
111
112 #[allow(clippy::too_many_arguments)]
114 pub fn for_invoice(
115 credit_memo_number: String,
116 company_code: String,
117 customer_id: String,
118 customer_name: String,
119 memo_date: NaiveDate,
120 invoice_number: String,
121 reason_code: CreditMemoReason,
122 reason_description: String,
123 currency: String,
124 ) -> Self {
125 let mut memo = Self::new(
126 credit_memo_number,
127 company_code,
128 customer_id,
129 customer_name,
130 memo_date,
131 reason_code,
132 reason_description,
133 currency,
134 );
135 memo.reference_invoice = Some(invoice_number);
136 memo
137 }
138
139 pub fn add_line(&mut self, line: ARCreditMemoLine) {
141 self.lines.push(line);
142 self.recalculate_totals();
143 }
144
145 pub fn recalculate_totals(&mut self) {
147 let net_total: Decimal = self.lines.iter().map(|l| l.net_amount).sum();
148 let tax_total: Decimal = self.lines.iter().map(|l| l.tax_amount).sum();
149 let gross_total = net_total + tax_total;
150
151 self.net_amount.document_amount = net_total;
152 self.net_amount.local_amount = net_total * self.net_amount.exchange_rate;
153 self.tax_amount.document_amount = tax_total;
154 self.tax_amount.local_amount = tax_total * self.tax_amount.exchange_rate;
155 self.gross_amount.document_amount = gross_total;
156 self.gross_amount.local_amount = gross_total * self.gross_amount.exchange_rate;
157 self.amount_remaining = gross_total - self.amount_applied;
158 }
159
160 pub fn apply_to_invoice(&mut self, invoice_number: String, amount: Decimal) {
162 let application = CreditMemoApplication {
163 invoice_number,
164 amount_applied: amount,
165 application_date: chrono::Local::now().date_naive(),
166 };
167
168 self.applied_invoices.push(application);
169 self.amount_applied += amount;
170 self.amount_remaining = self.gross_amount.document_amount - self.amount_applied;
171
172 if self.amount_remaining <= Decimal::ZERO {
173 self.status = SubledgerDocumentStatus::Cleared;
174 } else {
175 self.status = SubledgerDocumentStatus::PartiallyCleared;
176 }
177 }
178
179 pub fn approve(&mut self, approver: String, approval_date: NaiveDate) {
181 self.approval_status = ApprovalStatus::Approved;
182 self.approved_by = Some(approver);
183 self.approved_date = Some(approval_date);
184 }
185
186 pub fn reject(&mut self, reason: String) {
188 self.approval_status = ApprovalStatus::Rejected;
189 self.notes = Some(format!(
190 "{}Rejected: {}",
191 self.notes
192 .as_ref()
193 .map(|n| format!("{}. ", n))
194 .unwrap_or_default(),
195 reason
196 ));
197 }
198
199 pub fn set_gl_reference(&mut self, reference: GLReference) {
201 self.gl_reference = Some(reference);
202 }
203
204 pub fn with_return_order(mut self, return_order: String) -> Self {
206 self.reference_return = Some(return_order);
207 self.memo_type = ARCreditMemoType::Return;
208 self
209 }
210
211 pub fn requires_approval(&self, threshold: Decimal) -> bool {
213 self.gross_amount.document_amount > threshold
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
219pub enum ARCreditMemoType {
220 #[default]
222 Standard,
223 Return,
225 PriceAdjustment,
227 QuantityAdjustment,
229 Rebate,
231 Promotional,
233 Cancellation,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
239pub enum CreditMemoReason {
240 Return,
242 Damaged,
244 WrongItem,
246 PriceError,
248 QuantityError,
250 QualityIssue,
252 LateDelivery,
254 Promotional,
256 VolumeRebate,
258 Goodwill,
260 BillingError,
262 ContractAdjustment,
264 #[default]
266 Other,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct ARCreditMemoLine {
272 pub line_number: u32,
274 pub material_id: Option<String>,
276 pub description: String,
278 pub quantity: Decimal,
280 pub unit: String,
282 pub unit_price: Decimal,
284 pub net_amount: Decimal,
286 pub tax_code: Option<String>,
288 pub tax_rate: Decimal,
290 pub tax_amount: Decimal,
292 pub gross_amount: Decimal,
294 pub revenue_account: String,
296 pub reference_invoice_line: Option<u32>,
298 pub cost_center: Option<String>,
300 pub profit_center: Option<String>,
302}
303
304impl ARCreditMemoLine {
305 pub fn new(
307 line_number: u32,
308 description: String,
309 quantity: Decimal,
310 unit: String,
311 unit_price: Decimal,
312 revenue_account: String,
313 ) -> Self {
314 let net_amount = (quantity * unit_price).round_dp(2);
315 Self {
316 line_number,
317 material_id: None,
318 description,
319 quantity,
320 unit,
321 unit_price,
322 net_amount,
323 tax_code: None,
324 tax_rate: Decimal::ZERO,
325 tax_amount: Decimal::ZERO,
326 gross_amount: net_amount,
327 revenue_account,
328 reference_invoice_line: None,
329 cost_center: None,
330 profit_center: None,
331 }
332 }
333
334 pub fn with_tax(mut self, tax_code: String, tax_rate: Decimal) -> Self {
336 self.tax_code = Some(tax_code);
337 self.tax_rate = tax_rate;
338 self.tax_amount = (self.net_amount * tax_rate / rust_decimal_macros::dec!(100)).round_dp(2);
339 self.gross_amount = self.net_amount + self.tax_amount;
340 self
341 }
342
343 pub fn with_invoice_reference(mut self, line_number: u32) -> Self {
345 self.reference_invoice_line = Some(line_number);
346 self
347 }
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct CreditMemoApplication {
353 pub invoice_number: String,
355 pub amount_applied: Decimal,
357 pub application_date: NaiveDate,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
363pub enum ApprovalStatus {
364 #[default]
366 Pending,
367 Approved,
369 Rejected,
371 NotRequired,
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use rust_decimal_macros::dec;
379
380 #[test]
381 fn test_credit_memo_creation() {
382 let memo = ARCreditMemo::new(
383 "CM001".to_string(),
384 "1000".to_string(),
385 "CUST001".to_string(),
386 "Test Customer".to_string(),
387 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
388 CreditMemoReason::Return,
389 "Goods returned".to_string(),
390 "USD".to_string(),
391 );
392
393 assert_eq!(memo.status, SubledgerDocumentStatus::Open);
394 assert_eq!(memo.approval_status, ApprovalStatus::Pending);
395 }
396
397 #[test]
398 fn test_credit_memo_totals() {
399 let mut memo = ARCreditMemo::new(
400 "CM001".to_string(),
401 "1000".to_string(),
402 "CUST001".to_string(),
403 "Test Customer".to_string(),
404 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
405 CreditMemoReason::PriceError,
406 "Price correction".to_string(),
407 "USD".to_string(),
408 );
409
410 let line = ARCreditMemoLine::new(
411 1,
412 "Product A".to_string(),
413 dec!(5),
414 "EA".to_string(),
415 dec!(100),
416 "4000".to_string(),
417 )
418 .with_tax("VAT".to_string(), dec!(20));
419
420 memo.add_line(line);
421
422 assert_eq!(memo.net_amount.document_amount, dec!(500));
423 assert_eq!(memo.tax_amount.document_amount, dec!(100));
424 assert_eq!(memo.gross_amount.document_amount, dec!(600));
425 }
426
427 #[test]
428 fn test_apply_to_invoice() {
429 let mut memo = ARCreditMemo::new(
430 "CM001".to_string(),
431 "1000".to_string(),
432 "CUST001".to_string(),
433 "Test Customer".to_string(),
434 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
435 CreditMemoReason::Return,
436 "Goods returned".to_string(),
437 "USD".to_string(),
438 );
439
440 let line = ARCreditMemoLine::new(
441 1,
442 "Product A".to_string(),
443 dec!(10),
444 "EA".to_string(),
445 dec!(50),
446 "4000".to_string(),
447 );
448 memo.add_line(line);
449
450 memo.apply_to_invoice("INV001".to_string(), dec!(300));
451
452 assert_eq!(memo.amount_applied, dec!(300));
453 assert_eq!(memo.amount_remaining, dec!(200));
454 assert_eq!(memo.status, SubledgerDocumentStatus::PartiallyCleared);
455 }
456
457 #[test]
458 fn test_approval_workflow() {
459 let mut memo = ARCreditMemo::new(
460 "CM001".to_string(),
461 "1000".to_string(),
462 "CUST001".to_string(),
463 "Test Customer".to_string(),
464 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
465 CreditMemoReason::Return,
466 "Goods returned".to_string(),
467 "USD".to_string(),
468 );
469
470 memo.approve(
471 "MANAGER1".to_string(),
472 NaiveDate::from_ymd_opt(2024, 2, 16).unwrap(),
473 );
474
475 assert_eq!(memo.approval_status, ApprovalStatus::Approved);
476 assert_eq!(memo.approved_by, Some("MANAGER1".to_string()));
477 }
478}