Allow charging statement line to multiple accounts
This commit is contained in:
parent
511914fd1b
commit
3debfb1122
@ -22,6 +22,16 @@ class Transaction:
|
||||
self.description = description
|
||||
self.postings = postings or []
|
||||
|
||||
def assert_valid(self):
|
||||
"""Assert that debits equal credits, and commodities are compatible"""
|
||||
|
||||
if any(p.commodity != '$' for p in self.postings):
|
||||
# FIXME: Allow non-$ commodities
|
||||
raise AssertionError('Transaction contains multiple commodities')
|
||||
|
||||
if sum(p.quantity for p in self.postings) != 0:
|
||||
raise AssertionError('Transaction debits and credits do not balance')
|
||||
|
||||
class Posting:
|
||||
def __init__(self, description=None, account=None, quantity=None, commodity=None):
|
||||
self.description = description
|
||||
@ -44,3 +54,6 @@ class Amount:
|
||||
|
||||
def format(self):
|
||||
return '{0}{1:,.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
|
||||
|
||||
def quantity_string(self):
|
||||
return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
|
||||
|
@ -55,6 +55,15 @@ class StatementLine(Base):
|
||||
]
|
||||
)
|
||||
|
||||
def is_complex(self):
|
||||
if len(self.postings) > 1:
|
||||
return True
|
||||
|
||||
if any(len(p.transaction.postings) > 2 for p in self.postings):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class StatementLineTransaction(Base, Transaction):
|
||||
__tablename__ = 'statement_line_transactions'
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
from flask import abort, redirect, render_template, request
|
||||
|
||||
from .. import AMOUNT_DPS
|
||||
from ..database import db_session
|
||||
from ..webapp import app
|
||||
from .models import StatementLine, StatementLinePosting, StatementLineTransaction
|
||||
@ -74,14 +75,31 @@ def statement_line_edit_transaction():
|
||||
# Queue for deletion
|
||||
db_session.delete(posting.transaction)
|
||||
|
||||
transaction = StatementLineTransaction(
|
||||
dt=statement_line.dt,
|
||||
description=request.form['description'],
|
||||
if len(request.form.getlist('charge-account')) == 1:
|
||||
# Simple transaction
|
||||
postings = [
|
||||
StatementLinePosting(statement_line=statement_line, account=statement_line.source_account, quantity=statement_line.quantity, commodity=statement_line.commodity),
|
||||
StatementLinePosting(account=request.form['charge-account'], quantity=-statement_line.quantity, commodity=statement_line.commodity)
|
||||
]
|
||||
else:
|
||||
# Complex transaction, multiple postings
|
||||
postings = [
|
||||
StatementLinePosting(statement_line=statement_line, account=statement_line.source_account, quantity=statement_line.quantity, commodity=statement_line.commodity)
|
||||
]
|
||||
|
||||
for charge_account, charge_amount_str in zip(request.form.getlist('charge-account'), request.form.getlist('charge-amount')):
|
||||
charge_quantity = round(float(charge_amount_str) * (10**AMOUNT_DPS))
|
||||
if statement_line.quantity >= 0:
|
||||
# If source is debit, charge is credit
|
||||
charge_quantity = -charge_quantity
|
||||
postings.append(StatementLinePosting(account=charge_account, quantity=charge_quantity, commodity=statement_line.commodity))
|
||||
|
||||
transaction = StatementLineTransaction(
|
||||
dt=statement_line.dt,
|
||||
description=request.form['description'],
|
||||
postings=postings
|
||||
)
|
||||
transaction.assert_valid()
|
||||
|
||||
db_session.add(transaction)
|
||||
db_session.commit()
|
||||
|
@ -34,7 +34,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<tr id="statement-line" data-quantity="{{ statement_line.amount().quantity_string() }}" data-commodity="{{ statement_line.commodity }}">
|
||||
<td>{{ statement_line.source_account }}</td>
|
||||
<td>{{ statement_line.dt.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ statement_line.description }}</td>
|
||||
@ -59,26 +59,27 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{# FIXME: Customise date, etc. #}
|
||||
{# FIXME: Complex transactions #}
|
||||
<tr>
|
||||
<td>{{ statement_line.dt.strftime('%Y-%m-%d') }}</td>
|
||||
<td colspan="2"><input type="text" name="description" value="{{ statement_line.description }}" class="w-100"></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-side="source">
|
||||
{# Source line #}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><i>{{ 'Dr' if statement_line.quantity >= 0 else 'Cr' }}</i> {{ statement_line.source_account }}</td>
|
||||
<td>{{ statement_line.amount().format() if statement_line.quantity >= 0 else '' }}</td>
|
||||
<td>{{ (statement_line.amount()|abs).format() if statement_line.quantity < 0 else '' }}</td>
|
||||
<td><i>{{ 'Dr' if statement_line.quantity >= 0 else 'Cr' }}</i> {{ statement_line.source_account }} {#<a class="text-primary" href="#" onclick="addPosting(this);return false;"><i class="bi bi-plus-circle-fill"></i></a>#}</td>
|
||||
{% if statement_line.quantity >= 0 %}<td class="has-amount">{{ statement_line.amount().format() }}</td>{% else %}<td></td>{% endif %}
|
||||
{% if statement_line.quantity < 0 %}<td class="has-amount">{{ (statement_line.amount()|abs).format() }}</td>{% else %}<td></td>{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-side="charge">
|
||||
{# Charge line #}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><i>{{ 'Cr' if statement_line.quantity >= 0 else 'Dr' }}</i> <input type="text" name="charge-account"></td>
|
||||
<td>{{ (statement_line.amount()|abs).format() if statement_line.quantity < 0 else '' }}</td>
|
||||
<td>{{ statement_line.amount().format() if statement_line.quantity >= 0 else '' }}</td>
|
||||
<td><i>{{ 'Cr' if statement_line.quantity >= 0 else 'Dr' }}</i> <input type="text" name="charge-account"> <a class="text-primary" href="#" onclick="addPosting(this);return false;"><i class="bi bi-plus-circle-fill"></i></a></td>
|
||||
{% if statement_line.quantity < 0 %}<td class="has-amount">{{ (statement_line.amount()|abs).format() }}</td>{% else %}<td></td>{% endif %}
|
||||
{% if statement_line.quantity >= 0 %}<td class="has-amount">{{ statement_line.amount().format() }}</td>{% else %}<td></td>{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -88,4 +89,30 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addPosting(el) {
|
||||
let trPosting = el.parentNode.parentNode;
|
||||
let trLine = document.getElementById('statement-line');
|
||||
let qtyf = parseFloat(trLine.dataset.quantity);
|
||||
|
||||
if (trPosting.dataset['side'] === 'source') {
|
||||
alert('NYI');
|
||||
return;
|
||||
}
|
||||
|
||||
if (trPosting.dataset['side'] === 'charge') {
|
||||
let inputAmount = trLine.dataset.commodity + '<input type="number" name="charge-amount" step="0.01">';
|
||||
|
||||
// Add new posting row
|
||||
let trNew = document.createElement('tr');
|
||||
trNew.dataset['side'] = 'charge';
|
||||
trNew.innerHTML = '<td></td><td></td><td><i>' + (qtyf >= 0 ? 'Cr' : 'Dr') + '</i> <input type="text" name="charge-account"> <a class="text-primary" href="#" onclick="addPosting(this);return false;"><i class="bi bi-plus-circle-fill"></i></a></td><td>' + (qtyf < 0 ? inputAmount : '') + '</td><td>' + (qtyf >= 0 ? inputAmount : '') + '</td>';
|
||||
trPosting.after(trNew);
|
||||
|
||||
// Put input box in existing row
|
||||
trPosting.querySelector('.has-amount').innerHTML = inputAmount;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -40,10 +40,10 @@
|
||||
<td>{{ line.source_account }}</td>
|
||||
<td>{{ line.dt.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ line.description }}</td>
|
||||
{% if line.postings|length > 1 %}
|
||||
<td class="charge-account"><i class="bi bi-info-circle-fill" title="Complex transaction"></i></td>
|
||||
{% elif line.postings|length == 0 %}
|
||||
{% if line.postings|length == 0 %}
|
||||
<td class="charge-account"><a href="#" class="text-danger" onclick="classifyLine({{ line.id }});return false;">Unclassified</a> <a href="/statement-lines/edit-transaction?line-id={{ line.id }}" class="text-muted"><i class="bi bi-pencil"></i></a></td>
|
||||
{% elif line.is_complex() %}
|
||||
<td class="charge-account"><i>(Complex)</i></td>
|
||||
{% else %}
|
||||
<td class="charge-account"><a href="#" class="text-body" onclick="classifyLine({{ line.id }});return false;">{{ line.postings[0].transaction.charge_account(line.source_account) }}</a> <a href="/statement-lines/edit-transaction?line-id={{ line.id }}" class="text-muted"><i class="bi bi-pencil"></i></a></td>
|
||||
{% endif %}
|
||||
|
@ -36,7 +36,7 @@ def all_transactions():
|
||||
|
||||
@app.route('/general-ledger')
|
||||
def general_ledger():
|
||||
return render_template('general_ledger.html', transactions=all_transactions())
|
||||
return render_template('general_ledger.html', transactions=sorted(all_transactions(), key=lambda t: t.dt))
|
||||
|
||||
@app.route('/trial-balance')
|
||||
def trial_balance():
|
||||
|
Loading…
Reference in New Issue
Block a user