diff --git a/austax/__init__.py b/austax/__init__.py
index a8b6d8d..b88e43a 100644
--- a/austax/__init__.py
+++ b/austax/__init__.py
@@ -16,14 +16,17 @@
from flask import render_template
-from drcr.models import AccountConfiguration, Posting, Transaction, TrialBalancer
+from drcr.models import AccountConfiguration, Amount, Posting, Transaction, TrialBalancer
from drcr.database import db
import drcr.plugins
+from drcr.plugins import render_plugin_template
from drcr.webapp import app
+from .models import CGTAsset
from .reports import eofy_date, tax_summary_report
def plugin_init():
+ drcr.plugins.advanced_reports.append(('/tax/cgt-assets', 'CGT assets'))
drcr.plugins.advanced_reports.append(('/tax/summary', 'Tax summary'))
drcr.plugins.account_kinds.append(('austax.income1', 'Salary or wages (1)'))
@@ -32,9 +35,54 @@ def plugin_init():
drcr.plugins.account_kinds.append(('austax.d4', 'Work-related self-education expenses (D4)'))
drcr.plugins.account_kinds.append(('austax.d5', 'Other work-related expenses (D5)'))
drcr.plugins.account_kinds.append(('austax.paygw', 'PAYG withheld amounts'))
+ drcr.plugins.account_kinds.append(('austax.cgtasset', 'CGT asset'))
drcr.plugins.transaction_providers.append(make_tax_transactions)
+@app.route('/tax/cgt-assets')
+def cgt_assets():
+ # Find all CGT asset accounts
+ cgt_accounts = []
+ account_configurations = AccountConfiguration.get_all_kinds()
+ for account_name, kinds in account_configurations.items():
+ if 'austax.cgtasset' in kinds:
+ cgt_accounts.append(account_name)
+
+ # Get all postings to CGT asset accounts
+ cgt_postings = db.session.scalars(
+ db.select(Posting)
+ .where(Posting.account.in_(cgt_accounts))
+ .join(Posting.transaction)
+ .order_by(Transaction.dt)
+ ).all()
+
+ # Process postings to determine final balances
+ assets = []
+
+ for posting in cgt_postings:
+ if posting.quantity >= 0:
+ assets.append(CGTAsset(posting.quantity, posting.commodity, posting.account, posting.transaction.dt))
+ elif posting.quantity < 0:
+ asset = next((a for a in assets if a.commodity == posting.commodity and a.account == posting.account), None)
+
+ if asset is None:
+ raise Exception('Attempted credit {} without preceding debit balance'.quantity(posting.amount()))
+ if asset.quantity + posting.quantity < 0:
+ raise Exception('Attempted credit {} with insufficient debit balance {}'.quantity(posting.amount(), asset.amount()))
+
+ if asset.quantity + posting.quantity != 0:
+ raise NotImplementedError('Partial disposal of CGT asset not implemented')
+
+ asset.disposal_date = posting.transaction.dt
+
+ # Calculate disposal value by searching for matching asset postings
+ asset.disposal_value = Amount(0, '$')
+ 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
+
+ return render_plugin_template('austax', 'cgt_assets.html', assets=assets)
+
@app.route('/tax/summary')
def tax_summary():
report = tax_summary_report()
diff --git a/austax/models.py b/austax/models.py
new file mode 100644
index 0000000..2b844d8
--- /dev/null
+++ b/austax/models.py
@@ -0,0 +1,33 @@
+# DrCr: Web-based double-entry bookkeeping framework
+# Copyright (C) 2022–2023 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 Amount
+
+class CGTAsset(Amount):
+ def __init__(self, quantity, commodity, account, acquisition_date):
+ super().__init__(quantity, commodity)
+
+ self.account = account
+ self.acquisition_date = acquisition_date
+
+ self.disposal_date = None
+ self.disposal_value = None
+
+ def __repr__(self):
+ return '<{}: {} [{:%Y-%m-%d}]>'.format(self.__class__.__name__, self.format(True), self.acquisition_date)
+
+ def commodity_name(self):
+ return self.commodity[:self.commodity.index('{')].strip()
diff --git a/austax/templates/cgt_assets.html b/austax/templates/cgt_assets.html
new file mode 100644
index 0000000..112107a
--- /dev/null
+++ b/austax/templates/cgt_assets.html
@@ -0,0 +1,60 @@
+{# DrCr: Web-based double-entry bookkeeping framework
+ Copyright (C) 2022–2023 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 .
+#}
+
+{% extends 'base.html' %}
+{% block title %}CGT assets{% endblock %}
+
+{% block content %}
+
CGT assets
+
+
+
+
+ |
+ |
+ |
+ Acquisition |
+ Disposal |
+ |
+
+
+ Account |
+ Asset |
+ Units |
+ Date |
+ Value |
+ Date |
+ Value |
+ Gain |
+
+
+
+ {% for asset in assets %}
+
+ {{ asset.account }} |
+ {{ asset.commodity_name() }} |
+ {{ asset.format('hide') }} |
+ {{ asset.acquisition_date.strftime('%Y-%m-%d') }} |
+ {{ asset.as_cost().format() }} |
+ {{ asset.disposal_date.strftime('%Y-%m-%d') if asset.disposal_date else '' }} |
+ {{ asset.disposal_value.format() if asset.disposal_value else '' }} |
+ {% if asset.disposal_date %}{{ (asset.disposal_value - asset.as_cost()).format_accounting() }}{% endif %} |
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/drcr/models.py b/drcr/models.py
index 1a4736e..0dd6662 100644
--- a/drcr/models.py
+++ b/drcr/models.py
@@ -94,6 +94,9 @@ class Amount:
commodity = amount_str[amount_str.index(' ')+1:]
return Amount(quantity, commodity)
+ def __repr__(self):
+ return '<{}: {}>'.format(self.__class__.__name__, self.format('force'))
+
def __abs__(self):
return Amount(abs(self.quantity), self.commodity)
@@ -108,8 +111,11 @@ class Amount:
def __sub__(self, other):
return self + (-other)
- def format(self, force_commodity=False):
- if self.commodity == '$' and not force_commodity:
+ def format(self, commodity='non_reporting'):
+ 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':
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(',', ' '))
diff --git a/drcr/plugins.py b/drcr/plugins.py
index 712b8fc..fa48622 100644
--- a/drcr/plugins.py
+++ b/drcr/plugins.py
@@ -14,6 +14,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from flask import render_template
+from jinja2 import PackageLoader
+
from .webapp import app
import importlib
@@ -33,3 +36,6 @@ def init_plugins():
for plugin in app.config['PLUGINS']:
module = importlib.import_module(plugin)
module.plugin_init()
+
+def render_plugin_template(plugin_name, template_path, **kwargs):
+ return render_template(PackageLoader(plugin_name).load(app.jinja_env, template_path, app.jinja_env.globals), **kwargs)
diff --git a/drcr/templates/general_ledger.html b/drcr/templates/general_ledger.html
index d4edcbe..edcdbab 100644
--- a/drcr/templates/general_ledger.html
+++ b/drcr/templates/general_ledger.html
@@ -1,5 +1,5 @@
{# DrCr: Web-based double-entry bookkeeping framework
- Copyright (C) 2022 Lee Yingtong Li (RunasSudo)
+ Copyright (C) 2022–2023 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
@@ -53,8 +53,8 @@
{{ 'Dr' if posting.quantity >= 0 else 'Cr' }} |
{{ posting.account }} |
{% if commodity_detail %}
- {{ posting.amount().format(True) if posting.quantity >= 0 else '' }} |
- {{ (posting.amount()|abs).format(True) if posting.quantity < 0 else '' }} |
+ {{ posting.amount().format('force') if posting.quantity >= 0 else '' }} |
+ {{ (posting.amount()|abs).format('force') if posting.quantity < 0 else '' }} |
{% else %}
{{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }} |
{{ (posting.amount()|abs).as_cost().format() if posting.quantity < 0 else '' }} |
diff --git a/drcr/templates/journal/journal.html b/drcr/templates/journal/journal.html
index 0c251c9..84b9a49 100644
--- a/drcr/templates/journal/journal.html
+++ b/drcr/templates/journal/journal.html
@@ -54,8 +54,8 @@
{{ 'Dr' if posting.quantity >= 0 else 'Cr' }} |
{{ posting.account }} |
{% if commodity_detail %}
- {{ posting.amount().format(True) if posting.quantity >= 0 else '' }} |
- {{ (posting.amount()|abs).format(True) if posting.quantity < 0 else '' }} |
+ {{ posting.amount().format('force') if posting.quantity >= 0 else '' }} |
+ {{ (posting.amount()|abs).format('force') if posting.quantity < 0 else '' }} |
{% else %}
{{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }} |
{{ (posting.amount()|abs).as_cost().format() if posting.quantity < 0 else '' }} |
diff --git a/drcr/templates/transactions_commodity_detail.html b/drcr/templates/transactions_commodity_detail.html
index 988c42d..35db3d6 100644
--- a/drcr/templates/transactions_commodity_detail.html
+++ b/drcr/templates/transactions_commodity_detail.html
@@ -1,5 +1,5 @@
{# DrCr: Web-based double-entry bookkeeping framework
- Copyright (C) 2022 Lee Yingtong Li (RunasSudo)
+ Copyright (C) 2022–2023 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
@@ -59,8 +59,8 @@
|
|
{{ 'Dr' if posting.quantity >= 0 else 'Cr' }} |
- {{ (posting.amount()|abs).format(True) }} |
- {{ (amount|abs).format(True) }} |
+ {{ (posting.amount()|abs).format('force') }} |
+ {{ (amount|abs).format('force') }} |
{{ 'Dr' if amount.quantity >= 0 else 'Cr' }} |
{% else %}
@@ -69,7 +69,7 @@
|
|
|
- {{ (amount|abs).format(True) }} |
+ {{ (amount|abs).format('force') }} |
{{ 'Dr' if amount.quantity >= 0 else 'Cr' }} |
{% endfor %}