1use chrono::NaiveDate;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13use super::journal_entry::JournalEntry;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AcdocaEntry {
22 pub rldnr: String,
25 pub rbukrs: String,
27 pub gjahr: u16,
29 pub belnr: String,
31 pub docln: String,
33
34 pub blart: String,
37 pub budat: NaiveDate,
39 pub bldat: NaiveDate,
41 pub cpudt: NaiveDate,
43 pub cputm: String,
45 pub usnam: String,
47 pub poper: u8,
49 pub stgrd: Option<String>,
51 pub xblnr: Option<String>,
53 pub bktxt: Option<String>,
55
56 pub racct: String,
59 pub rcntr: Option<String>,
61 pub prctr: Option<String>,
63 pub segment: Option<String>,
65 pub rfarea: Option<String>,
67 pub rbusa: Option<String>,
69 pub ps_psp_pnr: Option<String>,
71 pub aufnr: Option<String>,
73 pub kdauf: Option<String>,
75 pub kdpos: Option<String>,
77
78 pub pbukrs: Option<String>,
81 pub pprctr: Option<String>,
83 pub psegment: Option<String>,
85 pub rassc: Option<String>,
87
88 pub wsl: Decimal,
91 pub rwcur: String,
93 pub hsl: Decimal,
95 pub rhcur: String,
97 pub ksl: Option<Decimal>,
99 pub rkcur: Option<String>,
101 pub osl: Option<Decimal>,
103 pub rocur: Option<String>,
105
106 pub msl: Option<Decimal>,
109 pub runit: Option<String>,
111
112 pub sgtxt: Option<String>,
115 pub zuonr: Option<String>,
117
118 pub awsys: String,
121 pub awtyp: String,
123 pub awkey: String,
125 pub awitem: Option<String>,
127 pub aworg: Option<String>,
129
130 pub mwskz: Option<String>,
133 pub txjcd: Option<String>,
135 pub hwbas: Option<Decimal>,
137
138 pub xstov: bool,
141 pub xsauf: bool,
143 pub drcrk: String,
145 pub bschl: String,
147
148 pub anln1: Option<String>,
151 pub anln2: Option<String>,
153 pub anbwa: Option<String>,
155
156 pub lifnr: Option<String>,
159 pub kunnr: Option<String>,
161
162 #[serde(rename = "ZSIM_BATCH_ID")]
165 pub sim_batch_id: Option<Uuid>,
166 #[serde(rename = "ZSIM_IS_FRAUD")]
168 pub sim_is_fraud: bool,
169 #[serde(rename = "ZSIM_FRAUD_TYPE")]
171 pub sim_fraud_type: Option<String>,
172 #[serde(rename = "ZSIM_BUSINESS_PROCESS")]
174 pub sim_business_process: Option<String>,
175 #[serde(rename = "ZSIM_USER_PERSONA")]
177 pub sim_user_persona: Option<String>,
178 #[serde(rename = "ZSIM_JE_UUID")]
180 pub sim_je_uuid: Option<Uuid>,
181
182 #[serde(rename = "ZSIM_CONTROL_IDS")]
185 pub sim_control_ids: Option<String>,
186 #[serde(rename = "ZSIM_SOX_RELEVANT")]
188 pub sim_sox_relevant: bool,
189 #[serde(rename = "ZSIM_CONTROL_STATUS")]
191 pub sim_control_status: Option<String>,
192 #[serde(rename = "ZSIM_SOD_VIOLATION")]
194 pub sim_sod_violation: bool,
195 #[serde(rename = "ZSIM_SOD_CONFLICT")]
197 pub sim_sod_conflict: Option<String>,
198}
199
200impl Default for AcdocaEntry {
201 fn default() -> Self {
202 Self {
203 rldnr: "0L".to_string(),
204 rbukrs: String::new(),
205 gjahr: 0,
206 belnr: String::new(),
207 docln: String::new(),
208 blart: "SA".to_string(),
209 budat: NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
210 bldat: NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
211 cpudt: NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
212 cputm: "000000".to_string(),
213 usnam: String::new(),
214 poper: 1,
215 stgrd: None,
216 xblnr: None,
217 bktxt: None,
218 racct: String::new(),
219 rcntr: None,
220 prctr: None,
221 segment: None,
222 rfarea: None,
223 rbusa: None,
224 ps_psp_pnr: None,
225 aufnr: None,
226 kdauf: None,
227 kdpos: None,
228 pbukrs: None,
229 pprctr: None,
230 psegment: None,
231 rassc: None,
232 wsl: Decimal::ZERO,
233 rwcur: "USD".to_string(),
234 hsl: Decimal::ZERO,
235 rhcur: "USD".to_string(),
236 ksl: None,
237 rkcur: None,
238 osl: None,
239 rocur: None,
240 msl: None,
241 runit: None,
242 sgtxt: None,
243 zuonr: None,
244 awsys: "SYNTH".to_string(),
245 awtyp: "BKPF".to_string(),
246 awkey: String::new(),
247 awitem: None,
248 aworg: None,
249 mwskz: None,
250 txjcd: None,
251 hwbas: None,
252 xstov: false,
253 xsauf: false,
254 drcrk: "S".to_string(),
255 bschl: "40".to_string(),
256 anln1: None,
257 anln2: None,
258 anbwa: None,
259 lifnr: None,
260 kunnr: None,
261 sim_batch_id: None,
262 sim_is_fraud: false,
263 sim_fraud_type: None,
264 sim_business_process: None,
265 sim_user_persona: None,
266 sim_je_uuid: None,
267 sim_control_ids: None,
269 sim_sox_relevant: false,
270 sim_control_status: None,
271 sim_sod_violation: false,
272 sim_sod_conflict: None,
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct BsegEntry {
283 pub mandt: String,
285 pub bukrs: String,
287 pub belnr: String,
289 pub gjahr: u16,
291 pub buzei: String,
293
294 pub bschl: String,
297 pub hkont: String,
299 pub umskz: Option<String>,
301
302 pub wrbtr: Decimal,
305 pub shkzg: String,
307 pub dmbtr: Decimal,
309 pub waers: String,
311 pub wmwst: Option<Decimal>,
313
314 pub kostl: Option<String>,
317 pub prctr: Option<String>,
319 pub anln1: Option<String>,
321 pub anln2: Option<String>,
323 pub lifnr: Option<String>,
325 pub kunnr: Option<String>,
327
328 pub sgtxt: Option<String>,
331 pub zuonr: Option<String>,
333 pub mwskz: Option<String>,
335}
336
337impl Default for BsegEntry {
338 fn default() -> Self {
339 Self {
340 mandt: "100".to_string(),
341 bukrs: String::new(),
342 belnr: String::new(),
343 gjahr: 0,
344 buzei: String::new(),
345 bschl: "40".to_string(),
346 hkont: String::new(),
347 umskz: None,
348 wrbtr: Decimal::ZERO,
349 shkzg: "S".to_string(),
350 dmbtr: Decimal::ZERO,
351 waers: "USD".to_string(),
352 wmwst: None,
353 kostl: None,
354 prctr: None,
355 anln1: None,
356 anln2: None,
357 lifnr: None,
358 kunnr: None,
359 sgtxt: None,
360 zuonr: None,
361 mwskz: None,
362 }
363 }
364}
365
366#[derive(Debug, Clone)]
371pub struct AcdocaFactory {
372 ledger: String,
374 source_system: String,
376 local_currency: String,
378 group_currency: Option<String>,
380 client: String,
382}
383
384impl AcdocaFactory {
385 pub fn new(ledger: &str, source_system: &str) -> Self {
387 Self {
388 ledger: ledger.to_string(),
389 source_system: source_system.to_string(),
390 local_currency: "USD".to_string(),
391 group_currency: None,
392 client: "100".to_string(),
393 }
394 }
395
396 pub fn with_local_currency(mut self, currency: &str) -> Self {
398 self.local_currency = currency.to_string();
399 self
400 }
401
402 pub fn with_group_currency(mut self, currency: &str) -> Self {
404 self.group_currency = Some(currency.to_string());
405 self
406 }
407
408 pub fn with_client(mut self, client: &str) -> Self {
410 self.client = client.to_string();
411 self
412 }
413
414 pub fn from_journal_entry(&self, je: &JournalEntry, document_number: &str) -> Vec<AcdocaEntry> {
416 let created_at = je.header.created_at;
417
418 je.lines
419 .iter()
420 .map(|line| {
421 let is_debit = line.debit_amount > Decimal::ZERO;
423 let amount = if is_debit {
424 line.debit_amount
425 } else {
426 line.credit_amount
427 };
428
429 let wsl = if is_debit { amount } else { -amount };
431 let hsl = wsl * je.header.exchange_rate;
432
433 let bschl = if is_debit { "40" } else { "50" };
435 let drcrk = if is_debit { "S" } else { "H" };
436
437 AcdocaEntry {
438 rldnr: self.ledger.clone(),
439 rbukrs: je.header.company_code.clone(),
440 gjahr: je.header.fiscal_year,
441 belnr: document_number.to_string(),
442 docln: format!("{:06}", line.line_number),
443 blart: je.header.document_type.clone(),
444 budat: je.header.posting_date,
445 bldat: je.header.document_date,
446 cpudt: created_at.date_naive(),
447 cputm: created_at.format("%H%M%S").to_string(),
448 usnam: je.header.created_by.clone(),
449 poper: je.header.fiscal_period,
450 stgrd: None,
451 xblnr: je.header.reference.clone(),
452 bktxt: je.header.header_text.clone(),
453 racct: line.gl_account.clone(),
454 rcntr: line.cost_center.clone(),
455 prctr: line.profit_center.clone(),
456 segment: line.segment.clone(),
457 rfarea: line.functional_area.clone(),
458 rbusa: None,
459 ps_psp_pnr: None,
460 aufnr: None,
461 kdauf: None,
462 kdpos: None,
463 pbukrs: None,
464 pprctr: None,
465 psegment: None,
466 rassc: line.trading_partner.clone(),
467 wsl,
468 rwcur: je.header.currency.clone(),
469 hsl,
470 rhcur: self.local_currency.clone(),
471 ksl: self.group_currency.as_ref().map(|_| hsl),
472 rkcur: self.group_currency.clone(),
473 osl: None,
474 rocur: None,
475 msl: line.quantity,
476 runit: line.unit_of_measure.clone(),
477 sgtxt: line.line_text.clone(),
478 zuonr: line.assignment.clone(),
479 awsys: self.source_system.clone(),
480 awtyp: "BKPF".to_string(),
481 awkey: format!(
482 "{}{}{}",
483 je.header.company_code, document_number, je.header.fiscal_year
484 ),
485 awitem: Some(format!("{:06}", line.line_number)),
486 aworg: None,
487 mwskz: line.tax_code.clone(),
488 txjcd: None,
489 hwbas: line.tax_amount,
490 xstov: matches!(
491 je.header.source,
492 super::journal_entry::TransactionSource::Reversal
493 ),
494 xsauf: matches!(
495 je.header.source,
496 super::journal_entry::TransactionSource::Statistical
497 ),
498 drcrk: drcrk.to_string(),
499 bschl: bschl.to_string(),
500 anln1: None,
501 anln2: None,
502 anbwa: None,
503 lifnr: None,
504 kunnr: None,
505 sim_batch_id: je.header.batch_id,
506 sim_is_fraud: je.header.is_fraud,
507 sim_fraud_type: je.header.fraud_type.map(|ft| format!("{:?}", ft)),
508 sim_business_process: je.header.business_process.map(|bp| format!("{:?}", bp)),
509 sim_user_persona: Some(je.header.user_persona.clone()),
510 sim_je_uuid: Some(je.header.document_id),
511 sim_control_ids: if je.header.control_ids.is_empty() {
512 None
513 } else {
514 Some(je.header.control_ids.join(","))
515 },
516 sim_sox_relevant: je.header.sox_relevant,
517 sim_control_status: Some(je.header.control_status.to_string()),
518 sim_sod_violation: je.header.sod_violation,
519 sim_sod_conflict: je.header.sod_conflict_type.map(|t| t.to_string()),
520 }
521 })
522 .collect()
523 }
524
525 pub fn to_bseg_entries(&self, je: &JournalEntry, document_number: &str) -> Vec<BsegEntry> {
527 je.lines
528 .iter()
529 .map(|line| {
530 let is_debit = line.debit_amount > Decimal::ZERO;
531 let amount = if is_debit {
532 line.debit_amount
533 } else {
534 line.credit_amount
535 };
536
537 BsegEntry {
538 mandt: self.client.clone(),
539 bukrs: je.header.company_code.clone(),
540 belnr: document_number.to_string(),
541 gjahr: je.header.fiscal_year,
542 buzei: format!("{:03}", line.line_number),
543 bschl: if is_debit { "40" } else { "50" }.to_string(),
544 hkont: line.gl_account.clone(),
545 umskz: None,
546 wrbtr: amount,
547 shkzg: if is_debit { "S" } else { "H" }.to_string(),
548 dmbtr: line.local_amount.abs(),
549 waers: je.header.currency.clone(),
550 wmwst: line.tax_amount,
551 kostl: line.cost_center.clone(),
552 prctr: line.profit_center.clone(),
553 anln1: None,
554 anln2: None,
555 lifnr: None,
556 kunnr: None,
557 sgtxt: line.line_text.clone(),
558 zuonr: line.assignment.clone(),
559 mwskz: line.tax_code.clone(),
560 }
561 })
562 .collect()
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use crate::models::journal_entry::{JournalEntryHeader, JournalEntryLine};
570
571 #[test]
572 fn test_acdoca_factory_conversion() {
573 let header = JournalEntryHeader::new(
574 "1000".to_string(),
575 NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
576 );
577 let mut je = JournalEntry::new(header);
578
579 je.add_line(JournalEntryLine::debit(
580 je.header.document_id,
581 1,
582 "100000".to_string(),
583 Decimal::from(5000),
584 ));
585 je.add_line(JournalEntryLine::credit(
586 je.header.document_id,
587 2,
588 "200000".to_string(),
589 Decimal::from(5000),
590 ));
591
592 let factory = AcdocaFactory::new("0L", "SYNTH");
593 let acdoca_entries = factory.from_journal_entry(&je, "0000000001");
594
595 assert_eq!(acdoca_entries.len(), 2);
596 assert_eq!(acdoca_entries[0].racct, "100000");
597 assert_eq!(acdoca_entries[0].drcrk, "S");
598 assert_eq!(acdoca_entries[0].wsl, Decimal::from(5000));
599 assert_eq!(acdoca_entries[1].racct, "200000");
600 assert_eq!(acdoca_entries[1].drcrk, "H");
601 assert_eq!(acdoca_entries[1].wsl, Decimal::from(-5000));
602 }
603}