Major rewrite - implement comparative periods for all reports
This commit is contained in:
parent
7ec5ef5d99
commit
bb66931b0e
@ -14,9 +14,11 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from . import accounting
|
||||||
from . import ledger
|
from . import ledger
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
app = flask.Flask(__name__, template_folder='jinja2')
|
app = flask.Flask(__name__, template_folder='jinja2')
|
||||||
@ -32,57 +34,91 @@ def index():
|
|||||||
def trial():
|
def trial():
|
||||||
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
|
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
|
||||||
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
|
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
|
||||||
cash = flask.request.args.get('cash', False)
|
compare = int(flask.request.args['compare'])
|
||||||
|
#cash = flask.request.args.get('cash', False)
|
||||||
|
|
||||||
# Get trial balance
|
if compare == 0:
|
||||||
accounts = ledger.trial_balance(date, pstart, cash)
|
# Get trial balance
|
||||||
|
trial_balance = ledger.trial_balance(date, pstart)
|
||||||
total_dr = sum(a.balance for a in accounts if a.balance > 0)
|
|
||||||
total_cr = -sum(a.balance for a in accounts if a.balance < 0)
|
total_dr = Decimal(0)
|
||||||
|
total_cr = Decimal(0)
|
||||||
return flask.render_template('trial.html', date=date, accounts=accounts, total_dr=total_dr, total_cr=total_cr)
|
|
||||||
|
for account in trial_balance.accounts.values():
|
||||||
|
balance = trial_balance.get_balance(account.name)
|
||||||
|
if balance > 0:
|
||||||
|
total_dr += balance
|
||||||
|
else:
|
||||||
|
total_cr -= balance
|
||||||
|
|
||||||
|
return flask.render_template('trial.html', date=date, trial_balance=trial_balance, total_dr=total_dr, total_cr=total_cr)
|
||||||
|
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)]
|
||||||
|
|
||||||
|
trial_balances = [ledger.trial_balance(d, p) for d, p in zip(dates, pstarts)]
|
||||||
|
|
||||||
|
# Delete accounts with always zero balances
|
||||||
|
accounts = list(trial_balances[0].accounts.values())
|
||||||
|
for account in accounts[:]:
|
||||||
|
if all(t.get_balance(account.name) == 0 for t in trial_balances):
|
||||||
|
accounts.remove(account)
|
||||||
|
|
||||||
|
return flask.render_template('trial_multiple.html', dates=dates, trial_balances=trial_balances, accounts=accounts)
|
||||||
|
|
||||||
@app.route('/balance')
|
@app.route('/balance')
|
||||||
def balance():
|
def balance():
|
||||||
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
|
date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d')
|
||||||
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
|
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
|
||||||
cash = flask.request.args.get('cash', False)
|
compare = int(flask.request.args['compare'])
|
||||||
|
#cash = flask.request.args.get('cash', False)
|
||||||
|
|
||||||
# Get trial balance
|
dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)]
|
||||||
accounts = ledger.trial_balance(date, pstart, cash)
|
pstarts = [pstart.replace(year=pstart.year - i) for i in range(0, compare + 1)]
|
||||||
accounts_map = ledger.make_account_tree(accounts)
|
|
||||||
|
|
||||||
# Calculate Profit/Loss
|
balance_sheets = [accounting.balance_sheet(d, p) for d, p in zip(dates, pstarts)]
|
||||||
total_pandl = accounts_map[ledger.config['income_account']].total() + accounts_map[ledger.config['expenses_account']].total()
|
|
||||||
|
|
||||||
# Add Current Year Earnings account
|
# Delete accounts with always zero balances
|
||||||
accounts.append(ledger.Account(ledger.config['current_year_earnings'], total_pandl))
|
accounts = list(balance_sheets[0].accounts.values())
|
||||||
accounts_map = ledger.make_account_tree(accounts)
|
for account in accounts[:]:
|
||||||
|
if all(b.get_balance(account.name) == 0 and b.get_total(account.name) == 0 for b in balance_sheets):
|
||||||
|
accounts.remove(account)
|
||||||
|
|
||||||
return flask.render_template('balance.html', date=date, cash=cash, assets=accounts_map.get(ledger.config['assets_account'], ledger.Account('Assets')), liabilities=accounts_map.get(ledger.config['liabilities_account'], ledger.Account('Liabilities')), equity=accounts_map.get(ledger.config['equity_account'], ledger.Account('Equity')))
|
return flask.render_template('balance.html', dates=dates, balance_sheets=balance_sheets, accounts=accounts, config=ledger.config)
|
||||||
|
|
||||||
@app.route('/pandl')
|
@app.route('/pandl')
|
||||||
def pandl():
|
def pandl():
|
||||||
date_beg = datetime.strptime(flask.request.args['date_beg'], '%Y-%m-%d')
|
date_beg = datetime.strptime(flask.request.args['date_beg'], '%Y-%m-%d')
|
||||||
date_end = datetime.strptime(flask.request.args['date_end'], '%Y-%m-%d')
|
date_end = datetime.strptime(flask.request.args['date_end'], '%Y-%m-%d')
|
||||||
cash = flask.request.args.get('cash', False)
|
compare = int(flask.request.args['compare'])
|
||||||
|
#cash = flask.request.args.get('cash', False)
|
||||||
|
|
||||||
# Get P&L
|
dates_beg = [date_beg.replace(year=date_beg.year - i) for i in range(0, compare + 1)]
|
||||||
accounts = ledger.pandl(date_beg, date_end, cash)
|
dates_end = [date_end.replace(year=date_end.year - i) for i in range(0, compare + 1)]
|
||||||
accounts_map = ledger.make_account_tree(accounts)
|
|
||||||
|
pandls = [ledger.trial_balance(de, db) for de, db in zip(dates_end, dates_beg)]
|
||||||
|
|
||||||
|
# Delete accounts with always zero balances
|
||||||
|
accounts = list(pandls[0].accounts.values())
|
||||||
|
for account in accounts[:]:
|
||||||
|
if all(p.get_balance(account.name) == 0 and p.get_total(account.name) == 0 for p in pandls):
|
||||||
|
accounts.remove(account)
|
||||||
|
|
||||||
if date_end == (date_beg.replace(year=date_beg.year + 1) - timedelta(days=1)):
|
if date_end == (date_beg.replace(year=date_beg.year + 1) - timedelta(days=1)):
|
||||||
period = 'year ended {}'.format(date_end.strftime('%d %B %Y'))
|
period = 'year ended {}'.format(date_end.strftime('%d %B %Y'))
|
||||||
elif date_beg == ledger.financial_year(date_end):
|
elif date_beg == ledger.financial_year(date_end):
|
||||||
period = 'financial year to {}'.format(date_end.strftime('%d %B %Y'))
|
period = 'financial year to {}'.format(date_end.strftime('%d %B %Y'))
|
||||||
else:
|
else:
|
||||||
period = 'period from {} to {}'.format(date_begin.strftime('%d %B %Y'), date_end.strftime('%d %B %Y'))
|
period = 'period from {} to {}'.format(date_beg.strftime('%d %B %Y'), date_end.strftime('%d %B %Y'))
|
||||||
|
|
||||||
return flask.render_template('pandl.html', period=period, income=accounts_map[ledger.config['income_account']], expenses=accounts_map[ledger.config['expenses_account']])
|
return flask.render_template('pandl.html', period=period, dates_end=dates_end, pandls=pandls, accounts=accounts, config=ledger.config)
|
||||||
|
|
||||||
@app.template_filter('a')
|
@app.template_filter('a')
|
||||||
def filter_amount(amt):
|
def filter_amount(amt):
|
||||||
if amt > 0:
|
if amt < 0.005 and amt >= -0.005:
|
||||||
|
return flask.Markup('0.00 ')
|
||||||
|
elif amt > 0:
|
||||||
return flask.Markup('{:,.2f} '.format(amt).replace(',', ' ')) # Narrow no-break space
|
return flask.Markup('{:,.2f} '.format(amt).replace(',', ' ')) # Narrow no-break space
|
||||||
else:
|
else:
|
||||||
return flask.Markup('({:,.2f})'.format(-amt).replace(',', ' '))
|
return flask.Markup('({:,.2f})'.format(-amt).replace(',', ' '))
|
||||||
|
31
ledger_pyreport/accounting.py
Normal file
31
ledger_pyreport/accounting.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from . import ledger
|
||||||
|
|
||||||
|
# Generate balance sheet
|
||||||
|
|
||||||
|
def balance_sheet(date, pstart):
|
||||||
|
# Get trial balance
|
||||||
|
trial_balance = ledger.trial_balance(date, pstart)
|
||||||
|
|
||||||
|
# Calculate Profit/Loss
|
||||||
|
total_pandl = trial_balance.get_total(ledger.config['income_account']) + trial_balance.get_total(ledger.config['expenses_account'])
|
||||||
|
|
||||||
|
# Add Current Year Earnings account
|
||||||
|
trial_balance.set_balance(ledger.config['current_year_earnings'], trial_balance.get_balance(ledger.config['current_year_earnings']) + total_pandl)
|
||||||
|
|
||||||
|
return trial_balance
|
@ -16,81 +16,82 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% macro do_account(account, acc_level=0, disp_level=0, invert=False) %}
|
{% macro print_rows(account, invert=False, level=0) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-left: calc(2px + {{ disp_level }}em);">{{ ':'.join(account.name_parts[acc_level+1:]) }}</td>
|
<td style="padding-left: calc(2px + {{ level }}em);">{{ account.bits[-1] }}</td>
|
||||||
<td>{% if account.balance != 0 %}{{ (account.balance * (-1 if invert else 1))|a }}{% endif %}</td>
|
{% for balance_sheet in balance_sheets %}
|
||||||
|
{% set amount = balance_sheet.get_balance(account.name) * (-1 if invert else 1) %}
|
||||||
|
<td>{% if amount != 0 %}{{ amount|a }}{% endif %}</td>
|
||||||
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{{ walk_children(account.children, acc_level + 1, disp_level + 1, invert) }}
|
|
||||||
|
{% for child in account.children if child in accounts %}
|
||||||
|
{{ print_rows(child, invert, level + 1) }}
|
||||||
|
{% endfor %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro walk_children(children, acc_level=0, disp_level=0, invert=False) %}
|
{% macro do_accounts(root, label, invert, year_headers) %}
|
||||||
{% for account in children %}
|
{% for account_class in balance_sheets[0].get_account(root).children if account_class in accounts %}
|
||||||
{{ do_account(account, acc_level, disp_level, invert) }}
|
{% set isfirst = (loop.index0 == 0) %}
|
||||||
|
<tr>
|
||||||
|
{% if isfirst and year_headers %}
|
||||||
|
<th class="h2">{{ account_class.bits[-1] }} {{ label }}</th>
|
||||||
|
{% for date in dates %}<th class="h2">{{ date.strftime('%Y') }} </th>{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<th class="h2" colspan="{{ dates|length + 1 }}">{{ account_class.bits[-1] }} {{ label }}</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for account in account_class.children if account in accounts %}
|
||||||
|
{{ print_rows(account, invert=invert) }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<tr class="total">
|
||||||
|
<td>Total {{ account_class.bits[-1] }} {{ label }}</td>
|
||||||
|
{% for balance_sheet in balance_sheets %}<td>{{ (balance_sheet.get_total(account_class.name) * (-1 if invert else 1))|a }}</td>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
<tr><td colspan="2"> </td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<tr class="total">
|
||||||
|
<td>Total {{ label }}</td>
|
||||||
|
{% for balance_sheet in balance_sheets %}<td>{{ (balance_sheet.get_total(root) * (-1 if invert else 1))|a }}</td>{% endfor %}
|
||||||
|
</tr>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Balance Sheet as at {{ date.strftime('%d %B %Y') }}</title>
|
<title>Balance Sheet as at {{ dates[0].strftime('%d %B %Y') }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" crossorigin="anonymous">
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Balance Sheet</h1>
|
<h1>Balance Sheet</h1>
|
||||||
<h2>As at {{ date.strftime('%d %B %Y') }}</h2>
|
<h2>As at {{ dates[0].strftime('%d %B %Y') }}</h2>
|
||||||
|
|
||||||
<table class="ledger two-amounts">
|
<table class="ledger">
|
||||||
<tr><th class="h1" colspan="2">Assets</th></tr>
|
{# Assets #}
|
||||||
|
<tr><th class="h1" colspan="{{ dates|length + 1 }}">Assets</th></tr>
|
||||||
|
{{ do_accounts(config['assets_account'], 'Assets', False, True) }}
|
||||||
|
<tr><td colspan="2"> </td></tr>
|
||||||
|
|
||||||
{% for asset_class in assets.children %}
|
{# Liabilities #}
|
||||||
<tr><th class="h2" colspan="2">{{ asset_class.name_parts[-1] }} Assets</th></tr>
|
<tr><th class="h1" colspan="{{ dates|length + 1 }}">Liabilities</th></tr>
|
||||||
{{ walk_children(asset_class.children, acc_level=1) }}
|
{{ do_accounts(config['liabilities_account'], 'Liabilities', True, False) }}
|
||||||
<tr class="total">
|
<tr><td colspan="2"> </td></tr>
|
||||||
<td>Total {{ asset_class.name_parts[-1] }} Assets</td>
|
|
||||||
<td>{{ asset_class.total()|a }}</td>
|
{# Equity #}
|
||||||
</tr>
|
<tr><th class="h1" colspan="{{ dates|length + 1 }}">Equity</th></tr>
|
||||||
<tr><td colspan="2"> </td></tr>
|
|
||||||
|
{% for account in balance_sheets[0].get_account(config['equity_account']).children if account in accounts %}
|
||||||
|
{{ print_rows(account, invert=True) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<tr class="total">
|
|
||||||
<td>Total Assets</td>
|
|
||||||
<td>{{ assets.total()|a }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr><td colspan="2"> </td></tr>
|
|
||||||
|
|
||||||
{% if not cash %}
|
|
||||||
<tr><th class="h1" colspan="2">Liabilities</th></tr>
|
|
||||||
|
|
||||||
{% for liability_class in liabilities.children %}
|
|
||||||
<tr><th class="h2" colspan="2">{{ liability_class.name_parts[-1] }} Liabilities</th></tr>
|
|
||||||
{{ walk_children(liability_class.children, acc_level=1, invert=True) }}
|
|
||||||
<tr class="total">
|
|
||||||
<td>Total {{ liability_class.name_parts[-1] }} Liabilities</td>
|
|
||||||
<td>{{ -liability_class.total()|a }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr><td colspan="2"> </td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<tr class="total">
|
|
||||||
<td>Total Liabilities</td>
|
|
||||||
<td>{{ -liabilities.total()|a }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="total">
|
|
||||||
<td>Net Assets</td>
|
|
||||||
<td>{{ (assets.total() + liabilities.total())|a }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr><td colspan="2"> </td></tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<tr><th class="h1" colspan="2">Equity</th></tr>
|
|
||||||
|
|
||||||
{{ walk_children(equity.children, invert=True) }}
|
|
||||||
<tr class="total">
|
<tr class="total">
|
||||||
<td>Total Equity</td>
|
<td>Total Equity</td>
|
||||||
<td>{{ -equity.total()|a }}</td>
|
{% for balance_sheet in balance_sheets %}<td>{{ -balance_sheet.get_total(config['equity_account'])|a }}</td>{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
|
@ -26,24 +26,43 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><form action="{{ url_for('trial') }}">
|
<li><form action="{{ url_for('trial') }}">
|
||||||
<button type="submit">Trial balance</button>
|
<button type="submit">Trial balance</button>
|
||||||
<label>Date: <input name="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;"></label>
|
<label>Date: <input name="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
|
||||||
<label>Period start: <input name="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;"></label>
|
<label>Period start: <input name="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
|
||||||
<label><input name="cash" type="checkbox"> Cash basis</label>
|
<label>Compare <input name="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
|
||||||
|
<label><input name="cash" type="checkbox" oninput="chbc(this)"> Cash basis</label>
|
||||||
</form></li>
|
</form></li>
|
||||||
|
|
||||||
<li><form action="{{ url_for('balance') }}">
|
<li><form action="{{ url_for('balance') }}">
|
||||||
<button type="submit">Balance sheet</button>
|
<button type="submit">Balance sheet</button>
|
||||||
<label>Date: <input name="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;"></label>
|
<label>Date: <input name="date" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
|
||||||
<label>Period start: <input name="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;"></label>
|
<label>Period start: <input name="pstart" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
|
||||||
<label><input name="cash" type="checkbox"> Cash basis</label>
|
<label>Compare <input name="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
|
||||||
|
<label><input name="cash" type="checkbox" oninput="chbc(this)"> Cash basis</label>
|
||||||
</form></li>
|
</form></li>
|
||||||
|
|
||||||
<li><form action="{{ url_for('pandl') }}">
|
<li><form action="{{ url_for('pandl') }}">
|
||||||
<button type="submit">Income statement</button>
|
<button type="submit">Income statement</button>
|
||||||
<label>Begin date: <input name="date_beg" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;"></label>
|
<label>Begin date: <input name="date_beg" value="{{ pstart.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
|
||||||
<label>End date: <input name="date_end" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;"></label>
|
<label>End date: <input name="date_end" value="{{ date.strftime('%Y-%m-%d') }}" style="width: 6em;" oninput="txtc(this)"></label>
|
||||||
<label><input name="cash" type="checkbox"> Cash basis</label>
|
<label>Compare <input name="compare" value="0" style="width: 2em;" oninput="txtc(this)"> periods</label>
|
||||||
|
<label><input name="cash" type="checkbox" oninput="chbc(this)"> Cash basis</label>
|
||||||
</form></li>
|
</form></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Called whenever a text input changes - update others to match
|
||||||
|
function txtc(el) {
|
||||||
|
for (var e2 of document.querySelectorAll('input[name="' + el.name + '"]')) {
|
||||||
|
e2.value = el.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ditto for checkboxes
|
||||||
|
function chbc(el) {
|
||||||
|
for (var e2 of document.querySelectorAll('input[name="' + el.name + '"]')) {
|
||||||
|
e2.checked = el.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -16,18 +16,39 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% macro do_account(account, acc_level=0, disp_level=0, invert=False) %}
|
{% macro print_rows(account, invert=False, level=0) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-left: calc(2px + {{ disp_level }}em);">{{ ':'.join(account.name_parts[acc_level+1:]) }}</td>
|
<td style="padding-left: calc(2px + {{ level }}em);">{{ account.bits[-1] }}</td>
|
||||||
<td>{% if account.balance != 0 %}{{ (account.balance * (-1 if invert else 1))|a }}{% endif %}</td>
|
{% for pandl in pandls %}
|
||||||
|
{% set amount = pandl.get_balance(account.name) * (-1 if invert else 1) %}
|
||||||
|
<td>{% if amount != 0 %}{{ amount|a }}{% endif %}</td>
|
||||||
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{{ walk_children(account.children, acc_level + 1, disp_level + 1, invert) }}
|
|
||||||
|
{% for child in account.children if child in accounts %}
|
||||||
|
{{ print_rows(child, invert, level + 1) }}
|
||||||
|
{% endfor %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro walk_children(children, acc_level=0, disp_level=0, invert=False) %}
|
{% macro do_accounts(root, label, invert, year_headers) %}
|
||||||
{% for account in children %}
|
<tr>
|
||||||
{{ do_account(account, acc_level, disp_level, invert) }}
|
{% if year_headers %}
|
||||||
|
{# CSS hackery to centre-align the heading #}
|
||||||
|
<th class="h1" style="padding-left: calc(2px + {{ dates_end|length * 6 }}em);">{{ label }}</th>
|
||||||
|
{% for date in dates_end %}<th class="h2">{{ date.strftime('%Y') }} </th>{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<th class="h1" colspan="{{ dates_end|length + 1 }}">{{ label }}</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for account in pandls[0].get_account(root).children if account in accounts %}
|
||||||
|
{{ print_rows(account, invert=invert) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<tr class="total">
|
||||||
|
<td>Total {{ label }}</td>
|
||||||
|
{% for pandl in pandls %}<td>{{ (pandl.get_total(root) * (-1 if invert else 1))|a }}</td>{% endfor %}
|
||||||
|
</tr>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -42,26 +63,16 @@
|
|||||||
<h1>Income Statement</h1>
|
<h1>Income Statement</h1>
|
||||||
<h2>For the {{ period }}</h2>
|
<h2>For the {{ period }}</h2>
|
||||||
|
|
||||||
<table class="ledger two-amounts">
|
<table class="ledger">
|
||||||
<tr><th class="h1" colspan="2">Income</th></tr>
|
{{ do_accounts(config['income_account'], 'Income', True, True) }}
|
||||||
{{ walk_children(income.children, invert=True) }}
|
|
||||||
<tr class="total">
|
|
||||||
<td>Total Income</td>
|
|
||||||
<td>{{ -income.total()|a }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr><td colspan="2"> </td></tr>
|
<tr><td colspan="2"> </td></tr>
|
||||||
|
|
||||||
<tr><th class="h1" colspan="2">Expenses</th></tr>
|
{{ do_accounts(config['expenses_account'], 'Expenses', False, False) }}
|
||||||
{{ walk_children(expenses.children) }}
|
|
||||||
<tr class="total">
|
|
||||||
<td>Total Expenses</td>
|
|
||||||
<td>{{ expenses.total()|a }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr><td colspan="2"> </td></tr>
|
<tr><td colspan="2"> </td></tr>
|
||||||
|
|
||||||
<tr class="total">
|
<tr class="total">
|
||||||
<td>Net Surplus (Loss)</td>
|
<td>Net Surplus (Loss)</td>
|
||||||
<td>{{ -(income.total() + expenses.total())|a }}</td>
|
{% for pandl in pandls %}<td>{{ -(pandl.get_total(config['income_account']) + pandl.get_total(config['expenses_account']))|a }}</td>{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
|
@ -28,18 +28,21 @@
|
|||||||
<h1>Trial Balance</h1>
|
<h1>Trial Balance</h1>
|
||||||
<h2>As at {{ date.strftime('%d %B %Y') }}</h2>
|
<h2>As at {{ date.strftime('%d %B %Y') }}</h2>
|
||||||
|
|
||||||
<table class="ledger two-amounts">
|
<table class="ledger">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="h1">Dr</th>
|
<th class="h1">Dr</th>
|
||||||
<th class="h1">Cr</th>
|
<th class="h1">Cr</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for account in accounts %}
|
{% for account in trial_balance.accounts.values() %}
|
||||||
<tr>
|
{% set balance = trial_balance.get_balance(account.name) %}
|
||||||
<td>{{ account.name }}</td>
|
{% if balance != 0 %}
|
||||||
<td>{% if account.balance > 0 %}{{ account.balance|b }}{% endif %}</td>
|
<tr>
|
||||||
<td>{% if account.balance < 0 %}{{ -account.balance|b }}{% endif %}</td>
|
<td>{{ account.name }}</td>
|
||||||
</tr>
|
<td>{% if balance > 0 %}{{ balance|b }}{% endif %}</td>
|
||||||
|
<td>{% if balance < 0 %}{{ -balance|b }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr class="total">
|
<tr class="total">
|
||||||
<td></td>
|
<td></td>
|
||||||
|
47
ledger_pyreport/jinja2/trial_multiple.html
Normal file
47
ledger_pyreport/jinja2/trial_multiple.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{#
|
||||||
|
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/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Trial Balance as at {{ dates[0].strftime('%d %B %Y') }}</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Trial Balance</h1>
|
||||||
|
<h2>As at {{ dates[0].strftime('%d %B %Y') }}</h2>
|
||||||
|
|
||||||
|
<table class="ledger">
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
{% for date in dates %}<th class="h2">{{ date.strftime('%Y') }} </th>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% for account in accounts %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ account.name }}</td>
|
||||||
|
{% for trial_balance in trial_balances %}
|
||||||
|
{% set balance = trial_balance.get_balance(account.name) %}
|
||||||
|
<td>{% if balance != 0 %}{{ balance|a }}{% endif %}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -17,6 +17,7 @@
|
|||||||
import csv
|
import csv
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -24,26 +25,7 @@ import yaml
|
|||||||
with open('config.yml', 'r') as f:
|
with open('config.yml', 'r') as f:
|
||||||
config = yaml.safe_load(f)
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
class Account:
|
# Helper commands to run Ledger
|
||||||
def __init__(self, name, balance=None):
|
|
||||||
if balance is None:
|
|
||||||
balance = Decimal(0)
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.balance = balance
|
|
||||||
|
|
||||||
self.parent = None
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name_parts(self):
|
|
||||||
return self.name.split(':')
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
result = self.balance
|
|
||||||
for child in self.children:
|
|
||||||
result += child.total()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def run_ledger(*args):
|
def run_ledger(*args):
|
||||||
proc = subprocess.Popen(['ledger', '--args-only', '--file', config['ledger_file'], '-X', config['report_currency'], '--unround'] + config['ledger_args'] + list(args), encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
proc = subprocess.Popen(['ledger', '--args-only', '--file', config['ledger_file'], '-X', config['report_currency'], '--unround'] + config['ledger_args'] + list(args), encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
@ -57,48 +39,7 @@ def run_ledger(*args):
|
|||||||
def run_ledger_date(date, *args):
|
def run_ledger_date(date, *args):
|
||||||
return run_ledger('--end', (date + timedelta(days=1)).strftime('%Y-%m-%d'), *args)
|
return run_ledger('--end', (date + timedelta(days=1)).strftime('%Y-%m-%d'), *args)
|
||||||
|
|
||||||
BALANCE_FORMAT = '%(quoted(display_total)),%(quoted(account))\n'
|
# General financial logic
|
||||||
|
|
||||||
def parse_balance(output):
|
|
||||||
reader = csv.reader(output.splitlines())
|
|
||||||
|
|
||||||
accounts = []
|
|
||||||
|
|
||||||
# Parse balance lines
|
|
||||||
for row in reader:
|
|
||||||
balance = row[0]
|
|
||||||
if balance.startswith(config['report_currency']):
|
|
||||||
balance = balance[1:]
|
|
||||||
accounts.append(Account(row[1], Decimal(balance)))
|
|
||||||
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
def make_account_tree(accounts):
|
|
||||||
accounts_map = {}
|
|
||||||
|
|
||||||
for account in accounts:
|
|
||||||
accounts_map[account.name] = account
|
|
||||||
|
|
||||||
for i in range(1, len(account.name_parts)):
|
|
||||||
parent_name = ':'.join(account.name_parts[:i])
|
|
||||||
if parent_name not in accounts_map:
|
|
||||||
accounts_map[parent_name] = Account(parent_name, Decimal(0))
|
|
||||||
|
|
||||||
for account in accounts_map.values():
|
|
||||||
if len(account.name_parts) > 1:
|
|
||||||
account.parent = accounts_map[':'.join(account.name_parts[:-1])]
|
|
||||||
account.parent.children.append(account)
|
|
||||||
|
|
||||||
return accounts_map
|
|
||||||
|
|
||||||
# Return a regex for an account and its children
|
|
||||||
def aregex(account):
|
|
||||||
return '^{0}:|^{0}$'.format(account)
|
|
||||||
|
|
||||||
def amatch(needle, haystack):
|
|
||||||
if haystack == needle or haystack.startswith(needle + ':'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def financial_year(date):
|
def financial_year(date):
|
||||||
pstart = date.replace(day=1, month=7)
|
pstart = date.replace(day=1, month=7)
|
||||||
@ -106,77 +47,182 @@ def financial_year(date):
|
|||||||
pstart = pstart.replace(year=pstart.year - 1)
|
pstart = pstart.replace(year=pstart.year - 1)
|
||||||
return pstart
|
return pstart
|
||||||
|
|
||||||
# Calculate Unrealized Gains
|
# Ledger logic
|
||||||
def unrealized_gains(date):
|
|
||||||
accounts_cost = parse_balance(run_ledger_date(date, 'balance', '--balance-format', BALANCE_FORMAT, '--no-total', '--flat', '--cost', aregex(config['assets_account']), aregex(config['liabilities_account'])))
|
|
||||||
accounts_current = parse_balance(run_ledger_date(date, 'balance', '--balance-format', BALANCE_FORMAT, '--no-total', '--flat', '--market', aregex(config['assets_account']), aregex(config['liabilities_account'])))
|
|
||||||
total_cost = sum(a.balance for a in accounts_cost)
|
|
||||||
total_current = sum(a.balance for a in accounts_current)
|
|
||||||
unrealized_gains = total_current - total_cost
|
|
||||||
|
|
||||||
return unrealized_gains
|
|
||||||
|
|
||||||
# Get account balances at date
|
class Account:
|
||||||
def get_accounts(date, cash=False):
|
def __init__(self, name):
|
||||||
# Calculate Unrealized Gains
|
if not isinstance(name, str):
|
||||||
unrealized_gains_amt = unrealized_gains(date)
|
raise Exception('Account name must be a string')
|
||||||
|
|
||||||
# Get account balances
|
|
||||||
accounts = parse_balance(run_ledger_date(date, 'balance', '--balance-format', BALANCE_FORMAT, '--no-total', '--flat', '--cost', aregex(config['income_account']), aregex(config['expenses_account'])))
|
|
||||||
accounts += parse_balance(run_ledger_date(date, 'balance', '--balance-format', BALANCE_FORMAT, '--no-total', '--flat', '--market', aregex(config['assets_account']), aregex(config['liabilities_account']), aregex(config['equity_account'])))
|
|
||||||
|
|
||||||
# Add Unrealized Gains
|
|
||||||
accounts.append(Account(config['unrealized_gains'], -unrealized_gains_amt))
|
|
||||||
accounts.sort(key=lambda a: a.name)
|
|
||||||
|
|
||||||
# Convert to cash basis
|
|
||||||
if cash:
|
|
||||||
accounts_map = make_account_tree(accounts)
|
|
||||||
|
|
||||||
for account in accounts[:]:
|
self.name = name
|
||||||
if amatch(config['liabilities_account'], account.name) or (amatch(config['assets_account'], account.name) and not any(amatch(x, account.name) for x in config['cash_asset_accounts'])):
|
|
||||||
drcr = parse_balance(run_ledger_date(date, 'balance', '--related', '--balance-format', BALANCE_FORMAT, '--no-total', '--flat', '--cost' if amatch(config['income_account'], account.name) or amatch(config['expenses_account'], account.name) else '--market', aregex(account.name)))
|
self.parent = None
|
||||||
|
self.children = []
|
||||||
for drcr_account in drcr:
|
|
||||||
accounts_map[drcr_account.name].balance -= drcr_account.balance
|
def __repr__(self):
|
||||||
|
return '<Account "{}">'.format(self.name)
|
||||||
accounts.remove(account)
|
|
||||||
del accounts_map[account.name]
|
@property
|
||||||
|
def bits(self):
|
||||||
|
return self.name.split(':')
|
||||||
|
|
||||||
|
def matches(self, needle):
|
||||||
|
if self.name == needle or self.name.startswith(needle + ':'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def insert_into_tree(self, accounts):
|
||||||
|
if ':' in self.name:
|
||||||
|
parent_name = self.name[:self.name.rindex(':')]
|
||||||
|
if parent_name not in accounts:
|
||||||
|
parent = Account(parent_name)
|
||||||
|
accounts[parent_name] = parent
|
||||||
|
parent.insert_into_tree(accounts)
|
||||||
|
|
||||||
|
self.parent = accounts[parent_name]
|
||||||
|
if self not in self.parent.children:
|
||||||
|
self.parent.children.append(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_asset(self):
|
||||||
|
return self.matches(config['assets_account'])
|
||||||
|
@property
|
||||||
|
def is_liability(self):
|
||||||
|
return self.matches(config['liabilities_account'])
|
||||||
|
@property
|
||||||
|
def is_equity(self):
|
||||||
|
return self.matches(config['equity_account'])
|
||||||
|
@property
|
||||||
|
def is_income(self):
|
||||||
|
return self.matches(config['income_account'])
|
||||||
|
@property
|
||||||
|
def is_expense(self):
|
||||||
|
return self.matches(config['expenses_account'])
|
||||||
|
|
||||||
|
#def get_balance(self, date, is_market=None):
|
||||||
|
# if is_market is None:
|
||||||
|
# if self.is_income or self.is_expense:
|
||||||
|
# basis = '--cost'
|
||||||
|
# else:
|
||||||
|
# basis = '--market'
|
||||||
|
# elif is_market == True:
|
||||||
|
# basis = '--market'
|
||||||
|
# else:
|
||||||
|
# basis = '--cost'
|
||||||
|
#
|
||||||
|
# output = run_ledger_date(date, 'balance', '--balance-format', '%(display_total)', '--no-total', '--flat', '--empty', basis, '--limit', 'account=~/^{}$/'.format(re.escape(self.name).replace('/', '\\/')))
|
||||||
|
#
|
||||||
|
# return parse_amount(output)
|
||||||
|
|
||||||
|
class Snapshot:
|
||||||
|
def __init__(self):
|
||||||
|
self.accounts = {}
|
||||||
|
self.balances = {}
|
||||||
|
|
||||||
|
def get_account(self, account_name):
|
||||||
|
if account_name not in self.accounts:
|
||||||
|
account = Account(account_name)
|
||||||
|
self.accounts[account_name] = account
|
||||||
|
account.insert_into_tree(self.accounts)
|
||||||
|
|
||||||
|
return self.accounts[account_name]
|
||||||
|
|
||||||
|
def set_balance(self, account_name, balance):
|
||||||
|
if account_name not in self.accounts:
|
||||||
|
account = Account(account_name)
|
||||||
|
self.accounts[account_name] = account
|
||||||
|
account.insert_into_tree(self.accounts)
|
||||||
|
|
||||||
|
if account_name not in self.balances:
|
||||||
|
self.balances[account_name] = Decimal(0)
|
||||||
|
|
||||||
|
self.balances[account_name] = balance
|
||||||
|
|
||||||
|
def get_balance(self, account_name):
|
||||||
|
if account_name not in self.accounts:
|
||||||
|
self.set_balance(account_name, Decimal(0))
|
||||||
|
|
||||||
|
if account_name not in self.balances:
|
||||||
|
self.balances[account_name] = Decimal(0)
|
||||||
|
|
||||||
|
return self.balances[account_name]
|
||||||
|
|
||||||
|
def get_total(self, account_name):
|
||||||
|
return self.get_balance(account_name) + sum(self.get_total(c.name) for c in self.accounts[account_name].children)
|
||||||
|
|
||||||
|
def parse_amount(amount):
|
||||||
|
if amount == '' or amount == '0':
|
||||||
|
return Decimal(0)
|
||||||
|
if not amount.startswith(config['report_currency']):
|
||||||
|
raise Exception('Unexpected currency returned by ledger: {}'.format(amount))
|
||||||
|
return Decimal(amount[len(config['report_currency']):])
|
||||||
|
|
||||||
|
def get_accounts():
|
||||||
|
output = run_ledger('balance', '--balance-format', '%(account)\n', '--no-total', '--flat', '--empty')
|
||||||
|
account_names = output.rstrip('\n').split('\n')
|
||||||
|
|
||||||
|
accounts = {n: Account(n) for n in account_names}
|
||||||
|
|
||||||
|
for account in list(accounts.values()):
|
||||||
|
account.insert_into_tree(accounts)
|
||||||
|
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
# Calculate trial balance
|
# Raw Ledger output, unlikely to balance
|
||||||
def trial_balance(date, pstart, cash=False):
|
def get_raw_snapshot(date, basis=None):
|
||||||
|
snapshot = Snapshot()
|
||||||
|
|
||||||
|
# Get balances from Ledger
|
||||||
|
output = (
|
||||||
|
run_ledger_date(date, 'balance', '--balance-format', '%(quoted(account)),%(quoted(display_total))\n', '--no-total', '--flat', '--empty', basis if basis is not None else '--market', config['assets_account'], config['liabilities_account'], config['equity_account']) +
|
||||||
|
run_ledger_date(date, 'balance', '--balance-format', '%(quoted(account)),%(quoted(display_total))\n', '--no-total', '--flat', '--empty', basis if basis is not None else '--cost', config['income_account'], config['expenses_account'])
|
||||||
|
)
|
||||||
|
reader = csv.reader(output.splitlines())
|
||||||
|
for row in reader:
|
||||||
|
snapshot.set_balance(row[0], parse_amount(row[1]))
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
# Ledger output, adjusted for Unrealized Gains
|
||||||
|
def get_snapshot(date):
|
||||||
|
snapshot_cost = get_raw_snapshot(date, '--cost')
|
||||||
|
snapshot = get_raw_snapshot(date)
|
||||||
|
|
||||||
|
market_total = Decimal(0)
|
||||||
|
cost_total = Decimal(0)
|
||||||
|
|
||||||
|
# Calculate unrealized gains
|
||||||
|
for account in snapshot.accounts.values():
|
||||||
|
if account.is_asset or account.is_liability:
|
||||||
|
market_total += snapshot.get_balance(account.name)
|
||||||
|
cost_total += snapshot_cost.get_balance(account.name)
|
||||||
|
|
||||||
|
# Add Unrealized Gains account
|
||||||
|
unrealized_gains_amt = market_total - cost_total
|
||||||
|
snapshot.set_balance(config['unrealized_gains'], snapshot.get_balance(config['unrealized_gains']) - unrealized_gains_amt)
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
# Ledger output, simulating closing of books
|
||||||
|
def trial_balance(date, pstart):
|
||||||
# Get balances at period start
|
# Get balances at period start
|
||||||
accounts_pstart = get_accounts(pstart - timedelta(days=1), cash)
|
snapshot_pstart = get_snapshot(pstart)
|
||||||
accounts_map_pstart = make_account_tree(accounts_pstart)
|
|
||||||
|
|
||||||
# Get balances at date
|
# Get balances at date
|
||||||
accounts = get_accounts(date, cash)
|
snapshot = get_snapshot(date)
|
||||||
|
|
||||||
# Adjust Retained Earnings
|
# Calculate Retained Earnings, and adjust income/expense accounts
|
||||||
total_pandl = Decimal(0)
|
total_pandl = Decimal(0)
|
||||||
if config['income_account'] in accounts_map_pstart:
|
for account in snapshot_pstart.accounts.values():
|
||||||
total_pandl = accounts_map_pstart[config['income_account']].total()
|
if account.is_income or account.is_expense:
|
||||||
if config['expenses_account'] in accounts_map_pstart:
|
total_pandl += snapshot_pstart.get_balance(account.name)
|
||||||
total_pandl += accounts_map_pstart[config['expenses_account']].total()
|
|
||||||
|
|
||||||
next(a for a in accounts if a.name == config['retained_earnings']).balance += total_pandl
|
# Add Retained Earnings account
|
||||||
|
snapshot.set_balance(config['retained_earnings'], snapshot.get_balance(config['retained_earnings']) + total_pandl)
|
||||||
|
|
||||||
# Adjust income/expense accounts
|
# Adjust income/expense accounts
|
||||||
for account in accounts:
|
for account in snapshot.accounts.values():
|
||||||
if amatch(config['income_account'], account.name) or amatch(config['expenses_account'], account.name):
|
if account.is_income or account.is_expense:
|
||||||
if account.name in accounts_map_pstart:
|
snapshot.set_balance(account.name, snapshot.get_balance(account.name) - snapshot_pstart.get_balance(account.name))
|
||||||
account.balance -= accounts_map_pstart[account.name].balance
|
|
||||||
|
|
||||||
return accounts
|
return snapshot
|
||||||
|
|
||||||
# Calculate profit and loss
|
|
||||||
def pandl(date_beg, date_end, cash=False):
|
|
||||||
accounts = trial_balance(date_end, date_beg, cash)
|
|
||||||
|
|
||||||
for account in accounts[:]:
|
|
||||||
if not (amatch(config['income_account'], account.name) or amatch(config['expenses_account'], account.name)):
|
|
||||||
accounts.remove(account)
|
|
||||||
|
|
||||||
return accounts
|
|
||||||
|
@ -57,8 +57,9 @@ table.ledger tr.total td {
|
|||||||
border-bottom: 1pt solid black;
|
border-bottom: 1pt solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.two-amounts tr td:nth-child(2), table.two-amounts tr td:nth-child(3) {
|
table.ledger tr td:not(:first-child), table.ledger tr th:not(:first-child) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
width: 6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen {
|
@media screen {
|
||||||
|
Reference in New Issue
Block a user