From 0d9f9a096e8c45048ac3316008944eb7a45207f0 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 22 Jul 2020 02:41:37 +1000 Subject: [PATCH] Separately account unrealised gains and unrealised losses --- config.example.yml | 2 +- ledger_pyreport/accounting.py | 65 ++++++++++++++++++++++++----------- ledger_pyreport/model.py | 21 ++++++----- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/config.example.yml b/config.example.yml index 919427f..8ab7c87 100644 --- a/config.example.yml +++ b/config.example.yml @@ -15,7 +15,7 @@ oci_account: OCI # Other Comprehensive Income separate_pandl: [] # These accounts will automatically be populated on reports -unrealized_gains: 'OCI:Unrealized Gains' +unrealized_gains: ['OCI:Unrealized Gains', 'OCI: Unrealized Losses'] accumulated_oci: 'Equity:Accumulated Other Comprehensive Income' current_year_oci: 'Equity:Current Year Other Comprehensive Income' diff --git a/ledger_pyreport/accounting.py b/ledger_pyreport/accounting.py index dbe24d3..aad4c3d 100644 --- a/ledger_pyreport/accounting.py +++ b/ledger_pyreport/accounting.py @@ -47,39 +47,54 @@ def trial_balance(ledger, date, pstart, commodity, label=None): for account in set(list(r_date.keys()) + list(r_pstart.keys())): if account in r_pstart: - tb_date.balances[account.name] = tb_date.get_balance(account) + r_pstart[account].postings[0].amount - if r_pstart[account].postings[1].account.is_income or r_pstart[account].postings[1].account.is_expense: - tb_date.balances[config['retained_earnings']] = tb_date.get_balance(ledger.get_account(config['retained_earnings'])) - r_pstart[account].postings[0].amount - elif r_pstart[account].postings[1].account.is_oci: - tb_date.balances[config['accumulated_oci']] = tb_date.get_balance(ledger.get_account(config['accumulated_oci'])) - r_pstart[account].postings[0].amount - else: - tb_date.balances[config['unrealized_gains']] = tb_date.get_balance(ledger.get_account(config['accumulated_oci'])) - r_pstart[account].postings[0].amount - - # Reversing entry - trn_reversal = r_pstart[account].reverse(None, pstart, '') - ledger.transactions.insert(0, trn_reversal) - - tb_date.balances[account.name] = tb_date.get_balance(account) + trn_reversal.postings[0].amount - tb_date.balances[config['unrealized_gains']] = tb_date.get_balance(ledger.get_account(config['unrealized_gains'])) - trn_reversal.postings[0].amount + for trn in r_pstart[account]: + # Update/accumulate trial balances + tb_date.balances[account.name] = tb_date.get_balance(account) + trn.postings[0].amount + + if trn.postings[1].account.is_income or trn.postings[1].account.is_expense: + tb_date.balances[config['retained_earnings']] = tb_date.get_balance(ledger.get_account(config['retained_earnings'])) - trn.postings[0].amount + elif trn.postings[1].account.is_oci: + tb_date.balances[config['accumulated_oci']] = tb_date.get_balance(ledger.get_account(config['accumulated_oci'])) - trn.postings[0].amount + else: + tb_date.balances[trn.postings[1].account.name] = tb_date.get_balance(trn.postings[1].account) - trn.postings[0].amount + + # Reversing entry + trn_reversal = trn.reverse(None, pstart, ''.format(trn.description[1:-1])) + ledger.transactions.insert(0, trn_reversal) + + tb_date.balances[account.name] = tb_date.get_balance(account) + trn_reversal.postings[0].amount + tb_date.balances[trn_reversal.postings[1].account.name] = tb_date.get_balance(trn_reversal.postings[1].account) - trn_reversal.postings[0].amount if account in r_date: - tb_date.balances[account.name] = tb_date.get_balance(account) + r_date[account].postings[0].amount - tb_date.balances[config['unrealized_gains']] = tb_date.get_balance(ledger.get_account(config['unrealized_gains'])) - r_date[account].postings[0].amount + for trn in r_date[account]: + # Update trial balances + tb_date.balances[account.name] = tb_date.get_balance(account) + trn.postings[0].amount + tb_date.balances[trn.postings[1].account.name] = tb_date.get_balance(trn.postings[1].account) - trn.postings[0].amount return tb_date # Adjust (in place) a trial balance for unrealized gains without accumulating OCI def _add_unrealized_gains(tb, commodity): results = {} - unrealized_gain_account = tb.ledger.get_account(config['unrealized_gains']) + unrealized_gain_account = tb.ledger.get_account(config['unrealized_gains'][0]) + unrealized_loss_account = tb.ledger.get_account(config['unrealized_gains'][1]) for account in list(tb.ledger.accounts.values()): if not account.is_market: continue - total_cost = tb.get_balance(account).exchange(commodity, True) - total_market = tb.get_balance(account).exchange(commodity, False, tb.date, tb.ledger) - unrealized_gain = total_market - total_cost + unrealized_gain = Amount(0, commodity) + unrealized_loss = Amount(0, commodity) + + for amount in tb.get_balance(account).amounts: + amt_cost = amount.exchange(commodity, True) + amt_market = amount.exchange(commodity, False, date=tb.date, ledger=tb.ledger) + amt_gain = amt_market - amt_cost + + if amt_gain > 0: + unrealized_gain += amt_gain + if amt_gain < 0: + unrealized_loss += amt_gain if unrealized_gain != 0: transaction = Transaction(tb.ledger, None, tb.date, '') @@ -87,7 +102,15 @@ def _add_unrealized_gains(tb, commodity): transaction.postings.append(Posting(transaction, unrealized_gain_account, -unrealized_gain)) tb.ledger.transactions.append(transaction) - results[account] = transaction + results[account] = results.get(account, []) + [transaction] + + if unrealized_loss != 0: + transaction = Transaction(tb.ledger, None, tb.date, '') + transaction.postings.append(Posting(transaction, account, unrealized_loss)) + transaction.postings.append(Posting(transaction, unrealized_loss_account, -unrealized_loss)) + tb.ledger.transactions.append(transaction) + + results[account] = results.get(account, []) + [transaction] return tb, results diff --git a/ledger_pyreport/model.py b/ledger_pyreport/model.py index 0ee15db..0a6e590 100644 --- a/ledger_pyreport/model.py +++ b/ledger_pyreport/model.py @@ -267,7 +267,7 @@ class Amount: def __rsub__(self, other): return Amount(other - self.amount, self.commodity) - def exchange(self, commodity, is_cost, price=None): + def exchange(self, commodity, is_cost, price=None, date=None, ledger=None): if self.commodity.name == commodity.name: return Amount(self) @@ -277,6 +277,16 @@ class Amount: if price: return Amount(self.amount * price.amount, commodity) + if date and ledger: + if any(p[1] == self.commodity.name for p in ledger.prices): + # This commodity has price information + # Measured at fair value + return self.exchange(commodity, is_cost, ledger.get_price(self.commodity, commodity, date)) + else: + # This commodity has no price information + # Measured at historical cost + return self.exchange(commodity, True) + raise TypeError('Cannot exchange {} to {}'.format(self.commodity, commodity)) @property @@ -308,14 +318,7 @@ class Balance: if is_cost or amount.commodity.name == commodity.name: result += amount.exchange(commodity, is_cost) else: - if any(p[1] == amount.commodity.name for p in ledger.prices): - # This commodity has price information - # Measured at fair value - result += amount.exchange(commodity, is_cost, ledger.get_price(amount.commodity, commodity, date)) - else: - # This commodity has no price information - # Measured at historical cost - result += amount.exchange(commodity, True) + result += amount.exchange(commodity, is_cost, date=date, ledger=ledger) return result def __neg__(self):