1use std::collections::HashMap;
17
18use chrono::NaiveDate;
19use rust_decimal::Decimal;
20
21use datasynth_core::accounts::{
22 cash_accounts, control_accounts, expense_accounts, revenue_accounts, tax_accounts,
23};
24use datasynth_core::models::{
25 documents::{CustomerInvoice, Delivery, GoodsReceipt, Payment, VendorInvoice},
26 BusinessProcess, JournalEntry, JournalEntryHeader, JournalEntryLine, TransactionSource,
27};
28use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
29
30use super::{O2CDocumentChain, P2PDocumentChain};
31
32#[derive(Debug, Clone)]
34pub struct DocumentFlowJeConfig {
35 pub inventory_account: String,
37 pub gr_ir_clearing_account: String,
39 pub ap_account: String,
41 pub cash_account: String,
43 pub ar_account: String,
45 pub revenue_account: String,
47 pub cogs_account: String,
49 pub vat_output_account: String,
51 pub vat_input_account: String,
53 pub populate_fec_fields: bool,
56}
57
58impl Default for DocumentFlowJeConfig {
59 fn default() -> Self {
60 Self {
61 inventory_account: control_accounts::INVENTORY.to_string(),
62 gr_ir_clearing_account: control_accounts::GR_IR_CLEARING.to_string(),
63 ap_account: control_accounts::AP_CONTROL.to_string(),
64 cash_account: cash_accounts::OPERATING_CASH.to_string(),
65 ar_account: control_accounts::AR_CONTROL.to_string(),
66 revenue_account: revenue_accounts::PRODUCT_REVENUE.to_string(),
67 cogs_account: expense_accounts::COGS.to_string(),
68 vat_output_account: tax_accounts::VAT_PAYABLE.to_string(),
69 vat_input_account: tax_accounts::INPUT_VAT.to_string(),
70 populate_fec_fields: false,
71 }
72 }
73}
74
75impl DocumentFlowJeConfig {
76 pub fn french_gaap() -> Self {
78 use datasynth_core::pcg;
79 Self {
80 inventory_account: pcg::control_accounts::INVENTORY.to_string(),
81 gr_ir_clearing_account: pcg::control_accounts::GR_IR_CLEARING.to_string(),
82 ap_account: pcg::control_accounts::AP_CONTROL.to_string(),
83 cash_account: pcg::cash_accounts::BANK_ACCOUNT.to_string(),
84 ar_account: pcg::control_accounts::AR_CONTROL.to_string(),
85 revenue_account: pcg::revenue_accounts::PRODUCT_REVENUE.to_string(),
86 cogs_account: pcg::expense_accounts::COGS.to_string(),
87 vat_output_account: pcg::tax_accounts::OUTPUT_VAT.to_string(),
88 vat_input_account: pcg::tax_accounts::INPUT_VAT.to_string(),
89 populate_fec_fields: true,
90 }
91 }
92}
93
94impl From<&datasynth_core::FrameworkAccounts> for DocumentFlowJeConfig {
95 fn from(fa: &datasynth_core::FrameworkAccounts) -> Self {
96 Self {
97 inventory_account: fa.inventory.clone(),
98 gr_ir_clearing_account: fa.gr_ir_clearing.clone(),
99 ap_account: fa.ap_control.clone(),
100 cash_account: fa.bank_account.clone(),
101 ar_account: fa.ar_control.clone(),
102 revenue_account: fa.product_revenue.clone(),
103 cogs_account: fa.cogs.clone(),
104 vat_output_account: fa.vat_payable.clone(),
105 vat_input_account: fa.input_vat.clone(),
106 populate_fec_fields: fa.audit_export.fec_enabled,
107 }
108 }
109}
110
111pub struct DocumentFlowJeGenerator {
113 config: DocumentFlowJeConfig,
114 uuid_factory: DeterministicUuidFactory,
115 auxiliary_account_lookup: HashMap<String, String>,
120}
121
122impl DocumentFlowJeGenerator {
123 pub fn new() -> Self {
125 Self::with_config_and_seed(DocumentFlowJeConfig::default(), 0)
126 }
127
128 pub fn with_config_and_seed(config: DocumentFlowJeConfig, seed: u64) -> Self {
130 Self {
131 config,
132 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::DocumentFlow),
133 auxiliary_account_lookup: HashMap::new(),
134 }
135 }
136
137 pub fn set_auxiliary_account_lookup(&mut self, lookup: HashMap<String, String>) {
143 self.auxiliary_account_lookup = lookup;
144 }
145
146 fn account_description_map(&self) -> HashMap<String, String> {
148 let mut map = HashMap::new();
149 map.insert(
150 self.config.inventory_account.clone(),
151 "Inventory".to_string(),
152 );
153 map.insert(
154 self.config.gr_ir_clearing_account.clone(),
155 "GR/IR Clearing".to_string(),
156 );
157 map.insert(
158 self.config.ap_account.clone(),
159 "Accounts Payable".to_string(),
160 );
161 map.insert(
162 self.config.cash_account.clone(),
163 "Cash and Cash Equivalents".to_string(),
164 );
165 map.insert(
166 self.config.ar_account.clone(),
167 "Accounts Receivable".to_string(),
168 );
169 map.insert(
170 self.config.revenue_account.clone(),
171 "Product Revenue".to_string(),
172 );
173 map.insert(
174 self.config.cogs_account.clone(),
175 "Cost of Goods Sold".to_string(),
176 );
177 map.insert(
178 self.config.vat_output_account.clone(),
179 "VAT Payable".to_string(),
180 );
181 map.insert(
182 self.config.vat_input_account.clone(),
183 "Input VAT".to_string(),
184 );
185 map
186 }
187
188 const COST_CENTER_POOL: &'static [&'static str] =
190 &["CC1000", "CC2000", "CC3000", "CC4000", "CC5000"];
191
192 fn enrich_line_items(&self, entry: &mut JournalEntry) {
198 let desc_map = self.account_description_map();
199 let posting_date = entry.header.posting_date;
200 let company_code = &entry.header.company_code;
201 let header_text = entry.header.header_text.clone();
202 let business_process = entry.header.business_process;
203
204 let doc_id_bytes = entry.header.document_id.as_bytes();
206 let mut cc_seed: usize = 0;
207 for &b in doc_id_bytes {
208 cc_seed = cc_seed.wrapping_add(b as usize);
209 }
210
211 for (i, line) in entry.lines.iter_mut().enumerate() {
212 if line.account_description.is_none() {
214 line.account_description = desc_map.get(&line.gl_account).cloned();
215 }
216
217 if line.cost_center.is_none() {
219 let first_char = line.gl_account.chars().next().unwrap_or('0');
220 if first_char == '5' || first_char == '6' {
221 let idx = cc_seed.wrapping_add(i) % Self::COST_CENTER_POOL.len();
222 line.cost_center = Some(Self::COST_CENTER_POOL[idx].to_string());
223 }
224 }
225
226 if line.profit_center.is_none() {
228 let suffix = match business_process {
229 Some(BusinessProcess::P2P) => "-P2P",
230 Some(BusinessProcess::O2C) => "-O2C",
231 _ => "",
232 };
233 line.profit_center = Some(format!("PC-{}{}", company_code, suffix));
234 }
235
236 if line.line_text.is_none() {
238 line.line_text = header_text.clone();
239 }
240
241 if line.value_date.is_none()
243 && (line.gl_account == self.config.ar_account
244 || line.gl_account == self.config.ap_account)
245 {
246 line.value_date = Some(posting_date);
247 }
248
249 if line.assignment.is_none()
251 && (line.gl_account == self.config.ap_account
252 || line.gl_account == self.config.ar_account)
253 {
254 if let Some(ref ht) = header_text {
255 if let Some(partner_part) = ht.rsplit(" - ").next() {
256 line.assignment = Some(partner_part.to_string());
257 }
258 }
259 }
260 }
261 }
262
263 fn set_auxiliary_fields(
272 &self,
273 line: &mut JournalEntryLine,
274 partner_id: &str,
275 partner_label: &str,
276 ) {
277 if !self.config.populate_fec_fields {
278 return;
279 }
280 if line.gl_account == self.config.ap_account || line.gl_account == self.config.ar_account {
281 let aux_account = self
284 .auxiliary_account_lookup
285 .get(partner_id)
286 .cloned()
287 .unwrap_or_else(|| partner_id.to_string());
288 line.auxiliary_account_number = Some(aux_account);
289 line.auxiliary_account_label = Some(partner_label.to_string());
290 }
291 }
292
293 fn apply_lettrage(
299 &self,
300 entries: &mut [JournalEntry],
301 chain_id: &str,
302 lettrage_date: NaiveDate,
303 ) {
304 if !self.config.populate_fec_fields {
305 return;
306 }
307 let code = format!("LTR-{}", &chain_id[..chain_id.len().min(8)]);
308 for entry in entries.iter_mut() {
309 for line in entry.lines.iter_mut() {
310 if line.gl_account == self.config.ap_account
311 || line.gl_account == self.config.ar_account
312 {
313 line.lettrage = Some(code.clone());
314 line.lettrage_date = Some(lettrage_date);
315 }
316 }
317 }
318 }
319
320 pub fn generate_from_p2p_chain(&mut self, chain: &P2PDocumentChain) -> Vec<JournalEntry> {
322 let mut entries = Vec::new();
323
324 for gr in &chain.goods_receipts {
326 if let Some(je) = self.generate_from_goods_receipt(gr) {
327 entries.push(je);
328 }
329 }
330
331 if let Some(ref invoice) = chain.vendor_invoice {
333 if let Some(je) = self.generate_from_vendor_invoice(invoice) {
334 entries.push(je);
335 }
336 }
337
338 if let Some(ref payment) = chain.payment {
340 if let Some(je) = self.generate_from_ap_payment(payment) {
341 entries.push(je);
342 }
343 }
344
345 for payment in &chain.remainder_payments {
347 if let Some(je) = self.generate_from_ap_payment(payment) {
348 entries.push(je);
349 }
350 }
351
352 if self.config.populate_fec_fields && chain.is_complete {
354 if let Some(ref payment) = chain.payment {
355 let posting_date = payment
356 .header
357 .posting_date
358 .unwrap_or(payment.header.document_date);
359 self.apply_lettrage(
360 &mut entries,
361 &chain.purchase_order.header.document_id,
362 posting_date,
363 );
364 }
365 }
366
367 entries
368 }
369
370 pub fn generate_from_o2c_chain(&mut self, chain: &O2CDocumentChain) -> Vec<JournalEntry> {
372 let mut entries = Vec::new();
373
374 for delivery in &chain.deliveries {
376 if let Some(je) = self.generate_from_delivery(delivery) {
377 entries.push(je);
378 }
379 }
380
381 if let Some(ref invoice) = chain.customer_invoice {
383 if let Some(je) = self.generate_from_customer_invoice(invoice) {
384 entries.push(je);
385 }
386 }
387
388 if let Some(ref receipt) = chain.customer_receipt {
390 if let Some(je) = self.generate_from_ar_receipt(receipt) {
391 entries.push(je);
392 }
393 }
394
395 for receipt in &chain.remainder_receipts {
397 if let Some(je) = self.generate_from_ar_receipt(receipt) {
398 entries.push(je);
399 }
400 }
401
402 if self.config.populate_fec_fields && chain.customer_receipt.is_some() {
404 if let Some(ref receipt) = chain.customer_receipt {
405 let posting_date = receipt
406 .header
407 .posting_date
408 .unwrap_or(receipt.header.document_date);
409 self.apply_lettrage(
410 &mut entries,
411 &chain.sales_order.header.document_id,
412 posting_date,
413 );
414 }
415 }
416
417 entries
418 }
419
420 pub fn generate_from_goods_receipt(&mut self, gr: &GoodsReceipt) -> Option<JournalEntry> {
423 if gr.items.is_empty() {
424 return None;
425 }
426
427 let document_id = self.uuid_factory.next();
428
429 let total_amount = if gr.total_value > Decimal::ZERO {
431 gr.total_value
432 } else {
433 gr.items
434 .iter()
435 .map(|item| item.base.net_amount)
436 .sum::<Decimal>()
437 };
438
439 if total_amount == Decimal::ZERO {
440 return None;
441 }
442
443 let posting_date = gr.header.posting_date.unwrap_or(gr.header.document_date);
445
446 let mut header = JournalEntryHeader::with_deterministic_id(
447 gr.header.company_code.clone(),
448 posting_date,
449 document_id,
450 );
451 header.source = TransactionSource::Automated;
452 header.business_process = Some(BusinessProcess::P2P);
453 header.document_type = "WE".to_string();
454 header.reference = Some(format!("GR:{}", gr.header.document_id));
455 header.header_text = Some(format!(
456 "Goods Receipt {} - {}",
457 gr.header.document_id,
458 gr.vendor_id.as_deref().unwrap_or("Unknown")
459 ));
460
461 let mut entry = JournalEntry::new(header);
462
463 let debit_line = JournalEntryLine::debit(
465 entry.header.document_id,
466 1,
467 self.config.inventory_account.clone(),
468 total_amount,
469 );
470 entry.add_line(debit_line);
471
472 let credit_line = JournalEntryLine::credit(
474 entry.header.document_id,
475 2,
476 self.config.gr_ir_clearing_account.clone(),
477 total_amount,
478 );
479 entry.add_line(credit_line);
480
481 self.enrich_line_items(&mut entry);
482 Some(entry)
483 }
484
485 pub fn generate_from_vendor_invoice(
496 &mut self,
497 invoice: &VendorInvoice,
498 ) -> Option<JournalEntry> {
499 if invoice.payable_amount == Decimal::ZERO {
500 return None;
501 }
502
503 let document_id = self.uuid_factory.next();
504
505 let posting_date = invoice
507 .header
508 .posting_date
509 .unwrap_or(invoice.header.document_date);
510
511 let mut header = JournalEntryHeader::with_deterministic_id(
512 invoice.header.company_code.clone(),
513 posting_date,
514 document_id,
515 );
516 header.source = TransactionSource::Automated;
517 header.business_process = Some(BusinessProcess::P2P);
518 header.document_type = "KR".to_string();
519 header.reference = Some(format!("VI:{}", invoice.header.document_id));
520 header.header_text = Some(format!(
521 "Vendor Invoice {} - {}",
522 invoice.vendor_invoice_number, invoice.vendor_id
523 ));
524
525 let mut entry = JournalEntry::new(header);
526
527 let has_vat = invoice.tax_amount > Decimal::ZERO;
528 let clearing_amount = if has_vat {
529 invoice.net_amount
530 } else {
531 invoice.payable_amount
532 };
533
534 let debit_line = JournalEntryLine::debit(
536 entry.header.document_id,
537 1,
538 self.config.gr_ir_clearing_account.clone(),
539 clearing_amount,
540 );
541 entry.add_line(debit_line);
542
543 if has_vat {
545 let vat_line = JournalEntryLine::debit(
546 entry.header.document_id,
547 2,
548 self.config.vat_input_account.clone(),
549 invoice.tax_amount,
550 );
551 entry.add_line(vat_line);
552 }
553
554 let mut credit_line = JournalEntryLine::credit(
556 entry.header.document_id,
557 if has_vat { 3 } else { 2 },
558 self.config.ap_account.clone(),
559 invoice.payable_amount,
560 );
561 self.set_auxiliary_fields(&mut credit_line, &invoice.vendor_id, &invoice.vendor_id);
562 entry.add_line(credit_line);
563
564 self.enrich_line_items(&mut entry);
565 Some(entry)
566 }
567
568 pub fn generate_from_ap_payment(&mut self, payment: &Payment) -> Option<JournalEntry> {
571 if payment.amount == Decimal::ZERO {
572 return None;
573 }
574
575 let document_id = self.uuid_factory.next();
576
577 let posting_date = payment
579 .header
580 .posting_date
581 .unwrap_or(payment.header.document_date);
582
583 let mut header = JournalEntryHeader::with_deterministic_id(
584 payment.header.company_code.clone(),
585 posting_date,
586 document_id,
587 );
588 header.source = TransactionSource::Automated;
589 header.business_process = Some(BusinessProcess::P2P);
590 header.document_type = "KZ".to_string();
591 header.reference = Some(format!("PAY:{}", payment.header.document_id));
592 header.header_text = Some(format!(
593 "Payment {} - {}",
594 payment.header.document_id, payment.business_partner_id
595 ));
596
597 let mut entry = JournalEntry::new(header);
598
599 let mut debit_line = JournalEntryLine::debit(
601 entry.header.document_id,
602 1,
603 self.config.ap_account.clone(),
604 payment.amount,
605 );
606 self.set_auxiliary_fields(
607 &mut debit_line,
608 &payment.business_partner_id,
609 &payment.business_partner_id,
610 );
611 entry.add_line(debit_line);
612
613 let credit_line = JournalEntryLine::credit(
615 entry.header.document_id,
616 2,
617 self.config.cash_account.clone(),
618 payment.amount,
619 );
620 entry.add_line(credit_line);
621
622 self.enrich_line_items(&mut entry);
623 Some(entry)
624 }
625
626 pub fn generate_from_delivery(&mut self, delivery: &Delivery) -> Option<JournalEntry> {
629 if delivery.items.is_empty() {
630 return None;
631 }
632
633 let document_id = self.uuid_factory.next();
634
635 let total_cost = delivery
637 .items
638 .iter()
639 .map(|item| item.base.net_amount)
640 .sum::<Decimal>();
641
642 if total_cost == Decimal::ZERO {
643 return None;
644 }
645
646 let posting_date = delivery
648 .header
649 .posting_date
650 .unwrap_or(delivery.header.document_date);
651
652 let mut header = JournalEntryHeader::with_deterministic_id(
653 delivery.header.company_code.clone(),
654 posting_date,
655 document_id,
656 );
657 header.source = TransactionSource::Automated;
658 header.business_process = Some(BusinessProcess::O2C);
659 header.document_type = "WL".to_string();
660 header.reference = Some(format!("DEL:{}", delivery.header.document_id));
661 header.header_text = Some(format!(
662 "Delivery {} - {}",
663 delivery.header.document_id, delivery.customer_id
664 ));
665
666 let mut entry = JournalEntry::new(header);
667
668 let debit_line = JournalEntryLine::debit(
670 entry.header.document_id,
671 1,
672 self.config.cogs_account.clone(),
673 total_cost,
674 );
675 entry.add_line(debit_line);
676
677 let credit_line = JournalEntryLine::credit(
679 entry.header.document_id,
680 2,
681 self.config.inventory_account.clone(),
682 total_cost,
683 );
684 entry.add_line(credit_line);
685
686 self.enrich_line_items(&mut entry);
687 Some(entry)
688 }
689
690 pub fn generate_from_customer_invoice(
701 &mut self,
702 invoice: &CustomerInvoice,
703 ) -> Option<JournalEntry> {
704 if invoice.total_gross_amount == Decimal::ZERO {
705 return None;
706 }
707
708 let document_id = self.uuid_factory.next();
709
710 let posting_date = invoice
712 .header
713 .posting_date
714 .unwrap_or(invoice.header.document_date);
715
716 let mut header = JournalEntryHeader::with_deterministic_id(
717 invoice.header.company_code.clone(),
718 posting_date,
719 document_id,
720 );
721 header.source = TransactionSource::Automated;
722 header.business_process = Some(BusinessProcess::O2C);
723 header.document_type = "DR".to_string();
724 header.reference = Some(format!("CI:{}", invoice.header.document_id));
725 header.header_text = Some(format!(
726 "Customer Invoice {} - {}",
727 invoice.header.document_id, invoice.customer_id
728 ));
729
730 let mut entry = JournalEntry::new(header);
731
732 let mut debit_line = JournalEntryLine::debit(
734 entry.header.document_id,
735 1,
736 self.config.ar_account.clone(),
737 invoice.total_gross_amount,
738 );
739 self.set_auxiliary_fields(&mut debit_line, &invoice.customer_id, &invoice.customer_id);
740 entry.add_line(debit_line);
741
742 let revenue_amount = if invoice.total_tax_amount > Decimal::ZERO {
744 invoice.total_net_amount
745 } else {
746 invoice.total_gross_amount
747 };
748 let credit_line = JournalEntryLine::credit(
749 entry.header.document_id,
750 2,
751 self.config.revenue_account.clone(),
752 revenue_amount,
753 );
754 entry.add_line(credit_line);
755
756 if invoice.total_tax_amount > Decimal::ZERO {
758 let vat_line = JournalEntryLine::credit(
759 entry.header.document_id,
760 3,
761 self.config.vat_output_account.clone(),
762 invoice.total_tax_amount,
763 );
764 entry.add_line(vat_line);
765 }
766
767 self.enrich_line_items(&mut entry);
768 Some(entry)
769 }
770
771 pub fn generate_from_ar_receipt(&mut self, payment: &Payment) -> Option<JournalEntry> {
774 if payment.amount == Decimal::ZERO {
775 return None;
776 }
777
778 let document_id = self.uuid_factory.next();
779
780 let posting_date = payment
782 .header
783 .posting_date
784 .unwrap_or(payment.header.document_date);
785
786 let mut header = JournalEntryHeader::with_deterministic_id(
787 payment.header.company_code.clone(),
788 posting_date,
789 document_id,
790 );
791 header.source = TransactionSource::Automated;
792 header.business_process = Some(BusinessProcess::O2C);
793 header.document_type = "DZ".to_string();
794 header.reference = Some(format!("RCP:{}", payment.header.document_id));
795 header.header_text = Some(format!(
796 "Customer Receipt {} - {}",
797 payment.header.document_id, payment.business_partner_id
798 ));
799
800 let mut entry = JournalEntry::new(header);
801
802 let debit_line = JournalEntryLine::debit(
804 entry.header.document_id,
805 1,
806 self.config.cash_account.clone(),
807 payment.amount,
808 );
809 entry.add_line(debit_line);
810
811 let mut credit_line = JournalEntryLine::credit(
813 entry.header.document_id,
814 2,
815 self.config.ar_account.clone(),
816 payment.amount,
817 );
818 self.set_auxiliary_fields(
819 &mut credit_line,
820 &payment.business_partner_id,
821 &payment.business_partner_id,
822 );
823 entry.add_line(credit_line);
824
825 self.enrich_line_items(&mut entry);
826 Some(entry)
827 }
828}
829
830impl Default for DocumentFlowJeGenerator {
831 fn default() -> Self {
832 Self::new()
833 }
834}
835
836#[cfg(test)]
837#[allow(clippy::unwrap_used)]
838mod tests {
839 use super::*;
840 use chrono::NaiveDate;
841 use datasynth_core::models::documents::{GoodsReceiptItem, MovementType};
842
843 fn create_test_gr() -> GoodsReceipt {
844 let mut gr = GoodsReceipt::from_purchase_order(
845 "GR-001".to_string(),
846 "1000",
847 "PO-001",
848 "V-001",
849 "P1000",
850 "0001",
851 2024,
852 1,
853 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
854 "JSMITH",
855 );
856
857 let item = GoodsReceiptItem::from_po(
858 10,
859 "Test Material",
860 Decimal::from(100),
861 Decimal::from(50),
862 "PO-001",
863 10,
864 )
865 .with_movement_type(MovementType::GrForPo);
866
867 gr.add_item(item);
868 gr.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
869
870 gr
871 }
872
873 fn create_test_vendor_invoice() -> VendorInvoice {
874 use datasynth_core::models::documents::VendorInvoiceItem;
875
876 let mut invoice = VendorInvoice::new(
877 "VI-001".to_string(),
878 "1000",
879 "V-001",
880 "INV-12345".to_string(),
881 2024,
882 1,
883 NaiveDate::from_ymd_opt(2024, 1, 20).unwrap(),
884 "JSMITH",
885 );
886
887 let item = VendorInvoiceItem::from_po_gr(
888 10,
889 "Test Material",
890 Decimal::from(100),
891 Decimal::from(50),
892 "PO-001",
893 10,
894 Some("GR-001".to_string()),
895 Some(10),
896 );
897
898 invoice.add_item(item);
899 invoice.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 20).unwrap());
900
901 invoice
902 }
903
904 fn create_test_payment() -> Payment {
905 let mut payment = Payment::new_ap_payment(
906 "PAY-001".to_string(),
907 "1000",
908 "V-001",
909 Decimal::from(5000),
910 2024,
911 2,
912 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
913 "JSMITH",
914 );
915
916 payment.post("JSMITH", NaiveDate::from_ymd_opt(2024, 2, 15).unwrap());
917
918 payment
919 }
920
921 #[test]
922 fn test_generate_from_goods_receipt() {
923 let mut generator = DocumentFlowJeGenerator::new();
924 let gr = create_test_gr();
925
926 let je = generator.generate_from_goods_receipt(&gr);
927
928 assert!(je.is_some());
929 let je = je.unwrap();
930
931 assert!(je.is_balanced());
933
934 assert_eq!(je.line_count(), 2);
936
937 assert!(je.total_debit() > Decimal::ZERO);
939 assert_eq!(je.total_debit(), je.total_credit());
940
941 assert!(je.header.reference.is_some());
943 assert!(je.header.reference.as_ref().unwrap().contains("GR:"));
944 }
945
946 #[test]
947 fn test_generate_from_vendor_invoice() {
948 let mut generator = DocumentFlowJeGenerator::new();
949 let invoice = create_test_vendor_invoice();
950
951 let je = generator.generate_from_vendor_invoice(&invoice);
952
953 assert!(je.is_some());
954 let je = je.unwrap();
955
956 assert!(je.is_balanced());
957 assert_eq!(je.line_count(), 2);
958 assert!(je.header.reference.as_ref().unwrap().contains("VI:"));
959 }
960
961 #[test]
962 fn test_generate_from_ap_payment() {
963 let mut generator = DocumentFlowJeGenerator::new();
964 let payment = create_test_payment();
965
966 let je = generator.generate_from_ap_payment(&payment);
967
968 assert!(je.is_some());
969 let je = je.unwrap();
970
971 assert!(je.is_balanced());
972 assert_eq!(je.line_count(), 2);
973 assert!(je.header.reference.as_ref().unwrap().contains("PAY:"));
974 }
975
976 #[test]
977 fn test_all_entries_are_balanced() {
978 let mut generator = DocumentFlowJeGenerator::new();
979
980 let gr = create_test_gr();
981 let invoice = create_test_vendor_invoice();
982 let payment = create_test_payment();
983
984 let entries = vec![
985 generator.generate_from_goods_receipt(&gr),
986 generator.generate_from_vendor_invoice(&invoice),
987 generator.generate_from_ap_payment(&payment),
988 ];
989
990 for entry in entries.into_iter().flatten() {
991 assert!(
992 entry.is_balanced(),
993 "Entry {} is not balanced",
994 entry.header.document_id
995 );
996 }
997 }
998
999 #[test]
1004 fn test_french_gaap_auxiliary_on_ap_ar_lines_only() {
1005 let mut generator =
1007 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
1008
1009 let invoice = create_test_vendor_invoice();
1011 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1012
1013 assert!(
1015 je.lines[0].auxiliary_account_number.is_none(),
1016 "GR/IR clearing line should not have auxiliary"
1017 );
1018
1019 assert_eq!(
1021 je.lines[1].auxiliary_account_number.as_deref(),
1022 Some("V-001"),
1023 "AP line should have vendor ID as auxiliary"
1024 );
1025 assert_eq!(
1026 je.lines[1].auxiliary_account_label.as_deref(),
1027 Some("V-001"),
1028 );
1029 }
1030
1031 #[test]
1032 fn test_french_gaap_lettrage_on_complete_p2p_chain() {
1033 use datasynth_core::models::documents::PurchaseOrder;
1034
1035 let mut generator =
1036 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
1037
1038 let po = PurchaseOrder::new(
1039 "PO-001",
1040 "1000",
1041 "V-001",
1042 2024,
1043 1,
1044 NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(),
1045 "JSMITH",
1046 );
1047
1048 let chain = P2PDocumentChain {
1049 purchase_order: po,
1050 goods_receipts: vec![create_test_gr()],
1051 vendor_invoice: Some(create_test_vendor_invoice()),
1052 payment: Some(create_test_payment()),
1053 remainder_payments: Vec::new(),
1054 is_complete: true,
1055 three_way_match_passed: true,
1056 payment_timing: None,
1057 };
1058
1059 let entries = generator.generate_from_p2p_chain(&chain);
1060 assert!(!entries.is_empty());
1061
1062 let ap_account = &generator.config.ap_account;
1064 let mut lettrage_codes: Vec<&str> = Vec::new();
1065 for entry in &entries {
1066 for line in &entry.lines {
1067 if &line.gl_account == ap_account {
1068 assert!(
1069 line.lettrage.is_some(),
1070 "AP line should have lettrage on complete chain"
1071 );
1072 assert!(line.lettrage_date.is_some());
1073 lettrage_codes.push(line.lettrage.as_deref().unwrap());
1074 } else {
1075 assert!(
1076 line.lettrage.is_none(),
1077 "Non-AP line should not have lettrage"
1078 );
1079 }
1080 }
1081 }
1082
1083 assert!(!lettrage_codes.is_empty());
1085 assert!(
1086 lettrage_codes.iter().all(|c| *c == lettrage_codes[0]),
1087 "All AP lines should share the same lettrage code"
1088 );
1089 assert!(lettrage_codes[0].starts_with("LTR-"));
1090 }
1091
1092 #[test]
1093 fn test_incomplete_chain_has_no_lettrage() {
1094 use datasynth_core::models::documents::PurchaseOrder;
1095
1096 let mut generator =
1097 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
1098
1099 let po = PurchaseOrder::new(
1100 "PO-002",
1101 "1000",
1102 "V-001",
1103 2024,
1104 1,
1105 NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(),
1106 "JSMITH",
1107 );
1108
1109 let chain = P2PDocumentChain {
1111 purchase_order: po,
1112 goods_receipts: vec![create_test_gr()],
1113 vendor_invoice: Some(create_test_vendor_invoice()),
1114 payment: None,
1115 remainder_payments: Vec::new(),
1116 is_complete: false,
1117 three_way_match_passed: false,
1118 payment_timing: None,
1119 };
1120
1121 let entries = generator.generate_from_p2p_chain(&chain);
1122
1123 for entry in &entries {
1124 for line in &entry.lines {
1125 assert!(
1126 line.lettrage.is_none(),
1127 "Incomplete chain should have no lettrage"
1128 );
1129 }
1130 }
1131 }
1132
1133 #[test]
1134 fn test_default_config_no_fec_fields() {
1135 let mut generator = DocumentFlowJeGenerator::new();
1137
1138 let invoice = create_test_vendor_invoice();
1139 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1140
1141 for line in &je.lines {
1142 assert!(line.auxiliary_account_number.is_none());
1143 assert!(line.auxiliary_account_label.is_none());
1144 assert!(line.lettrage.is_none());
1145 assert!(line.lettrage_date.is_none());
1146 }
1147 }
1148
1149 #[test]
1150 fn test_auxiliary_lookup_uses_gl_account_instead_of_partner_id() {
1151 let mut generator =
1154 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
1155
1156 let mut lookup = HashMap::new();
1157 lookup.insert("V-001".to_string(), "4010001".to_string());
1158 generator.set_auxiliary_account_lookup(lookup);
1159
1160 let invoice = create_test_vendor_invoice();
1161 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1162
1163 assert_eq!(
1165 je.lines[1].auxiliary_account_number.as_deref(),
1166 Some("4010001"),
1167 "AP line should use auxiliary GL account from lookup"
1168 );
1169 assert_eq!(
1171 je.lines[1].auxiliary_account_label.as_deref(),
1172 Some("V-001"),
1173 );
1174 }
1175
1176 #[test]
1177 fn test_auxiliary_lookup_fallback_to_partner_id() {
1178 let mut generator =
1181 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
1182
1183 let mut lookup = HashMap::new();
1185 lookup.insert("V-999".to_string(), "4019999".to_string());
1186 generator.set_auxiliary_account_lookup(lookup);
1187
1188 let invoice = create_test_vendor_invoice();
1189 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1190
1191 assert_eq!(
1193 je.lines[1].auxiliary_account_number.as_deref(),
1194 Some("V-001"),
1195 "Should fall back to partner ID when not in lookup"
1196 );
1197 }
1198
1199 #[test]
1200 fn test_auxiliary_lookup_works_for_customer_receipt() {
1201 let mut generator =
1203 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
1204
1205 let mut lookup = HashMap::new();
1206 lookup.insert("C-001".to_string(), "4110001".to_string());
1207 generator.set_auxiliary_account_lookup(lookup);
1208
1209 let mut receipt = Payment::new_ar_receipt(
1210 "RCP-001".to_string(),
1211 "1000",
1212 "C-001",
1213 Decimal::from(3000),
1214 2024,
1215 3,
1216 NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
1217 "JSMITH",
1218 );
1219 receipt.post("JSMITH", NaiveDate::from_ymd_opt(2024, 3, 15).unwrap());
1220
1221 let je = generator.generate_from_ar_receipt(&receipt).unwrap();
1222
1223 assert_eq!(
1225 je.lines[1].auxiliary_account_number.as_deref(),
1226 Some("4110001"),
1227 "AR line should use auxiliary GL account from lookup"
1228 );
1229 }
1230
1231 fn create_test_customer_invoice_with_tax() -> CustomerInvoice {
1237 use datasynth_core::models::documents::CustomerInvoiceItem;
1238
1239 let mut invoice = CustomerInvoice::new(
1240 "CI-001",
1241 "1000",
1242 "C-001",
1243 2024,
1244 1,
1245 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1246 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
1247 "JSMITH",
1248 );
1249
1250 let mut item =
1252 CustomerInvoiceItem::new(1, "Product A", Decimal::from(10), Decimal::from(100));
1253 item.base.tax_amount = Decimal::from(100);
1254 invoice.add_item(item);
1255 invoice.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1256
1257 invoice
1258 }
1259
1260 fn create_test_customer_invoice_no_tax() -> CustomerInvoice {
1262 use datasynth_core::models::documents::CustomerInvoiceItem;
1263
1264 let mut invoice = CustomerInvoice::new(
1265 "CI-002",
1266 "1000",
1267 "C-002",
1268 2024,
1269 1,
1270 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1271 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
1272 "JSMITH",
1273 );
1274
1275 let item = CustomerInvoiceItem::new(1, "Product B", Decimal::from(10), Decimal::from(100));
1276 invoice.add_item(item);
1277 invoice.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1278
1279 invoice
1280 }
1281
1282 fn create_test_vendor_invoice_with_tax() -> VendorInvoice {
1284 use datasynth_core::models::documents::VendorInvoiceItem;
1285
1286 let mut invoice = VendorInvoice::new(
1287 "VI-002".to_string(),
1288 "1000",
1289 "V-001",
1290 "INV-TAX-001".to_string(),
1291 2024,
1292 1,
1293 NaiveDate::from_ymd_opt(2024, 1, 20).unwrap(),
1294 "JSMITH",
1295 );
1296
1297 let item = VendorInvoiceItem::from_po_gr(
1299 10,
1300 "Test Material",
1301 Decimal::from(100),
1302 Decimal::from(50),
1303 "PO-001",
1304 10,
1305 Some("GR-001".to_string()),
1306 Some(10),
1307 )
1308 .with_tax("VAT10", Decimal::from(500));
1309
1310 invoice.add_item(item);
1311 invoice.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 20).unwrap());
1312
1313 invoice
1314 }
1315
1316 #[test]
1317 fn test_customer_invoice_with_tax_produces_three_lines() {
1318 let mut generator = DocumentFlowJeGenerator::new();
1319 let invoice = create_test_customer_invoice_with_tax();
1320
1321 assert_eq!(invoice.total_net_amount, Decimal::from(1000));
1322 assert_eq!(invoice.total_tax_amount, Decimal::from(100));
1323 assert_eq!(invoice.total_gross_amount, Decimal::from(1100));
1324
1325 let je = generator.generate_from_customer_invoice(&invoice).unwrap();
1326
1327 assert_eq!(
1329 je.line_count(),
1330 3,
1331 "Expected 3 JE lines for invoice with tax"
1332 );
1333 assert!(je.is_balanced(), "Entry must be balanced");
1334
1335 assert_eq!(je.lines[0].gl_account, control_accounts::AR_CONTROL);
1337 assert_eq!(je.lines[0].debit_amount, Decimal::from(1100));
1338 assert_eq!(je.lines[0].credit_amount, Decimal::ZERO);
1339
1340 assert_eq!(je.lines[1].gl_account, revenue_accounts::PRODUCT_REVENUE);
1342 assert_eq!(je.lines[1].credit_amount, Decimal::from(1000));
1343 assert_eq!(je.lines[1].debit_amount, Decimal::ZERO);
1344
1345 assert_eq!(je.lines[2].gl_account, tax_accounts::VAT_PAYABLE);
1347 assert_eq!(je.lines[2].credit_amount, Decimal::from(100));
1348 assert_eq!(je.lines[2].debit_amount, Decimal::ZERO);
1349 }
1350
1351 #[test]
1352 fn test_customer_invoice_no_tax_produces_two_lines() {
1353 let mut generator = DocumentFlowJeGenerator::new();
1354 let invoice = create_test_customer_invoice_no_tax();
1355
1356 assert_eq!(invoice.total_tax_amount, Decimal::ZERO);
1357 assert_eq!(invoice.total_net_amount, Decimal::from(1000));
1358 assert_eq!(invoice.total_gross_amount, Decimal::from(1000));
1359
1360 let je = generator.generate_from_customer_invoice(&invoice).unwrap();
1361
1362 assert_eq!(
1364 je.line_count(),
1365 2,
1366 "Expected 2 JE lines for invoice without tax"
1367 );
1368 assert!(je.is_balanced(), "Entry must be balanced");
1369
1370 assert_eq!(je.lines[0].gl_account, control_accounts::AR_CONTROL);
1372 assert_eq!(je.lines[0].debit_amount, Decimal::from(1000));
1373
1374 assert_eq!(je.lines[1].gl_account, revenue_accounts::PRODUCT_REVENUE);
1376 assert_eq!(je.lines[1].credit_amount, Decimal::from(1000));
1377 }
1378
1379 #[test]
1380 fn test_vendor_invoice_with_tax_produces_three_lines() {
1381 let mut generator = DocumentFlowJeGenerator::new();
1382 let invoice = create_test_vendor_invoice_with_tax();
1383
1384 assert_eq!(invoice.net_amount, Decimal::from(5000));
1385 assert_eq!(invoice.tax_amount, Decimal::from(500));
1386 assert_eq!(invoice.gross_amount, Decimal::from(5500));
1387 assert_eq!(invoice.payable_amount, Decimal::from(5500));
1388
1389 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1390
1391 assert_eq!(
1393 je.line_count(),
1394 3,
1395 "Expected 3 JE lines for vendor invoice with tax"
1396 );
1397 assert!(je.is_balanced(), "Entry must be balanced");
1398
1399 assert_eq!(je.lines[0].gl_account, control_accounts::GR_IR_CLEARING);
1401 assert_eq!(je.lines[0].debit_amount, Decimal::from(5000));
1402 assert_eq!(je.lines[0].credit_amount, Decimal::ZERO);
1403
1404 assert_eq!(je.lines[1].gl_account, tax_accounts::INPUT_VAT);
1406 assert_eq!(je.lines[1].debit_amount, Decimal::from(500));
1407 assert_eq!(je.lines[1].credit_amount, Decimal::ZERO);
1408
1409 assert_eq!(je.lines[2].gl_account, control_accounts::AP_CONTROL);
1411 assert_eq!(je.lines[2].credit_amount, Decimal::from(5500));
1412 assert_eq!(je.lines[2].debit_amount, Decimal::ZERO);
1413 }
1414
1415 #[test]
1416 fn test_vendor_invoice_no_tax_produces_two_lines() {
1417 let mut generator = DocumentFlowJeGenerator::new();
1419 let invoice = create_test_vendor_invoice();
1420
1421 assert_eq!(invoice.tax_amount, Decimal::ZERO);
1422
1423 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1424
1425 assert_eq!(
1427 je.line_count(),
1428 2,
1429 "Expected 2 JE lines for vendor invoice without tax"
1430 );
1431 assert!(je.is_balanced(), "Entry must be balanced");
1432
1433 assert_eq!(je.lines[0].gl_account, control_accounts::GR_IR_CLEARING);
1435 assert_eq!(je.lines[0].debit_amount, invoice.payable_amount);
1436
1437 assert_eq!(je.lines[1].gl_account, control_accounts::AP_CONTROL);
1439 assert_eq!(je.lines[1].credit_amount, invoice.payable_amount);
1440 }
1441
1442 #[test]
1443 fn test_vat_accounts_configurable() {
1444 let mut config = DocumentFlowJeConfig::default();
1446 config.vat_output_account = "2999".to_string();
1447 config.vat_input_account = "1999".to_string();
1448
1449 let mut generator = DocumentFlowJeGenerator::with_config_and_seed(config, 42);
1450
1451 let ci = create_test_customer_invoice_with_tax();
1453 let je = generator.generate_from_customer_invoice(&ci).unwrap();
1454 assert_eq!(
1455 je.lines[2].gl_account, "2999",
1456 "VAT output account should be configurable"
1457 );
1458
1459 let vi = create_test_vendor_invoice_with_tax();
1461 let je = generator.generate_from_vendor_invoice(&vi).unwrap();
1462 assert_eq!(
1463 je.lines[1].gl_account, "1999",
1464 "VAT input account should be configurable"
1465 );
1466 }
1467
1468 #[test]
1469 fn test_vat_entries_from_framework_accounts() {
1470 let fa = datasynth_core::FrameworkAccounts::us_gaap();
1472 let config = DocumentFlowJeConfig::from(&fa);
1473
1474 assert_eq!(config.vat_output_account, tax_accounts::VAT_PAYABLE);
1475 assert_eq!(config.vat_input_account, tax_accounts::INPUT_VAT);
1476
1477 let fa_fr = datasynth_core::FrameworkAccounts::french_gaap();
1478 let config_fr = DocumentFlowJeConfig::from(&fa_fr);
1479
1480 assert_eq!(config_fr.vat_output_account, "445710");
1481 assert_eq!(config_fr.vat_input_account, "445660");
1482 }
1483
1484 #[test]
1485 fn test_french_gaap_vat_accounts() {
1486 let config = DocumentFlowJeConfig::french_gaap();
1487 assert_eq!(config.vat_output_account, "445710"); assert_eq!(config.vat_input_account, "445660"); }
1490
1491 #[test]
1492 fn test_vat_balanced_with_multiple_items() {
1493 use datasynth_core::models::documents::CustomerInvoiceItem;
1495
1496 let mut invoice = CustomerInvoice::new(
1497 "CI-003",
1498 "1000",
1499 "C-003",
1500 2024,
1501 1,
1502 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1503 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
1504 "JSMITH",
1505 );
1506
1507 let mut item1 = CustomerInvoiceItem::new(1, "A", Decimal::from(5), Decimal::from(100));
1509 item1.base.tax_amount = Decimal::from(50);
1510 invoice.add_item(item1);
1511
1512 let mut item2 = CustomerInvoiceItem::new(2, "B", Decimal::from(3), Decimal::from(100));
1514 item2.base.tax_amount = Decimal::from(30);
1515 invoice.add_item(item2);
1516
1517 invoice.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1518
1519 assert_eq!(invoice.total_net_amount, Decimal::from(800));
1521 assert_eq!(invoice.total_tax_amount, Decimal::from(80));
1522 assert_eq!(invoice.total_gross_amount, Decimal::from(880));
1523
1524 let mut generator = DocumentFlowJeGenerator::new();
1525 let je = generator.generate_from_customer_invoice(&invoice).unwrap();
1526
1527 assert_eq!(je.line_count(), 3);
1528 assert!(je.is_balanced());
1529 assert_eq!(je.total_debit(), Decimal::from(880));
1530 assert_eq!(je.total_credit(), Decimal::from(880));
1531 }
1532
1533 #[test]
1534 fn test_document_types_per_source_document() {
1535 let mut generator = DocumentFlowJeGenerator::new();
1536
1537 let gr = create_test_gr();
1538 let invoice = create_test_vendor_invoice();
1539 let payment = create_test_payment();
1540
1541 let gr_je = generator.generate_from_goods_receipt(&gr).unwrap();
1542 assert_eq!(
1543 gr_je.header.document_type, "WE",
1544 "Goods receipt should be WE"
1545 );
1546
1547 let vi_je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1548 assert_eq!(
1549 vi_je.header.document_type, "KR",
1550 "Vendor invoice should be KR"
1551 );
1552
1553 let pay_je = generator.generate_from_ap_payment(&payment).unwrap();
1554 assert_eq!(pay_je.header.document_type, "KZ", "AP payment should be KZ");
1555
1556 let types: std::collections::HashSet<&str> = [
1558 gr_je.header.document_type.as_str(),
1559 vi_je.header.document_type.as_str(),
1560 pay_je.header.document_type.as_str(),
1561 ]
1562 .into_iter()
1563 .collect();
1564
1565 assert!(
1566 types.len() >= 3,
1567 "Expected at least 3 distinct document types from P2P flow, got {:?}",
1568 types,
1569 );
1570 }
1571
1572 #[test]
1573 fn test_enrichment_account_descriptions_populated() {
1574 let mut generator = DocumentFlowJeGenerator::new();
1575 let gr = create_test_gr();
1576 let invoice = create_test_vendor_invoice();
1577 let payment = create_test_payment();
1578
1579 let gr_je = generator.generate_from_goods_receipt(&gr).unwrap();
1580 let vi_je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1581 let pay_je = generator.generate_from_ap_payment(&payment).unwrap();
1582
1583 for je in [&gr_je, &vi_je, &pay_je] {
1585 for line in &je.lines {
1586 assert!(
1587 line.account_description.is_some(),
1588 "Line for account {} should have description, entry doc {}",
1589 line.gl_account,
1590 je.header.document_id,
1591 );
1592 }
1593 }
1594
1595 assert_eq!(
1597 gr_je.lines[0].account_description.as_deref(),
1598 Some("Inventory"),
1599 );
1600 assert_eq!(
1601 gr_je.lines[1].account_description.as_deref(),
1602 Some("GR/IR Clearing"),
1603 );
1604 }
1605
1606 #[test]
1607 fn test_enrichment_profit_center_and_line_text() {
1608 let mut generator = DocumentFlowJeGenerator::new();
1609 let gr = create_test_gr();
1610
1611 let je = generator.generate_from_goods_receipt(&gr).unwrap();
1612
1613 for line in &je.lines {
1614 assert!(
1616 line.profit_center.is_some(),
1617 "Line {} should have profit_center",
1618 line.gl_account,
1619 );
1620 let pc = line.profit_center.as_ref().unwrap();
1621 assert!(
1622 pc.starts_with("PC-"),
1623 "Profit center should start with PC-, got {}",
1624 pc,
1625 );
1626
1627 assert!(
1629 line.line_text.is_some(),
1630 "Line {} should have line_text",
1631 line.gl_account,
1632 );
1633 }
1634 }
1635
1636 #[test]
1637 fn test_enrichment_cost_center_for_expense_accounts() {
1638 let mut generator = DocumentFlowJeGenerator::new();
1639
1640 use datasynth_core::models::documents::{Delivery, DeliveryItem};
1642 let mut delivery = Delivery::new(
1643 "DEL-001".to_string(),
1644 "1000",
1645 "SO-001",
1646 "C-001",
1647 2024,
1648 1,
1649 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1650 "JSMITH",
1651 );
1652 let item = DeliveryItem::from_sales_order(
1653 10,
1654 "Test Material",
1655 Decimal::from(100),
1656 Decimal::from(50),
1657 "SO-001",
1658 10,
1659 );
1660 delivery.add_item(item);
1661 delivery.post_goods_issue("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1662
1663 let je = generator.generate_from_delivery(&delivery).unwrap();
1664
1665 let cogs_line = je.lines.iter().find(|l| l.gl_account == "5000").unwrap();
1667 assert!(
1668 cogs_line.cost_center.is_some(),
1669 "COGS line should have cost_center assigned",
1670 );
1671 let cc = cogs_line.cost_center.as_ref().unwrap();
1672 assert!(
1673 cc.starts_with("CC"),
1674 "Cost center should start with CC, got {}",
1675 cc,
1676 );
1677
1678 let inv_line = je.lines.iter().find(|l| l.gl_account == "1200").unwrap();
1680 assert!(
1681 inv_line.cost_center.is_none(),
1682 "Non-expense line should not have cost_center",
1683 );
1684 }
1685
1686 #[test]
1687 fn test_enrichment_value_date_for_ap_ar() {
1688 let mut generator = DocumentFlowJeGenerator::new();
1689
1690 let invoice = create_test_vendor_invoice();
1691 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
1692
1693 let ap_line = je.lines.iter().find(|l| l.gl_account == "2000").unwrap();
1695 assert!(
1696 ap_line.value_date.is_some(),
1697 "AP line should have value_date set",
1698 );
1699 assert_eq!(ap_line.value_date, Some(je.header.posting_date));
1700
1701 let clearing_line = je.lines.iter().find(|l| l.gl_account == "2900").unwrap();
1703 assert!(
1704 clearing_line.value_date.is_none(),
1705 "Non-AP/AR line should not have value_date",
1706 );
1707 }
1708}