1use chrono::{DateTime, NaiveDate, Utc};
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6use serde::{Deserialize, Serialize};
7
8use crate::models::subledger::{CurrencyAmount, GLReference, SubledgerDocumentStatus, TaxInfo};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct APDebitMemo {
13 pub debit_memo_number: String,
15 pub company_code: String,
17 pub vendor_id: String,
19 pub vendor_name: String,
21 pub memo_date: NaiveDate,
23 pub posting_date: NaiveDate,
25 pub memo_type: APDebitMemoType,
27 pub status: SubledgerDocumentStatus,
29 pub reason_code: DebitMemoReason,
31 pub reason_description: String,
33 pub lines: Vec<APDebitMemoLine>,
35 pub net_amount: CurrencyAmount,
37 pub tax_amount: CurrencyAmount,
39 pub gross_amount: CurrencyAmount,
41 pub amount_applied: Decimal,
43 pub amount_remaining: Decimal,
45 pub tax_details: Vec<TaxInfo>,
47 pub reference_invoice: Option<String>,
49 pub reference_po: Option<String>,
51 pub reference_gr: Option<String>,
53 pub applied_invoices: Vec<DebitMemoApplication>,
55 pub gl_reference: Option<GLReference>,
57 pub requires_approval: bool,
59 pub approval_status: APApprovalStatus,
61 pub approved_by: Option<String>,
63 pub approved_date: Option<NaiveDate>,
65 pub created_at: DateTime<Utc>,
67 pub created_by: Option<String>,
69 pub notes: Option<String>,
71}
72
73impl APDebitMemo {
74 #[allow(clippy::too_many_arguments)]
76 pub fn new(
77 debit_memo_number: String,
78 company_code: String,
79 vendor_id: String,
80 vendor_name: String,
81 memo_date: NaiveDate,
82 reason_code: DebitMemoReason,
83 reason_description: String,
84 currency: String,
85 ) -> Self {
86 Self {
87 debit_memo_number,
88 company_code,
89 vendor_id,
90 vendor_name,
91 memo_date,
92 posting_date: memo_date,
93 memo_type: APDebitMemoType::Standard,
94 status: SubledgerDocumentStatus::Open,
95 reason_code,
96 reason_description,
97 lines: Vec::new(),
98 net_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
99 tax_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
100 gross_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency),
101 amount_applied: Decimal::ZERO,
102 amount_remaining: Decimal::ZERO,
103 tax_details: Vec::new(),
104 reference_invoice: None,
105 reference_po: None,
106 reference_gr: None,
107 applied_invoices: Vec::new(),
108 gl_reference: None,
109 requires_approval: true,
110 approval_status: APApprovalStatus::Pending,
111 approved_by: None,
112 approved_date: None,
113 created_at: Utc::now(),
114 created_by: None,
115 notes: None,
116 }
117 }
118
119 #[allow(clippy::too_many_arguments)]
121 pub fn for_invoice(
122 debit_memo_number: String,
123 company_code: String,
124 vendor_id: String,
125 vendor_name: String,
126 memo_date: NaiveDate,
127 invoice_number: String,
128 reason_code: DebitMemoReason,
129 reason_description: String,
130 currency: String,
131 ) -> Self {
132 let mut memo = Self::new(
133 debit_memo_number,
134 company_code,
135 vendor_id,
136 vendor_name,
137 memo_date,
138 reason_code,
139 reason_description,
140 currency,
141 );
142 memo.reference_invoice = Some(invoice_number);
143 memo
144 }
145
146 pub fn add_line(&mut self, line: APDebitMemoLine) {
148 self.lines.push(line);
149 self.recalculate_totals();
150 }
151
152 pub fn recalculate_totals(&mut self) {
154 let net_total: Decimal = self.lines.iter().map(|l| l.net_amount).sum();
155 let tax_total: Decimal = self.lines.iter().map(|l| l.tax_amount).sum();
156 let gross_total = net_total + tax_total;
157
158 self.net_amount.document_amount = net_total;
159 self.net_amount.local_amount = net_total * self.net_amount.exchange_rate;
160 self.tax_amount.document_amount = tax_total;
161 self.tax_amount.local_amount = tax_total * self.tax_amount.exchange_rate;
162 self.gross_amount.document_amount = gross_total;
163 self.gross_amount.local_amount = gross_total * self.gross_amount.exchange_rate;
164 self.amount_remaining = gross_total - self.amount_applied;
165 }
166
167 pub fn apply_to_invoice(&mut self, invoice_number: String, amount: Decimal) {
169 let application = DebitMemoApplication {
170 invoice_number,
171 amount_applied: amount,
172 application_date: chrono::Local::now().date_naive(),
173 };
174
175 self.applied_invoices.push(application);
176 self.amount_applied += amount;
177 self.amount_remaining = self.gross_amount.document_amount - self.amount_applied;
178
179 if self.amount_remaining <= Decimal::ZERO {
180 self.status = SubledgerDocumentStatus::Cleared;
181 } else {
182 self.status = SubledgerDocumentStatus::PartiallyCleared;
183 }
184 }
185
186 pub fn with_po_reference(mut self, po_number: String) -> Self {
188 self.reference_po = Some(po_number);
189 self
190 }
191
192 pub fn with_gr_reference(mut self, gr_number: String) -> Self {
194 self.reference_gr = Some(gr_number);
195 self.memo_type = APDebitMemoType::Return;
196 self
197 }
198
199 pub fn approve(&mut self, approver: String, approval_date: NaiveDate) {
201 self.approval_status = APApprovalStatus::Approved;
202 self.approved_by = Some(approver);
203 self.approved_date = Some(approval_date);
204 }
205
206 pub fn reject(&mut self, reason: String) {
208 self.approval_status = APApprovalStatus::Rejected;
209 self.notes = Some(format!(
210 "{}Rejected: {}",
211 self.notes
212 .as_ref()
213 .map(|n| format!("{}. ", n))
214 .unwrap_or_default(),
215 reason
216 ));
217 }
218
219 pub fn set_gl_reference(&mut self, reference: GLReference) {
221 self.gl_reference = Some(reference);
222 }
223
224 pub fn check_approval_threshold(&mut self, threshold: Decimal) {
226 self.requires_approval = self.gross_amount.document_amount > threshold;
227 if !self.requires_approval {
228 self.approval_status = APApprovalStatus::NotRequired;
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
235pub enum APDebitMemoType {
236 #[default]
238 Standard,
239 Return,
241 PriceAdjustment,
243 QuantityAdjustment,
245 Rebate,
247 QualityClaim,
249 Cancellation,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
255pub enum DebitMemoReason {
256 Return,
258 Damaged,
260 WrongItem,
262 PriceOvercharge,
264 QuantityShortage,
266 QualityIssue,
268 LateDeliveryPenalty,
270 DuplicateInvoice,
272 ServiceNotPerformed,
274 ContractAdjustment,
276 #[default]
278 Other,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct APDebitMemoLine {
284 pub line_number: u32,
286 pub material_id: Option<String>,
288 pub description: String,
290 pub quantity: Decimal,
292 pub unit: String,
294 pub unit_price: Decimal,
296 pub net_amount: Decimal,
298 pub tax_code: Option<String>,
300 pub tax_rate: Decimal,
302 pub tax_amount: Decimal,
304 pub gross_amount: Decimal,
306 pub gl_account: String,
308 pub reference_invoice_line: Option<u32>,
310 pub cost_center: Option<String>,
312}
313
314impl APDebitMemoLine {
315 pub fn new(
317 line_number: u32,
318 description: String,
319 quantity: Decimal,
320 unit: String,
321 unit_price: Decimal,
322 gl_account: String,
323 ) -> Self {
324 let net_amount = (quantity * unit_price).round_dp(2);
325 Self {
326 line_number,
327 material_id: None,
328 description,
329 quantity,
330 unit,
331 unit_price,
332 net_amount,
333 tax_code: None,
334 tax_rate: Decimal::ZERO,
335 tax_amount: Decimal::ZERO,
336 gross_amount: net_amount,
337 gl_account,
338 reference_invoice_line: None,
339 cost_center: None,
340 }
341 }
342
343 pub fn with_tax(mut self, tax_code: String, tax_rate: Decimal) -> Self {
345 self.tax_code = Some(tax_code);
346 self.tax_rate = tax_rate;
347 self.tax_amount = (self.net_amount * tax_rate / dec!(100)).round_dp(2);
348 self.gross_amount = self.net_amount + self.tax_amount;
349 self
350 }
351
352 pub fn with_invoice_reference(mut self, line_number: u32) -> Self {
354 self.reference_invoice_line = Some(line_number);
355 self
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct DebitMemoApplication {
362 pub invoice_number: String,
364 pub amount_applied: Decimal,
366 pub application_date: NaiveDate,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
372pub enum APApprovalStatus {
373 #[default]
375 Pending,
376 Approved,
378 Rejected,
380 NotRequired,
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_debit_memo_creation() {
390 let memo = APDebitMemo::new(
391 "DM001".to_string(),
392 "1000".to_string(),
393 "VEND001".to_string(),
394 "Test Vendor".to_string(),
395 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
396 DebitMemoReason::Return,
397 "Goods returned".to_string(),
398 "USD".to_string(),
399 );
400
401 assert_eq!(memo.status, SubledgerDocumentStatus::Open);
402 assert_eq!(memo.approval_status, APApprovalStatus::Pending);
403 }
404
405 #[test]
406 fn test_debit_memo_totals() {
407 let mut memo = APDebitMemo::new(
408 "DM001".to_string(),
409 "1000".to_string(),
410 "VEND001".to_string(),
411 "Test Vendor".to_string(),
412 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
413 DebitMemoReason::PriceOvercharge,
414 "Price correction".to_string(),
415 "USD".to_string(),
416 );
417
418 let line = APDebitMemoLine::new(
419 1,
420 "Product A".to_string(),
421 dec!(10),
422 "EA".to_string(),
423 dec!(50),
424 "5000".to_string(),
425 )
426 .with_tax("VAT".to_string(), dec!(10));
427
428 memo.add_line(line);
429
430 assert_eq!(memo.net_amount.document_amount, dec!(500));
431 assert_eq!(memo.tax_amount.document_amount, dec!(50));
432 assert_eq!(memo.gross_amount.document_amount, dec!(550));
433 }
434
435 #[test]
436 fn test_apply_to_invoice() {
437 let mut memo = APDebitMemo::new(
438 "DM001".to_string(),
439 "1000".to_string(),
440 "VEND001".to_string(),
441 "Test Vendor".to_string(),
442 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
443 DebitMemoReason::Return,
444 "Goods returned".to_string(),
445 "USD".to_string(),
446 );
447
448 let line = APDebitMemoLine::new(
449 1,
450 "Product A".to_string(),
451 dec!(10),
452 "EA".to_string(),
453 dec!(100),
454 "5000".to_string(),
455 );
456 memo.add_line(line);
457
458 memo.apply_to_invoice("INV001".to_string(), dec!(500));
459
460 assert_eq!(memo.amount_applied, dec!(500));
461 assert_eq!(memo.amount_remaining, dec!(500));
462 assert_eq!(memo.status, SubledgerDocumentStatus::PartiallyCleared);
463 }
464
465 #[test]
466 fn test_approval_workflow() {
467 let mut memo = APDebitMemo::new(
468 "DM001".to_string(),
469 "1000".to_string(),
470 "VEND001".to_string(),
471 "Test Vendor".to_string(),
472 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
473 DebitMemoReason::Return,
474 "Goods returned".to_string(),
475 "USD".to_string(),
476 );
477
478 memo.approve(
479 "MANAGER1".to_string(),
480 NaiveDate::from_ymd_opt(2024, 2, 16).unwrap(),
481 );
482
483 assert_eq!(memo.approval_status, APApprovalStatus::Approved);
484 assert_eq!(memo.approved_by, Some("MANAGER1".to_string()));
485 }
486
487 #[test]
488 fn test_approval_threshold() {
489 let mut memo = APDebitMemo::new(
490 "DM001".to_string(),
491 "1000".to_string(),
492 "VEND001".to_string(),
493 "Test Vendor".to_string(),
494 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
495 DebitMemoReason::Return,
496 "Goods returned".to_string(),
497 "USD".to_string(),
498 );
499
500 let line = APDebitMemoLine::new(
501 1,
502 "Product A".to_string(),
503 dec!(1),
504 "EA".to_string(),
505 dec!(50),
506 "5000".to_string(),
507 );
508 memo.add_line(line);
509
510 memo.check_approval_threshold(dec!(100));
512 assert_eq!(memo.approval_status, APApprovalStatus::NotRequired);
513 assert!(!memo.requires_approval);
514 }
515}