Implement monthly comparative reports

This commit is contained in:
RunasSudo 2020-05-01 02:30:51 +10:00
parent 3364f0ec62
commit d9a0b5d1b5
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 115 additions and 44 deletions

View File

@ -19,6 +19,7 @@ from . import ledger
from .config import config
from .model import *
import calendar
from datetime import datetime, timedelta
from decimal import Decimal
import flask
@ -33,11 +34,58 @@ def index():
return flask.render_template('index.html', date=date, pstart=pstart)
def make_period(pstart, date, compare, cmp_period):
pstarts = []
dates = []
labels = []
for i in range(0, compare + 1):
if cmp_period == 'period':
date2 = date.replace(year=date.year - i)
pstarts.append(pstart.replace(year=pstart.year - i))
dates.append(date2)
labels.append(date2.strftime('%Y'))
elif cmp_period == 'month':
pstart2 = pstart
date2 = date
for _ in range(i):
# Go backward one month
pstart2 = pstart2.replace(day=1) - timedelta(days=1)
date2 = date2.replace(day=1) - timedelta(days=1)
pstart2 = pstart2.replace(day=pstart.day)
# Is this the last day of the month?
is_last = calendar.monthrange(date.year, date.month)[1] == date.day
if is_last:
date2 = date2.replace(day=calendar.monthrange(date2.year, date2.month)[1])
else:
if date.day > calendar.monthrange(date.year, date.month)[1]:
date2 = date2.replace(day=calendar.monthrange(date2.year, date2.month)[1])
else:
date2 = date2.replace(day=date.day)
pstarts.append(pstart2)
dates.append(date2)
labels.append(date2.strftime('%b %Y'))
return pstarts, dates, labels
def describe_period(date_end, date_beg):
if date_end == (date_beg.replace(year=date_beg.year + 1) - timedelta(days=1)):
return 'year ended {}'.format(date_end.strftime('%d %B %Y'))
elif date_beg == ledger.financial_year(date_end):
return 'financial year to {}'.format(date_end.strftime('%d %B %Y'))
else:
return 'period from {} to {}'.format(date_beg.strftime('%d %B %Y'), date_end.strftime('%d %B %Y'))
@app.route('/trial')
def trial():
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
compare = int(flask.request.args['compare'])
compare = int(flask.request.args.get('compare', '0'))
cmp_period = flask.request.args.get('cmpperiod', 'period')
cash = flask.request.args.get('cash', False)
if compare == 0:
@ -66,14 +114,13 @@ def trial():
return flask.render_template('trial.html', date=date, pstart=pstart, trial_balance=trial_balance, accounts=accounts, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity)
else:
# Get multiple trial balances for comparison
dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)]
pstarts = [pstart.replace(year=pstart.year - i) for i in range(0, compare + 1)]
pstarts, dates, labels = make_period(pstart, date, compare, cmp_period)
l = ledger.raw_transactions_at_date(date)
report_commodity = l.get_commodity(config['report_commodity'])
if cash:
l = accounting.ledger_to_cash(l, report_commodity)
trial_balances = [accounting.trial_balance(l.clone(), d, p, report_commodity) for d, p in zip(dates, pstarts)]
trial_balances = [accounting.trial_balance(l.clone(), d, p, report_commodity, label=lbl) for d, p, lbl in zip(dates, pstarts, labels)]
# Identify which accounts have transactions in which periods
accounts = sorted(l.accounts.values(), key=lambda a: a.name)
@ -91,17 +138,17 @@ def trial():
def balance():
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
compare = int(flask.request.args['compare'])
compare = int(flask.request.args.get('compare', '0'))
cmp_period = flask.request.args.get('cmpperiod', 'period')
cash = flask.request.args.get('cash', False)
dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)]
pstarts = [pstart.replace(year=pstart.year - i) for i in range(0, compare + 1)]
pstarts, dates, labels = make_period(pstart, date, compare, cmp_period)
l = ledger.raw_transactions_at_date(date)
report_commodity = l.get_commodity(config['report_commodity'])
if cash:
l = accounting.ledger_to_cash(l, report_commodity)
balance_sheets = [accounting.balance_sheet(accounting.trial_balance(l.clone(), d, p, report_commodity)) for d, p in zip(dates, pstarts)]
balance_sheets = [accounting.balance_sheet(accounting.trial_balance(l.clone(), d, p, report_commodity, label=lbl)) for d, p, lbl in zip(dates, pstarts, labels)]
# Delete accounts with always zero balances
accounts = list(l.accounts.values())
@ -111,30 +158,22 @@ def balance():
return flask.render_template('balance.html', ledger=l, balance_sheets=balance_sheets, accounts=accounts, config=config, report_commodity=report_commodity, cash=cash)
def describe_period(date_end, date_beg):
if date_end == (date_beg.replace(year=date_beg.year + 1) - timedelta(days=1)):
return 'year ended {}'.format(date_end.strftime('%d %B %Y'))
elif date_beg == ledger.financial_year(date_end):
return 'financial year to {}'.format(date_end.strftime('%d %B %Y'))
else:
return 'period from {} to {}'.format(date_beg.strftime('%d %B %Y'), date_end.strftime('%d %B %Y'))
@app.route('/pandl')
def pandl():
date_beg = datetime.strptime(flask.request.args['date_beg'], '%Y-%m-%d')
date_end = datetime.strptime(flask.request.args['date_end'], '%Y-%m-%d')
compare = int(flask.request.args['compare'])
compare = int(flask.request.args.get('compare', '0'))
cmp_period = flask.request.args.get('cmpperiod', 'period')
cash = flask.request.args.get('cash', False)
scope = flask.request.args['scope']
scope = flask.request.args.get('scope', 'pandl')
dates_beg = [date_beg.replace(year=date_beg.year - i) for i in range(0, compare + 1)]
dates_end = [date_end.replace(year=date_end.year - i) for i in range(0, compare + 1)]
dates_beg, dates_end, labels = make_period(date_beg, date_end, compare, cmp_period)
l = ledger.raw_transactions_at_date(date_end)
report_commodity = l.get_commodity(config['report_commodity'])
if cash:
l = accounting.ledger_to_cash(l, report_commodity)
pandls = [accounting.trial_balance(l.clone(), de, db, report_commodity) for de, db in zip(dates_end, dates_beg)]
pandls = [accounting.trial_balance(l.clone(), de, db, report_commodity, label=lbl) for de, db, lbl in zip(dates_end, dates_beg, labels)]
# Process separate P&L accounts
separate_pandls = []
@ -170,11 +209,11 @@ def pandl():
def cashflow():
date_beg = datetime.strptime(flask.request.args['date_beg'], '%Y-%m-%d')
date_end = datetime.strptime(flask.request.args['date_end'], '%Y-%m-%d')
compare = int(flask.request.args['compare'])
compare = int(flask.request.args.get('compare', '0'))
cmp_period = flask.request.args.get('cmpperiod', 'period')
method = flask.request.args['method']
dates_beg = [date_beg.replace(year=date_beg.year - i) for i in range(0, compare + 1)]
dates_end = [date_end.replace(year=date_end.year - i) for i in range(0, compare + 1)]
dates_beg, dates_end, labels = make_period(date_beg, date_end, compare, cmp_period)
l = ledger.raw_transactions_at_date(date_end)
report_commodity = l.get_commodity(config['report_commodity'])
@ -186,7 +225,7 @@ def cashflow():
closing_balances = []
cashflows = []
profits = []
for de, db in zip(dates_end, dates_beg):
for de, db, lbl in zip(dates_end, dates_beg, labels):
tb = accounting.trial_balance(l.clone(), db - timedelta(days=1), db, report_commodity)
opening_balances.append(sum((tb.get_balance(a) for a in cash_accounts), Balance()).exchange(report_commodity, True))
@ -195,14 +234,14 @@ def cashflow():
if method == 'direct':
# Determine transactions affecting cash assets
cashflows.append(accounting.account_flows(tb.ledger, de, db, cash_accounts, True))
cashflows.append(accounting.account_flows(tb.ledger, de, db, cash_accounts, True, label=lbl))
else:
# Determine net profit (loss)
profits.append(-(tb.get_total(tb.ledger.get_account(config['income_account'])) + tb.get_total(tb.ledger.get_account(config['expenses_account'])) + tb.get_total(tb.ledger.get_account(config['oci_account']))).exchange(report_commodity, True))
# Determine transactions affecting equity, liabilities and non-cash assets
noncash_accounts = [a for a in l.accounts.values() if a.is_equity or a.is_liability or (a.is_asset and not a.is_cash)]
cashflows.append(accounting.account_flows(tb.ledger, de, db, noncash_accounts, False))
cashflows.append(accounting.account_flows(tb.ledger, de, db, noncash_accounts, False, label=lbl))
# Delete accounts with always zero balances
accounts = list(l.accounts.values())

View File

@ -23,8 +23,8 @@ from .model import *
# Generate a trial balance
# Perform closing of books based on specified dates
def trial_balance_raw(ledger, date, pstart):
tb = TrialBalance(ledger, date, pstart)
def trial_balance_raw(ledger, date, pstart, label=None):
tb = TrialBalance(ledger, date, pstart, label=label)
for transaction in ledger.transactions:
if transaction.date > date:
@ -41,8 +41,8 @@ def trial_balance_raw(ledger, date, pstart):
return tb
# Trial balance with unrealized gains and OCI
def trial_balance(ledger, date, pstart, commodity):
tb_date, r_date = _add_unrealized_gains(trial_balance_raw(ledger, date, pstart), commodity)
def trial_balance(ledger, date, pstart, commodity, label=None):
tb_date, r_date = _add_unrealized_gains(trial_balance_raw(ledger, date, pstart, label=label), commodity)
tb_pstart, r_pstart = _add_unrealized_gains(trial_balance_raw(ledger, pstart - timedelta(days=1), pstart), commodity)
for account in set(list(r_date.keys()) + list(r_pstart.keys())):
@ -168,10 +168,10 @@ def ledger_to_cash(ledger, commodity):
return ledger
# Summarise related transactions
def account_flows(ledger, date, pstart, accounts, related):
def account_flows(ledger, date, pstart, accounts, related, label=None):
transactions = [t for t in ledger.transactions if any(p.account in accounts for p in t.postings) and t.date <= date and t.date >= pstart]
tb = TrialBalance(ledger, date, pstart)
tb = TrialBalance(ledger, date, pstart, label=label)
for transaction in transactions:
for posting in transaction.postings:

View File

@ -62,7 +62,7 @@
<tr>
{% if loop.first and year_headers %}
<th class="h2">{{ account_class.bits[-1] }} {{ label }}</th>
{% for balance_sheet in balance_sheets %}<th class="h2">{{ balance_sheet.date.strftime('%Y') }}&nbsp;</th>{% endfor %}
{% for balance_sheet in balance_sheets %}<th class="h2">{{ balance_sheet.label }}&nbsp;</th>{% endfor %}
{% else %}
<th class="h2" colspan="{{ balance_sheets|length + 1 }}">{{ account_class.bits[-1] }} {{ label }}</th>
{% endif %}

View File

@ -48,7 +48,7 @@
{# Cash flows #}
<tr>
<th class="h2">Cash Inflows (Outflows)</th>
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.date.strftime('%Y') }}&nbsp;</th>{% endfor %}
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.label }}&nbsp;</th>{% endfor %}
</tr>
{% for account in ledger.root_account.children|sort(attribute='name') if account in accounts %}
{{ print_rows(account, invert=invert) }}

View File

@ -48,7 +48,7 @@
{# Profit and loss #}
<tr>
<th></th>
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.date.strftime('%Y') }}&nbsp;</th>{% endfor %}
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.label }}&nbsp;</th>{% endfor %}
</tr>
<tr>
<td>Total Comprehensive Income</td>

View File

@ -26,7 +26,13 @@
<button type="submit">Trial balance</button>
<label>Date: <input name="date" data-inputgroup="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>Period start: <input name="pstart" data-inputgroup="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
<label>
Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)">
<select name="cmpperiod" data-inputgroup="cmpperiod" oninput="selc(this)">
<option value="period" selected>periods</option>
<option value="month">months</option>
</select>
</label>
<label><input name="cash" data-inputgroup="cash" type="checkbox" oninput="chbc(this)"> Cash basis</label>
</form>
</div>
@ -36,7 +42,13 @@
<button type="submit">Balance sheet</button>
<label>Date: <input name="date" data-inputgroup="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>Period start: <input name="pstart" data-inputgroup="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
<label>
Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)">
<select name="cmpperiod" data-inputgroup="cmpperiod" oninput="selc(this)">
<option value="period" selected>periods</option>
<option value="month">months</option>
</select>
</label>
<label><input name="cash" data-inputgroup="cash" type="checkbox" oninput="chbc(this)"> Cash basis</label>
</form>
</div>
@ -46,7 +58,13 @@
<button type="submit">Income statement</button>
<label>Begin date: <input name="date_beg" data-inputgroup="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>End date: <input name="date_end" data-inputgroup="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
<label>
Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)">
<select name="cmpperiod" data-inputgroup="cmpperiod" oninput="selc(this)">
<option value="period" selected>periods</option>
<option value="month">months</option>
</select>
</label>
<label><input name="cash" data-inputgroup="cash" type="checkbox" oninput="chbc(this)"> Cash basis</label>
<label>Scope: <select name="scope">
<option value="pandl" selected>P&L only</option>
@ -61,7 +79,13 @@
<button type="submit">Cash flows</button>
<label>Begin date: <input name="date_beg" data-inputgroup="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>End date: <input name="date_end" data-inputgroup="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
<label>Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
<label>
Compare <input name="compare" data-inputgroup="compare" value="0" style="width: 2em;" oninput="txtc(this)">
<select name="cmpperiod" data-inputgroup="cmpperiod" oninput="selc(this)">
<option value="period" selected>periods</option>
<option value="month">months</option>
</select>
</label>
<label>Method: <select name="method">
<option value="indirect" selected>Indirect</option>
<option value="direct">Direct</option>
@ -92,5 +116,12 @@
e2.checked = el.checked;
}
}
// Ditto for dropdowns
function selc(el) {
for (var e2 of document.querySelectorAll('select[data-inputgroup="' + el.dataset['inputgroup'] + '"]')) {
e2.value = el.value;
}
}
</script>
{% endblock %}

View File

@ -43,7 +43,7 @@
{% if year_headers %}
{# CSS hackery to centre-align the heading #}
<th class="h1" style="padding-left: calc(2px + {{ pandls|length * 6 }}em);">{{ label }}</th>
{% for pandl in pandls %}<th class="h2">{{ pandl.date.strftime('%Y') }}&nbsp;</th>{% endfor %}
{% for pandl in pandls %}<th class="h2">{{ pandl.label }}&nbsp;</th>{% endfor %}
{% else %}
<th class="h1" colspan="{{ pandls|length + 1 }}">{{ label }}</th>
{% endif %}
@ -77,7 +77,7 @@
<tr>
{% if loop.first %}
<th class="h2">{{ acc_income.bits[-1] }} Income</th>
{% for pandl in pandls %}<th class="h2">{{ pandl.date.strftime('%Y') }}&nbsp;</th>{% endfor %}
{% for pandl in pandls %}<th class="h2">{{ pandl.label }}&nbsp;</th>{% endfor %}
{% else %}
<th class="h2" colspan="{{ pandls|length + 1 }}">{{ acc_income.bits[-1] }} Income</th>
{% endif %}

View File

@ -27,7 +27,7 @@
<table class="ledger onedesc">
<tr>
<th></th>
{% for trial_balance in trial_balances %}<th class="h2">{{ trial_balance.date.strftime('%Y') }}&nbsp;</th>{% endfor %}
{% for trial_balance in trial_balances %}<th class="h2">{{ trial_balance.label }}</th>{% endfor %}
</tr>
{% for account in accounts %}
<tr>

View File

@ -355,10 +355,11 @@ class Commodity:
return Commodity(self.name, self.is_prefix)
class TrialBalance:
def __init__(self, ledger, date, pstart):
def __init__(self, ledger, date, pstart, label=None):
self.ledger = ledger
self.date = date
self.pstart = pstart
self.label = label
self.balances = {}