From 0ebbebc43bd656c2acfe208cfcb2140779f40189 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 7 Jan 2023 02:05:21 +1100 Subject: [PATCH] austax: Basic CGT asset register --- austax/__init__.py | 50 +++++++++++++++- austax/models.py | 33 ++++++++++ austax/templates/cgt_assets.html | 60 +++++++++++++++++++ drcr/models.py | 10 +++- drcr/plugins.py | 6 ++ drcr/templates/general_ledger.html | 6 +- drcr/templates/journal/journal.html | 4 +- .../transactions_commodity_detail.html | 8 +-- 8 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 austax/models.py create mode 100644 austax/templates/cgt_assets.html 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

+ + + + + + + + + + + + + + + + + + + + + + + + {% for asset in assets %} + + + + + + + + + + + {% endfor %} + +
AcquisitionDisposal
AccountAssetUnitsDateValueDateValueGain 
{{ 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 %}
+{% 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 %}