1
0
Fork 1
ledgerneo/src/spreadsheet.rs

672 lines
23 KiB
Rust

use crate::{AssetAccount, Books, ExternalAccount};
use chrono::{NaiveDate, NaiveTime};
use icu_locid::locale;
use rust_decimal::prelude::{ToPrimitive, Zero};
use rust_decimal::Decimal;
use spreadsheet_ods::style::units::TextAlign;
use spreadsheet_ods::Value::Currency;
use spreadsheet_ods::{format, CellStyle, Length, Sheet, Value, WorkBook};
use std::cmp::Ordering;
use std::str::FromStr;
pub fn export_to_spreadsheet(name: &str, books: &Books, opening_date: &String, close_date: &String) {
let mut wb = WorkBook::new(locale!("en_CA"));
let mut header_style = CellStyle::new_empty();
header_style.set_font_bold();
header_style.set_text_align(TextAlign::Center);
let header_style_ref = wb.add_cellstyle(header_style);
let currency_format = format::create_currency_prefix("cad", locale!("en_CA"), format!("$"));
let currency_style = wb.add_currency_format(currency_format);
let mut general_text_style = CellStyle::new_empty();
general_text_style.set_text_align(TextAlign::Right);
let general_text_style = wb.add_cellstyle(general_text_style);
let mut currency_pretty_print = CellStyle::new_empty();
currency_pretty_print.set_text_align(TextAlign::Right);
currency_pretty_print.set_decimal_places(2);
currency_pretty_print.set_value_format(&currency_style);
let currency_pretty_print_style = wb.add_cellstyle(currency_pretty_print);
// Create a Balance Statement
let mut balance_statement = Sheet::new(format!("BalanceSheet"));
balance_statement.set_styled_value(
0,
0,
Value::Text(format!("Balance Sheet")),
&header_style_ref,
);
balance_statement.set_col_width(0, Length::Pc(25.0));
balance_statement.set_col_width(1, Length::Pc(25.0));
balance_statement.set_col_width(2, Length::Pc(25.0));
balance_statement.set_col_width(3, Length::Pc(25.0));
balance_statement.set_col_width(4, Length::Pc(25.0));
// We first need a list of all of our assets and liabilities
let mut asset_accounts: Vec<(String, AssetAccount)> = books
.asset_accounts
.iter()
.map(|(a, b)| (a.clone(), b.clone()))
.collect();
asset_accounts.sort_by(|a, b| {
if a.0.starts_with("Asset") && b.0.starts_with("Liability") {
return Ordering::Less;
}
if a.0.starts_with("Liability") && b.0.starts_with("Asset") {
return Ordering::Greater;
} else {
return b
.1
.total_nominal(&books.commodities_oracle, close_date)
.total_cmp(&a.1.total_nominal(&books.commodities_oracle, close_date));
}
});
// Assets Section
balance_statement.set_styled_value(2, 0, Value::Text(format!("Assets")), &header_style_ref);
balance_statement.set_styled_value(
2,
2,
Value::Text(format!("Closing Value")),
&header_style_ref,
);
balance_statement.set_styled_value(
2,
3,
Value::Text(format!("Opening Value")),
&header_style_ref,
);
let mut row_index = 3;
let start = row_index;
for (account_name, account) in asset_accounts.iter() {
if account_name.starts_with("Asset") {
let total_nominal = account.total_nominal(&books.commodities_oracle, close_date);
let total_opening = account.opening_nominal(&books.commodities_oracle, opening_date);
balance_statement.set_styled_value(
row_index,
1,
Value::Text(account_name.clone().replace("Assets:", "")),
&general_text_style,
);
balance_statement.set_styled_value(
row_index,
2,
Value::Currency(total_nominal, format!("CAD")),
&currency_pretty_print_style,
);
balance_statement.set_styled_value(
row_index,
3,
Value::Currency(total_opening, format!("CAD")),
&currency_pretty_print_style,
);
row_index += 1;
}
}
balance_statement.set_styled_value(row_index, 0, format!("Total"), &header_style_ref);
balance_statement.set_formula(row_index, 2, format!("SUM(C{}:C{})", start + 1, row_index));
balance_statement.set_cellstyle(row_index, 2, &currency_pretty_print_style);
balance_statement.set_formula(row_index, 3, format!("SUM(D{}:D{})", start + 1, row_index));
balance_statement.set_cellstyle(row_index, 3, &currency_pretty_print_style);
let mut assets_total_row = row_index;
row_index += 2;
balance_statement.set_styled_value(
row_index,
0,
Value::Text(format!("Liabilities")),
&header_style_ref,
);
let start = row_index;
for (account_name, account) in asset_accounts.iter() {
if account_name.starts_with("Liability") {
let total_nominal = account.total_nominal(&books.commodities_oracle, close_date);
let total_opening = account.opening_nominal(&books.commodities_oracle, opening_date);
if total_opening.is_zero() && total_nominal.is_zero() {
continue;
}
balance_statement.set_styled_value(
row_index,
1,
Value::Text(account_name.clone().replace("Liability:", "")),
&general_text_style,
);
balance_statement.set_styled_value(
row_index,
2,
Value::Currency(total_nominal, format!("CAD")),
&currency_pretty_print_style,
);
balance_statement.set_styled_value(
row_index,
3,
Value::Currency(total_opening, format!("CAD")),
&currency_pretty_print_style,
);
row_index += 1;
}
}
balance_statement.set_styled_value(row_index, 0, format!("Total"), &header_style_ref);
balance_statement.set_formula(row_index, 2, format!("SUM(C{}:C{})", start + 1, row_index));
balance_statement.set_cellstyle(row_index, 2, &currency_pretty_print_style);
balance_statement.set_formula(row_index, 3, format!("SUM(D{}:D{})", start + 1, row_index));
balance_statement.set_cellstyle(row_index, 3, &currency_pretty_print_style);
let mut liability_total_row = row_index;
row_index += 2;
balance_statement.set_styled_value(row_index, 0, format!("Net Assets"), &header_style_ref);
balance_statement.set_formula(
row_index,
2,
format!("C{} - C{}", assets_total_row + 1, liability_total_row + 1),
);
balance_statement.set_cellstyle(row_index, 2, &currency_pretty_print_style);
balance_statement.set_formula(
row_index,
3,
format!("D{} - D{}", assets_total_row + 1, liability_total_row + 1),
);
balance_statement.set_cellstyle(row_index, 3, &currency_pretty_print_style);
let balance_sheet_opening_assets_row = row_index;
let balance_sheet_total_net_assets_row = row_index;
// Create an Income Statement
let mut income_statement = Sheet::new(format!("IncomeStatement"));
income_statement.set_styled_value(
0,
0,
Value::Text(format!("Income Statement")),
&header_style_ref,
);
income_statement.set_col_width(0, Length::Pc(25.0));
income_statement.set_col_width(1, Length::Pc(25.0));
// We first need a list of all of our incomes and expenses, ordered by value...
let mut external_accounts: Vec<(String, ExternalAccount)> = books
.external_accounts
.iter()
.map(|(a, b)| (a.clone(), b.clone()))
.collect();
external_accounts.sort_by(|a, b| {
if a.0.starts_with("Income") && b.0.starts_with("Expense") {
return Ordering::Less;
}
if a.0.starts_with("Expense") && b.0.starts_with("Income") {
return Ordering::Greater;
} else {
return a
.1
.total_nominal(&books.commodities_oracle)
.total_cmp(&b.1.total_nominal(&books.commodities_oracle));
}
});
// Revenue Section
income_statement.set_styled_value(2, 0, Value::Text(format!("Revenue")), &header_style_ref);
let mut row_index = 3;
let start = row_index;
for (account_name, account) in external_accounts.iter() {
if account_name.starts_with("Income") {
income_statement.set_styled_value(
row_index,
0,
Value::Text(account_name.clone().replace("Income:", "")),
&general_text_style,
);
income_statement.set_styled_value(
row_index,
1,
Value::Currency(
account.total_nominal(&books.commodities_oracle),
format!("CAD"),
),
&currency_pretty_print_style,
);
row_index += 1;
}
}
income_statement.set_styled_value(row_index, 0, format!("Total"), &header_style_ref);
income_statement.set_formula(row_index, 1, format!("SUM(B{}:B{})", start + 1, row_index));
income_statement.set_cellstyle(row_index, 1, &currency_pretty_print_style);
let mut revenue_total_row = row_index;
row_index += 2;
// Now to Expenses...
income_statement.set_styled_value(
row_index,
0,
Value::Text(format!("Expenses")),
&general_text_style,
);
row_index += 1;
let start = row_index;
for (account_name, account) in external_accounts.iter() {
if account_name.starts_with("Expense") {
income_statement.set_styled_value(
row_index,
0,
Value::Text(account_name.clone().replace("Expenses:", "")),
&general_text_style,
);
income_statement.set_styled_value(
row_index,
1,
Value::Currency(
account.total_nominal(&books.commodities_oracle),
format!("CAD"),
),
&currency_pretty_print_style,
);
row_index += 1;
}
}
income_statement.set_styled_value(row_index, 0, format!("Total"), &header_style_ref);
income_statement.set_formula(row_index, 1, format!("SUM(B{}:B{})", start + 1, row_index));
income_statement.set_cellstyle(row_index, 1, &currency_pretty_print_style);
let mut expense_total_row = row_index;
row_index += 2;
income_statement.set_styled_value(row_index, 0, format!("Net Income"), &header_style_ref);
income_statement.set_formula(
row_index,
1,
format!("B{} - B{}", revenue_total_row + 1, expense_total_row + 1),
);
income_statement.set_cellstyle(row_index, 1, &currency_pretty_print_style);
let balance_sheet_summary_row = balance_sheet_total_net_assets_row + 2;
balance_statement.set_styled_value(
balance_sheet_summary_row,
0,
format!("Income statement balance for the year"),
&header_style_ref,
);
balance_statement.set_formula(
balance_sheet_summary_row,
1,
format!("IncomeStatement.B{}", row_index + 1),
);
balance_statement.set_styled_value(
balance_sheet_summary_row + 1,
0,
format!("Opening Assets + Income"),
&header_style_ref,
);
balance_statement.set_formula(
balance_sheet_summary_row + 1,
1,
format!(
"B{}+D{}",
balance_sheet_summary_row + 1,
balance_sheet_opening_assets_row + 1
),
);
let exchange_gains = books.calculate_gains(close_date);
balance_statement.set_styled_value(
balance_sheet_summary_row + 2,
0,
format!("Unrealized Gains/Losses (Assets)"),
&header_style_ref,
);
balance_statement.set_styled_value(
balance_sheet_summary_row + 2,
1,
Currency(exchange_gains, format!("CAD")),
&currency_pretty_print_style,
);
balance_statement.set_styled_value(
balance_sheet_summary_row + 3,
0,
format!("Opening Assets + Income + Unrealized Gains/Losses (Assets)"),
&header_style_ref,
);
balance_statement.set_formula(
balance_sheet_summary_row + 3,
1,
format!(
"B{}+B{}",
balance_sheet_summary_row + 2,
balance_sheet_summary_row + 3
),
);
wb.push_sheet(balance_statement);
wb.push_sheet(income_statement);
// Create a General Ledger
let mut general_ledger = Sheet::new(format!("GeneralLedger"));
general_ledger.set_styled_value(
0,
0,
Value::Text(format!("General Ledger")),
&header_style_ref,
);
let start_row = 1;
general_ledger.set_styled_value(
start_row,
0,
Value::Text(String::from("Ref")),
&header_style_ref,
);
general_ledger.set_col_width(0, Length::Pc(15.0));
general_ledger.set_styled_value(
start_row,
1,
Value::Text(String::from("Date")),
&header_style_ref,
);
general_ledger.set_col_width(1, Length::Pc(15.0));
general_ledger.set_styled_value(
start_row,
2,
Value::Text(String::from("Account")),
&header_style_ref,
);
general_ledger.set_col_width(2, Length::Pc(30.0));
general_ledger.set_styled_value(
start_row,
3,
Value::Text(String::from("Recorded CAD Value")),
&header_style_ref,
);
general_ledger.set_col_width(3, Length::Pc(15.0));
general_ledger.set_styled_value(
start_row,
4,
Value::Text(String::from("Recorded Commodity Value")),
&header_style_ref,
);
general_ledger.set_col_width(4, Length::Pc(15.0));
general_ledger.set_styled_value(
start_row,
5,
Value::Text(String::from("Commodity")),
&header_style_ref,
);
general_ledger.set_col_width(5, Length::Pc(15.0));
general_ledger.set_header_rows(start_row, 1);
let mut row_index = 2;
// Create a sheet for each tracked account
let mut running_total = Decimal::new(0, 0);
for (txid, date, postings) in books.general_ledger.iter() {
general_ledger.set_styled_value(row_index, 0, Value::Number(*txid as f64), &header_style_ref);
let recorded_date = NaiveDate::from_str(date).unwrap();
general_ledger.set_styled_value(
row_index,
1,
Value::DateTime(recorded_date.and_time(NaiveTime::default())),
&header_style_ref,
);
row_index += 1;
for (account, commodity, quantity, nominal_value) in postings.iter() {
running_total += nominal_value;
general_ledger.set_styled_value(
row_index,
2,
Value::Text(account.clone()),
&general_text_style,
);
general_ledger.set_styled_value(
row_index,
3,
Value::Currency(nominal_value.to_f64().unwrap(), String::from("CAD")),
&currency_pretty_print_style,
);
general_ledger.set_styled_value(
row_index,
4,
Value::Currency(quantity.to_f64().unwrap(), commodity.clone()),
&general_text_style,
);
general_ledger.set_styled_value(
row_index,
5,
Value::Text(commodity.clone().replace("$", "CAD")),
&general_text_style,
);
row_index += 1;
}
general_ledger.set_styled_value(
row_index,
3,
Value::Currency(running_total.to_f64().unwrap(), String::from("CAD")),
&currency_pretty_print_style,
);
row_index += 2;
}
wb.push_sheet(general_ledger);
// Create a sheet for each asset account
// We first need a list of all of our incomes and expenses, ordered by value...
let mut asset_accounts: Vec<(String, AssetAccount)> = books
.asset_accounts
.iter()
.map(|(a, b)| (a.clone(), b.clone()))
.collect();
asset_accounts.sort_by(|a, b| {
if a.0.starts_with("Assets") && b.0.starts_with("Liability") {
return Ordering::Less;
}
if a.0.starts_with("Liability") && b.0.starts_with("Assets") {
return Ordering::Greater;
} else {
return b
.1
.total_nominal(&books.commodities_oracle, close_date)
.total_cmp(&a.1.total_nominal(&books.commodities_oracle, close_date));
}
});
for (external_account, account) in asset_accounts.iter() {
let mut sheet = Sheet::new(format!(
"{}",
external_account.to_lowercase().replace(":", "")
));
sheet.set_styled_value(
0,
0,
Value::Text(format!("Account: {}", external_account)),
&header_style_ref,
);
let start_row = 1;
sheet.set_styled_value(
start_row,
0,
Value::Text(String::from("Ref")),
&header_style_ref,
);
sheet.set_col_width(0, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
1,
Value::Text(String::from("Date")),
&header_style_ref,
);
sheet.set_col_width(1, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
2,
Value::Text(String::from("Recorded CAD Value")),
&header_style_ref,
);
sheet.set_col_width(2, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
3,
Value::Text(String::from("Recorded Commodity Value")),
&header_style_ref,
);
sheet.set_col_width(3, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
4,
Value::Text(String::from("Commodity")),
&header_style_ref,
);
sheet.set_col_width(4, Length::Pc(15.0));
sheet.set_header_rows(start_row, 1);
let start_row = 2;
for (row, (txref, date, commodity, cost, quantity, booked_account)) in account.transactions.iter().enumerate() {
let index = start_row + (row as u32);
let recorded_date = NaiveDate::from_str(date).unwrap();
let nominal_value = if commodity != "CAD" && commodity != "$" && commodity != "StickerSheets" {
books.commodities_oracle.lookup(commodity, date) * quantity
} else {
quantity * Decimal::new(1, 0)
};
sheet.set_styled_value(index, 0, Value::Number(*txref as f64), &header_style_ref);
sheet.set_styled_value(
index,
1,
Value::DateTime(recorded_date.and_time(NaiveTime::default())),
&general_text_style,
);
sheet.set_styled_value(
index,
2,
Value::Currency(nominal_value.to_f64().unwrap(), String::from("CAD")),
&currency_pretty_print_style,
);
sheet.set_styled_value(
index,
3,
Value::Currency(quantity.to_f64().unwrap(), commodity.clone()),
&general_text_style,
);
sheet.set_styled_value(
index,
4,
Value::Text(commodity.clone().replace("$", "CAD")),
&general_text_style,
);
sheet.set_styled_value(
index,
5,
Value::Text(booked_account.clone()),
&general_text_style,
);
}
wb.push_sheet(sheet);
}
// Create a sheet for each tracked account
for (external_account, account) in external_accounts.iter() {
let mut sheet = Sheet::new(format!(
"{}",
external_account.to_lowercase().replace(":", "")
));
sheet.set_styled_value(
0,
0,
Value::Text(format!("Account: {}", external_account)),
&header_style_ref,
);
let mut start_row = 1;
sheet.set_styled_value(
start_row,
0,
Value::Text(String::from("Ref")),
&header_style_ref,
);
sheet.set_col_width(0, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
1,
Value::Text(String::from("Date")),
&header_style_ref,
);
sheet.set_col_width(1, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
2,
Value::Text(String::from("Recorded CAD Value")),
&header_style_ref,
);
sheet.set_col_width(2, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
3,
Value::Text(String::from("Recorded Commodity Value")),
&header_style_ref,
);
sheet.set_col_width(3, Length::Pc(15.0));
sheet.set_styled_value(
start_row,
4,
Value::Text(String::from("Commodity")),
&header_style_ref,
);
sheet.set_col_width(4, Length::Pc(15.0));
sheet.set_header_rows(start_row, 1);
start_row += 1;
for (row, (txref, date, commodity, quantity)) in account.transactions.iter().enumerate() {
let index = start_row + (row as u32);
let recorded_date = NaiveDate::from_str(date).unwrap();
let nominal_value = if commodity != "CAD" && commodity != "$" && commodity != "StickerSheets" {
books.commodities_oracle.lookup(commodity, date) * quantity
} else {
Decimal::new(1, 0) * quantity
};
sheet.set_styled_value(index, 0, Value::Number(*txref as f64), &header_style_ref);
sheet.set_styled_value(
index,
1,
Value::DateTime(recorded_date.and_time(NaiveTime::default())),
&general_text_style,
);
sheet.set_styled_value(
index,
2,
Value::Currency(nominal_value.to_f64().unwrap(), String::from("CAD")),
&currency_pretty_print_style,
);
sheet.set_styled_value(
index,
3,
Value::Currency(quantity.to_f64().unwrap(), commodity.clone()),
&general_text_style,
);
sheet.set_styled_value(
index,
4,
Value::Text(commodity.clone().replace("$", "CAD")),
&general_text_style,
);
}
wb.push_sheet(sheet);
}
spreadsheet_ods::write_ods(&mut wb, name);
}