Proper complete implementation of cash basis based on FIFO

This commit is contained in:
RunasSudo 2020-03-23 00:47:38 +11:00
parent 3346f046ad
commit 1ce132d4b0
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 154 additions and 26 deletions

View File

@ -45,7 +45,7 @@ def trial():
# Get trial balance
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.cash_basis(l, report_currency)
l = accounting.ledger_to_cash(l, report_currency)
trial_balance = accounting.trial_balance(l, date, pstart)
trial_balance = accounting.add_unrealized_gains(trial_balance, report_currency)
@ -69,7 +69,7 @@ def trial():
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.cash_basis(l, report_currency)
l = accounting.ledger_to_cash(l, report_currency)
trial_balances = [accounting.add_unrealized_gains(accounting.trial_balance(l, d, p), report_currency) for d, p in zip(dates, pstarts)]
# Delete accounts with always zero balances
@ -93,7 +93,7 @@ def balance():
report_currency = Currency(*config['report_currency'])
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.cash_basis(l, report_currency)
l = accounting.ledger_to_cash(l, report_currency)
balance_sheets = [accounting.balance_sheet(accounting.add_unrealized_gains(accounting.trial_balance(l, d, p), report_currency)) for d, p in zip(dates, pstarts)]
# Delete accounts with always zero balances
@ -125,7 +125,7 @@ def pandl():
report_currency = Currency(*config['report_currency'])
l = ledger.raw_transactions_at_date(date_end)
if cash:
l = accounting.cash_basis(l, report_currency)
l = accounting.ledger_to_cash(l, report_currency)
pandls = [accounting.trial_balance(l, de, db) for de, db in zip(dates_end, dates_beg)]
# Delete accounts with always zero balances
@ -148,7 +148,7 @@ def transactions():
# General ledger
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.cash_basis(l, report_currency)
l = accounting.ledger_to_cash(l, report_currency)
# Unrealized gains
l = accounting.add_unrealized_gains(accounting.trial_balance(l, date, pstart), report_currency).ledger
@ -159,7 +159,7 @@ def transactions():
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_currency, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_currency, True)
return flask.render_template('transactions.html', date=date, pstart=pstart, account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)
return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)
else:
account = l.get_account(account)
transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and any(p.account == account for p in t.postings)]
@ -169,6 +169,8 @@ def transactions():
return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, cash=cash, timedelta=timedelta)
# Template filters
@app.template_filter('a')
def filter_amount(amt):
if amt.amount < 0.005 and amt.amount >= -0.005:
@ -186,3 +188,41 @@ def filter_amount_positive(amt):
@app.template_filter('bb')
def filter_balance_positive(balance):
return flask.Markup('<br>'.join(filter_amount_positive(a) for a in balance.amounts))
# Debug views
@app.route('/debug/noncash_transactions')
def debug_noncash_transactions():
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
account = flask.request.args.get('account')
report_currency = Currency(*config['report_currency'])
l = ledger.raw_transactions_at_date(date)
account = l.get_account(account)
transactions = [t for t in l.transactions if any(p.account == account for p in t.postings)]
accounting.account_to_cash(account, report_currency)
return flask.render_template('debug_noncash_transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, report_currency=report_currency)
@app.route('/debug/imbalances')
def debug_imbalances():
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
cash = flask.request.args.get('cash', False)
report_currency = Currency(*config['report_currency'])
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and abs(sum((p.amount for p in t.postings), Balance()).exchange(report_currency, True).amount) > 0.005]
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_currency, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_currency, True)
return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)

View File

@ -16,6 +16,7 @@
import csv
from decimal import Decimal
import math
from .model import *
@ -65,27 +66,64 @@ def balance_sheet(tb):
return tb
# Adjust (in place) a ledger to convert accounting to a cash basis
def cash_basis(ledger, currency):
for transaction in ledger.transactions:
non_cash_postings = [p for p in transaction.postings if not (p.account.is_cash or p.account.is_income or p.account.is_expense or p.account.is_equity)]
if non_cash_postings:
# We have liabilities or non-cash assets which need to be excluded
cash_postings = [p for p in transaction.postings if p.account.is_income or p.account.is_expense or p.account.is_equity]
cash_total = sum((p.amount for p in cash_postings), Balance()).exchange(currency, True).amount
if cash_postings:
for posting in non_cash_postings:
posting_amount = posting.amount.exchange(currency, True).amount
for posting_xfer in cash_postings:
posting_xfer_amount = posting_xfer.amount.exchange(currency, True).amount
transaction.postings.append(Posting(transaction, posting_xfer.account, Amount(posting_amount * posting_xfer_amount / cash_total, currency)))
def account_to_cash(account, currency):
# Apply FIFO methodology to match postings
balance = [] # list of [posting, amount to balance, amount remaining, balancing list of [posting, amount balanced]]
for transaction in account.ledger.transactions[:]:
if any(p.account == account for p in transaction.postings):
for posting in transaction.postings[:]:
if posting.account == account:
#transaction.postings.remove(posting)
pass
else:
# Try to balance postings
amount_to_balance = posting.amount.exchange(currency, True).amount
transaction.postings.remove(posting)
while amount_to_balance != 0:
balancing_posting = next((b for b in balance if b[2] != 0 and math.copysign(1, b[2]) != math.copysign(1, amount_to_balance)), None)
if balancing_posting is None:
break
if abs(balancing_posting[2]) >= abs(amount_to_balance):
balancing_posting[3].append([posting, amount_to_balance])
balancing_posting[2] += amount_to_balance
amount_to_balance = Decimal(0)
break
else:
balancing_posting[3].append([posting, -balancing_posting[2]])
amount_to_balance += balancing_posting[2]
balancing_posting[2] = Decimal(0)
if amount_to_balance != 0:
# New unbalanced remainder
balance.append([posting, amount_to_balance, amount_to_balance, []])
transaction.postings = []
# Finalise balanced postings
for orig_posting, amount_to_balance, amount_remaining, balancing_postings in balance:
posting = Posting(orig_posting.transaction, orig_posting.account, Amount(amount_to_balance, currency))
posting.transaction.postings.append(posting)
for balancing_posting, amount_balanced in balancing_postings:
posting.transaction.postings.append(Posting(posting.transaction, balancing_posting.account, Amount(amount_balanced, currency)))
if balancing_posting in balancing_posting.transaction.postings:
balancing_posting.transaction.postings.remove(balancing_posting)
if amount_remaining != 0:
if account.is_asset:
# Cash - charge any unbalanced remainder to Other Income
posting.transaction.postings.append(Posting(posting.transaction, account.ledger.get_account(config['cash_other_income']), Amount(-amount_remaining, currency)))
else:
for posting in non_cash_postings:
posting.account = ledger.get_account(config['cash_other_income'])
# Liabilities, etc. - discard any unbalanced remainder
posting.amount.amount -= amount_remaining
# Adjust (in place) a ledger to convert accounting to a cash basis
def ledger_to_cash(ledger, currency):
for account in list(ledger.accounts.values()):
if not (account.is_cash or account.is_income or account.is_expense or account.is_equity):
account_to_cash(account, currency)
return ledger

View File

@ -0,0 +1,50 @@
{#
ledger-pyreport
Copyright © 2020 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
#}
{% extends 'base_report.html' %}
{% block title %}Account Transactions as at {{ date.strftime('%d %B %Y') }}{% endblock %}
{% block report %}
<h1>Account Transactions</h1>
<h2 style="margin-bottom: 0;">For {{ account.name }}</h2>
<h2>For the {{ period }}</h2>
<table class="ledger">
<tr>
<th style="width: 5em;">Date</th>
<th>Description</th>
<th style="max-width: 8em;">Account</th>
<th class="h1" style="text-align: right; width: 5em;">Dr</th>
<th class="h1" style="text-align: right; width: 5em;">Cr</th>
</tr>
{% for transaction in transactions %}
{% for posting in transaction.postings %}
{% set amount = posting.exchange(report_currency, transaction.date) %}
<tr>
<td>{% if loop.first %}{{ transaction.date.strftime('%Y-%m-%d') }}{% endif %}</td>
<td>{% if loop.first %}{{ transaction.description }}{% endif %}</td>
<td>{{ (posting.account.name|e).__str__().replace(':', ':<wbr>')|safe }}</td>
<td style="text-align: right;">{% if amount > 0 %}<span title="{{ posting.amount.tostr(False) }}">{{ amount|b }}</span>{% endif %}</td>
<td style="text-align: right;">{% if amount < 0 %}<span title="{{ (-posting.amount).tostr(False) }}">{{ -amount|b }}</span>{% endif %}</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% endblock %}