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,
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 populate_fec_fields: bool,
52}
53
54impl Default for DocumentFlowJeConfig {
55 fn default() -> Self {
56 Self {
57 inventory_account: control_accounts::INVENTORY.to_string(),
58 gr_ir_clearing_account: control_accounts::GR_IR_CLEARING.to_string(),
59 ap_account: control_accounts::AP_CONTROL.to_string(),
60 cash_account: cash_accounts::OPERATING_CASH.to_string(),
61 ar_account: control_accounts::AR_CONTROL.to_string(),
62 revenue_account: revenue_accounts::PRODUCT_REVENUE.to_string(),
63 cogs_account: expense_accounts::COGS.to_string(),
64 populate_fec_fields: false,
65 }
66 }
67}
68
69impl DocumentFlowJeConfig {
70 pub fn french_gaap() -> Self {
72 use datasynth_core::pcg;
73 Self {
74 inventory_account: pcg::control_accounts::INVENTORY.to_string(),
75 gr_ir_clearing_account: pcg::control_accounts::GR_IR_CLEARING.to_string(),
76 ap_account: pcg::control_accounts::AP_CONTROL.to_string(),
77 cash_account: pcg::cash_accounts::BANK_ACCOUNT.to_string(),
78 ar_account: pcg::control_accounts::AR_CONTROL.to_string(),
79 revenue_account: pcg::revenue_accounts::PRODUCT_REVENUE.to_string(),
80 cogs_account: pcg::expense_accounts::COGS.to_string(),
81 populate_fec_fields: true,
82 }
83 }
84}
85
86impl From<&datasynth_core::FrameworkAccounts> for DocumentFlowJeConfig {
87 fn from(fa: &datasynth_core::FrameworkAccounts) -> Self {
88 Self {
89 inventory_account: fa.inventory.clone(),
90 gr_ir_clearing_account: fa.gr_ir_clearing.clone(),
91 ap_account: fa.ap_control.clone(),
92 cash_account: fa.bank_account.clone(),
93 ar_account: fa.ar_control.clone(),
94 revenue_account: fa.product_revenue.clone(),
95 cogs_account: fa.cogs.clone(),
96 populate_fec_fields: fa.audit_export.fec_enabled,
97 }
98 }
99}
100
101pub struct DocumentFlowJeGenerator {
103 config: DocumentFlowJeConfig,
104 uuid_factory: DeterministicUuidFactory,
105 auxiliary_account_lookup: HashMap<String, String>,
110}
111
112impl DocumentFlowJeGenerator {
113 pub fn new() -> Self {
115 Self::with_config_and_seed(DocumentFlowJeConfig::default(), 0)
116 }
117
118 pub fn with_config_and_seed(config: DocumentFlowJeConfig, seed: u64) -> Self {
120 Self {
121 config,
122 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::DocumentFlow),
123 auxiliary_account_lookup: HashMap::new(),
124 }
125 }
126
127 pub fn set_auxiliary_account_lookup(&mut self, lookup: HashMap<String, String>) {
133 self.auxiliary_account_lookup = lookup;
134 }
135
136 fn set_auxiliary_fields(
145 &self,
146 line: &mut JournalEntryLine,
147 partner_id: &str,
148 partner_label: &str,
149 ) {
150 if !self.config.populate_fec_fields {
151 return;
152 }
153 if line.gl_account == self.config.ap_account || line.gl_account == self.config.ar_account {
154 let aux_account = self
157 .auxiliary_account_lookup
158 .get(partner_id)
159 .cloned()
160 .unwrap_or_else(|| partner_id.to_string());
161 line.auxiliary_account_number = Some(aux_account);
162 line.auxiliary_account_label = Some(partner_label.to_string());
163 }
164 }
165
166 fn apply_lettrage(
172 &self,
173 entries: &mut [JournalEntry],
174 chain_id: &str,
175 lettrage_date: NaiveDate,
176 ) {
177 if !self.config.populate_fec_fields {
178 return;
179 }
180 let code = format!("LTR-{}", &chain_id[..chain_id.len().min(8)]);
181 for entry in entries.iter_mut() {
182 for line in entry.lines.iter_mut() {
183 if line.gl_account == self.config.ap_account
184 || line.gl_account == self.config.ar_account
185 {
186 line.lettrage = Some(code.clone());
187 line.lettrage_date = Some(lettrage_date);
188 }
189 }
190 }
191 }
192
193 pub fn generate_from_p2p_chain(&mut self, chain: &P2PDocumentChain) -> Vec<JournalEntry> {
195 let mut entries = Vec::new();
196
197 for gr in &chain.goods_receipts {
199 if let Some(je) = self.generate_from_goods_receipt(gr) {
200 entries.push(je);
201 }
202 }
203
204 if let Some(ref invoice) = chain.vendor_invoice {
206 if let Some(je) = self.generate_from_vendor_invoice(invoice) {
207 entries.push(je);
208 }
209 }
210
211 if let Some(ref payment) = chain.payment {
213 if let Some(je) = self.generate_from_ap_payment(payment) {
214 entries.push(je);
215 }
216 }
217
218 if self.config.populate_fec_fields && chain.is_complete {
220 if let Some(ref payment) = chain.payment {
221 let posting_date = payment
222 .header
223 .posting_date
224 .unwrap_or(payment.header.document_date);
225 self.apply_lettrage(
226 &mut entries,
227 &chain.purchase_order.header.document_id,
228 posting_date,
229 );
230 }
231 }
232
233 entries
234 }
235
236 pub fn generate_from_o2c_chain(&mut self, chain: &O2CDocumentChain) -> Vec<JournalEntry> {
238 let mut entries = Vec::new();
239
240 for delivery in &chain.deliveries {
242 if let Some(je) = self.generate_from_delivery(delivery) {
243 entries.push(je);
244 }
245 }
246
247 if let Some(ref invoice) = chain.customer_invoice {
249 if let Some(je) = self.generate_from_customer_invoice(invoice) {
250 entries.push(je);
251 }
252 }
253
254 if let Some(ref receipt) = chain.customer_receipt {
256 if let Some(je) = self.generate_from_ar_receipt(receipt) {
257 entries.push(je);
258 }
259 }
260
261 if self.config.populate_fec_fields && chain.customer_receipt.is_some() {
263 if let Some(ref receipt) = chain.customer_receipt {
264 let posting_date = receipt
265 .header
266 .posting_date
267 .unwrap_or(receipt.header.document_date);
268 self.apply_lettrage(
269 &mut entries,
270 &chain.sales_order.header.document_id,
271 posting_date,
272 );
273 }
274 }
275
276 entries
277 }
278
279 pub fn generate_from_goods_receipt(&mut self, gr: &GoodsReceipt) -> Option<JournalEntry> {
282 if gr.items.is_empty() {
283 return None;
284 }
285
286 let document_id = self.uuid_factory.next();
287
288 let total_amount = if gr.total_value > Decimal::ZERO {
290 gr.total_value
291 } else {
292 gr.items
293 .iter()
294 .map(|item| item.base.net_amount)
295 .sum::<Decimal>()
296 };
297
298 if total_amount == Decimal::ZERO {
299 return None;
300 }
301
302 let posting_date = gr.header.posting_date.unwrap_or(gr.header.document_date);
304
305 let mut header = JournalEntryHeader::with_deterministic_id(
306 gr.header.company_code.clone(),
307 posting_date,
308 document_id,
309 );
310 header.source = TransactionSource::Automated;
311 header.business_process = Some(BusinessProcess::P2P);
312 header.reference = Some(format!("GR:{}", gr.header.document_id));
313 header.header_text = Some(format!(
314 "Goods Receipt {} - {}",
315 gr.header.document_id,
316 gr.vendor_id.as_deref().unwrap_or("Unknown")
317 ));
318
319 let mut entry = JournalEntry::new(header);
320
321 let debit_line = JournalEntryLine::debit(
323 entry.header.document_id,
324 1,
325 self.config.inventory_account.clone(),
326 total_amount,
327 );
328 entry.add_line(debit_line);
329
330 let credit_line = JournalEntryLine::credit(
332 entry.header.document_id,
333 2,
334 self.config.gr_ir_clearing_account.clone(),
335 total_amount,
336 );
337 entry.add_line(credit_line);
338
339 Some(entry)
340 }
341
342 pub fn generate_from_vendor_invoice(
345 &mut self,
346 invoice: &VendorInvoice,
347 ) -> Option<JournalEntry> {
348 if invoice.payable_amount == Decimal::ZERO {
349 return None;
350 }
351
352 let document_id = self.uuid_factory.next();
353
354 let posting_date = invoice
356 .header
357 .posting_date
358 .unwrap_or(invoice.header.document_date);
359
360 let mut header = JournalEntryHeader::with_deterministic_id(
361 invoice.header.company_code.clone(),
362 posting_date,
363 document_id,
364 );
365 header.source = TransactionSource::Automated;
366 header.business_process = Some(BusinessProcess::P2P);
367 header.reference = Some(format!("VI:{}", invoice.header.document_id));
368 header.header_text = Some(format!(
369 "Vendor Invoice {} - {}",
370 invoice.vendor_invoice_number, invoice.vendor_id
371 ));
372
373 let mut entry = JournalEntry::new(header);
374
375 let debit_line = JournalEntryLine::debit(
377 entry.header.document_id,
378 1,
379 self.config.gr_ir_clearing_account.clone(),
380 invoice.payable_amount,
381 );
382 entry.add_line(debit_line);
383
384 let mut credit_line = JournalEntryLine::credit(
386 entry.header.document_id,
387 2,
388 self.config.ap_account.clone(),
389 invoice.payable_amount,
390 );
391 self.set_auxiliary_fields(&mut credit_line, &invoice.vendor_id, &invoice.vendor_id);
392 entry.add_line(credit_line);
393
394 Some(entry)
395 }
396
397 pub fn generate_from_ap_payment(&mut self, payment: &Payment) -> Option<JournalEntry> {
400 if payment.amount == Decimal::ZERO {
401 return None;
402 }
403
404 let document_id = self.uuid_factory.next();
405
406 let posting_date = payment
408 .header
409 .posting_date
410 .unwrap_or(payment.header.document_date);
411
412 let mut header = JournalEntryHeader::with_deterministic_id(
413 payment.header.company_code.clone(),
414 posting_date,
415 document_id,
416 );
417 header.source = TransactionSource::Automated;
418 header.business_process = Some(BusinessProcess::P2P);
419 header.reference = Some(format!("PAY:{}", payment.header.document_id));
420 header.header_text = Some(format!(
421 "Payment {} - {}",
422 payment.header.document_id, payment.business_partner_id
423 ));
424
425 let mut entry = JournalEntry::new(header);
426
427 let mut debit_line = JournalEntryLine::debit(
429 entry.header.document_id,
430 1,
431 self.config.ap_account.clone(),
432 payment.amount,
433 );
434 self.set_auxiliary_fields(
435 &mut debit_line,
436 &payment.business_partner_id,
437 &payment.business_partner_id,
438 );
439 entry.add_line(debit_line);
440
441 let credit_line = JournalEntryLine::credit(
443 entry.header.document_id,
444 2,
445 self.config.cash_account.clone(),
446 payment.amount,
447 );
448 entry.add_line(credit_line);
449
450 Some(entry)
451 }
452
453 pub fn generate_from_delivery(&mut self, delivery: &Delivery) -> Option<JournalEntry> {
456 if delivery.items.is_empty() {
457 return None;
458 }
459
460 let document_id = self.uuid_factory.next();
461
462 let total_cost = delivery
464 .items
465 .iter()
466 .map(|item| item.base.net_amount)
467 .sum::<Decimal>();
468
469 if total_cost == Decimal::ZERO {
470 return None;
471 }
472
473 let posting_date = delivery
475 .header
476 .posting_date
477 .unwrap_or(delivery.header.document_date);
478
479 let mut header = JournalEntryHeader::with_deterministic_id(
480 delivery.header.company_code.clone(),
481 posting_date,
482 document_id,
483 );
484 header.source = TransactionSource::Automated;
485 header.business_process = Some(BusinessProcess::O2C);
486 header.reference = Some(format!("DEL:{}", delivery.header.document_id));
487 header.header_text = Some(format!(
488 "Delivery {} - {}",
489 delivery.header.document_id, delivery.customer_id
490 ));
491
492 let mut entry = JournalEntry::new(header);
493
494 let debit_line = JournalEntryLine::debit(
496 entry.header.document_id,
497 1,
498 self.config.cogs_account.clone(),
499 total_cost,
500 );
501 entry.add_line(debit_line);
502
503 let credit_line = JournalEntryLine::credit(
505 entry.header.document_id,
506 2,
507 self.config.inventory_account.clone(),
508 total_cost,
509 );
510 entry.add_line(credit_line);
511
512 Some(entry)
513 }
514
515 pub fn generate_from_customer_invoice(
518 &mut self,
519 invoice: &CustomerInvoice,
520 ) -> Option<JournalEntry> {
521 if invoice.total_gross_amount == Decimal::ZERO {
522 return None;
523 }
524
525 let document_id = self.uuid_factory.next();
526
527 let posting_date = invoice
529 .header
530 .posting_date
531 .unwrap_or(invoice.header.document_date);
532
533 let mut header = JournalEntryHeader::with_deterministic_id(
534 invoice.header.company_code.clone(),
535 posting_date,
536 document_id,
537 );
538 header.source = TransactionSource::Automated;
539 header.business_process = Some(BusinessProcess::O2C);
540 header.reference = Some(format!("CI:{}", invoice.header.document_id));
541 header.header_text = Some(format!(
542 "Customer Invoice {} - {}",
543 invoice.header.document_id, invoice.customer_id
544 ));
545
546 let mut entry = JournalEntry::new(header);
547
548 let mut debit_line = JournalEntryLine::debit(
550 entry.header.document_id,
551 1,
552 self.config.ar_account.clone(),
553 invoice.total_gross_amount,
554 );
555 self.set_auxiliary_fields(&mut debit_line, &invoice.customer_id, &invoice.customer_id);
556 entry.add_line(debit_line);
557
558 let credit_line = JournalEntryLine::credit(
560 entry.header.document_id,
561 2,
562 self.config.revenue_account.clone(),
563 invoice.total_gross_amount,
564 );
565 entry.add_line(credit_line);
566
567 Some(entry)
568 }
569
570 pub fn generate_from_ar_receipt(&mut self, payment: &Payment) -> Option<JournalEntry> {
573 if payment.amount == Decimal::ZERO {
574 return None;
575 }
576
577 let document_id = self.uuid_factory.next();
578
579 let posting_date = payment
581 .header
582 .posting_date
583 .unwrap_or(payment.header.document_date);
584
585 let mut header = JournalEntryHeader::with_deterministic_id(
586 payment.header.company_code.clone(),
587 posting_date,
588 document_id,
589 );
590 header.source = TransactionSource::Automated;
591 header.business_process = Some(BusinessProcess::O2C);
592 header.reference = Some(format!("RCP:{}", payment.header.document_id));
593 header.header_text = Some(format!(
594 "Customer Receipt {} - {}",
595 payment.header.document_id, payment.business_partner_id
596 ));
597
598 let mut entry = JournalEntry::new(header);
599
600 let debit_line = JournalEntryLine::debit(
602 entry.header.document_id,
603 1,
604 self.config.cash_account.clone(),
605 payment.amount,
606 );
607 entry.add_line(debit_line);
608
609 let mut credit_line = JournalEntryLine::credit(
611 entry.header.document_id,
612 2,
613 self.config.ar_account.clone(),
614 payment.amount,
615 );
616 self.set_auxiliary_fields(
617 &mut credit_line,
618 &payment.business_partner_id,
619 &payment.business_partner_id,
620 );
621 entry.add_line(credit_line);
622
623 Some(entry)
624 }
625}
626
627impl Default for DocumentFlowJeGenerator {
628 fn default() -> Self {
629 Self::new()
630 }
631}
632
633#[cfg(test)]
634#[allow(clippy::unwrap_used)]
635mod tests {
636 use super::*;
637 use chrono::NaiveDate;
638 use datasynth_core::models::documents::{GoodsReceiptItem, MovementType};
639
640 fn create_test_gr() -> GoodsReceipt {
641 let mut gr = GoodsReceipt::from_purchase_order(
642 "GR-001".to_string(),
643 "1000",
644 "PO-001",
645 "V-001",
646 "P1000",
647 "0001",
648 2024,
649 1,
650 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
651 "JSMITH",
652 );
653
654 let item = GoodsReceiptItem::from_po(
655 10,
656 "Test Material",
657 Decimal::from(100),
658 Decimal::from(50),
659 "PO-001",
660 10,
661 )
662 .with_movement_type(MovementType::GrForPo);
663
664 gr.add_item(item);
665 gr.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
666
667 gr
668 }
669
670 fn create_test_vendor_invoice() -> VendorInvoice {
671 use datasynth_core::models::documents::VendorInvoiceItem;
672
673 let mut invoice = VendorInvoice::new(
674 "VI-001".to_string(),
675 "1000",
676 "V-001",
677 "INV-12345".to_string(),
678 2024,
679 1,
680 NaiveDate::from_ymd_opt(2024, 1, 20).unwrap(),
681 "JSMITH",
682 );
683
684 let item = VendorInvoiceItem::from_po_gr(
685 10,
686 "Test Material",
687 Decimal::from(100),
688 Decimal::from(50),
689 "PO-001",
690 10,
691 Some("GR-001".to_string()),
692 Some(10),
693 );
694
695 invoice.add_item(item);
696 invoice.post("JSMITH", NaiveDate::from_ymd_opt(2024, 1, 20).unwrap());
697
698 invoice
699 }
700
701 fn create_test_payment() -> Payment {
702 let mut payment = Payment::new_ap_payment(
703 "PAY-001".to_string(),
704 "1000",
705 "V-001",
706 Decimal::from(5000),
707 2024,
708 2,
709 NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
710 "JSMITH",
711 );
712
713 payment.post("JSMITH", NaiveDate::from_ymd_opt(2024, 2, 15).unwrap());
714
715 payment
716 }
717
718 #[test]
719 fn test_generate_from_goods_receipt() {
720 let mut generator = DocumentFlowJeGenerator::new();
721 let gr = create_test_gr();
722
723 let je = generator.generate_from_goods_receipt(&gr);
724
725 assert!(je.is_some());
726 let je = je.unwrap();
727
728 assert!(je.is_balanced());
730
731 assert_eq!(je.line_count(), 2);
733
734 assert!(je.total_debit() > Decimal::ZERO);
736 assert_eq!(je.total_debit(), je.total_credit());
737
738 assert!(je.header.reference.is_some());
740 assert!(je.header.reference.as_ref().unwrap().contains("GR:"));
741 }
742
743 #[test]
744 fn test_generate_from_vendor_invoice() {
745 let mut generator = DocumentFlowJeGenerator::new();
746 let invoice = create_test_vendor_invoice();
747
748 let je = generator.generate_from_vendor_invoice(&invoice);
749
750 assert!(je.is_some());
751 let je = je.unwrap();
752
753 assert!(je.is_balanced());
754 assert_eq!(je.line_count(), 2);
755 assert!(je.header.reference.as_ref().unwrap().contains("VI:"));
756 }
757
758 #[test]
759 fn test_generate_from_ap_payment() {
760 let mut generator = DocumentFlowJeGenerator::new();
761 let payment = create_test_payment();
762
763 let je = generator.generate_from_ap_payment(&payment);
764
765 assert!(je.is_some());
766 let je = je.unwrap();
767
768 assert!(je.is_balanced());
769 assert_eq!(je.line_count(), 2);
770 assert!(je.header.reference.as_ref().unwrap().contains("PAY:"));
771 }
772
773 #[test]
774 fn test_all_entries_are_balanced() {
775 let mut generator = DocumentFlowJeGenerator::new();
776
777 let gr = create_test_gr();
778 let invoice = create_test_vendor_invoice();
779 let payment = create_test_payment();
780
781 let entries = vec![
782 generator.generate_from_goods_receipt(&gr),
783 generator.generate_from_vendor_invoice(&invoice),
784 generator.generate_from_ap_payment(&payment),
785 ];
786
787 for entry in entries.into_iter().flatten() {
788 assert!(
789 entry.is_balanced(),
790 "Entry {} is not balanced",
791 entry.header.document_id
792 );
793 }
794 }
795
796 #[test]
801 fn test_french_gaap_auxiliary_on_ap_ar_lines_only() {
802 let mut generator =
804 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
805
806 let invoice = create_test_vendor_invoice();
808 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
809
810 assert!(
812 je.lines[0].auxiliary_account_number.is_none(),
813 "GR/IR clearing line should not have auxiliary"
814 );
815
816 assert_eq!(
818 je.lines[1].auxiliary_account_number.as_deref(),
819 Some("V-001"),
820 "AP line should have vendor ID as auxiliary"
821 );
822 assert_eq!(
823 je.lines[1].auxiliary_account_label.as_deref(),
824 Some("V-001"),
825 );
826 }
827
828 #[test]
829 fn test_french_gaap_lettrage_on_complete_p2p_chain() {
830 use datasynth_core::models::documents::PurchaseOrder;
831
832 let mut generator =
833 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
834
835 let po = PurchaseOrder::new(
836 "PO-001",
837 "1000",
838 "V-001",
839 2024,
840 1,
841 NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(),
842 "JSMITH",
843 );
844
845 let chain = P2PDocumentChain {
846 purchase_order: po,
847 goods_receipts: vec![create_test_gr()],
848 vendor_invoice: Some(create_test_vendor_invoice()),
849 payment: Some(create_test_payment()),
850 is_complete: true,
851 three_way_match_passed: true,
852 payment_timing: None,
853 };
854
855 let entries = generator.generate_from_p2p_chain(&chain);
856 assert!(!entries.is_empty());
857
858 let ap_account = &generator.config.ap_account;
860 let mut lettrage_codes: Vec<&str> = Vec::new();
861 for entry in &entries {
862 for line in &entry.lines {
863 if &line.gl_account == ap_account {
864 assert!(
865 line.lettrage.is_some(),
866 "AP line should have lettrage on complete chain"
867 );
868 assert!(line.lettrage_date.is_some());
869 lettrage_codes.push(line.lettrage.as_deref().unwrap());
870 } else {
871 assert!(
872 line.lettrage.is_none(),
873 "Non-AP line should not have lettrage"
874 );
875 }
876 }
877 }
878
879 assert!(!lettrage_codes.is_empty());
881 assert!(
882 lettrage_codes.iter().all(|c| *c == lettrage_codes[0]),
883 "All AP lines should share the same lettrage code"
884 );
885 assert!(lettrage_codes[0].starts_with("LTR-"));
886 }
887
888 #[test]
889 fn test_incomplete_chain_has_no_lettrage() {
890 use datasynth_core::models::documents::PurchaseOrder;
891
892 let mut generator =
893 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
894
895 let po = PurchaseOrder::new(
896 "PO-002",
897 "1000",
898 "V-001",
899 2024,
900 1,
901 NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(),
902 "JSMITH",
903 );
904
905 let chain = P2PDocumentChain {
907 purchase_order: po,
908 goods_receipts: vec![create_test_gr()],
909 vendor_invoice: Some(create_test_vendor_invoice()),
910 payment: None,
911 is_complete: false,
912 three_way_match_passed: false,
913 payment_timing: None,
914 };
915
916 let entries = generator.generate_from_p2p_chain(&chain);
917
918 for entry in &entries {
919 for line in &entry.lines {
920 assert!(
921 line.lettrage.is_none(),
922 "Incomplete chain should have no lettrage"
923 );
924 }
925 }
926 }
927
928 #[test]
929 fn test_default_config_no_fec_fields() {
930 let mut generator = DocumentFlowJeGenerator::new();
932
933 let invoice = create_test_vendor_invoice();
934 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
935
936 for line in &je.lines {
937 assert!(line.auxiliary_account_number.is_none());
938 assert!(line.auxiliary_account_label.is_none());
939 assert!(line.lettrage.is_none());
940 assert!(line.lettrage_date.is_none());
941 }
942 }
943
944 #[test]
945 fn test_auxiliary_lookup_uses_gl_account_instead_of_partner_id() {
946 let mut generator =
949 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
950
951 let mut lookup = HashMap::new();
952 lookup.insert("V-001".to_string(), "4010001".to_string());
953 generator.set_auxiliary_account_lookup(lookup);
954
955 let invoice = create_test_vendor_invoice();
956 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
957
958 assert_eq!(
960 je.lines[1].auxiliary_account_number.as_deref(),
961 Some("4010001"),
962 "AP line should use auxiliary GL account from lookup"
963 );
964 assert_eq!(
966 je.lines[1].auxiliary_account_label.as_deref(),
967 Some("V-001"),
968 );
969 }
970
971 #[test]
972 fn test_auxiliary_lookup_fallback_to_partner_id() {
973 let mut generator =
976 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
977
978 let mut lookup = HashMap::new();
980 lookup.insert("V-999".to_string(), "4019999".to_string());
981 generator.set_auxiliary_account_lookup(lookup);
982
983 let invoice = create_test_vendor_invoice();
984 let je = generator.generate_from_vendor_invoice(&invoice).unwrap();
985
986 assert_eq!(
988 je.lines[1].auxiliary_account_number.as_deref(),
989 Some("V-001"),
990 "Should fall back to partner ID when not in lookup"
991 );
992 }
993
994 #[test]
995 fn test_auxiliary_lookup_works_for_customer_receipt() {
996 let mut generator =
998 DocumentFlowJeGenerator::with_config_and_seed(DocumentFlowJeConfig::french_gaap(), 42);
999
1000 let mut lookup = HashMap::new();
1001 lookup.insert("C-001".to_string(), "4110001".to_string());
1002 generator.set_auxiliary_account_lookup(lookup);
1003
1004 let mut receipt = Payment::new_ar_receipt(
1005 "RCP-001".to_string(),
1006 "1000",
1007 "C-001",
1008 Decimal::from(3000),
1009 2024,
1010 3,
1011 NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
1012 "JSMITH",
1013 );
1014 receipt.post("JSMITH", NaiveDate::from_ymd_opt(2024, 3, 15).unwrap());
1015
1016 let je = generator.generate_from_ar_receipt(&receipt).unwrap();
1017
1018 assert_eq!(
1020 je.lines[1].auxiliary_account_number.as_deref(),
1021 Some("4110001"),
1022 "AR line should use auxiliary GL account from lookup"
1023 );
1024 }
1025}