1
0
Fork 0

Initial Commit

This commit is contained in:
Sarah Jamie Lewis 2023-04-13 20:08:45 -07:00
commit dcb0212105
10 changed files with 2133 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1193
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "ledgerneo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ledger-parser = "5.1.1"
rust_decimal = "1.29.0"
spreadsheet-ods = "0.15.0"
chrono = "0.4.24"
icu_locid = "1.1.0"

77
rustfmt.toml Normal file
View File

@ -0,0 +1,77 @@
max_width = 200
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
indent_style = "Block"
use_small_heuristics = "Default"
fn_call_width = 60
attr_fn_like_width = 70
struct_lit_width = 18
struct_variant_width = 35
array_width = 60
chain_width = 60
single_line_if_else_max_width = 50
wrap_comments = false
format_code_in_doc_comments = false
doc_comment_code_block_width = 100
comment_width = 80
normalize_comments = false
normalize_doc_attributes = false
format_strings = false
format_macro_matchers = false
format_macro_bodies = true
skip_macro_invocations = []
hex_literal_case = "Preserve"
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
where_single_line = false
imports_indent = "Block"
imports_layout = "Mixed"
imports_granularity = "Preserve"
group_imports = "Preserve"
reorder_imports = true
reorder_modules = true
reorder_impl_items = false
type_punctuation_density = "Wide"
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
binop_separator = "Front"
remove_nested_parens = true
combine_control_expr = true
short_array_element_width_threshold = 10
overflow_delimited_expr = false
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
match_arm_blocks = true
match_arm_leading_pipes = "Never"
force_multiline_blocks = false
fn_params_layout = "Tall"
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"
trailing_semicolon = true
trailing_comma = "Vertical"
match_block_trailing_comma = false
blank_lines_upper_bound = 1
blank_lines_lower_bound = 0
edition = "2015"
version = "One"
inline_attribute_width = 0
format_generated_files = true
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
condense_wildcard_suffixes = false
color = "Auto"
required_version = "1.5.2"
unstable_features = false
disable_all_formatting = false
skip_children = false
hide_parse_errors = false
error_on_line_overflow = false
error_on_unformatted = false
ignore = []
emit_mode = "Files"
make_backup = false

189
src/asset_account.rs Normal file
View File

@ -0,0 +1,189 @@
use std::borrow::Borrow;
use crate::CommoditiesPriceOracle;
use rust_decimal::Decimal;
use std::collections::HashMap;
use chrono::Month::December;
use rust_decimal::prelude::ToPrimitive;
#[derive(Clone)]
pub struct AssetCommodity {
cost_basis: Decimal,
quantity: Decimal,
name: String,
}
impl AssetCommodity {
pub fn cost_basis_total(&self) -> Decimal {
self.cost_basis * self.quantity
}
}
#[derive(Clone)]
pub struct AssetAccount {
name: String,
commodities: HashMap<String, Vec<AssetCommodity>>,
running_total: HashMap<String, Decimal>,
opening: HashMap<String, Decimal>,
pub(crate) realized_gains: Decimal,
}
impl AssetAccount {
pub fn new(name: String) -> AssetAccount {
AssetAccount {
name,
commodities: HashMap::new(),
realized_gains: Decimal::new(0, 0),
running_total: HashMap::new(),
opening: HashMap::new(),
}
}
pub fn unrealized_gains(&self, oracle: &CommoditiesPriceOracle, date: &String) -> Vec<(String, Decimal)> {
let mut totals = vec![];
for (name, commodities) in self.commodities.iter() {
if name != "$" && name != "StickerSheets" {
let mut unrealized_gain = Decimal::new(0, 0);
let historical_cost = commodities.iter().fold(Decimal::new(0,0), |accum, x| {
accum + (x.cost_basis * x.quantity)
});
let current_cost = commodities.iter().fold(Decimal::new(0,0), |accum, x| {
accum + (oracle.lookup(&x.name, date) * x.quantity)
});
unrealized_gain += current_cost - historical_cost;
totals.push((name.clone(), unrealized_gain))
}
}
totals
}
pub fn total_nominal(&self, oracle: &CommoditiesPriceOracle, date: &String) -> f64 {
let mut total = 0.0;
for (commodity_name, quantity) in self.running_total.iter() {
if commodity_name!= "$" && commodity_name != "StickerSheets" {
total += (oracle.lookup(commodity_name, date) * quantity.abs()).to_f64().unwrap();
} else if commodity_name == "$" {
total += quantity.abs().to_f64().unwrap()
}
}
total
}
pub fn opening_nominal(&self, oracle: &CommoditiesPriceOracle, date: &String) -> f64 {
let mut total = 0.0;
for (commodity_name, quantity) in self.opening.iter() {
if commodity_name!= "$" && commodity_name != "StickerSheets" {
total += (oracle.lookup(commodity_name, date) * quantity.abs()).to_f64().unwrap();
} else if commodity_name == "$" {
total += quantity.abs().to_f64().unwrap()
}
}
total
}
pub fn withdraw(&mut self, name: String, cost: Decimal, quantity: Decimal, date: &String) {
let cost_basis = cost;
let asset_commodity = AssetCommodity {
cost_basis,
quantity,
name: name.clone(),
};
match self.commodities.contains_key(&name) {
false => {
self.commodities.insert(name.clone(), vec![]);
self.running_total.insert(name.clone(), Decimal::new(0,0));
if date == "2022-02-11" {
println!("Opening Total: {} {} {}", quantity, name, cost);
self.opening.insert(name.clone(), quantity);
}
}
_ => {
}
}
*self.running_total.get_mut(&name).unwrap() += quantity;
println!("Running Total: {} {} {} {}", quantity, name, cost, *self.running_total.get_mut(&name).unwrap() );
if self.name.starts_with("Assets") == false {
// we don't track cost-basis for liabilities
return;
}
match self.commodities.get_mut(&name) {
None => {
panic!();
}
Some(current_commodities) => {
current_commodities.sort_by(|a,b| {
a.cost_basis.cmp(&b.cost_basis)
});
current_commodities.reverse();
let mut quantity_needed = quantity.abs();
let mut base_cost = Decimal::new(0,0);
if name != "$" {
for commodity in current_commodities.iter_mut() {
if commodity.quantity <= quantity_needed {
// use all of this commodity....
println!("Taking {} {} @@ {}", commodity.quantity, commodity.name, commodity.cost_basis);
base_cost += (commodity.quantity * commodity.cost_basis);
quantity_needed -= commodity.quantity;
commodity.quantity = Decimal::new(0, 0);
} else {
// only use what we need from this...and then we are done...
println!("Taking {} {} @@ {}", quantity_needed, commodity.name, commodity.cost_basis);
base_cost += (quantity_needed * commodity.cost_basis);
commodity.quantity -= quantity_needed;
break;
}
if quantity_needed.is_zero() {
break;
}
}
if (base_cost - (cost * quantity.abs())).is_zero() == false {
println!("Realized Gains: {} {} {} {} {} {}", self.name, (cost * quantity.abs()) - base_cost, base_cost, cost * quantity.abs(), date, cost);
}
self.realized_gains += (cost * quantity.abs()) - base_cost;
} else {
current_commodities.push(AssetCommodity{
cost_basis: Decimal::new(1,0),
quantity: quantity,
name
})
}
}
}
}
pub fn deposit(&mut self, name: String, cost: Decimal, quantity: Decimal, date: &String) {
let cost_basis = cost;
let asset_commodity = AssetCommodity {
cost_basis,
quantity,
name: name.clone(),
};
match self.commodities.get_mut(&name) {
None => {
self.commodities.insert(name.clone(), vec![asset_commodity]);
self.running_total.insert(name.clone(), Decimal::new(0,0));
if date == "2022-02-11" {
println!("Opening: {} {} {}", quantity, name, cost);
self.opening.insert(name.clone(), quantity);
}
}
Some(account) => account.push(asset_commodity),
}
*self.running_total.get_mut(&name).unwrap() += quantity;
println!("Running Total: {} {} {} {}", quantity, name, cost, *self.running_total.get_mut(&name).unwrap() );
}
}

249
src/books.rs Normal file
View File

@ -0,0 +1,249 @@
use crate::{AssetAccount, CommoditiesPriceOracle, ExternalAccount};
use ledger_parser::{LedgerItem, PostingAmount};
use rust_decimal::Decimal;
use std::collections::{HashMap, HashSet};
use std::fs::read_to_string;
use std::str::FromStr;
use chrono::NaiveDate;
use rust_decimal::prelude::ToPrimitive;
pub type TransactionReference = usize;
pub struct Books {
pub(crate) asset_accounts: HashMap<String, AssetAccount>,
pub(crate) external_accounts: HashMap<String, ExternalAccount>,
pub(crate) commodities_oracle: CommoditiesPriceOracle,
pub general_ledger: Vec<(TransactionReference, String, Vec<(String, String, Decimal)>)>,
}
impl Books {
pub fn new() -> Books {
Books {
asset_accounts: HashMap::new(),
external_accounts: HashMap::new(),
commodities_oracle: CommoditiesPriceOracle::new(),
general_ledger: vec![],
}
}
pub fn load_ledger(&mut self, ledger_file: &str, to_date: &String) {
let ast = ledger_parser::parse(&read_to_string(ledger_file).unwrap()).unwrap();
for (id, li) in ast.items.iter().enumerate() {
match li {
LedgerItem::EmptyLine => {}
LedgerItem::LineComment(_) => {}
LedgerItem::Transaction(tx) => {
let mut final_line = None;
let mut total = Decimal::new(0, 0);
let mut commodities = HashSet::new();
let mut general_ledger_entry = vec![];
if tx.date > NaiveDate::from_str(&to_date).unwrap() {
break;
}
if tx.postings.len() == 2 {
match (&tx.postings[0].amount, &tx.postings[1].amount) {
(Some(a), Some(b)) => {
if a.amount.commodity != b.amount.commodity {
let a_account = tx.postings[0].account.clone();
let b_account = tx.postings[1].account.clone();
if a.amount.quantity.is_sign_negative() {
// Trading a for b
let cost_basis = b.amount.quantity / a.amount.quantity.abs() ;
self.asset_accounts.get_mut(&a_account).unwrap().withdraw(a.amount.commodity.name.clone(), cost_basis, a.amount.quantity, &tx.date.to_string());
self.asset_accounts.get_mut(&b_account).unwrap().deposit(b.amount.commodity.name.clone(), Decimal::new(1,0), b.amount.quantity, &tx.date.to_string());
} else {
panic!("Trade order should have selling first {} {}",a,b);
}
general_ledger_entry.push((a_account, a.amount.commodity.name.clone(), a.amount.quantity));
general_ledger_entry.push((b_account, b.amount.commodity.name.clone(), b.amount.quantity));
self.general_ledger.push((
id as TransactionReference,
tx.date.to_string(),
general_ledger_entry,
));
continue
}
}
_ => {
}
}
}
for posting in &tx.postings {
match &posting.amount {
None => {
final_line = Some(posting.clone());
}
Some(amount) => {
println!(
"{},{},{},{},{}",
id, tx.date, posting.account, amount.amount.commodity.name, amount.amount.quantity
);
total += amount.amount.quantity;
commodities.insert(amount.amount.commodity.name.clone());
general_ledger_entry.push((
posting.account.clone(),
amount.amount.commodity.name.clone(),
amount.amount.quantity.clone(),
));
self.update_account(
id as TransactionReference,
&posting.account,
&tx.date.to_string(),
&amount.amount.commodity.name.clone(),
amount.amount.quantity,
);
}
}
}
match final_line {
Some(posting) => {
if commodities.len() != 1 {
panic!("Trades should be handled separately")
}
println!("{},{},{},{}", id, tx.date, posting.account, total);
let commodity: Vec<&String> = commodities.iter().collect();
self.update_account(
id as TransactionReference,
&posting.account,
&tx.date.to_string(),
commodity[0],
-total,
);
general_ledger_entry.push((posting.account.clone(), commodity[0].clone(), -total));
}
_ => {}
}
self.general_ledger.push((
id as TransactionReference,
tx.date.to_string(),
general_ledger_entry,
))
}
LedgerItem::CommodityPrice(cprice) => self.commodities_oracle.insert(
cprice.commodity_name.clone(),
cprice.datetime.date().to_string(),
cprice.amount.quantity,
),
LedgerItem::Include(_) => {}
_ => {}
}
}
}
pub fn update_account(&mut self, tx_ref: TransactionReference, account_name: &String, tx_date: &String, commodity_name: &String, commodity_quantity: Decimal) {
if account_name.starts_with("Assets:") || account_name.starts_with("Liability:"){
match self.asset_accounts.contains_key(account_name) {
false => {
self.asset_accounts.insert(
account_name.clone(),
AssetAccount::new(account_name.clone()),
);
}
_ => {}
}
let asset_account = self.asset_accounts.get_mut(account_name).unwrap();
if commodity_name != "$" && commodity_name != "StickerSheets" {
let cost_basis = self.commodities_oracle.lookup(commodity_name, tx_date);
if commodity_quantity.is_sign_positive() {
asset_account.deposit(commodity_name.clone(), cost_basis, commodity_quantity, tx_date);
} else {
asset_account.withdraw(
commodity_name.clone(),
cost_basis,
commodity_quantity,
tx_date,
);
}
} else {
if commodity_quantity.is_sign_positive() {
asset_account.deposit(
commodity_name.clone(),
Decimal::new(1, 0),
commodity_quantity,
tx_date
);
} else {
asset_account.withdraw(
commodity_name.clone(),
Decimal::new(1, 0),
commodity_quantity,
tx_date,
);
}
}
} else {
// This is an external account
match self.external_accounts.contains_key(account_name) {
false => {
let external_account = if account_name.starts_with("Income") {
ExternalAccount::new(account_name.clone(), true)
} else if account_name.starts_with("Expenses") {
ExternalAccount::new(account_name.clone(), false)
} else {
// panic!("Unable to track {}", account_name)
return;
};
self.external_accounts
.insert(account_name.clone(), external_account);
}
_ => {}
}
let external_account = self.external_accounts.get_mut(account_name).unwrap();
if !external_account.income_account {
external_account.deposit(tx_ref, commodity_name.clone(), commodity_quantity, tx_date)
} else {
external_account.withdraw(tx_ref, commodity_name.clone(), commodity_quantity, tx_date)
}
}
}
pub fn summarize_accounts(&self) {
for (account_name, account) in self.external_accounts.iter() {
println!("{}", account_name);
println!("-----");
account.summarize_transactions();
println!()
}
}
pub fn calculate_gains(&self, date: &String) -> f64 {
let mut realized_gains = Decimal::new(0, 0);
let mut unrealized_gains = Decimal::new(0, 0);
for (name, asset_account) in self.asset_accounts.iter() {
println!("{} ", name);
realized_gains += asset_account.realized_gains;
println!(" Realized: {}", asset_account.realized_gains);
for (commodity, total) in asset_account.unrealized_gains(&self.commodities_oracle, &String::from(date)) {
println!(" Unrealized: {} {} ", commodity, total);
unrealized_gains += total;
}
}
println!("Total Realized Gains: {}", realized_gains);
println!("Total Unrealized Gains: {}", unrealized_gains);
println!(
"Total Realized + Unrealized Gains: {}",
unrealized_gains + realized_gains
);
return (unrealized_gains+realized_gains).to_f64().unwrap()
}
}

67
src/external_account.rs Normal file
View File

@ -0,0 +1,67 @@
use crate::books::TransactionReference;
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use crate::CommoditiesPriceOracle;
// For tracking income and expenses...
// All Transactions are one way.
#[derive(Clone)]
pub struct ExternalAccount {
name: String,
pub(crate) income_account: bool,
pub(crate) transactions: Vec<(TransactionReference, String, String, Decimal)>,
}
impl ExternalAccount {
pub fn new(name: String, income_account: bool) -> ExternalAccount {
ExternalAccount {
name,
transactions: vec![],
income_account,
}
}
// We can only deposit commodities to an Income Account.
pub fn deposit(&mut self, txref: TransactionReference, commodity: String, quantity: Decimal, date: &String) {
if !self.income_account {
self.transactions
.push((txref, date.clone(), commodity, quantity));
return;
}
panic!(
"cannot deposit to an income account...{} {} {} {}",
txref, commodity, quantity, date
)
}
// We can only withdraw commodities from an Income Account.
pub fn withdraw(&mut self, txref: TransactionReference, commodity: String, quantity: Decimal, date: &String) {
if self.income_account {
self.transactions
.push((txref, date.clone(), commodity, quantity));
return;
}
panic!(
"cannot withdraw from an expense account...{} {} {} {} {}",
self.name, txref, commodity, quantity, date
)
}
pub fn total_nominal(&self, oracle: &CommoditiesPriceOracle) -> f64 {
let mut total = 0.0;
for (txid, date, commodity, quantity) in self.transactions.iter() {
if commodity != "$" && commodity != "StickerSheets" {
total += (oracle.lookup(commodity, date) * quantity.abs()).to_f64().unwrap();
} else if commodity == "$" {
total += quantity.abs().to_f64().unwrap()
}
}
total
}
pub fn summarize_transactions(&self) {
for (txid, date, commodity, quantity) in self.transactions.iter() {
println!("{}, {}, {}, {}", txid, date, commodity, quantity)
}
}
}

18
src/main.rs Normal file
View File

@ -0,0 +1,18 @@
use crate::asset_account::AssetAccount;
use crate::books::Books;
use crate::external_account::ExternalAccount;
use crate::oracle::CommoditiesPriceOracle;
use crate::spreadsheet::export_to_spreadsheet;
pub mod asset_account;
pub mod books;
pub mod external_account;
pub mod oracle;
pub mod spreadsheet;
fn main() {
let mut books = Books::new();
let stop_date = format!("2023-02-10");
books.load_ledger("2022-archive.dat", &stop_date);
export_to_spreadsheet("test.ods", &books, &format!("2022-02-11"), &stop_date);
}

43
src/oracle.rs Normal file
View File

@ -0,0 +1,43 @@
use rust_decimal::Decimal;
use std::collections::HashMap;
pub struct CommoditiesPriceOracle {
commodities: HashMap<String, HashMap<String, Decimal>>,
}
impl CommoditiesPriceOracle {
pub fn new() -> CommoditiesPriceOracle {
CommoditiesPriceOracle {
commodities: HashMap::new(),
}
}
pub fn insert(&mut self, commodity: String, date: String, price: Decimal) {
match self.commodities.contains_key(&commodity) {
false => {
self.commodities.insert(commodity.clone(), HashMap::new());
}
_ => {}
}
println!("inserting {} {} {}", commodity, date, price);
self.commodities
.get_mut(&commodity)
.unwrap()
.insert(date, price);
}
pub fn lookup(&self, commodity: &String, date: &String) -> Decimal {
match self.commodities.get(commodity) {
None => {
panic!("no price history data for commodity: {}", commodity)
}
Some(price_history) => match price_history.get(date) {
None => {
panic!("no date history for {} on {}", commodity, date)
}
Some(price) => price.clone(),
},
}
}
}

283
src/spreadsheet.rs Normal file
View File

@ -0,0 +1,283 @@
use std::cmp::Ordering;
use std::str::FromStr;
use chrono::{NaiveDate, NaiveTime};
use rust_decimal::prelude::{ToPrimitive, Zero};
use spreadsheet_ods::{CellStyle, format, Length, Sheet, Value, WorkBook};
use crate::{AssetAccount, Books, ExternalAccount};
use icu_locid::locale;
use rust_decimal::Decimal;
use spreadsheet_ods::style::units::TextAlign;
use spreadsheet_ods::Value::Currency;
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 b.1.total_nominal(&books.commodities_oracle).total_cmp(&a.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!("Realized Gains/Losses"), &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!("Balance"), &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
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) in postings.iter() {
let nominal_value = if commodity != "$" && commodity != "StickerSheets" {
books.commodities_oracle.lookup(commodity, date) * quantity
} else {
Decimal::new(1,0 ) * quantity
};
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;
}
}
wb.push_sheet(general_ledger);
// 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 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, 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 != "$" && 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);
}