Basic handling of multiple commodities

This commit is contained in:
RunasSudo 2022-12-24 19:45:34 +11:00
parent f76e0d3dcb
commit 5d13e5cbef
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 51 additions and 16 deletions

View File

@ -20,7 +20,7 @@ from .. import AMOUNT_DPS
from ..database import db_session from ..database import db_session
from ..models import TrialBalancer from ..models import TrialBalancer
from ..webapp import all_transactions, app from ..webapp import all_transactions, app
from .models import BalanceAssertion, GeneralJournalPosting, GeneralJournalTransaction from .models import Amount, BalanceAssertion, GeneralJournalPosting, GeneralJournalTransaction
from datetime import datetime from datetime import datetime
@ -40,15 +40,15 @@ def general_journal_new():
postings=[] postings=[]
) )
for account, sign, amount in zip(request.form.getlist('account'), request.form.getlist('sign'), request.form.getlist('amount')): for account, sign, amount_str in zip(request.form.getlist('account'), request.form.getlist('sign'), request.form.getlist('amount')):
quantity = round(float(amount) * (10**AMOUNT_DPS)) amount = Amount.parse(amount_str)
if sign == 'cr': if sign == 'cr':
quantity = -quantity amount = -amount
posting = GeneralJournalPosting( posting = GeneralJournalPosting(
account=account, account=account,
quantity=quantity, quantity=amount.quantity,
commodity='$' # TODO: Commodities commodity=amount.commodity
) )
transaction.postings.append(posting) transaction.postings.append(posting)

View File

@ -25,8 +25,7 @@ class Transaction:
def assert_valid(self): def assert_valid(self):
"""Assert that debits equal credits, and commodities are compatible""" """Assert that debits equal credits, and commodities are compatible"""
if any(p.commodity != '$' for p in self.postings): if any(p.commodity != self.postings[0].commodity for p in self.postings[1:]):
# FIXME: Allow non-$ commodities
raise AssertionError('Transaction contains multiple commodities') raise AssertionError('Transaction contains multiple commodities')
if sum(p.quantity for p in self.postings) != 0: if sum(p.quantity for p in self.postings) != 0:
@ -49,15 +48,51 @@ class Amount:
self.quantity = quantity self.quantity = quantity
self.commodity = commodity self.commodity = commodity
@classmethod
def parse(self, amount_str):
if ' ' not in amount_str:
# Default commodity
quantity = round(float(amount_str) * (10**AMOUNT_DPS))
return Amount(quantity, '$') # TODO: Customisable default commodity
quantity_str = amount_str[:amount_str.index(' ')]
quantity = round(float(quantity_str) * (10**AMOUNT_DPS))
commodity = amount_str[amount_str.index(' ')+1:]
return Amount(quantity, commodity)
def __abs__(self): def __abs__(self):
return Amount(abs(self.quantity), self.commodity) return Amount(abs(self.quantity), self.commodity)
def __neg__(self):
return Amount(-self.quantity, self.commodity)
def format(self): def format(self):
if len(self.commodity) == 1:
return '{0}{1:,.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS) return '{0}{1:,.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
else:
return '{1:,.{dps}f} {0}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
def quantity_string(self): def quantity_string(self):
return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS) return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
def as_cost(self):
"""Convert commodity to reporting currency in cost basis"""
if self.commodity == '$':
return self
# TODO: Refactor this
if '{{' in self.commodity:
cost = float(self.commodity[self.commodity.index('{{')+2:self.commodity.index('}}')])
return Amount(round(cost * (10**AMOUNT_DPS)), '$')
elif '{' in self.commodity:
cost = float(self.commodity[self.commodity.index('{')+1:self.commodity.index('}')])
return Amount(round(cost * self.quantity), '$') # FIXME: Custom reporting currency
else:
raise Exception('No cost base for commodity {}'.format(self.commodity))
class TrialBalancer: class TrialBalancer:
""" """
Applies transactions to generate a trial balance Applies transactions to generate a trial balance
@ -70,10 +105,10 @@ class TrialBalancer:
for transaction in transactions: for transaction in transactions:
for posting in transaction.postings: for posting in transaction.postings:
if posting.account not in self.accounts: if posting.account not in self.accounts:
self.accounts[posting.account] = Amount(0, '$') # FIXME: Other commodities self.accounts[posting.account] = Amount(0, '$')
# FIXME: Handle commodities # FIXME: Handle commodities better
self.accounts[posting.account].quantity += posting.quantity self.accounts[posting.account].quantity += posting.amount().as_cost().quantity
def transfer_balance(self, source_account, destination_account, description=None): def transfer_balance(self, source_account, destination_account, description=None):
"""Transfer the balance of the source account to the destination account""" """Transfer the balance of the source account to the destination account"""

View File

@ -54,7 +54,7 @@
<td> <td>
<div class="input-group"> <div class="input-group">
<div class="input-group-text">$</div> <div class="input-group-text">$</div>
<input type="number" name="amount" step="0.01" value="" class="form-control"> <input type="text" name="amount" value="" class="form-control">
</div> </div>
<td></td> <td></td>
</tr> </tr>
@ -74,7 +74,7 @@
<td> <td>
<div class="input-group"> <div class="input-group">
<div class="input-group-text">$</div> <div class="input-group-text">$</div>
<input type="number" name="amount" step="0.01" value="" class="form-control"> <input type="text" name="amount" value="" class="form-control">
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -37,7 +37,7 @@
{% for transaction in transactions %} {% for transaction in transactions %}
{% if transaction.postings|length == 2 %} {% if transaction.postings|length == 2 %}
{% for posting in transaction.postings if posting.account == account %} {% for posting in transaction.postings if posting.account == account %}
{% set _ = running_total.__setattr__('quantity', running_total.quantity + posting.quantity) %} {% set _ = running_total.__setattr__('quantity', running_total.quantity + posting.amount().as_cost().quantity) %}
<tr> <tr>
<td>{{ transaction.dt.strftime('%Y-%m-%d') }}</td> <td>{{ transaction.dt.strftime('%Y-%m-%d') }}</td>
<td>{{ transaction.description }}</td> <td>{{ transaction.description }}</td>
@ -58,7 +58,7 @@
<td></td> <td></td>
</tr> </tr>
{% for posting in transaction.postings if posting.account == account %} {% for posting in transaction.postings if posting.account == account %}
{% set _ = running_total.__setattr__('quantity', running_total.quantity + posting.quantity) %} {% set _ = running_total.__setattr__('quantity', running_total.quantity + posting.amount().as_cost().quantity) %}
<tr> <tr>
<td></td> <td></td>
<td class="text-end"><i>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</i></td> <td class="text-end"><i>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</i></td>