diff --git a/ledger_pyreport/__init__.py b/ledger_pyreport/__init__.py
index 9bbab30..268b235 100644
--- a/ledger_pyreport/__init__.py
+++ b/ledger_pyreport/__init__.py
@@ -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
diff --git a/ledger_pyreport/jinja2/transaction.html b/ledger_pyreport/jinja2/transaction.html
index c1b6427..4ec95e1 100644
--- a/ledger_pyreport/jinja2/transaction.html
+++ b/ledger_pyreport/jinja2/transaction.html
@@ -22,7 +22,8 @@
{% block links %}
{{ super() }}
-
Show commodity detail
+ Show commodity detail
+ {% if not split %}Balance postings{% endif %}
{% endblock %}
{% block report %}
diff --git a/ledger_pyreport/jinja2/transaction_commodity.html b/ledger_pyreport/jinja2/transaction_commodity.html
index 6cfaf39..771dc68 100644
--- a/ledger_pyreport/jinja2/transaction_commodity.html
+++ b/ledger_pyreport/jinja2/transaction_commodity.html
@@ -20,6 +20,11 @@
{% block title %}{{ transaction.description }}{% endblock %}
+{% block links %}
+ {{ super() }}
+ {% if not split %}Balance postings{% endif %}
+{% endblock %}
+
{% block report %}
Transaction
diff --git a/ledger_pyreport/model.py b/ledger_pyreport/model.py
index 0a6e590..40f7b96 100644
--- a/ledger_pyreport/model.py
+++ b/ledger_pyreport/model.py
@@ -20,6 +20,7 @@ from decimal import Decimal
from enum import Enum
import functools
import itertools
+import math
class Ledger:
def __init__(self, date):
@@ -104,6 +105,52 @@ class Transaction:
result.postings.append(posting)
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):