From c379a28fbd4efb387018dd92ddf1bb408f98b7fb Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 23 Mar 2020 17:17:10 +1100 Subject: [PATCH] Implement report showing transactions by commodity --- ledger_pyreport/__init__.py | 37 +++++++- ledger_pyreport/jinja2/base_report.html | 6 +- ledger_pyreport/jinja2/transactions.html | 5 + .../jinja2/transactions_commodity.html | 93 +++++++++++++++++++ ledger_pyreport/model.py | 27 +++++- ledger_pyreport/static/main.css | 11 ++- 6 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 ledger_pyreport/jinja2/transactions_commodity.html diff --git a/ledger_pyreport/__init__.py b/ledger_pyreport/__init__.py index 519d3c9..1077540 100644 --- a/ledger_pyreport/__init__.py +++ b/ledger_pyreport/__init__.py @@ -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(',', ' ')) #return flask.Markup('{:,.2f} {}'.format(amt.amount, amt.currency.name).replace(',', ' ')) -@app.template_filter('bb') -def filter_balance_positive(balance): - return flask.Markup('
'.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(',', ' ')) + else: + return flask.Markup('{:,.2f} {}'.format(amt.amount, amt.currency.name).replace(',', ' ')) # Debug views diff --git a/ledger_pyreport/jinja2/base_report.html b/ledger_pyreport/jinja2/base_report.html index 3f65983..e60c8ba 100644 --- a/ledger_pyreport/jinja2/base_report.html +++ b/ledger_pyreport/jinja2/base_report.html @@ -19,7 +19,11 @@ {% extends 'base.html' %} {% block body %} - Home + {% block report %} {% endblock %} diff --git a/ledger_pyreport/jinja2/transactions.html b/ledger_pyreport/jinja2/transactions.html index 858372e..01a991c 100644 --- a/ledger_pyreport/jinja2/transactions.html +++ b/ledger_pyreport/jinja2/transactions.html @@ -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() }} + Show commodity detail +{% endblock %} + {% block report %} {% if account %}

Account Transactions

diff --git a/ledger_pyreport/jinja2/transactions_commodity.html b/ledger_pyreport/jinja2/transactions_commodity.html new file mode 100644 index 0000000..eb8c5fd --- /dev/null +++ b/ledger_pyreport/jinja2/transactions_commodity.html @@ -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 . +#} + +{% extends 'base_report.html' %} + +{% block title %}Account Transactions as at {{ date.strftime('%d %B %Y') }}{% endblock %} + +{% block report %} +

Account Transactions

+

For {{ account.name }}

+

For the {{ period }}

+ + + + + + + + + + + + + + {% 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 %} + + + + + + + + + + + {% 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) %} + + + + {% if posting %} + + + + {% else %} + + {% endif %} + + + + + {% endfor %} + {% set ns.balance = ns.balance.clean() %} + {% endfor %} + + {% for amount in closing_balance.amounts %} + + + + + + + + + + + {% endfor %} +
DateDescriptionAmountBalance
{% if loop.first %}{{ pstart.strftime('%Y-%m-%d') }}{% endif %}{% if loop.first %}Opening Balance{% endif %}{{ amount|abs|bc }}{% if amount.currency.price %}{{ '{' + amount.currency.price|bc + '}' }}{% endif %}{% if amount >= 0 %}Dr{% else %}Cr{% endif %}
{% if loop.first %}{{ transaction.date.strftime('%Y-%m-%d') }}{% endif %}{% if loop.first %}{{ transaction.description }}{% endif %}{% if posting.amount >= 0 %}Dr{% else %}Cr{% endif %}{{ posting.amount|abs|bc }}{% if posting.amount.currency.price %}{{ '{' + posting.amount.currency.price|bc + '}' }}{% endif %}{{ amount|abs|bc }}{% if amount.currency.price %}{{ '{' + amount.currency.price|bc + '}' }}{% endif %}{% if amount >= 0 %}Dr{% else %}Cr{% endif %}
{% if loop.first %}{{ date.strftime('%Y-%m-%d') }}{% endif %}{% if loop.first %}Closing Balance{% endif %}{{ amount|abs|bc }}{% if amount.currency.price %}{{ '{' + amount.currency.price|bc + '}' }}{% endif %}{% if amount >= 0 %}Dr{% else %}Cr{% endif %}
+{% endblock %} diff --git a/ledger_pyreport/model.py b/ledger_pyreport/model.py index 3c807ab..e6aad20 100644 --- a/ledger_pyreport/model.py +++ b/ledger_pyreport/model.py @@ -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): diff --git a/ledger_pyreport/static/main.css b/ledger_pyreport/static/main.css index d23e6d8..be6fcd8 100644 --- a/ledger_pyreport/static/main.css +++ b/ledger_pyreport/static/main.css @@ -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 {