Implement transaction detail page

This commit is contained in:
RunasSudo 2020-03-29 21:46:55 +11:00
parent c21b8c08f4
commit 63a4b6bdb2
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 112 additions and 15 deletions

View File

@ -197,6 +197,25 @@ def transactions_commodity():
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) 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)
@app.route('/transaction')
def transaction():
tid = flask.request.args['tid']
cash = flask.request.args.get('cash', False)
report_currency = Currency(*config['report_currency'])
# General ledger
l = ledger.raw_transactions_at_date(None)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
transaction = next((t for t in l.transactions if str(t.id) == tid))
total_dr = sum((p.amount for p in transaction.postings if p.amount > 0), Balance()).exchange(report_currency, True)
total_cr = sum((p.amount for p in transaction.postings if p.amount < 0), Balance()).exchange(report_currency, True)
return flask.render_template('transaction.html', ledger=l, transaction=transaction, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)
# Template filters # Template filters
@app.template_filter('a') @app.template_filter('a')

View File

@ -0,0 +1,66 @@
{#
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 %}{{ transaction.description }}{% endblock %}
{% block report %}
<h1 style="margin-bottom: 1em;">Transaction</h1>
<table class="ledger" style="margin-bottom: 1em;">
<tr>
<th style="width: 5em;">Date</th>
<th style="width: 5em;">Code</th>
<th>Description</th>
</tr>
<tr>
<td>{{ transaction.date.strftime('%Y-%m-%d') }}</td>
<td>{{ transaction.code }}</td>
<td>{{ transaction.description }}</td>
</tr>
</table>
<table class="ledger">
<tr>
{#<th style="width: 5em;">Date</th>#}
<th>Description</th>
<th style="max-width: 8em;">Account</th>
<th class="h1" style="text-align: right; width: 5em;">Dr</th>
<th class="h1" style="text-align: right; width: 5em;">Cr</th>
</tr>
{% for posting in transaction.postings %}
{% set amount = posting.exchange(report_currency, transaction.date) %}
{% set trn_url = '/transaction?' + {'tid': transaction.id, 'cash': 'on' if cash else ''}|urlencode %}
<tr>
<td>{{ posting.comment }}</td>
<td>{{ (posting.account.name|e).__str__().replace(':', ':<wbr>')|safe }}</td>
<td style="text-align: right;">{% if amount > 0 %}<span title="{{ posting.amount.tostr(False) }}">{{ amount|b }}</span>{% endif %}</td>
<td style="text-align: right;">{% if amount < 0 %}<span title="{{ (-posting.amount).tostr(False) }}">{{ -amount|b }}</span>{% endif %}</td>
</tr>
{% endfor %}
<tr class="total">
<td>Total</td>
<td></td>
<td style="text-align: right;">{{ total_dr|b }}</td>
<td style="text-align: right;">{{ -total_cr|b }}</td>
</tr>
</table>
{% endblock %}

View File

@ -38,7 +38,7 @@
<tr> <tr>
<th style="width: 5em;">Date</th> <th style="width: 5em;">Date</th>
<th>Description</th> <th>Description</th>
<th style="max-width: 8em;">Account</th> <th style="max-width: 8em;">{% if account %}Related {% endif %}Account</th>
<th class="h1" style="text-align: right; width: 5em;">Dr</th> <th class="h1" style="text-align: right; width: 5em;">Dr</th>
<th class="h1" style="text-align: right; width: 5em;">Cr</th> <th class="h1" style="text-align: right; width: 5em;">Cr</th>
{% if account %}<th class="h1" style="text-align: right; width: 6em;">Balance</th>{% endif %} {% if account %}<th class="h1" style="text-align: right; width: 6em;">Balance</th>{% endif %}
@ -69,9 +69,10 @@
{% for transaction in transactions %} {% for transaction in transactions %}
{% for posting in transaction.postings if posting.account != account %} {% for posting in transaction.postings if posting.account != account %}
{% set amount = posting.exchange(report_currency, transaction.date) %} {% set amount = posting.exchange(report_currency, transaction.date) %}
{% set trn_url = '/transaction?' + {'tid': transaction.id, 'cash': 'on' if cash else ''}|urlencode %}
<tr> <tr>
<td>{% if loop.first %}{{ transaction.date.strftime('%Y-%m-%d') }}{% endif %}</td> <td>{% if loop.first %}{% if transaction.id %}<a href="{{ trn_url }}">{% endif %}{{ transaction.date.strftime('%Y-%m-%d') }}{% if transaction.id %}</a>{% endif %}{% endif %}</td>
<td>{% if loop.first %}{{ transaction.description }}{% endif %}</td> <td>{% if loop.first %}{% if transaction.id %}<a href="{{ trn_url }}">{% endif %}{{ transaction.description }}{% if transaction.id %}</a>{% endif %}{% endif %}</td>
<td><a href="/transactions?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'account': posting.account.name, 'cash': 'on' if cash else ''}|urlencode }}">{{ (posting.account.name|e).__str__().replace(':', ':<wbr>')|safe }}</a></td> <td><a href="/transactions?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'account': posting.account.name, 'cash': 'on' if cash else ''}|urlencode }}">{{ (posting.account.name|e).__str__().replace(':', ':<wbr>')|safe }}</a></td>
{% if account %} {% if account %}
{# Reverse Dr/Cr so it's from the "perspective" of this account #} {# Reverse Dr/Cr so it's from the "perspective" of this account #}

View File

@ -54,14 +54,15 @@
{% set ns.balance = opening_balance %} {% set ns.balance = opening_balance %}
{% for transaction in transactions %} {% for transaction in transactions %}
{% set trn_url = '/transaction?' + {'tid': transaction.id, 'cash': 'on' if cash else ''}|urlencode %}
{% for posting in transaction.postings if posting.account == account %} {% for posting in transaction.postings if posting.account == account %}
{% set ns.balance = ns.balance + posting.amount %} {% set ns.balance = ns.balance + posting.amount %}
{% endfor %} {% endfor %}
{% for amount in ns.balance.amounts %} {% for amount in ns.balance.amounts %}
{% set posting = matching_posting(transaction, amount) %} {% set posting = matching_posting(transaction, amount) %}
<tr{% if loop.first %} style="border-top: 1px solid black;"{% endif %}> <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 %}{% if transaction.id %}<a href="{{ trn_url }}">{% endif %}{{ transaction.date.strftime('%Y-%m-%d') }}{% if transaction.id %}</a>{% endif %}{% endif %}</td>
<td>{% if loop.first %}{{ transaction.description }}{% endif %}</td> <td>{% if loop.first %}{% if transaction.id %}<a href="{{ trn_url }}">{% endif %}{{ transaction.description }}{% if transaction.id %}</a>{% endif %}{% endif %}</td>
{% if posting %} {% if posting %}
<td>{% if posting.amount >= 0 %}Dr{% else %}Cr{% endif %}</td> <td>{% if posting.amount >= 0 %}Dr{% else %}Cr{% endif %}</td>
<td style="text-align: right;"><span title="{{ posting.amount.tostr(False) }}">{{ posting.amount|abs|bc }}</span></td> <td style="text-align: right;"><span title="{{ posting.amount.tostr(False) }}">{{ posting.amount|abs|bc }}</span></td>

View File

@ -36,6 +36,8 @@ def run_ledger(*args):
return stdout return stdout
def run_ledger_date(date, *args): def run_ledger_date(date, *args):
if date is None:
return run_ledger(*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)
# General financial logic # General financial logic
@ -48,6 +50,8 @@ def financial_year(date):
# Ledger logic # Ledger logic
csv.register_dialect('ledger', doublequote=False, escapechar='\\')
def parse_amount(amount): def parse_amount(amount):
if '{' in amount: if '{' in amount:
amount_str = amount[:amount.index('{')].strip() amount_str = amount[:amount.index('{')].strip()
@ -60,7 +64,7 @@ def parse_amount(amount):
# Currency follows number # Currency follows number
bits = amount_str.split() bits = amount_str.split()
amount_num = Decimal(bits[0]) amount_num = Decimal(bits[0])
currency = Currency(bits[1], False) currency = Currency(bits[1].strip('"'), False)
else: else:
# Currency precedes number # Currency precedes number
currency = Currency(amount_str[0], True) currency = Currency(amount_str[0], True)
@ -76,9 +80,9 @@ def get_pricedb():
prices = [] prices = []
reader = csv.reader(output.splitlines()) reader = csv.reader(output.splitlines(), dialect='ledger')
for date_str, currency, price_str in reader: for date_str, currency, price_str in reader:
prices.append((datetime.strptime(date_str, '%Y-%m-%d'), currency, parse_amount(price_str))) prices.append((datetime.strptime(date_str, '%Y-%m-%d'), currency.strip('"'), parse_amount(price_str)))
return prices return prices
@ -86,18 +90,21 @@ def raw_transactions_at_date(date):
ledger = Ledger(date) ledger = Ledger(date)
ledger.prices = get_pricedb() ledger.prices = get_pricedb()
output = run_ledger_date(date, 'csv', '--csv-format', '%(quoted(parent.id)),%(quoted(format_date(date))),%(quoted(payee)),%(quoted(account)),%(quoted(display_amount))\n') output = run_ledger_date(date, 'csv', '--csv-format', '%(quoted(parent.id)),%(quoted(format_date(date))),%(quoted(parent.code)),%(quoted(payee)),%(quoted(account)),%(quoted(display_amount)),%(quoted(comment))\n')
reader = csv.reader(output.splitlines()) reader = csv.reader(output.splitlines(), dialect='ledger')
for trn_id, date_str, payee, account_str, amount_str in reader: for trn_id, date_str, code, payee, account_str, amount_str, comment in reader:
if not ledger.transactions or trn_id != ledger.transactions[-1].id: if not ledger.transactions or trn_id != ledger.transactions[-1].id:
transaction = Transaction(ledger, trn_id, datetime.strptime(date_str, '%Y-%m-%d'), payee) transaction = Transaction(ledger, trn_id, datetime.strptime(date_str, '%Y-%m-%d'), payee, code=code)
ledger.transactions.append(transaction) ledger.transactions.append(transaction)
else: else:
# Transaction ID matches: continuation of previous transaction # Transaction ID matches: continuation of previous transaction
transaction = ledger.transactions[-1] transaction = ledger.transactions[-1]
posting = Posting(transaction, ledger.get_account(account_str), parse_amount(amount_str)) if ';' in comment:
comment = comment[comment.index(';')+1:].strip()
posting = Posting(transaction, ledger.get_account(account_str), parse_amount(amount_str), comment=comment)
transaction.postings.append(posting) transaction.postings.append(posting)
return ledger return ledger

View File

@ -52,11 +52,13 @@ class Ledger:
return max(prices, key=lambda p: p[0])[2] return max(prices, key=lambda p: p[0])[2]
class Transaction: class Transaction:
def __init__(self, ledger, id, date, description): def __init__(self, ledger, id, date, description, code=None):
self.ledger = ledger self.ledger = ledger
self.id = id self.id = id
self.date = date self.date = date
self.description = description self.description = description
self.code = code
self.postings = [] self.postings = []
def __repr__(self): def __repr__(self):
@ -69,10 +71,11 @@ class Transaction:
return '\n'.join(result) return '\n'.join(result)
class Posting: class Posting:
def __init__(self, transaction, account, amount): def __init__(self, transaction, account, amount, comment=None):
self.transaction = transaction self.transaction = transaction
self.account = account self.account = account
self.amount = Amount(amount) self.amount = Amount(amount)
self.comment = comment
def __repr__(self): def __repr__(self):
return '<Posting "{}" {}>'.format(self.account.name, self.amount.tostr(False)) return '<Posting "{}" {}>'.format(self.account.name, self.amount.tostr(False))