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)]
386#[allow(clippy::unwrap_used)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_debit_memo_creation() {
392 let memo = APDebitMemo::new(
393 "DM001".to_string(),
394 "1000".to_string(),
395 "VEND001".to_string(),
396 "Test Vendor".to_string(),
397 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
398 DebitMemoReason::Return,
399 "Goods returned".to_string(),
400 "USD".to_string(),
401 );
402
403 assert_eq!(memo.status, SubledgerDocumentStatus::Open);
404 assert_eq!(memo.approval_status, APApprovalStatus::Pending);
405 }
406
407 #[test]
408 fn test_debit_memo_totals() {
409 let mut memo = APDebitMemo::new(
410 "DM001".to_string(),
411 "1000".to_string(),
412 "VEND001".to_string(),
413 "Test Vendor".to_string(),
414 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
415 DebitMemoReason::PriceOvercharge,
416 "Price correction".to_string(),
417 "USD".to_string(),
418 );
419
420 let line = APDebitMemoLine::new(
421 1,
422 "Product A".to_string(),
423 dec!(10),
424 "EA".to_string(),
425 dec!(50),
426 "5000".to_string(),
427 )
428 .with_tax("VAT".to_string(), dec!(10));
429
430 memo.add_line(line);
431
432 assert_eq!(memo.net_amount.document_amount, dec!(500));
433 assert_eq!(memo.tax_amount.document_amount, dec!(50));
434 assert_eq!(memo.gross_amount.document_amount, dec!(550));
435 }
436
437 #[test]
438 fn test_apply_to_invoice() {
439 let mut memo = APDebitMemo::new(
440 "DM001".to_string(),
441 "1000".to_string(),
442 "VEND001".to_string(),
443 "Test Vendor".to_string(),
444 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
445 DebitMemoReason::Return,
446 "Goods returned".to_string(),
447 "USD".to_string(),
448 );
449
450 let line = APDebitMemoLine::new(
451 1,
452 "Product A".to_string(),
453 dec!(10),
454 "EA".to_string(),
455 dec!(100),
456 "5000".to_string(),
457 );
458 memo.add_line(line);
459
460 memo.apply_to_invoice("INV001".to_string(), dec!(500));
461
462 assert_eq!(memo.amount_applied, dec!(500));
463 assert_eq!(memo.amount_remaining, dec!(500));
464 assert_eq!(memo.status, SubledgerDocumentStatus::PartiallyCleared);
465 }
466
467 #[test]
468 fn test_approval_workflow() {
469 let mut memo = APDebitMemo::new(
470 "DM001".to_string(),
471 "1000".to_string(),
472 "VEND001".to_string(),
473 "Test Vendor".to_string(),
474 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
475 DebitMemoReason::Return,
476 "Goods returned".to_string(),
477 "USD".to_string(),
478 );
479
480 memo.approve(
481 "MANAGER1".to_string(),
482 NaiveDate::from_ymd_opt(2024, 2, 16).unwrap(),
483 );
484
485 assert_eq!(memo.approval_status, APApprovalStatus::Approved);
486 assert_eq!(memo.approved_by, Some("MANAGER1".to_string()));
487 }
488
489 #[test]
490 fn test_approval_threshold() {
491 let mut memo = APDebitMemo::new(
492 "DM001".to_string(),
493 "1000".to_string(),
494 "VEND001".to_string(),
495 "Test Vendor".to_string(),
496 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
497 DebitMemoReason::Return,
498 "Goods returned".to_string(),
499 "USD".to_string(),
500 );
501
502 let line = APDebitMemoLine::new(
503 1,
504 "Product A".to_string(),
505 dec!(1),
506 "EA".to_string(),
507 dec!(50),
508 "5000".to_string(),
509 );
510 memo.add_line(line);
511
512 memo.check_approval_threshold(dec!(100));
514 assert_eq!(memo.approval_status, APApprovalStatus::NotRequired);
515 assert!(!memo.requires_approval);
516 }
517}