This repository has been archived on 2024-11-09. You can view files and clone it, but cannot push or open issues or pull requests.
ledger-pyreport/ledger_pyreport/__init__.py
RunasSudo 3fa8f8a829
Centralise commodity tracking logic
Per Ledger behaviour, commodities considered equivalent if same name regardless of whether prefix or suffix
Commodity display drawn from Ledger data, no longer explicitly specified in config
2020-04-04 04:28:43 +11:00

354 lines
17 KiB
Python

# 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 accounting
from . import ledger
from .config import config
from .model import *
from datetime import datetime, timedelta
from decimal import Decimal
import flask
import itertools
app = flask.Flask(__name__, template_folder='jinja2')
@app.route('/')
def index():
date = datetime.now()
pstart = ledger.financial_year(date)
return flask.render_template('index.html', date=date, pstart=pstart)
@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'])
cash = flask.request.args.get('cash', False)
if compare == 0:
# Get trial balance
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_balance = accounting.trial_balance(l, date, pstart, report_commodity)
total_dr = Amount(0, report_commodity)
total_cr = Amount(0, report_commodity)
for account in l.accounts.values():
# Display in "cost basis" as we have already accounted for unrealised gains
balance = trial_balance.get_balance(account).exchange(report_commodity, True)
if balance > 0:
total_dr += balance
else:
total_cr -= balance
return flask.render_template('trial.html', date=date, pstart=pstart, trial_balance=trial_balance, accounts=sorted(l.accounts.values(), key=lambda a: a.name), 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)]
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)]
# Delete accounts with always zero balances
accounts = sorted(l.accounts.values(), key=lambda a: a.name)
for account in accounts[:]:
if all(t.get_balance(account).exchange(report_commodity, True).near_zero for t in trial_balances):
accounts.remove(account)
return flask.render_template('trial_multiple.html', trial_balances=trial_balances, accounts=accounts, report_commodity=report_commodity, cash=cash)
@app.route('/balance')
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'])
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)]
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)]
# Delete accounts with always zero balances
accounts = list(l.accounts.values())
for account in accounts[:]:
if all(b.get_balance(account).exchange(report_commodity, True).near_zero and b.get_total(account).exchange(report_commodity, True).near_zero for b in balance_sheets):
accounts.remove(account)
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'])
cash = flask.request.args.get('cash', False)
scope = flask.request.args['scope']
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)]
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)]
# Delete accounts with always zero balances
accounts = list(l.accounts.values())
for account in accounts[:]:
if all(p.get_balance(account) == 0 and p.get_total(account) == 0 for p in pandls):
accounts.remove(account)
return flask.render_template('pandl.html', period=describe_period(date_end, date_beg), ledger=l, pandls=pandls, accounts=accounts, config=config, report_commodity=report_commodity, cash=cash, scope=scope)
@app.route('/cashflow')
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'])
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)]
l = ledger.raw_transactions_at_date(date_end)
report_commodity = l.get_commodity(config['report_commodity'])
cash_accounts = [a for a in l.accounts.values() if a.is_cash]
# Calculate opening and closing cash
opening_balances = []
closing_balances = []
cashflows = []
profits = []
for de, db in zip(dates_end, dates_beg):
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))
tb = accounting.trial_balance(l.clone(), de, db, report_commodity)
closing_balances.append(sum((tb.get_balance(a) for a in cash_accounts), Balance()).exchange(report_commodity, True))
if method == 'direct':
# Determine transactions affecting cash assets
cashflows.append(accounting.account_flows(tb.ledger, de, db, cash_accounts, True))
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))
# Delete accounts with always zero balances
accounts = list(l.accounts.values())
for account in accounts[:]:
if all(p.get_balance(account) == 0 and p.get_total(account) == 0 for p in cashflows):
accounts.remove(account)
if method == 'direct':
return flask.render_template('cashflow_direct.html', period=describe_period(date_end, date_beg), ledger=l, cashflows=cashflows, opening_balances=opening_balances, closing_balances=closing_balances, accounts=accounts, config=config, report_commodity=report_commodity)
else:
return flask.render_template('cashflow_indirect.html', period=describe_period(date_end, date_beg), ledger=l, cashflows=cashflows, profits=profits, opening_balances=opening_balances, closing_balances=closing_balances, accounts=accounts, config=config, report_commodity=report_commodity)
@app.route('/transactions')
def transactions():
date_beg = datetime.strptime(flask.request.args['date_beg'], '%Y-%m-%d')
date_end = datetime.strptime(flask.request.args['date_end'], '%Y-%m-%d')
account = flask.request.args.get('account', None)
cash = flask.request.args.get('cash', False)
commodity = flask.request.args.get('commodity', False)
# General ledger
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)
# Unrealized gains
l = accounting.trial_balance(l, date_end, date_beg, report_commodity).ledger
if not account:
# General Ledger
transactions = [t for t in l.transactions if t.date <= date_end and t.date >= date_beg]
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_commodity, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_commodity, True)
return flask.render_template('transactions.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity, cash=cash)
elif commodity:
# Account Transactions with commodity detail
account = l.get_account(account)
transactions = [t for t in l.transactions if t.date <= date_end and t.date >= date_beg and any(p.account == account for p in t.postings)]
# Use trial_balance_raw because ledger is already adjusted for unrealised gains, etc.
opening_balance = accounting.trial_balance_raw(l, date_beg - timedelta(days=1), date_beg).get_balance(account).clean()
closing_balance = accounting.trial_balance_raw(l, date_end, date_beg).get_balance(account).clean()
def matching_posting(transaction, amount):
return next((p for p in transaction.postings if p.account == account and p.amount.commodity == amount.commodity), None)
return flask.render_template('transactions_commodity.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_commodity=report_commodity, cash=cash, timedelta=timedelta, matching_posting=matching_posting)
else:
# Account Transactions
account = l.get_account(account)
transactions = [t for t in l.transactions if t.date <= date_end and t.date >= date_beg and any(p.account == account for p in t.postings)]
opening_balance = accounting.trial_balance_raw(l, date_beg - timedelta(days=1), date_beg).get_balance(account).exchange(report_commodity, True)
closing_balance = accounting.trial_balance_raw(l, date_end, date_beg).get_balance(account).exchange(report_commodity, True)
return flask.render_template('transactions.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_commodity=report_commodity, cash=cash, timedelta=timedelta)
@app.route('/transaction')
def transaction():
tid = flask.request.args['tid']
cash = flask.request.args.get('cash', False)
commodity = flask.request.args.get('commodity', False)
# General ledger
l = ledger.raw_transactions_at_date(None)
report_commodity = l.get_commodity(config['report_commodity'])
if cash:
l = accounting.ledger_to_cash(l, report_commodity)
transaction = next((t for t in l.transactions if str(t.id) == tid))
if commodity:
total_dr = sum((p.amount for p in transaction.postings if p.amount > 0), Balance()).clean()
total_cr = sum((p.amount for p in transaction.postings if p.amount < 0), Balance()).clean()
totals = itertools.zip_longest(total_dr.amounts, total_cr.amounts)
return flask.render_template('transaction_commodity.html', ledger=l, transaction=transaction, totals=totals, total_dr=total_dr.exchange(report_commodity, True), total_cr=total_cr.exchange(report_commodity, True), report_commodity=report_commodity, cash=cash)
else:
total_dr = sum((p.amount for p in transaction.postings if p.amount > 0), Balance()).exchange(report_commodity, True)
total_cr = sum((p.amount for p in transaction.postings if p.amount < 0), Balance()).exchange(report_commodity, True)
return flask.render_template('transaction.html', ledger=l, transaction=transaction, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity, cash=cash)
# Template filters
@app.template_filter('a')
def filter_amount(amt, link=None):
if amt.near_zero:
amt_str = '0.00'
is_pos = True
elif amt >= 0:
amt_str = '{:,.2f}'.format(amt.amount).replace(',', '&#8239;') # Narrow no-break space
is_pos = True
else:
amt_str = '{:,.2f}'.format(-amt.amount).replace(',', '&#8239;')
is_pos = False
if link:
if is_pos:
return flask.Markup('<a href="{}"><span title="{}">{}</span></a>&nbsp;'.format(link, amt.tostr(False), amt_str))
else:
return flask.Markup('<a href="{}"><span title="{}">({})</span></a>'.format(link, amt.tostr(False), amt_str))
else:
if is_pos:
return flask.Markup('<span title="{}">{}</span>&nbsp;'.format(amt.tostr(False), amt_str))
else:
return flask.Markup('<span title="{}">({})</span>'.format(amt.tostr(False), amt_str))
@app.template_filter('b')
def filter_amount_positive(amt):
return flask.Markup('<span title="{}">{:,.2f}</span>'.format(amt.tostr(False), amt.amount).replace(',', '&#8239;'))
@app.template_filter('bc')
def filter_commodity_positive(amt):
if amt.commodity.is_prefix:
return flask.Markup('<span title="{}">{}{:,.2f}</span>'.format(amt.tostr(False), amt.commodity.name, amt.amount).replace(',', '&#8239;'))
else:
return flask.Markup('<span title="{}">{:,.2f} {}</span>'.format(amt.tostr(False), amt.amount, amt.commodity.name).replace(',', '&#8239;'))
@app.template_filter('bt')
def filter_commodity_table_positive(amt, show_price, link=None):
result = []
if amt.commodity.is_prefix:
amt_str = filter_commodity_positive(amt)
cur_str = ''
else:
amt_str = '{:,.2f}'.format(amt.amount).replace(',', '&#8239;')
cur_str = amt.commodity.name
amt_full = amt.tostr(False)
result.append('<td style="text-align: right;"><a href="{}"><span title="{}">{}</span></a></td>'.format(link, amt_full, amt_str) if link else '<td style="text-align: right;"><span title="{}">{}</span></td>'.format(amt_full, amt_str))
result.append('<td><span title="{}">{}</span></td>'.format(amt_full, cur_str))
if show_price:
if amt.commodity.price:
result.append('<td><span title="{}">{{{}}}</span></td>'.format(amt_full, filter_commodity_positive(amt.commodity.price)))
else:
result.append('<td></td>')
return flask.Markup(''.join(result))
# 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')
l = ledger.raw_transactions_at_date(date)
report_commodity = l.get_commodity(config['report_commodity'])
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_commodity)
return flask.render_template('debug_noncash_transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, report_commodity=report_commodity)
@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)
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)
transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and not sum((p.amount for p in t.postings), Balance()).exchange(report_commodity, True).near_zero]
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_commodity, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_commodity, 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_commodity=report_commodity, cash=cash)