use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use chrono::NaiveDate;
use datasynth_core::error::SynthResult;
use datasynth_core::models::subledger::ap::APInvoice;
use datasynth_core::models::subledger::ar::ARInvoice;
use datasynth_core::models::subledger::SubledgerDocumentStatus;
use datasynth_core::models::JournalEntry;
use rust_decimal::Decimal;
use super::sap::SapExportConfig;
fn open_file(cfg: &SapExportConfig, path: &Path) -> SynthResult<BufWriter<File>> {
let file = File::create(path)?;
let mut writer = BufWriter::with_capacity(256 * 1024, file);
let bom = cfg.dialect.bom();
if !bom.is_empty() {
writer.write_all(bom)?;
}
Ok(writer)
}
fn write_row<W: Write>(writer: &mut W, delim: char, fields: &[String]) -> std::io::Result<()> {
for (i, f) in fields.iter().enumerate() {
if i > 0 {
write!(writer, "{delim}")?;
}
write!(writer, "{f}")?;
}
writeln!(writer)
}
fn write_header<W: Write>(writer: &mut W, delim: char, cols: &[&str]) -> std::io::Result<()> {
for (i, c) in cols.iter().enumerate() {
if i > 0 {
write!(writer, "{delim}")?;
}
write!(writer, "{c}")?;
}
writeln!(writer)
}
#[derive(Debug, Clone)]
pub struct SapOpenItemRow {
pub mandt: String,
pub bukrs: String,
pub hkont: String,
pub partner: Option<String>,
pub belnr: String,
pub buzei: String,
pub gjahr: u16,
pub budat: NaiveDate,
pub bldat: NaiveDate,
pub waers: String,
pub shkzg: String,
pub wrbtr: Decimal,
pub dmbtr: Decimal,
pub zfbdt: Option<NaiveDate>, pub zterm: Option<String>,
pub netdt: Option<NaiveDate>,
}
#[derive(Debug, Clone)]
pub struct SapClearedItemRow {
pub mandt: String,
pub bukrs: String,
pub hkont: String,
pub partner: Option<String>,
pub belnr: String,
pub buzei: String,
pub gjahr: u16,
pub budat: NaiveDate,
pub bldat: NaiveDate,
pub waers: String,
pub shkzg: String,
pub wrbtr: Decimal,
pub dmbtr: Decimal,
pub augbl: String,
pub augdt: NaiveDate,
pub wrshb: Decimal,
}
fn write_open_rows(
cfg: &SapExportConfig,
rows: &[SapOpenItemRow],
include_partner: bool,
partner_col: &str,
path: &Path,
) -> SynthResult<()> {
let mut w = open_file(cfg, path)?;
let d = cfg.delimiter();
let mut cols: Vec<&str> = vec!["MANDT", "BUKRS", "HKONT"];
if include_partner {
cols.push(partner_col);
}
cols.extend_from_slice(&[
"BELNR", "BUZEI", "GJAHR", "BUDAT", "BLDAT", "WAERS", "SHKZG", "WRBTR", "DMBTR", "ZFBDT",
"ZTERM", "NETDT",
]);
write_header(&mut w, d, &cols)?;
for r in rows {
let mut fields: Vec<String> = vec![r.mandt.clone(), r.bukrs.clone(), r.hkont.clone()];
if include_partner {
fields.push(r.partner.clone().unwrap_or_default());
}
fields.push(r.belnr.clone());
fields.push(r.buzei.clone());
fields.push(r.gjahr.to_string());
fields.push(cfg.format_date(r.budat));
fields.push(cfg.format_date(r.bldat));
fields.push(r.waers.clone());
fields.push(r.shkzg.clone());
fields.push(cfg.format_decimal(&r.wrbtr));
fields.push(cfg.format_decimal(&r.dmbtr));
fields.push(r.zfbdt.map(|d| cfg.format_date(d)).unwrap_or_default());
fields.push(r.zterm.clone().unwrap_or_default());
fields.push(r.netdt.map(|d| cfg.format_date(d)).unwrap_or_default());
write_row(&mut w, d, &fields)?;
}
w.flush()?;
Ok(())
}
fn write_cleared_rows(
cfg: &SapExportConfig,
rows: &[SapClearedItemRow],
include_partner: bool,
partner_col: &str,
path: &Path,
) -> SynthResult<()> {
let mut w = open_file(cfg, path)?;
let d = cfg.delimiter();
let mut cols: Vec<&str> = vec!["MANDT", "BUKRS", "HKONT"];
if include_partner {
cols.push(partner_col);
}
cols.extend_from_slice(&[
"BELNR", "BUZEI", "GJAHR", "BUDAT", "BLDAT", "WAERS", "SHKZG", "WRBTR", "DMBTR", "AUGBL",
"AUGDT", "WRSHB",
]);
write_header(&mut w, d, &cols)?;
for r in rows {
let mut fields: Vec<String> = vec![r.mandt.clone(), r.bukrs.clone(), r.hkont.clone()];
if include_partner {
fields.push(r.partner.clone().unwrap_or_default());
}
fields.push(r.belnr.clone());
fields.push(r.buzei.clone());
fields.push(r.gjahr.to_string());
fields.push(cfg.format_date(r.budat));
fields.push(cfg.format_date(r.bldat));
fields.push(r.waers.clone());
fields.push(r.shkzg.clone());
fields.push(cfg.format_decimal(&r.wrbtr));
fields.push(cfg.format_decimal(&r.dmbtr));
fields.push(r.augbl.clone());
fields.push(cfg.format_date(r.augdt));
fields.push(cfg.format_decimal(&r.wrshb));
write_row(&mut w, d, &fields)?;
}
w.flush()?;
Ok(())
}
fn bsis_rows_from_je(client: &str, entries: &[JournalEntry]) -> Vec<SapOpenItemRow> {
let mut rows = Vec::new();
for je in entries {
let header = &je.header;
for line in &je.lines {
let (shkzg, wrbtr) = if line.debit_amount > Decimal::ZERO {
("S".to_string(), line.debit_amount)
} else {
("H".to_string(), line.credit_amount)
};
rows.push(SapOpenItemRow {
mandt: client.to_string(),
bukrs: header.company_code.clone(),
hkont: line.account_code.clone(),
partner: None,
belnr: header.document_id.to_string(),
buzei: format!("{:03}", line.line_number),
gjahr: header.fiscal_year,
budat: header.posting_date,
bldat: header.document_date,
waers: header.currency.clone(),
shkzg,
wrbtr,
dmbtr: wrbtr,
zfbdt: Some(header.document_date),
zterm: None,
netdt: None,
});
}
}
rows
}
pub fn write_bsis(cfg: &SapExportConfig, entries: &[JournalEntry], path: &Path) -> SynthResult<()> {
let rows = bsis_rows_from_je(&cfg.client, entries);
write_open_rows(cfg, &rows, false, "", path)
}
pub fn write_bsas(cfg: &SapExportConfig, path: &Path) -> SynthResult<()> {
let rows: Vec<SapClearedItemRow> = Vec::new();
write_cleared_rows(cfg, &rows, false, "", path)
}
fn ar_reconciliation_account(inv: &ARInvoice) -> String {
inv.gl_reference
.as_ref()
.map(|gl| gl.gl_account.clone())
.unwrap_or_else(|| "1100".to_string())
}
fn bsid_bsad_rows(
client: &str,
invoices: &[ARInvoice],
) -> (Vec<SapOpenItemRow>, Vec<SapClearedItemRow>) {
let mut open_rows = Vec::new();
let mut cleared_rows = Vec::new();
for inv in invoices {
let hkont = ar_reconciliation_account(inv);
let gjahr = inv
.posting_date
.format("%Y")
.to_string()
.parse::<u16>()
.unwrap_or(2024);
let waers = inv.gross_amount.document_currency.clone();
if matches!(
inv.status,
SubledgerDocumentStatus::Open | SubledgerDocumentStatus::PartiallyCleared
) && inv.amount_remaining > Decimal::ZERO
{
open_rows.push(SapOpenItemRow {
mandt: client.to_string(),
bukrs: inv.company_code.clone(),
hkont: hkont.clone(),
partner: Some(inv.customer_id.clone()),
belnr: inv.invoice_number.clone(),
buzei: "001".to_string(),
gjahr,
budat: inv.posting_date,
bldat: inv.invoice_date,
waers: waers.clone(),
shkzg: "S".to_string(),
wrbtr: inv.amount_remaining,
dmbtr: inv.amount_remaining,
zfbdt: Some(inv.baseline_date),
zterm: Some(format!("{:?}", inv.payment_terms)),
netdt: Some(inv.due_date),
});
}
for (idx, clr) in inv.clearing_info.iter().enumerate() {
cleared_rows.push(SapClearedItemRow {
mandt: client.to_string(),
bukrs: inv.company_code.clone(),
hkont: hkont.clone(),
partner: Some(inv.customer_id.clone()),
belnr: inv.invoice_number.clone(),
buzei: format!("{:03}", idx + 1),
gjahr,
budat: inv.posting_date,
bldat: inv.invoice_date,
waers: waers.clone(),
shkzg: "S".to_string(),
wrbtr: clr.clearing_amount,
dmbtr: clr.clearing_amount,
augbl: clr.clearing_document.clone(),
augdt: clr.clearing_date,
wrshb: clr.clearing_amount,
});
}
}
(open_rows, cleared_rows)
}
pub fn write_bsid(cfg: &SapExportConfig, invoices: &[ARInvoice], path: &Path) -> SynthResult<()> {
let (open_rows, _) = bsid_bsad_rows(&cfg.client, invoices);
write_open_rows(cfg, &open_rows, true, "KUNNR", path)
}
pub fn write_bsad(cfg: &SapExportConfig, invoices: &[ARInvoice], path: &Path) -> SynthResult<()> {
let (_, cleared_rows) = bsid_bsad_rows(&cfg.client, invoices);
write_cleared_rows(cfg, &cleared_rows, true, "KUNNR", path)
}
fn ap_reconciliation_account(inv: &APInvoice) -> String {
inv.gl_reference
.as_ref()
.map(|gl| gl.gl_account.clone())
.unwrap_or_else(|| "2000".to_string())
}
fn bsik_bsak_rows(
client: &str,
invoices: &[APInvoice],
) -> (Vec<SapOpenItemRow>, Vec<SapClearedItemRow>) {
let mut open_rows = Vec::new();
let mut cleared_rows = Vec::new();
for inv in invoices {
let hkont = ap_reconciliation_account(inv);
let gjahr = inv
.posting_date
.format("%Y")
.to_string()
.parse::<u16>()
.unwrap_or(2024);
let waers = inv.gross_amount.document_currency.clone();
if matches!(
inv.status,
SubledgerDocumentStatus::Open | SubledgerDocumentStatus::PartiallyCleared
) && inv.amount_remaining > Decimal::ZERO
{
open_rows.push(SapOpenItemRow {
mandt: client.to_string(),
bukrs: inv.company_code.clone(),
hkont: hkont.clone(),
partner: Some(inv.vendor_id.clone()),
belnr: inv.invoice_number.clone(),
buzei: "001".to_string(),
gjahr,
budat: inv.posting_date,
bldat: inv.invoice_date,
waers: waers.clone(),
shkzg: "H".to_string(),
wrbtr: inv.amount_remaining,
dmbtr: inv.amount_remaining,
zfbdt: Some(inv.baseline_date),
zterm: Some(format!("{:?}", inv.payment_terms)),
netdt: Some(inv.due_date),
});
}
for (idx, clr) in inv.clearing_info.iter().enumerate() {
cleared_rows.push(SapClearedItemRow {
mandt: client.to_string(),
bukrs: inv.company_code.clone(),
hkont: hkont.clone(),
partner: Some(inv.vendor_id.clone()),
belnr: inv.invoice_number.clone(),
buzei: format!("{:03}", idx + 1),
gjahr,
budat: inv.posting_date,
bldat: inv.invoice_date,
waers: waers.clone(),
shkzg: "H".to_string(),
wrbtr: clr.clearing_amount,
dmbtr: clr.clearing_amount,
augbl: clr.clearing_document.clone(),
augdt: clr.clearing_date,
wrshb: clr.clearing_amount,
});
}
}
(open_rows, cleared_rows)
}
pub fn write_bsik(cfg: &SapExportConfig, invoices: &[APInvoice], path: &Path) -> SynthResult<()> {
let (open_rows, _) = bsik_bsak_rows(&cfg.client, invoices);
write_open_rows(cfg, &open_rows, true, "LIFNR", path)
}
pub fn write_bsak(cfg: &SapExportConfig, invoices: &[APInvoice], path: &Path) -> SynthResult<()> {
let (_, cleared_rows) = bsik_bsak_rows(&cfg.client, invoices);
write_cleared_rows(cfg, &cleared_rows, true, "LIFNR", path)
}
#[cfg(test)]
mod tests {
use super::super::sap::SapDialect;
use super::*;
#[test]
fn shared_helpers_compile() {
let cfg = SapExportConfig {
dialect: SapDialect::Hana,
..SapExportConfig::default()
};
assert_eq!(cfg.dialect.delimiter(), ';');
}
}