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 #[serde(with = "crate::serde_timestamp::utc")]
67 pub created_at: DateTime<Utc>,
68 pub created_by: Option<String>,
70 pub notes: Option<String>,
72}
73
74impl APDebitMemo {
75 #[allow(clippy::too_many_arguments)]
77 pub fn new(
78 debit_memo_number: String,
79 company_code: String,
80 vendor_id: String,
81 vendor_name: String,
82 memo_date: NaiveDate,
83 reason_code: DebitMemoReason,
84 reason_description: String,
85 currency: String,
86 ) -> Self {
87 Self {
88 debit_memo_number,
89 company_code,
90 vendor_id,
91 vendor_name,
92 memo_date,
93 posting_date: memo_date,
94 memo_type: APDebitMemoType::Standard,
95 status: SubledgerDocumentStatus::Open,
96 reason_code,
97 reason_description,
98 lines: Vec::new(),
99 net_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
100 tax_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
101 gross_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency),
102 amount_applied: Decimal::ZERO,
103 amount_remaining: Decimal::ZERO,
104 tax_details: Vec::new(),
105 reference_invoice: None,
106 reference_po: None,
107 reference_gr: None,
108 applied_invoices: Vec::new(),
109 gl_reference: None,
110 requires_approval: true,
111 approval_status: APApprovalStatus::Pending,
112 approved_by: None,
113 approved_date: None,
114 created_at: Utc::now(),
115 created_by: None,
116 notes: None,
117 }
118 }
119
120 #[allow(clippy::too_many_arguments)]
122 pub fn for_invoice(
123 debit_memo_number: String,
124 company_code: String,
125 vendor_id: String,
126 vendor_name: String,
127 memo_date: NaiveDate,
128 invoice_number: String,
129 reason_code: DebitMemoReason,
130 reason_description: String,
131 currency: String,
132 ) -> Self {
133 let mut memo = Self::new(
134 debit_memo_number,
135 company_code,
136 vendor_id,
137 vendor_name,
138 memo_date,
139 reason_code,
140 reason_description,
141 currency,
142 );
143 memo.reference_invoice = Some(invoice_number);
144 memo
145 }
146
147 pub fn add_line(&mut self, line: APDebitMemoLine) {
149 self.lines.push(line);
150 self.recalculate_totals();
151 }
152
153 pub fn recalculate_totals(&mut self) {
155 let net_total: Decimal = self.lines.iter().map(|l| l.net_amount).sum();
156 let tax_total: Decimal = self.lines.iter().map(|l| l.tax_amount).sum();
157 let gross_total = net_total + tax_total;
158
159 self.net_amount.document_amount = net_total;
160 self.net_amount.local_amount = net_total * self.net_amount.exchange_rate;
161 self.tax_amount.document_amount = tax_total;
162 self.tax_amount.local_amount = tax_total * self.tax_amount.exchange_rate;
163 self.gross_amount.document_amount = gross_total;
164 self.gross_amount.local_amount = gross_total * self.gross_amount.exchange_rate;
165 self.amount_remaining = gross_total - self.amount_applied;
166 }
167
168 pub fn apply_to_invoice(&mut self, invoice_number: String, amount: Decimal) {
170 let application = DebitMemoApplication {
171 invoice_number,
172 amount_applied: amount,
173 application_date: chrono::Local::now().date_naive(),
174 };
175
176 self.applied_invoices.push(application);
177 self.amount_applied += amount;
178 self.amount_remaining = self.gross_amount.document_amount - self.amount_applied;
179
180 if self.amount_remaining <= Decimal::ZERO {
181 self.status = SubledgerDocumentStatus::Cleared;
182 } else {
183 self.status = SubledgerDocumentStatus::PartiallyCleared;
184 }
185 }
186
187 pub fn with_po_reference(mut self, po_number: String) -> Self {
189 self.reference_po = Some(po_number);
190 self
191 }
192
193 pub fn with_gr_reference(mut self, gr_number: String) -> Self {
195 self.reference_gr = Some(gr_number);
196 self.memo_type = APDebitMemoType::Return;
197 self
198 }
199
200 pub fn approve(&mut self, approver: String, approval_date: NaiveDate) {
202 self.approval_status = APApprovalStatus::Approved;
203 self.approved_by = Some(approver);
204 self.approved_date = Some(approval_date);
205 }
206
207 pub fn reject(&mut self, reason: String) {
209 self.approval_status = APApprovalStatus::Rejected;
210 self.notes = Some(format!(
211 "{}Rejected: {}",
212 self.notes
213 .as_ref()
214 .map(|n| format!("{n}. "))
215 .unwrap_or_default(),
216 reason
217 ));
218 }
219
220 pub fn set_gl_reference(&mut self, reference: GLReference) {
222 self.gl_reference = Some(reference);
223 }
224
225 pub fn check_approval_threshold(&mut self, threshold: Decimal) {
227 self.requires_approval = self.gross_amount.document_amount > threshold;
228 if !self.requires_approval {
229 self.approval_status = APApprovalStatus::NotRequired;
230 }
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
236pub enum APDebitMemoType {
237 #[default]
239 Standard,
240 Return,
242 PriceAdjustment,
244 QuantityAdjustment,
246 Rebate,
248 QualityClaim,
250 Cancellation,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
256pub enum DebitMemoReason {
257 Return,
259 Damaged,
261 WrongItem,
263 PriceOvercharge,
265 QuantityShortage,
267 QualityIssue,
269 LateDeliveryPenalty,
271 DuplicateInvoice,
273 ServiceNotPerformed,
275 ContractAdjustment,
277 #[default]
279 Other,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct APDebitMemoLine {
285 pub line_number: u32,
287 pub material_id: Option<String>,
289 pub description: String,
291 pub quantity: Decimal,
293 pub unit: String,
295 pub unit_price: Decimal,
297 pub net_amount: Decimal,
299 pub tax_code: Option<String>,
301 pub tax_rate: Decimal,
303 pub tax_amount: Decimal,
305 pub gross_amount: Decimal,
307 pub gl_account: String,
309 pub reference_invoice_line: Option<u32>,
311 pub cost_center: Option<String>,
313}
314
315impl APDebitMemoLine {
316 pub fn new(
318 line_number: u32,
319 description: String,
320 quantity: Decimal,
321 unit: String,
322 unit_price: Decimal,
323 gl_account: String,
324 ) -> Self {
325 let net_amount = (quantity * unit_price).round_dp(2);
326 Self {
327 line_number,
328 material_id: None,
329 description,
330 quantity,
331 unit,
332 unit_price,
333 net_amount,
334 tax_code: None,
335 tax_rate: Decimal::ZERO,
336 tax_amount: Decimal::ZERO,
337 gross_amount: net_amount,
338 gl_account,
339 reference_invoice_line: None,
340 cost_center: None,
341 }
342 }
343
344 pub fn with_tax(mut self, tax_code: String, tax_rate: Decimal) -> Self {
346 self.tax_code = Some(tax_code);
347 self.tax_rate = tax_rate;
348 self.tax_amount = (self.net_amount * tax_rate / dec!(100)).round_dp(2);
349 self.gross_amount = self.net_amount + self.tax_amount;
350 self
351 }
352
353 pub fn with_invoice_reference(mut self, line_number: u32) -> Self {
355 self.reference_invoice_line = Some(line_number);
356 self
357 }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct DebitMemoApplication {
363 pub invoice_number: String,
365 pub amount_applied: Decimal,
367 pub application_date: NaiveDate,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
373pub enum APApprovalStatus {
374 #[default]
376 Pending,
377 Approved,
379 Rejected,
381 NotRequired,
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_debit_memo_creation() {
391 let memo = APDebitMemo::new(
392 "DM001".to_string(),
393 "1000".to_string(),
394 "VEND001".to_string(),
395 "Test Vendor".to_string(),
396 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
397 DebitMemoReason::Return,
398 "Goods returned".to_string(),
399 "USD".to_string(),
400 );
401
402 assert_eq!(memo.status, SubledgerDocumentStatus::Open);
403 assert_eq!(memo.approval_status, APApprovalStatus::Pending);
404 }
405
406 #[test]
407 fn test_debit_memo_totals() {
408 let mut memo = APDebitMemo::new(
409 "DM001".to_string(),
410 "1000".to_string(),
411 "VEND001".to_string(),
412 "Test Vendor".to_string(),
413 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
414 DebitMemoReason::PriceOvercharge,
415 "Price correction".to_string(),
416 "USD".to_string(),
417 );
418
419 let line = APDebitMemoLine::new(
420 1,
421 "Product A".to_string(),
422 dec!(10),
423 "EA".to_string(),
424 dec!(50),
425 "5000".to_string(),
426 )
427 .with_tax("VAT".to_string(), dec!(10));
428
429 memo.add_line(line);
430
431 assert_eq!(memo.net_amount.document_amount, dec!(500));
432 assert_eq!(memo.tax_amount.document_amount, dec!(50));
433 assert_eq!(memo.gross_amount.document_amount, dec!(550));
434 }
435
436 #[test]
437 fn test_apply_to_invoice() {
438 let mut memo = APDebitMemo::new(
439 "DM001".to_string(),
440 "1000".to_string(),
441 "VEND001".to_string(),
442 "Test Vendor".to_string(),
443 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
444 DebitMemoReason::Return,
445 "Goods returned".to_string(),
446 "USD".to_string(),
447 );
448
449 let line = APDebitMemoLine::new(
450 1,
451 "Product A".to_string(),
452 dec!(10),
453 "EA".to_string(),
454 dec!(100),
455 "5000".to_string(),
456 );
457 memo.add_line(line);
458
459 memo.apply_to_invoice("INV001".to_string(), dec!(500));
460
461 assert_eq!(memo.amount_applied, dec!(500));
462 assert_eq!(memo.amount_remaining, dec!(500));
463 assert_eq!(memo.status, SubledgerDocumentStatus::PartiallyCleared);
464 }
465
466 #[test]
467 fn test_approval_workflow() {
468 let mut memo = APDebitMemo::new(
469 "DM001".to_string(),
470 "1000".to_string(),
471 "VEND001".to_string(),
472 "Test Vendor".to_string(),
473 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
474 DebitMemoReason::Return,
475 "Goods returned".to_string(),
476 "USD".to_string(),
477 );
478
479 memo.approve(
480 "MANAGER1".to_string(),
481 NaiveDate::from_ymd_opt(2024, 2, 16).unwrap(),
482 );
483
484 assert_eq!(memo.approval_status, APApprovalStatus::Approved);
485 assert_eq!(memo.approved_by, Some("MANAGER1".to_string()));
486 }
487
488 #[test]
489 fn test_approval_threshold() {
490 let mut memo = APDebitMemo::new(
491 "DM001".to_string(),
492 "1000".to_string(),
493 "VEND001".to_string(),
494 "Test Vendor".to_string(),
495 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
496 DebitMemoReason::Return,
497 "Goods returned".to_string(),
498 "USD".to_string(),
499 );
500
501 let line = APDebitMemoLine::new(
502 1,
503 "Product A".to_string(),
504 dec!(1),
505 "EA".to_string(),
506 dec!(50),
507 "5000".to_string(),
508 );
509 memo.add_line(line);
510
511 memo.check_approval_threshold(dec!(100));
513 assert_eq!(memo.approval_status, APApprovalStatus::NotRequired);
514 assert!(!memo.requires_approval);
515 }
516}