Implement monthly comparative reports
This commit is contained in:
parent
3364f0ec62
commit
d9a0b5d1b5
@ -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())
|
||||
|
@ -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:
|
||||
|
@ -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') }} </th>{% endfor %}
|
||||
{% for balance_sheet in balance_sheets %}<th class="h2">{{ balance_sheet.label }} </th>{% endfor %}
|
||||
{% else %}
|
||||
<th class="h2" colspan="{{ balance_sheets|length + 1 }}">{{ account_class.bits[-1] }} {{ label }}</th>
|
||||
{% endif %}
|
||||
|
@ -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') }} </th>{% endfor %}
|
||||
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.label }} </th>{% endfor %}
|
||||
</tr>
|
||||
{% for account in ledger.root_account.children|sort(attribute='name') if account in accounts %}
|
||||
{{ print_rows(account, invert=invert) }}
|
||||
|
@ -48,7 +48,7 @@
|
||||
{# Profit and loss #}
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.date.strftime('%Y') }} </th>{% endfor %}
|
||||
{% for cashflow in cashflows %}<th class="h2">{{ cashflow.label }} </th>{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Comprehensive Income</td>
|
||||
|
@ -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 %}
|
||||
|
@ -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') }} </th>{% endfor %}
|
||||
{% for pandl in pandls %}<th class="h2">{{ pandl.label }} </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') }} </th>{% endfor %}
|
||||
{% for pandl in pandls %}<th class="h2">{{ pandl.label }} </th>{% endfor %}
|
||||
{% else %}
|
||||
<th class="h2" colspan="{{ pandls|length + 1 }}">{{ acc_income.bits[-1] }} Income</th>
|
||||
{% endif %}
|
||||
|
@ -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') }} </th>{% endfor %}
|
||||
{% for trial_balance in trial_balances %}<th class="h2">{{ trial_balance.label }}</th>{% endfor %}
|
||||
</tr>
|
||||
{% for account in accounts %}
|
||||
<tr>
|
||||
|
@ -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 = {}
|
||||
|
||||
|
Reference in New Issue
Block a user