1use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use smallvec::SmallVec;
11use uuid::Uuid;
12
13use super::anomaly::FraudType;
14use super::approval::ApprovalWorkflow;
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub enum DocumentRef {
22 PurchaseOrder(String),
24 VendorInvoice(String),
26 CustomerInvoice(String),
28 GoodsReceipt(String),
30 Delivery(String),
32 Payment(String),
34 Receipt(String),
36 Manual,
38}
39
40impl DocumentRef {
41 pub fn parse(reference: &str) -> Option<Self> {
45 if reference.starts_with("PO-") || reference.starts_with("PO ") {
46 Some(Self::PurchaseOrder(reference.to_string()))
47 } else if reference.starts_with("INV-") || reference.starts_with("INV ") {
48 Some(Self::VendorInvoice(reference.to_string()))
49 } else if reference.starts_with("SO-") || reference.starts_with("SO ") {
50 Some(Self::CustomerInvoice(reference.to_string()))
51 } else if reference.starts_with("GR-") || reference.starts_with("GR ") {
52 Some(Self::GoodsReceipt(reference.to_string()))
53 } else if reference.starts_with("PAY-") || reference.starts_with("PAY ") {
54 Some(Self::Payment(reference.to_string()))
55 } else {
56 None
57 }
58 }
59
60 pub fn document_id(&self) -> Option<&str> {
62 match self {
63 Self::PurchaseOrder(id)
64 | Self::VendorInvoice(id)
65 | Self::CustomerInvoice(id)
66 | Self::GoodsReceipt(id)
67 | Self::Delivery(id)
68 | Self::Payment(id)
69 | Self::Receipt(id) => Some(id.as_str()),
70 Self::Manual => None,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
80#[serde(rename_all = "snake_case")]
81pub enum TransactionSource {
82 #[default]
84 Manual,
85 Automated,
87 Recurring,
89 Reversal,
91 Adjustment,
93 Statistical,
95}
96
97impl std::fmt::Display for TransactionSource {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Self::Manual => write!(f, "manual"),
101 Self::Automated => write!(f, "automated"),
102 Self::Recurring => write!(f, "recurring"),
103 Self::Reversal => write!(f, "reversal"),
104 Self::Adjustment => write!(f, "adjustment"),
105 Self::Statistical => write!(f, "statistical"),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
118#[serde(rename_all = "UPPERCASE")]
119pub enum BusinessProcess {
120 O2C,
122 P2P,
124 #[default]
126 R2R,
127 H2R,
129 A2R,
131 S2C,
133 #[serde(rename = "MFG")]
135 Mfg,
136 #[serde(rename = "BANK")]
138 Bank,
139 #[serde(rename = "AUDIT")]
141 Audit,
142 Treasury,
144 Tax,
146 Intercompany,
148 #[serde(rename = "PROJECT")]
150 ProjectAccounting,
151 #[serde(rename = "ESG")]
153 Esg,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
160pub struct DocumentType {
161 pub code: String,
163 pub description: String,
165 pub business_process: BusinessProcess,
167 pub is_reversal: bool,
169}
170
171impl DocumentType {
172 pub fn gl_account() -> Self {
174 Self {
175 code: "SA".to_string(),
176 description: "G/L Account Document".to_string(),
177 business_process: BusinessProcess::R2R,
178 is_reversal: false,
179 }
180 }
181
182 pub fn vendor_invoice() -> Self {
184 Self {
185 code: "KR".to_string(),
186 description: "Vendor Invoice".to_string(),
187 business_process: BusinessProcess::P2P,
188 is_reversal: false,
189 }
190 }
191
192 pub fn customer_invoice() -> Self {
194 Self {
195 code: "DR".to_string(),
196 description: "Customer Invoice".to_string(),
197 business_process: BusinessProcess::O2C,
198 is_reversal: false,
199 }
200 }
201
202 pub fn vendor_payment() -> Self {
204 Self {
205 code: "KZ".to_string(),
206 description: "Vendor Payment".to_string(),
207 business_process: BusinessProcess::P2P,
208 is_reversal: false,
209 }
210 }
211
212 pub fn customer_payment() -> Self {
214 Self {
215 code: "DZ".to_string(),
216 description: "Customer Payment".to_string(),
217 business_process: BusinessProcess::O2C,
218 is_reversal: false,
219 }
220 }
221
222 pub fn asset_posting() -> Self {
224 Self {
225 code: "AA".to_string(),
226 description: "Asset Posting".to_string(),
227 business_process: BusinessProcess::A2R,
228 is_reversal: false,
229 }
230 }
231
232 pub fn payroll() -> Self {
234 Self {
235 code: "PR".to_string(),
236 description: "Payroll Document".to_string(),
237 business_process: BusinessProcess::H2R,
238 is_reversal: false,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct JournalEntryHeader {
249 pub document_id: Uuid,
251
252 pub company_code: String,
254
255 pub fiscal_year: u16,
257
258 pub fiscal_period: u8,
260
261 pub posting_date: NaiveDate,
263
264 pub document_date: NaiveDate,
266
267 #[serde(with = "crate::serde_timestamp::utc")]
269 pub created_at: DateTime<Utc>,
270
271 pub document_type: String,
273
274 pub currency: String,
276
277 #[serde(with = "crate::serde_decimal")]
279 pub exchange_rate: Decimal,
280
281 pub reference: Option<String>,
283
284 pub header_text: Option<String>,
286
287 pub created_by: String,
289
290 pub user_persona: String,
292
293 pub source: TransactionSource,
295
296 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub sap_source_code: Option<String>,
302
303 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub trading_partner: Option<String>,
309
310 pub business_process: Option<BusinessProcess>,
312
313 pub ledger: String,
315
316 pub is_fraud: bool,
318
319 pub fraud_type: Option<FraudType>,
321
322 #[serde(default)]
327 pub is_fraud_propagated: bool,
328
329 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub fraud_source_document_id: Option<String>,
333
334 #[serde(default)]
337 pub is_anomaly: bool,
338
339 #[serde(default)]
341 pub anomaly_id: Option<String>,
342
343 #[serde(default)]
345 pub anomaly_type: Option<String>,
346
347 pub batch_id: Option<Uuid>,
349
350 #[serde(default)]
354 pub is_manual: bool,
355
356 #[serde(default)]
359 pub is_post_close: bool,
360
361 #[serde(default)]
364 pub source_system: String,
365
366 #[serde(default, with = "crate::serde_timestamp::naive::option")]
370 pub created_date: Option<NaiveDateTime>,
371
372 #[serde(default)]
375 pub control_ids: Vec<String>,
376
377 #[serde(default)]
379 pub sox_relevant: bool,
380
381 #[serde(default)]
383 pub control_status: super::internal_control::ControlStatus,
384
385 #[serde(default)]
387 pub sod_violation: bool,
388
389 #[serde(default)]
391 pub sod_conflict_type: Option<super::sod::SodConflictType>,
392
393 #[serde(default)]
395 pub is_elimination: bool,
396
397 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub ic_pair_id: Option<crate::models::IcPairId>,
403
404 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub ic_partner_entity: Option<String>,
408
409 #[serde(default)]
412 pub approval_workflow: Option<ApprovalWorkflow>,
413
414 #[serde(default)]
417 pub source_document: Option<DocumentRef>,
418 #[serde(default)]
420 pub approved_by: Option<String>,
421 #[serde(default)]
423 pub approval_date: Option<NaiveDate>,
424
425 #[serde(default)]
428 pub ocpm_event_ids: Vec<Uuid>,
429
430 #[serde(default)]
432 pub ocpm_object_ids: Vec<Uuid>,
433
434 #[serde(default)]
436 pub ocpm_case_id: Option<Uuid>,
437}
438
439impl JournalEntryHeader {
440 pub fn new(company_code: String, posting_date: NaiveDate) -> Self {
442 Self {
443 document_id: Uuid::now_v7(),
444 company_code,
445 fiscal_year: posting_date.year() as u16,
446 fiscal_period: posting_date.month() as u8,
447 posting_date,
448 document_date: posting_date,
449 created_at: Utc::now(),
450 document_type: "SA".to_string(),
451 currency: "USD".to_string(),
452 exchange_rate: Decimal::ONE,
453 reference: None,
454 header_text: None,
455 created_by: "SYSTEM".to_string(),
456 user_persona: "automated_system".to_string(),
457 source: TransactionSource::Automated,
458 sap_source_code: None,
459 trading_partner: None,
460 business_process: Some(BusinessProcess::R2R),
461 ledger: "0L".to_string(),
462 is_fraud: false,
463 fraud_type: None,
464 is_fraud_propagated: false,
465 fraud_source_document_id: None,
466 is_anomaly: false,
468 anomaly_id: None,
469 anomaly_type: None,
470 batch_id: None,
471 is_manual: false,
473 is_post_close: false,
474 source_system: String::new(),
475 created_date: None,
476 control_ids: Vec::new(),
478 sox_relevant: false,
479 control_status: super::internal_control::ControlStatus::default(),
480 sod_violation: false,
481 sod_conflict_type: None,
482 is_elimination: false,
484 ic_pair_id: None,
486 ic_partner_entity: None,
487 approval_workflow: None,
489 source_document: None,
491 approved_by: None,
492 approval_date: None,
493 ocpm_event_ids: Vec::new(),
495 ocpm_object_ids: Vec::new(),
496 ocpm_case_id: None,
497 }
498 }
499
500 pub fn with_deterministic_id(
505 company_code: String,
506 posting_date: NaiveDate,
507 document_id: Uuid,
508 ) -> Self {
509 Self {
510 document_id,
511 company_code,
512 fiscal_year: posting_date.year() as u16,
513 fiscal_period: posting_date.month() as u8,
514 posting_date,
515 document_date: posting_date,
516 created_at: Utc::now(),
517 document_type: "SA".to_string(),
518 currency: "USD".to_string(),
519 exchange_rate: Decimal::ONE,
520 reference: None,
521 header_text: None,
522 created_by: "SYSTEM".to_string(),
523 user_persona: "automated_system".to_string(),
524 source: TransactionSource::Automated,
525 sap_source_code: None,
526 trading_partner: None,
527 business_process: Some(BusinessProcess::R2R),
528 ledger: "0L".to_string(),
529 is_fraud: false,
530 fraud_type: None,
531 is_fraud_propagated: false,
532 fraud_source_document_id: None,
533 is_anomaly: false,
535 anomaly_id: None,
536 anomaly_type: None,
537 batch_id: None,
538 is_manual: false,
540 is_post_close: false,
541 source_system: String::new(),
542 created_date: None,
543 control_ids: Vec::new(),
545 sox_relevant: false,
546 control_status: super::internal_control::ControlStatus::default(),
547 sod_violation: false,
548 sod_conflict_type: None,
549 is_elimination: false,
551 ic_pair_id: None,
553 ic_partner_entity: None,
554 approval_workflow: None,
556 source_document: None,
558 approved_by: None,
559 approval_date: None,
560 ocpm_event_ids: Vec::new(),
562 ocpm_object_ids: Vec::new(),
563 ocpm_case_id: None,
564 }
565 }
566
567 pub fn propagate_fraud_from_documents(
577 &mut self,
578 fraud_map: &std::collections::HashMap<String, crate::models::FraudType>,
579 ) -> bool {
580 let doc_id = match self
581 .source_document
582 .as_ref()
583 .and_then(DocumentRef::document_id)
584 {
585 Some(id) => id,
586 None => return false,
587 };
588 if let Some(ft) = fraud_map.get(doc_id) {
589 self.is_fraud = true;
590 self.fraud_type = Some(*ft);
591 self.is_fraud_propagated = true;
592 self.fraud_source_document_id = Some(doc_id.to_string());
593 return true;
594 }
595 false
596 }
597}
598
599use chrono::Datelike;
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct JournalEntryLine {
607 pub document_id: Uuid,
609
610 pub line_number: u32,
612
613 pub gl_account: String,
615
616 #[serde(default)]
618 pub account_code: String,
619
620 #[serde(default)]
622 pub account_description: Option<String>,
623
624 #[serde(with = "crate::serde_decimal")]
626 pub debit_amount: Decimal,
627
628 #[serde(with = "crate::serde_decimal")]
630 pub credit_amount: Decimal,
631
632 #[serde(with = "crate::serde_decimal")]
634 pub local_amount: Decimal,
635
636 #[serde(default, with = "crate::serde_decimal::option")]
638 pub group_amount: Option<Decimal>,
639
640 #[serde(default, with = "crate::serde_decimal::option")]
647 pub transaction_amount: Option<Decimal>,
648
649 pub cost_center: Option<String>,
651
652 pub profit_center: Option<String>,
654
655 #[serde(default)]
659 pub business_unit: Option<String>,
660
661 pub segment: Option<String>,
663
664 pub functional_area: Option<String>,
666
667 pub line_text: Option<String>,
669
670 #[serde(default)]
672 pub text: Option<String>,
673
674 #[serde(default)]
676 pub reference: Option<String>,
677
678 #[serde(default)]
680 pub value_date: Option<NaiveDate>,
681
682 pub tax_code: Option<String>,
684
685 #[serde(default, with = "crate::serde_decimal::option")]
687 pub tax_amount: Option<Decimal>,
688
689 pub assignment: Option<String>,
691
692 pub offsetting_account: Option<String>,
694
695 pub is_suspense: bool,
697
698 pub trading_partner: Option<String>,
700
701 #[serde(default, with = "crate::serde_decimal::option")]
703 pub quantity: Option<Decimal>,
704
705 pub unit_of_measure: Option<String>,
707
708 #[serde(default)]
710 pub unit: Option<String>,
711
712 #[serde(default)]
714 pub project_code: Option<String>,
715
716 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub auxiliary_account_number: Option<String>,
720
721 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub auxiliary_account_label: Option<String>,
725
726 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub lettrage: Option<String>,
730
731 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub lettrage_date: Option<NaiveDate>,
735
736 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub transaction_id: Option<String>,
744
745 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub predecessor_line_id: Option<String>,
760}
761
762impl JournalEntryLine {
763 #[inline]
765 pub fn debit(document_id: Uuid, line_number: u32, gl_account: String, amount: Decimal) -> Self {
766 Self {
767 document_id,
768 line_number,
769 gl_account: gl_account.clone(),
770 account_code: gl_account,
771 account_description: None,
772 debit_amount: amount,
773 credit_amount: Decimal::ZERO,
774 local_amount: amount,
775 group_amount: None,
776 transaction_amount: None,
777 cost_center: None,
778 profit_center: None,
779 business_unit: None,
780 segment: None,
781 functional_area: None,
782 line_text: None,
783 text: None,
784 reference: None,
785 value_date: None,
786 tax_code: None,
787 tax_amount: None,
788 assignment: None,
789 offsetting_account: None,
790 is_suspense: false,
791 trading_partner: None,
792 quantity: None,
793 unit_of_measure: None,
794 unit: None,
795 project_code: None,
796 auxiliary_account_number: None,
797 auxiliary_account_label: None,
798 lettrage: None,
799 lettrage_date: None,
800 transaction_id: None,
801 predecessor_line_id: None,
802 }
803 }
804
805 #[inline]
807 pub fn credit(
808 document_id: Uuid,
809 line_number: u32,
810 gl_account: String,
811 amount: Decimal,
812 ) -> Self {
813 Self {
814 document_id,
815 line_number,
816 gl_account: gl_account.clone(),
817 account_code: gl_account,
818 account_description: None,
819 debit_amount: Decimal::ZERO,
820 credit_amount: amount,
821 local_amount: -amount,
822 group_amount: None,
823 transaction_amount: None,
824 cost_center: None,
825 profit_center: None,
826 business_unit: None,
827 segment: None,
828 functional_area: None,
829 line_text: None,
830 text: None,
831 reference: None,
832 value_date: None,
833 tax_code: None,
834 tax_amount: None,
835 assignment: None,
836 offsetting_account: None,
837 is_suspense: false,
838 trading_partner: None,
839 quantity: None,
840 unit_of_measure: None,
841 unit: None,
842 project_code: None,
843 auxiliary_account_number: None,
844 auxiliary_account_label: None,
845 lettrage: None,
846 lettrage_date: None,
847 transaction_id: None,
848 predecessor_line_id: None,
849 }
850 }
851
852 #[inline]
854 pub fn is_debit(&self) -> bool {
855 self.debit_amount > Decimal::ZERO
856 }
857
858 #[inline]
860 pub fn is_credit(&self) -> bool {
861 self.credit_amount > Decimal::ZERO
862 }
863
864 #[inline]
866 pub fn signed_amount(&self) -> Decimal {
867 self.debit_amount - self.credit_amount
868 }
869
870 pub fn derive_transaction_id(document_id: Uuid, line_number: u32) -> String {
876 let mut input = Vec::with_capacity(20);
877 input.extend_from_slice(document_id.as_bytes());
878 input.extend_from_slice(&line_number.to_le_bytes());
879 Uuid::new_v5(&Uuid::NAMESPACE_OID, &input).to_string()
880 }
881
882 pub fn ensure_transaction_id(&mut self) {
884 if self.transaction_id.is_none() {
885 self.transaction_id = Some(Self::derive_transaction_id(
886 self.document_id,
887 self.line_number,
888 ));
889 }
890 }
891
892 #[allow(clippy::misnamed_getters)]
896 pub fn account_code(&self) -> &str {
897 &self.gl_account
898 }
899
900 pub fn account_description(&self) -> &str {
902 ""
904 }
905}
906
907impl Default for JournalEntryLine {
908 fn default() -> Self {
909 Self {
910 document_id: Uuid::nil(),
911 line_number: 0,
912 gl_account: String::new(),
913 account_code: String::new(),
914 account_description: None,
915 debit_amount: Decimal::ZERO,
916 credit_amount: Decimal::ZERO,
917 local_amount: Decimal::ZERO,
918 group_amount: None,
919 transaction_amount: None,
920 cost_center: None,
921 profit_center: None,
922 business_unit: None,
923 segment: None,
924 functional_area: None,
925 line_text: None,
926 text: None,
927 reference: None,
928 value_date: None,
929 tax_code: None,
930 tax_amount: None,
931 assignment: None,
932 offsetting_account: None,
933 is_suspense: false,
934 trading_partner: None,
935 quantity: None,
936 unit_of_measure: None,
937 unit: None,
938 project_code: None,
939 auxiliary_account_number: None,
940 auxiliary_account_label: None,
941 lettrage: None,
942 lettrage_date: None,
943 transaction_id: None,
944 predecessor_line_id: None,
945 }
946 }
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
959pub struct JournalEntry {
960 pub header: JournalEntryHeader,
962 pub lines: SmallVec<[JournalEntryLine; 4]>,
965}
966
967impl JournalEntry {
968 pub fn new(header: JournalEntryHeader) -> Self {
970 Self {
971 header,
972 lines: SmallVec::new(),
973 }
974 }
975
976 pub fn new_simple(
982 document_number: String,
983 company_code: String,
984 posting_date: NaiveDate,
985 description: String,
986 ) -> Self {
987 let mut header = JournalEntryHeader::new(company_code, posting_date);
988 header.header_text = Some(description);
989 header.reference = Some(document_number);
990 Self {
991 header,
992 lines: SmallVec::new(),
993 }
994 }
995
996 #[inline]
1000 pub fn add_line(&mut self, mut line: JournalEntryLine) {
1001 line.document_id = self.header.document_id;
1002 self.lines.push(line);
1003 }
1004
1005 pub fn total_debit(&self) -> Decimal {
1007 self.lines.iter().map(|l| l.debit_amount).sum()
1008 }
1009
1010 pub fn total_credit(&self) -> Decimal {
1012 self.lines.iter().map(|l| l.credit_amount).sum()
1013 }
1014
1015 pub fn is_balanced(&self) -> bool {
1017 self.total_debit() == self.total_credit()
1018 }
1019
1020 pub fn balance_difference(&self) -> Decimal {
1022 self.total_debit() - self.total_credit()
1023 }
1024
1025 pub fn line_count(&self) -> usize {
1027 self.lines.len()
1028 }
1029
1030 pub fn has_even_line_count(&self) -> bool {
1032 self.lines.len().is_multiple_of(2)
1033 }
1034
1035 pub fn debit_credit_counts(&self) -> (usize, usize) {
1037 let debits = self.lines.iter().filter(|l| l.is_debit()).count();
1038 let credits = self.lines.iter().filter(|l| l.is_credit()).count();
1039 (debits, credits)
1040 }
1041
1042 pub fn has_equal_debit_credit_counts(&self) -> bool {
1044 let (d, c) = self.debit_credit_counts();
1045 d == c
1046 }
1047
1048 pub fn unique_accounts(&self) -> Vec<&str> {
1050 let mut accounts: Vec<&str> = self.lines.iter().map(|l| l.gl_account.as_str()).collect();
1051 accounts.sort();
1052 accounts.dedup();
1053 accounts
1054 }
1055
1056 pub fn has_suspense_posting(&self) -> bool {
1058 self.lines.iter().any(|l| l.is_suspense)
1059 }
1060
1061 pub fn company_code(&self) -> &str {
1065 &self.header.company_code
1066 }
1067
1068 pub fn document_number(&self) -> String {
1070 self.header.document_id.to_string()
1071 }
1072
1073 pub fn posting_date(&self) -> NaiveDate {
1075 self.header.posting_date
1076 }
1077
1078 pub fn document_date(&self) -> NaiveDate {
1080 self.header.document_date
1081 }
1082
1083 pub fn fiscal_year(&self) -> u16 {
1085 self.header.fiscal_year
1086 }
1087
1088 pub fn fiscal_period(&self) -> u8 {
1090 self.header.fiscal_period
1091 }
1092
1093 pub fn currency(&self) -> &str {
1095 &self.header.currency
1096 }
1097
1098 pub fn is_fraud(&self) -> bool {
1100 self.header.is_fraud
1101 }
1102
1103 pub fn has_sod_violation(&self) -> bool {
1105 self.header.sod_violation
1106 }
1107
1108 pub fn description(&self) -> Option<&str> {
1110 self.header.header_text.as_deref()
1111 }
1112
1113 pub fn set_description(&mut self, description: String) {
1115 self.header.header_text = Some(description);
1116 }
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122
1123 #[test]
1124 fn test_balanced_entry() {
1125 let header = JournalEntryHeader::new(
1126 "1000".to_string(),
1127 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1128 );
1129 let mut entry = JournalEntry::new(header);
1130
1131 entry.add_line(JournalEntryLine::debit(
1132 entry.header.document_id,
1133 1,
1134 "100000".to_string(),
1135 Decimal::from(1000),
1136 ));
1137 entry.add_line(JournalEntryLine::credit(
1138 entry.header.document_id,
1139 2,
1140 "200000".to_string(),
1141 Decimal::from(1000),
1142 ));
1143
1144 assert!(entry.is_balanced());
1145 assert_eq!(entry.line_count(), 2);
1146 assert!(entry.has_even_line_count());
1147 assert!(entry.has_equal_debit_credit_counts());
1148 }
1149
1150 #[test]
1151 fn test_unbalanced_entry() {
1152 let header = JournalEntryHeader::new(
1153 "1000".to_string(),
1154 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1155 );
1156 let mut entry = JournalEntry::new(header);
1157
1158 entry.add_line(JournalEntryLine::debit(
1159 entry.header.document_id,
1160 1,
1161 "100000".to_string(),
1162 Decimal::from(1000),
1163 ));
1164 entry.add_line(JournalEntryLine::credit(
1165 entry.header.document_id,
1166 2,
1167 "200000".to_string(),
1168 Decimal::from(500),
1169 ));
1170
1171 assert!(!entry.is_balanced());
1172 assert_eq!(entry.balance_difference(), Decimal::from(500));
1173 }
1174}