diff --git a/austax/util.py b/austax/util.py
new file mode 100644
index 0000000..79ccf46
--- /dev/null
+++ b/austax/util.py
@@ -0,0 +1,31 @@
+# DrCr: Web-based double-entry bookkeeping framework
+# Copyright (C) 2022–2024 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 .
+
+from drcr.models import reporting_commodity
+
+import functools
+
+def assert_aud(f):
+ """Wrap a function to assert that the reporting_commodity is $"""
+
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ if reporting_commodity() != '$':
+ raise Exception('austax requires reporting_commodity to be $')
+
+ return f(*args, **kwargs)
+
+ return wrapper
diff --git a/austax/views.py b/austax/views.py
index abc4459..b097399 100644
--- a/austax/views.py
+++ b/austax/views.py
@@ -16,18 +16,20 @@
from flask import redirect, render_template, request, url_for
-from drcr.models import AccountConfiguration, Amount, Posting, Transaction
+from drcr.models import AccountConfiguration, Amount, Posting, Transaction, reporting_commodity
from drcr.database import db
from drcr.plugins import render_plugin_template
from drcr.webapp import all_accounts, app, eofy_date
from .models import CGTAsset, CGTCostAdjustment
from .reports import tax_summary_report
+from .util import assert_aud
from datetime import datetime
from math import copysign
@app.route('/tax/cgt-adjustments')
+@assert_aud
def cgt_adjustments():
adjustments = db.select(CGTCostAdjustment).order_by(CGTCostAdjustment.dt.desc(), CGTCostAdjustment.account, CGTCostAdjustment.id.desc())
if 'account' in request.args:
@@ -43,6 +45,7 @@ def cgt_adjustments():
return render_plugin_template('austax', 'cgt_adjustments.html', cgt_adjustments=adjustments)
@app.route('/tax/cgt-adjustments/new', methods=['GET', 'POST'])
+@assert_aud
def cgt_adjustment_new():
if request.method == 'GET':
return render_plugin_template('austax', 'cgt_adjustments_edit.html', adjustment=None, all_accounts=all_accounts())
@@ -63,6 +66,7 @@ def cgt_adjustment_new():
return redirect(url_for('cgt_adjustments'))
@app.route('/tax/cgt-adjustments/edit', methods=['GET', 'POST'])
+@assert_aud
def cgt_adjustment_edit():
if request.method == 'GET':
return render_plugin_template('austax', 'cgt_adjustments_edit.html', adjustment=db.session.get(CGTCostAdjustment, request.args['id']), all_accounts=all_accounts())
@@ -83,6 +87,7 @@ def cgt_adjustment_edit():
return redirect(url_for('cgt_adjustments'))
@app.route('/tax/cgt-adjustments/multi-new', methods=['GET', 'POST'])
+@assert_aud
def cgt_adjustment_multinew():
if request.method == 'GET':
return render_plugin_template(
@@ -172,6 +177,7 @@ def cgt_adjustment_multinew():
return redirect(url_for('cgt_adjustments'))
@app.route('/tax/cgt-assets')
+@assert_aud
def cgt_assets():
# Find all CGT asset accounts
cgt_accounts = []
@@ -192,7 +198,7 @@ def cgt_assets():
assets = []
for posting in cgt_postings:
- if posting.commodity == '$':
+ if posting.commodity == reporting_commodity():
# FIXME: Detect this better
continue
@@ -212,7 +218,7 @@ def cgt_assets():
asset.disposal_date = posting.transaction.dt
# Calculate disposal value by searching for matching asset postings
- asset.disposal_value = Amount(0, '$')
+ asset.disposal_value = Amount(0, reporting_commodity())
for other_posting in posting.transaction.postings:
if posting != other_posting and 'drcr.asset' in account_configurations.get(other_posting.account, []):
asset.disposal_value.quantity += other_posting.amount().as_cost().quantity
@@ -229,6 +235,7 @@ def cgt_assets():
return render_plugin_template('austax', 'cgt_assets.html', assets=assets, eofy_date=eofy_date())
@app.route('/tax/summary')
+@assert_aud
def tax_summary():
report = tax_summary_report()
return render_template('report.html', report=report)
diff --git a/drcr/journal/views.py b/drcr/journal/views.py
index 4ba4fa7..f8c4e40 100644
--- a/drcr/journal/views.py
+++ b/drcr/journal/views.py
@@ -18,7 +18,7 @@ from flask import abort, redirect, render_template, request, url_for
from .. import AMOUNT_DPS
from ..database import db
-from ..models import Amount, Posting, Transaction, TrialBalancer
+from ..models import Amount, Posting, Transaction, TrialBalancer, reporting_commodity
from ..webapp import all_accounts, all_transactions, app
from .models import BalanceAssertion
from ..statements.models import StatementLineReconciliation
@@ -158,7 +158,7 @@ def balance_assertions_new():
description=request.form['description'],
account=request.form['account'],
quantity=quantity,
- commodity='$'
+ commodity=reporting_commodity()
)
db.session.add(assertion)
db.session.commit()
diff --git a/drcr/models.py b/drcr/models.py
index 1da7b7f..1ba222c 100644
--- a/drcr/models.py
+++ b/drcr/models.py
@@ -1,5 +1,5 @@
# DrCr: Web-based double-entry bookkeeping framework
-# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
+# Copyright (C) 2022–2024 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
@@ -19,6 +19,7 @@ from markupsafe import Markup
from . import AMOUNT_DPS
from .database import db
+import functools
from itertools import groupby
class Transaction(db.Model):
@@ -86,7 +87,7 @@ class Amount:
if ' ' not in amount_str:
# Default commodity
quantity = round(float(amount_str) * (10**AMOUNT_DPS))
- return Amount(quantity, '$') # TODO: Customisable default commodity
+ return Amount(quantity, reporting_commodity())
quantity_str = amount_str[:amount_str.index(' ')]
quantity = round(float(quantity_str) * (10**AMOUNT_DPS))
@@ -118,7 +119,7 @@ class Amount:
if commodity not in ('non_reporting', 'force', 'hide'):
raise ValueError('Invalid commodity reporting option')
- if (self.commodity == '$' and commodity in ('non_reporting', 'force')) or commodity == 'hide':
+ if (self.commodity == reporting_commodity() and commodity in ('non_reporting', 'force')) or commodity == 'hide':
return Markup('{:,.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
elif len(self.commodity) == 1:
return Markup('{0}{1:,.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
@@ -139,7 +140,7 @@ class Amount:
return Markup('{}{}'.format(link, text, space))
def quantity_string(self):
- if self.commodity == '$':
+ if self.commodity == reporting_commodity():
return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
elif len(self.commodity) == 1:
return '{0}{1:.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
@@ -149,7 +150,7 @@ class Amount:
def as_cost(self):
"""Convert commodity to reporting currency in cost basis"""
- if self.commodity == '$':
+ if self.commodity == reporting_commodity():
return self
# TODO: Refactor this
@@ -158,10 +159,10 @@ class Amount:
cost = float(self.commodity[self.commodity.index('{{')+2:self.commodity.index('}}')])
if self.quantity < 0:
cost = -cost
- return Amount(round(cost * (10**AMOUNT_DPS)), '$')
+ return Amount(round(cost * (10**AMOUNT_DPS)), reporting_commodity())
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
+ return Amount(round(cost * self.quantity), reporting_commodity()) # FIXME: Custom reporting currency
else:
raise Exception('No cost base for commodity {}'.format(self.commodity))
@@ -199,7 +200,7 @@ class TrialBalancer:
for transaction in transactions:
for posting in transaction.postings:
if posting.account not in self.accounts:
- self.accounts[posting.account] = Amount(0, '$')
+ self.accounts[posting.account] = Amount(0, reporting_commodity())
# FIXME: Handle commodities better
self.accounts[posting.account].quantity += posting.amount().as_cost().quantity
@@ -217,7 +218,7 @@ class TrialBalancer:
return
if destination_account not in self.accounts:
- self.accounts[destination_account] = Amount(0, '$') # FIXME: Other commodities
+ self.accounts[destination_account] = Amount(0, reporting_commodity())
# FIXME: Handle commodities
self.accounts[destination_account].quantity += self.accounts[source_account].quantity
@@ -246,6 +247,9 @@ class AccountConfiguration(db.Model):
return kinds
+# ----------------
+# Metadata helpers
+
class Metadata(db.Model):
__tablename__ = 'metadata'
@@ -257,3 +261,9 @@ class Metadata(db.Model):
@staticmethod
def get(key):
return Metadata.query.filter_by(key=key).one().value
+
+@functools.cache # Very poor performance if result is not cached!
+def reporting_commodity():
+ """Get the native reporting commodity"""
+
+ return Metadata.get('reporting_commodity')
diff --git a/drcr/reports.py b/drcr/reports.py
index 0d07090..73c93da 100644
--- a/drcr/reports.py
+++ b/drcr/reports.py
@@ -1,5 +1,5 @@
# DrCr: Web-based double-entry bookkeeping framework
-# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
+# Copyright (C) 2022–2024 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
@@ -16,7 +16,7 @@
from flask import url_for
-from .models import AccountConfiguration, Amount, TrialBalancer
+from .models import AccountConfiguration, Amount, TrialBalancer, reporting_commodity
from .webapp import all_transactions, eofy_date, sofy_date
from datetime import datetime, timedelta
@@ -110,7 +110,7 @@ class Subtotal:
if self.floor:
amount = (amount // self.floor) * self.floor
- self.amount = Amount(amount, '$')
+ self.amount = Amount(amount, reporting_commodity())
class Calculated(Entry):
def __init__(self, text=None, calc=None, **kwargs):
diff --git a/drcr/statements/importers/ofx1.py b/drcr/statements/importers/ofx1.py
index b937020..fa55bff 100644
--- a/drcr/statements/importers/ofx1.py
+++ b/drcr/statements/importers/ofx1.py
@@ -1,5 +1,5 @@
# DrCr: Web-based double-entry bookkeeping framework
-# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
+# Copyright (C) 2022–2024 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
@@ -16,7 +16,7 @@
import lxml.etree as ET
-from ..models import StatementLine
+from ..models import StatementLine, reporting_commodity
from datetime import datetime
from io import StringIO
@@ -51,7 +51,7 @@ def import_ofx1(file):
dt=datetime.strptime(date, '%Y-%m-%d'),
description=description,
quantity=round(float(amount)*100),
- commodity='$'
+ commodity=reporting_commodity()
))
return imported_statement_lines
diff --git a/drcr/statements/importers/ofx2.py b/drcr/statements/importers/ofx2.py
index 5dbc5fb..9813a44 100644
--- a/drcr/statements/importers/ofx2.py
+++ b/drcr/statements/importers/ofx2.py
@@ -1,5 +1,5 @@
# DrCr: Web-based double-entry bookkeeping framework
-# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
+# Copyright (C) 2022–2024 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
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from ..models import StatementLine
+from ..models import StatementLine, reporting_commodity
from datetime import datetime
from io import StringIO
@@ -55,7 +55,7 @@ def import_ofx2(file):
dt=datetime.strptime(date, '%Y-%m-%d'),
description=description,
quantity=round(float(amount)*100),
- commodity='$'
+ commodity=reporting_commodity()
))
return imported_statement_lines
diff --git a/drcr/templates/journal/balance_assertions_edit.html b/drcr/templates/journal/balance_assertions_edit.html
index 57bf901..2aa9bcb 100644
--- a/drcr/templates/journal/balance_assertions_edit.html
+++ b/drcr/templates/journal/balance_assertions_edit.html
@@ -42,7 +42,7 @@
- $
+ {{ reporting_commodity }}
{# TODO: Display existing credit assertion as credit, not as negative debit #}
diff --git a/drcr/templates/journal/journal_edit_transaction.html b/drcr/templates/journal/journal_edit_transaction.html
index b090276..931b641 100644
--- a/drcr/templates/journal/journal_edit_transaction.html
+++ b/drcr/templates/journal/journal_edit_transaction.html
@@ -47,7 +47,8 @@
- $
+ {# FIXME: Gracefully handle when the reporting commodity is not a single character #}
+ {{ reporting_commodity }}
diff --git a/drcr/views.py b/drcr/views.py
index eea17a6..c91ade8 100644
--- a/drcr/views.py
+++ b/drcr/views.py
@@ -17,7 +17,7 @@
from flask import redirect, render_template, request, url_for
from .database import db
-from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer
+from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer, reporting_commodity
from .plugins import account_kinds, advanced_reports, data_sources
from .reports import balance_sheet_report, income_statement_report
from .webapp import all_transactions, app
@@ -72,8 +72,8 @@ def trial_balance():
balancer = TrialBalancer()
balancer.apply_transactions(all_transactions())
- total_dr = Amount(sum(v.quantity for v in balancer.accounts.values() if v.quantity > 0), '$')
- total_cr = Amount(sum(v.quantity for v in balancer.accounts.values() if v.quantity < 0), '$')
+ total_dr = Amount(sum(v.quantity for v in balancer.accounts.values() if v.quantity > 0), reporting_commodity())
+ total_cr = Amount(sum(v.quantity for v in balancer.accounts.values() if v.quantity < 0), reporting_commodity())
return render_template('trial_balance.html', accounts=dict(sorted(balancer.accounts.items())), total_dr=total_dr, total_cr=total_cr)
@@ -105,7 +105,7 @@ def account_transactions():
# Pre-compute running totals
# There can be more than one posting per account per transaction, so track the running total at the level of individual postings
running_totals = {}
- running_total = Amount(0, '$')
+ running_total = Amount(0, reporting_commodity())
for transaction in sorted(transactions, key=lambda t: t.dt):
for posting in transaction.postings:
if posting.account == request.args['account']:
diff --git a/drcr/webapp.py b/drcr/webapp.py
index 87436ae..8b2324f 100644
--- a/drcr/webapp.py
+++ b/drcr/webapp.py
@@ -1,5 +1,5 @@
# DrCr: Web-based double-entry bookkeeping framework
-# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
+# Copyright (C) 2022–2024 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
@@ -22,7 +22,7 @@ app.config.from_file('config.toml', load=toml.load)
from flask_sqlalchemy.record_queries import get_recorded_queries
from .database import db
-from .models import Metadata, Transaction
+from .models import Metadata, Transaction, reporting_commodity
from .plugins import init_plugins, transaction_providers
from .statements.models import StatementLine
@@ -84,6 +84,10 @@ def initdb():
# FIXME: Need to init metadata
+@app.context_processor
+def add_reporting_commodity():
+ return dict(reporting_commodity=reporting_commodity())
+
if app.debug:
@app.before_request
def before_request():