ica_csv.rs
use crate::gnucash::{GCTransaction, GCTransactionBuffer};
use serde::Deserialize;
use std::error::Error;
// By default, struct field names are deserialized based on the position of
// a corresponding field in the CSV data's header record.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct IcaTransaction {
datum: String,
text: String,
typ: String,
budgetgrupp: String,
belopp: String,
saldo: Option<String>,
}
pub fn parse_csv(args: Vec<String>) -> Result<(), Box<dyn Error>> {
let input_file = &args[1];
let output_file = &args[2];
// If we get an argument, this is the special case when creating
// CSV based on Utdrag PDF files.
// There the order is sequential, with first entry first
let utdrag_entry_order = !&args[3].is_empty();
// Read the CSV file
// assuming it has a header
#[cfg(not(feature = "pipe"))]
let mut rdr = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.trim(csv::Trim::All)
.from_path(input_file)?;
#[cfg(feature = "pipe")]
let mut rdr = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.trim(csv::Trim::All)
.from_reader(io::stdin());
#[cfg(not(feature = "pipe"))]
// Truncates any pre-existing file
let mut wtr = csv::Writer::from_path(output_file)?;
#[cfg(feature = "pipe")]
let mut wtr = csv::Writer::from_writer(io::stdout());
// Write headers manually.
wtr.write_record(GCTransaction::headers())?;
let mut last_seen_date = "".to_string();
let mut transaction_buffer = GCTransactionBuffer::new();
// The file is read from top to bottom, ICA exports
// the most recent transactions at the top
// thus the deserialization goes "backwards" in time
//
// This is of special importance when parsing transaction_number
// with multiple transactions during the same day
for result in rdr.deserialize() {
// Notice that we need to provide a type hint for automatic
// deserialization.
let record: IcaTransaction = result?;
// Match based on action which account
// Default match on Brukskonto
let account_name = match record.typ.as_str() {
"E-faktura" => "Tillgångar:Likvida Medel:Brukskonto",
"Pg-bg" => "Tillgångar:Likvida Medel:Brukskonto",
"Autogiro" => "Tillgångar:Likvida Medel:Brukskonto",
"Korttransaktion" => "Tillgångar:Likvida Medel:Brukskonto",
"Uttag" => "Tillgångar:Likvida Medel:Brukskonto",
"Utlandsbetalning" => "Tillgångar:Likvida Medel:Brukskonto",
"Försäkring" => "Tillgångar:Likvida Medel:Brukskonto",
"Fritid" => "Tillgångar:Likvida Medel:Brukskonto",
"Övrigt" => "Tillgångar:Likvida Medel:Brukskonto",
"Reserverat Belopp" => "Tillgångar:Likvida Medel:Brukskonto",
"Insättning" => "Tillgångar:Likvida Medel:Brukskonto",
_ => "Tillgångar:Likvida Medel:Brukskonto",
};
let value_num = record
.belopp
.replace(" kr", "")
.replace(',', ".")
.chars()
.filter(|c| c.is_ascii())
.filter(|c| !c.is_whitespace())
.collect::<String>()
.parse::<f32>()
.unwrap();
let amount_balance = record.saldo.map(|saldo| {
saldo
.replace(" kr", "")
.replace(',', ".")
.chars()
.filter(|c| c.is_ascii())
.filter(|c| !c.is_whitespace())
.collect::<String>()
.parse::<f32>()
.unwrap()
});
// If this is the second time a date appears
if last_seen_date == record.datum {
// Store the record in write buffer
} else {
// A fresh new date, print all buffer contents
write_csv(&mut wtr, &mut transaction_buffer, utdrag_entry_order)?;
// Set the last seen date
last_seen_date = record.datum.clone();
}
transaction_buffer.push_back(GCTransaction {
// Date
date: record.datum,
// Transaction ID, let GnuCash generate
transaction_id: "".into(),
// Number, 1 by default,
// if multiple in a day this gets overwritten
number: 1.to_string(),
// Description
description: Some(record.text.to_owned()),
// Notes
// Extra, the account balance stored in a note
notes: amount_balance.map(|value| value.to_string().replace('.', ",")),
// Currency
commodity_currency: Some("CURRENCY::SEK".into()),
// Void Reason
void_reason: None,
// Action
action: Some(record.typ),
// Memo
memo: Some(record.budgetgrupp),
// Full Account Name,
full_account_name: Some(account_name.to_owned()),
// Account Name,
account_name: None,
// Amount With Sym.,
amount_with_sym: None,
// Amount Num,
amount_num: Some(value_num),
// Value With Sym.,
value_with_sym: None,
// Value Num,
value_num: Some(value_num),
// Reconcile,
reconcile: Some("n".to_owned()),
// Reconcile Date,
reconcile_date: None,
// Rate/Price,
rate_price: Some("1".to_string()),
});
}
// Make sure any unprinted lines still in the buffer gets printed
write_csv(&mut wtr, &mut transaction_buffer, utdrag_entry_order)?;
Ok(())
}
fn write_csv(
wtr: &mut csv::Writer<std::fs::File>,
buf: &mut GCTransactionBuffer,
utdrag_entry_order: bool,
) -> Result<(), Box<dyn Error>> {
// Iterate in reverse order to assign correct transaction_number
if utdrag_entry_order {
for (counter, row) in buf.iter_mut().enumerate() {
row.set_number((counter + 1).to_string());
}
// Write CSV in original order, most recent first
while let Some(row) = buf.pop_front() {
wtr.write_record(row.output())?;
}
} else {
for (counter, row) in buf.iter_mut().rev().enumerate() {
row.set_number((counter + 1).to_string());
}
// Write CSV in original order, most recent first
while let Some(row) = buf.pop_front() {
wtr.write_record(row.output())?;
}
}
Ok(())
}