Implement report showing transactions by commodity

This commit is contained in:
RunasSudo 2020-03-23 17:17:10 +11:00
parent 91343abd3b
commit c379a28fbd
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 171 additions and 8 deletions

View File

@ -169,6 +169,34 @@ def transactions():
return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, cash=cash, timedelta=timedelta)
@app.route('/transactions_commodity')
def transactions_commodity():
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', None)
cash = flask.request.args.get('cash', False)
report_currency = Currency(*config['report_currency'])
# General ledger
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
# Unrealized gains
l = accounting.add_unrealized_gains(accounting.trial_balance(l, date, pstart), report_currency).ledger
account = l.get_account(account)
transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and any(p.account == account for p in t.postings)]
opening_balance = accounting.trial_balance(l, pstart, pstart).get_balance(account)
closing_balance = accounting.trial_balance(l, date, pstart).get_balance(account)
def matching_posting(transaction, amount):
return next((p for p in transaction.postings if p.account == account and p.amount.currency == amount.currency), None)
return flask.render_template('transactions_commodity.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, cash=cash, timedelta=timedelta, matching_posting=matching_posting)
# Template filters
@app.template_filter('a')
@ -185,9 +213,12 @@ def filter_amount_positive(amt):
return flask.Markup('{:,.2f}'.format(amt.amount).replace(',', '&#8239;'))
#return flask.Markup('{:,.2f} {}'.format(amt.amount, amt.currency.name).replace(',', '&#8239;'))
@app.template_filter('bb')
def filter_balance_positive(balance):
return flask.Markup('<br>'.join(filter_amount_positive(a) for a in balance.amounts))
@app.template_filter('bc')
def filter_currency_positive(amt):
if amt.currency.is_prefix:
return flask.Markup('{}{:,.2f}'.format(amt.currency.name, amt.amount).replace(',', '&#8239;'))
else:
return flask.Markup('{:,.2f} {}'.format(amt.amount, amt.currency.name).replace(',', '&#8239;'))
# Debug views

View File

@ -19,7 +19,11 @@
{% extends 'base.html' %}
{% block body %}
<a class="homelink" href="/">Home</a>
<div class="nav-header">
{% block links %}
<a href="/">Home</a>
{% endblock %}
</div>
{% block report %}
{% endblock %}

View File

@ -20,6 +20,11 @@
{% block title %}{% if account %}Account Transactions{% else %}General Ledger{% endif %} as at {{ date.strftime('%d %B %Y') }}{% endblock %}
{% block links %}
{{ super() }}
<a href="/transactions_commodity?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode }}">Show commodity detail</a>
{% endblock %}
{% block report %}
{% if account %}
<h1>Account Transactions</h1>

View File

@ -0,0 +1,93 @@
{#
ledger-pyreport
Copyright © 2020 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
#}
{% extends 'base_report.html' %}
{% block title %}Account Transactions as at {{ date.strftime('%d %B %Y') }}{% endblock %}
{% block report %}
<h1>Account Transactions</h1>
<h2 style="margin-bottom: 0;">For {{ account.name }}</h2>
<h2>For the {{ period }}</h2>
<table class="ledger">
<tr>
<th style="width: 5em;">Date</th>
<th>Description</th>
<th class="h1" style="width: 1em;"></th>
<th class="h1" style="text-align: right; width: 5em;">Amount</th>
<th class="h1" style="width: 4em;"></th>
<th class="h1" style="text-align: right; width: 5em;">Balance</th>
<th class="h1" style="width: 4em;"></th>
<th class="h1" style="width: 1em;"></th>
</tr>
{% set ns = namespace(balance=None) %}
{% set prevlink = '/transactions_commodity?' + {'date': (pstart - timedelta(days=1)).strftime('%Y-%m-%d'), 'pstart': pstart.replace(year=pstart.year-1).strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode %}
{% for amount in opening_balance.amounts %}
<tr class="total">
<td>{% if loop.first %}<a href="{{ prevlink }}">{{ pstart.strftime('%Y-%m-%d') }}</a>{% endif %}</td>
<td>{% if loop.first %}<a href="{{ prevlink }}">Opening Balance</a>{% endif %}</td>
<td style="text-align: right;"></td>
<td></td>
<td></td>
<td style="text-align: right;"><a href="{{ prevlink }}">{{ amount|abs|bc }}</a></td>
<td><a href="{{ prevlink }}">{% if amount.currency.price %}{{ '{' + amount.currency.price|bc + '}' }}{% endif %}</a></td>
<td>{% if amount >= 0 %}Dr{% else %}Cr{% endif %}</td>
</tr>
{% endfor %}
{% set ns.balance = opening_balance %}
{% for transaction in transactions %}
{% for posting in transaction.postings if posting.account == account %}
{% set ns.balance = ns.balance + posting.amount %}
{% endfor %}
{% for amount in ns.balance.amounts %}
{% set posting = matching_posting(transaction, amount) %}
<tr{% if loop.first %} style="border-top: 1px solid black;"{% endif %}>
<td>{% if loop.first %}{{ transaction.date.strftime('%Y-%m-%d') }}{% endif %}</td>
<td>{% if loop.first %}{{ transaction.description }}{% endif %}</td>
{% if posting %}
<td>{% if posting.amount >= 0 %}Dr{% else %}Cr{% endif %}</td>
<td style="text-align: right;">{{ posting.amount|abs|bc }}</td>
<td>{% if posting.amount.currency.price %}{{ '{' + posting.amount.currency.price|bc + '}' }}{% endif %}</td>
{% else %}
<td colspan="3"></td>
{% endif %}
<td style="text-align: right;">{{ amount|abs|bc }}</td>
<td>{% if amount.currency.price %}{{ '{' + amount.currency.price|bc + '}' }}{% endif %}</td>
<td>{% if amount >= 0 %}Dr{% else %}Cr{% endif %}</td>
</tr>
{% endfor %}
{% set ns.balance = ns.balance.clean() %}
{% endfor %}
{% for amount in closing_balance.amounts %}
<tr class="total explicit-rules" style="{% if loop.first %}border-top: 1pt solid black;{% endif %}{% if loop.last %}border-bottom: 1pt solid black;{% endif %}">
<td>{% if loop.first %}{{ date.strftime('%Y-%m-%d') }}{% endif %}</td>
<td>{% if loop.first %}Closing Balance{% endif %}</td>
<td style="text-align: right;"></td>
<td></td>
<td></td>
<td style="text-align: right;">{{ amount|abs|bc }}</td>
<td>{% if amount.currency.price %}{{ '{' + amount.currency.price|bc + '}' }}{% endif %}</td>
<td>{% if amount >= 0 %}Dr{% else %}Cr{% endif %}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -176,10 +176,21 @@ class Amount:
def __neg__(self):
return Amount(-self.amount, self.currency)
def __abs__(self):
return Amount(abs(self.amount), self.currency)
@compatible_currency
def __eq__(self, other):
return self.amount == other
if isinstance(other, Amount):
if self.amount == 0 and other.amount == 0:
return True
if other.currency != self.currency:
return False
return self.amount == other.amount
if other == 0:
return self.amount == 0
raise TypeError('Cannot compare Amount with non-zero number')
@compatible_currency
def __ne__(self, other):
return self.amount != other
@ -237,6 +248,9 @@ class Balance:
new_amount = next((a for a in new_amounts if a.currency == amount.currency), None)
return Balance(new_amounts)
def clean(self):
return Balance([a for a in self.amounts if a != 0])
def exchange(self, currency, is_cost, date=None, ledger=None):
result = Amount(0, currency)
for amount in self.amounts:
@ -269,18 +283,27 @@ class Balance:
new_amount = Amount(0, amount.currency)
new_amounts.append(new_amount)
new_amount.amount += amount.amount
#if new_amount == 0:
# new_amounts.remove(new_amount)
elif isinstance(other, Amount):
new_amount = next((a for a in new_amounts if a.currency == other.currency), None)
if new_amount is None:
new_amount = Amount(0, other.currency)
new_amounts.append(new_amount)
new_amount.amount += other.amount
#if new_amount == 0:
# new_amounts.remove(new_amount)
elif other == 0:
pass
else:
raise Exception('NYI')
return Balance(new_amounts)
def __sub__(self, other):
return self + (-other)
class Currency:
def __init__(self, name, is_prefix, price=None):

View File

@ -52,7 +52,8 @@ table.ledger th.h1 {
table.ledger tr.total td {
font-weight: bold;
}
table.ledger tr.total:not(.explicit-rules) td {
border-top: 1pt solid black;
border-bottom: 1pt solid black;
}
@ -72,12 +73,18 @@ table.ledger a:hover {
text-decoration: underline;
}
a.homelink {
.nav-header {
color: #888;
position: absolute;
top: 0;
left: 0;
}
.nav-header a {
color: #888;
}
.nav-header a:hover {
color: #666;
}
@media screen {
body {