datasynth_core/models/subledger/ap/
debit_memo.rs

1//! AP Debit Memo model.
2
3use 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/// AP Debit Memo (reduces amount owed to vendor).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct APDebitMemo {
13    /// Unique debit memo number.
14    pub debit_memo_number: String,
15    /// Company code.
16    pub company_code: String,
17    /// Vendor ID.
18    pub vendor_id: String,
19    /// Vendor name.
20    pub vendor_name: String,
21    /// Debit memo date.
22    pub memo_date: NaiveDate,
23    /// Posting date.
24    pub posting_date: NaiveDate,
25    /// Debit memo type.
26    pub memo_type: APDebitMemoType,
27    /// Debit memo status.
28    pub status: SubledgerDocumentStatus,
29    /// Reason code.
30    pub reason_code: DebitMemoReason,
31    /// Reason description.
32    pub reason_description: String,
33    /// Debit memo lines.
34    pub lines: Vec<APDebitMemoLine>,
35    /// Net amount (before tax).
36    pub net_amount: CurrencyAmount,
37    /// Tax amount.
38    pub tax_amount: CurrencyAmount,
39    /// Gross amount (total credit to AP).
40    pub gross_amount: CurrencyAmount,
41    /// Amount applied to invoices.
42    pub amount_applied: Decimal,
43    /// Amount remaining.
44    pub amount_remaining: Decimal,
45    /// Tax details.
46    pub tax_details: Vec<TaxInfo>,
47    /// Reference invoice (if applicable).
48    pub reference_invoice: Option<String>,
49    /// Reference purchase order.
50    pub reference_po: Option<String>,
51    /// Reference goods receipt.
52    pub reference_gr: Option<String>,
53    /// Applied to invoices.
54    pub applied_invoices: Vec<DebitMemoApplication>,
55    /// GL reference.
56    pub gl_reference: Option<GLReference>,
57    /// Requires approval.
58    pub requires_approval: bool,
59    /// Approval status.
60    pub approval_status: APApprovalStatus,
61    /// Approved by.
62    pub approved_by: Option<String>,
63    /// Approval date.
64    pub approved_date: Option<NaiveDate>,
65    /// Created timestamp.
66    pub created_at: DateTime<Utc>,
67    /// Created by user.
68    pub created_by: Option<String>,
69    /// Notes.
70    pub notes: Option<String>,
71}
72
73impl APDebitMemo {
74    /// Creates a new debit memo.
75    #[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    /// Creates debit memo for a specific invoice.
120    #[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    /// Adds a debit memo line.
147    pub fn add_line(&mut self, line: APDebitMemoLine) {
148        self.lines.push(line);
149        self.recalculate_totals();
150    }
151
152    /// Recalculates totals from lines.
153    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    /// Applies debit memo to an invoice.
168    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    /// Sets purchase order reference.
187    pub fn with_po_reference(mut self, po_number: String) -> Self {
188        self.reference_po = Some(po_number);
189        self
190    }
191
192    /// Sets goods receipt reference.
193    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    /// Approves the debit memo.
200    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    /// Rejects the debit memo.
207    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    /// Sets the GL reference.
220    pub fn set_gl_reference(&mut self, reference: GLReference) {
221        self.gl_reference = Some(reference);
222    }
223
224    /// Checks if approval is required based on threshold.
225    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/// Type of debit memo.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
235pub enum APDebitMemoType {
236    /// Standard debit memo.
237    #[default]
238    Standard,
239    /// Return to vendor.
240    Return,
241    /// Price adjustment.
242    PriceAdjustment,
243    /// Quantity adjustment.
244    QuantityAdjustment,
245    /// Rebate/volume discount.
246    Rebate,
247    /// Quality claim.
248    QualityClaim,
249    /// Cancellation.
250    Cancellation,
251}
252
253/// Reason code for debit memo.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
255pub enum DebitMemoReason {
256    /// Goods returned.
257    Return,
258    /// Damaged goods received.
259    Damaged,
260    /// Wrong item received.
261    WrongItem,
262    /// Price overcharge.
263    PriceOvercharge,
264    /// Quantity shortage.
265    QuantityShortage,
266    /// Quality issue.
267    QualityIssue,
268    /// Late delivery penalty.
269    LateDeliveryPenalty,
270    /// Duplicate invoice.
271    DuplicateInvoice,
272    /// Service not performed.
273    ServiceNotPerformed,
274    /// Contract adjustment.
275    ContractAdjustment,
276    /// Other.
277    #[default]
278    Other,
279}
280
281/// Debit memo line item.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct APDebitMemoLine {
284    /// Line number.
285    pub line_number: u32,
286    /// Material/service ID.
287    pub material_id: Option<String>,
288    /// Description.
289    pub description: String,
290    /// Quantity.
291    pub quantity: Decimal,
292    /// Unit of measure.
293    pub unit: String,
294    /// Unit price.
295    pub unit_price: Decimal,
296    /// Net amount.
297    pub net_amount: Decimal,
298    /// Tax code.
299    pub tax_code: Option<String>,
300    /// Tax rate.
301    pub tax_rate: Decimal,
302    /// Tax amount.
303    pub tax_amount: Decimal,
304    /// Gross amount.
305    pub gross_amount: Decimal,
306    /// GL account.
307    pub gl_account: String,
308    /// Reference invoice line.
309    pub reference_invoice_line: Option<u32>,
310    /// Cost center.
311    pub cost_center: Option<String>,
312}
313
314impl APDebitMemoLine {
315    /// Creates a new debit memo line.
316    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    /// Sets tax information.
344    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    /// Sets reference to original invoice line.
353    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/// Application of debit memo to invoice.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct DebitMemoApplication {
362    /// Invoice number.
363    pub invoice_number: String,
364    /// Amount applied.
365    pub amount_applied: Decimal,
366    /// Application date.
367    pub application_date: NaiveDate,
368}
369
370/// Approval status for AP documents.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
372pub enum APApprovalStatus {
373    /// Pending approval.
374    #[default]
375    Pending,
376    /// Approved.
377    Approved,
378    /// Rejected.
379    Rejected,
380    /// Not required (under threshold).
381    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        // Under threshold
511        memo.check_approval_threshold(dec!(100));
512        assert_eq!(memo.approval_status, APApprovalStatus::NotRequired);
513        assert!(!memo.requires_approval);
514    }
515}