Proper complete implementation of cash basis based on FIFO
This commit is contained in:
parent
3346f046ad
commit
1ce132d4b0
@ -45,7 +45,7 @@ def trial():
|
|||||||
# Get trial balance
|
# Get trial balance
|
||||||
l = ledger.raw_transactions_at_date(date)
|
l = ledger.raw_transactions_at_date(date)
|
||||||
if cash:
|
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.trial_balance(l, date, pstart)
|
||||||
|
|
||||||
trial_balance = accounting.add_unrealized_gains(trial_balance, report_currency)
|
trial_balance = accounting.add_unrealized_gains(trial_balance, report_currency)
|
||||||
@ -69,7 +69,7 @@ def trial():
|
|||||||
|
|
||||||
l = ledger.raw_transactions_at_date(date)
|
l = ledger.raw_transactions_at_date(date)
|
||||||
if cash:
|
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)]
|
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
|
# Delete accounts with always zero balances
|
||||||
@ -93,7 +93,7 @@ def balance():
|
|||||||
report_currency = Currency(*config['report_currency'])
|
report_currency = Currency(*config['report_currency'])
|
||||||
l = ledger.raw_transactions_at_date(date)
|
l = ledger.raw_transactions_at_date(date)
|
||||||
if cash:
|
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)]
|
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
|
# Delete accounts with always zero balances
|
||||||
@ -125,7 +125,7 @@ def pandl():
|
|||||||
report_currency = Currency(*config['report_currency'])
|
report_currency = Currency(*config['report_currency'])
|
||||||
l = ledger.raw_transactions_at_date(date_end)
|
l = ledger.raw_transactions_at_date(date_end)
|
||||||
if cash:
|
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)]
|
pandls = [accounting.trial_balance(l, de, db) for de, db in zip(dates_end, dates_beg)]
|
||||||
|
|
||||||
# Delete accounts with always zero balances
|
# Delete accounts with always zero balances
|
||||||
@ -148,7 +148,7 @@ def transactions():
|
|||||||
# General ledger
|
# General ledger
|
||||||
l = ledger.raw_transactions_at_date(date)
|
l = ledger.raw_transactions_at_date(date)
|
||||||
if cash:
|
if cash:
|
||||||
l = accounting.cash_basis(l, report_currency)
|
l = accounting.ledger_to_cash(l, report_currency)
|
||||||
|
|
||||||
# Unrealized gains
|
# Unrealized gains
|
||||||
l = accounting.add_unrealized_gains(accounting.trial_balance(l, date, pstart), report_currency).ledger
|
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_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)
|
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:
|
else:
|
||||||
account = l.get_account(account)
|
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)]
|
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)
|
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')
|
@app.template_filter('a')
|
||||||
def filter_amount(amt):
|
def filter_amount(amt):
|
||||||
if amt.amount < 0.005 and amt.amount >= -0.005:
|
if amt.amount < 0.005 and amt.amount >= -0.005:
|
||||||
@ -186,3 +188,41 @@ def filter_amount_positive(amt):
|
|||||||
@app.template_filter('bb')
|
@app.template_filter('bb')
|
||||||
def filter_balance_positive(balance):
|
def filter_balance_positive(balance):
|
||||||
return flask.Markup('<br>'.join(filter_amount_positive(a) for a in balance.amounts))
|
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)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import csv
|
import csv
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import math
|
||||||
|
|
||||||
from .model import *
|
from .model import *
|
||||||
|
|
||||||
@ -65,27 +66,64 @@ def balance_sheet(tb):
|
|||||||
|
|
||||||
return tb
|
return tb
|
||||||
|
|
||||||
# Adjust (in place) a ledger to convert accounting to a cash basis
|
def account_to_cash(account, currency):
|
||||||
def cash_basis(ledger, currency):
|
# Apply FIFO methodology to match postings
|
||||||
for transaction in ledger.transactions:
|
balance = [] # list of [posting, amount to balance, amount remaining, balancing list of [posting, amount balanced]]
|
||||||
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:
|
for transaction in account.ledger.transactions[:]:
|
||||||
# We have liabilities or non-cash assets which need to be excluded
|
if any(p.account == account for p in transaction.postings):
|
||||||
|
for posting in transaction.postings[:]:
|
||||||
cash_postings = [p for p in transaction.postings if p.account.is_income or p.account.is_expense or p.account.is_equity]
|
if posting.account == account:
|
||||||
cash_total = sum((p.amount for p in cash_postings), Balance()).exchange(currency, True).amount
|
#transaction.postings.remove(posting)
|
||||||
|
pass
|
||||||
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)))
|
|
||||||
|
|
||||||
transaction.postings.remove(posting)
|
|
||||||
else:
|
else:
|
||||||
for posting in non_cash_postings:
|
# Try to balance postings
|
||||||
posting.account = ledger.get_account(config['cash_other_income'])
|
amount_to_balance = posting.amount.exchange(currency, True).amount
|
||||||
|
|
||||||
|
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:
|
||||||
|
# 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
|
return ledger
|
||||||
|
50
ledger_pyreport/jinja2/debug_noncash_transactions.html
Normal file
50
ledger_pyreport/jinja2/debug_noncash_transactions.html
Normal 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 %}
|
Reference in New Issue
Block a user