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