Add feature to split transaction into balanced pairs
Might be useful later for cash basis
This commit is contained in:
parent
0d9f9a096e
commit
454801cd63
@ -309,6 +309,7 @@ def transaction():
|
||||
uuid = flask.request.args['uuid']
|
||||
cash = flask.request.args.get('cash', False)
|
||||
commodity = flask.request.args.get('commodity', False)
|
||||
split = flask.request.args.get('split', False)
|
||||
|
||||
# General ledger
|
||||
l = ledger.raw_transactions_at_date(None)
|
||||
@ -320,15 +321,20 @@ def transaction():
|
||||
|
||||
transaction = next((t for t in l.transactions if str(t.uuid) == uuid))
|
||||
|
||||
if split:
|
||||
postings = transaction.split(report_commodity)
|
||||
transaction = Transaction(l, transaction.id, transaction.date, transaction.description, transaction.code, transaction.uuid)
|
||||
transaction.postings = [p for r in postings for p in r]
|
||||
|
||||
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, date=date, pstart=pstart)
|
||||
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, split=split, date=date, pstart=pstart)
|
||||
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, date=date, pstart=pstart)
|
||||
return flask.render_template('transaction.html', ledger=l, transaction=transaction, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity, cash=cash, split=split, date=date, pstart=pstart)
|
||||
|
||||
# Template filters
|
||||
|
||||
|
@ -22,7 +22,8 @@
|
||||
|
||||
{% block links %}
|
||||
{{ super() }}
|
||||
<li><a href="/transaction?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'uuid': transaction.uuid, 'cash': 'on' if cash else '', 'commodity': 'on'}|urlencode }}">Show commodity detail</a></li>
|
||||
<li><a href="/transaction?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'uuid': transaction.uuid, 'cash': 'on' if cash else '', 'commodity': 'on', 'split': 'on' if split else ''}|urlencode }}">Show commodity detail</a></li>
|
||||
{% if not split %}<li><a href="/transaction?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'uuid': transaction.uuid, 'cash': 'on' if cash else '', 'split': 'on'}|urlencode }}">Balance postings</a></li>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block report %}
|
||||
|
@ -20,6 +20,11 @@
|
||||
|
||||
{% block title %}{{ transaction.description }}{% endblock %}
|
||||
|
||||
{% block links %}
|
||||
{{ super() }}
|
||||
{% if not split %}<li><a href="/transaction?{{ {'date': date.strftime('%Y-%m-%d'), 'pstart': pstart.strftime('%Y-%m-%d'), 'uuid': transaction.uuid, 'cash': 'on' if cash else '', 'commodity': 'on', 'split': 'on'}|urlencode }}">Balance postings</a></li>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block report %}
|
||||
<h1 style="margin-bottom: 1em;">Transaction</h1>
|
||||
|
||||
|
@ -20,6 +20,7 @@ from decimal import Decimal
|
||||
from enum import Enum
|
||||
import functools
|
||||
import itertools
|
||||
import math
|
||||
|
||||
class Ledger:
|
||||
def __init__(self, date):
|
||||
@ -105,6 +106,52 @@ class Transaction:
|
||||
|
||||
return result
|
||||
|
||||
def split(self, report_commodity):
|
||||
# Split postings into debit-credit pairs (cost price)
|
||||
unbalanced_postings = [] # List of [posting, base amount in report commodity, amount to balance in report commodity]
|
||||
result = [] # List of balanced pairs
|
||||
|
||||
for posting in self.postings:
|
||||
base_amount = posting.amount.exchange(report_commodity, True).amount # Used to apportion other commodities
|
||||
amount_to_balance = base_amount
|
||||
|
||||
# Try to balance against previously unbalanced postings
|
||||
for unbalanced_posting in unbalanced_postings[:]:
|
||||
if math.copysign(1, amount_to_balance) != math.copysign(1, unbalanced_posting[2]):
|
||||
if abs(unbalanced_posting[2]) == abs(amount_to_balance):
|
||||
# Just enough
|
||||
unbalanced_postings.remove(unbalanced_posting)
|
||||
result.append((
|
||||
Posting(self, posting.account, Amount(amount_to_balance / base_amount * posting.amount.amount, posting.amount.commodity), posting.comment, posting.state),
|
||||
Posting(self, unbalanced_posting[0].account, Amount(-amount_to_balance / unbalanced_posting[1] * unbalanced_posting[0].amount.amount, unbalanced_posting[0].amount.commodity), unbalanced_posting[0].comment, unbalanced_posting[0].state)
|
||||
))
|
||||
amount_to_balance = 0
|
||||
elif abs(unbalanced_posting[2]) > abs(amount_to_balance):
|
||||
# Excess - partial balancing of unbalanced posting
|
||||
unbalanced_posting[2] += amount_to_balance
|
||||
result.append((
|
||||
Posting(self, posting.account, Amount(amount_to_balance / base_amount * posting.amount.amount, posting.amount.commodity), posting.comment, posting.state),
|
||||
Posting(self, unbalanced_posting[0].account, Amount(-amount_to_balance / unbalanced_posting[1] * unbalanced_posting[0].amount.amount, unbalanced_posting[0].amount.commodity), unbalanced_posting[0].comment, unbalanced_posting[0].state)
|
||||
))
|
||||
amount_to_balance = 0
|
||||
elif abs(unbalanced_posting[2]) < abs(amount_to_balance):
|
||||
# Not enough - partial balancing of this posting
|
||||
amount_to_balance += unbalanced_posting[2]
|
||||
result.append((
|
||||
Posting(self, posting.account, Amount(-unbalanced_posting[2] / base_amount * posting.amount.amount, posting.amount.commodity), posting.comment, posting.state),
|
||||
Posting(self, unbalanced_posting[0].account, Amount(unbalanced_posting[2] / unbalanced_posting[1] * unbalanced_posting[0].amount.amount, unbalanced_posting[0].amount.commodity), unbalanced_posting[0].comment, unbalanced_posting[0].state)
|
||||
))
|
||||
unbalanced_posting[2] = 0
|
||||
|
||||
if amount_to_balance:
|
||||
# Unbalanced remainder - add it to the list
|
||||
unbalanced_postings.append([posting, base_amount, amount_to_balance])
|
||||
|
||||
if unbalanced_postings:
|
||||
raise Exception('Unexpectedly imbalanced transaction')
|
||||
|
||||
return result
|
||||
|
||||
class Posting:
|
||||
class State(Enum):
|
||||
UNCLEARED = 0
|
||||
|
Reference in New Issue
Block a user