austax: Basic CGT asset register

This commit is contained in:
RunasSudo 2023-01-07 02:05:21 +11:00
parent 66b3ddff54
commit 0ebbebc43b
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 165 additions and 12 deletions

View File

@ -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()

33
austax/models.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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()

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% extends 'base.html' %}
{% block title %}CGT assets{% endblock %}
{% block content %}
<h1 class="h2 mt-4">CGT assets</h1>
<table class="table">
<thead>
<tr>
<th style="border-bottom-width:0"></th>
<th style="border-bottom-width:0"></th>
<th style="border-bottom-width:0"></th>
<th style="border-left-width:1px" colspan="2">Acquisition</th>
<th style="border-left-width:1px" colspan="2">Disposal</th>
<th style="border-bottom-width:0;border-left-width:1px"></th>
</tr>
<tr>
<th>Account</th>
<th>Asset</th>
<th class="text-end">Units</th>
<th style="border-left-width:1px">Date</th>
<th class="text-end">Value</th>
<th style="border-left-width:1px">Date</th>
<th class="text-end">Value</th>
<th style="border-left-width:1px" class="text-end">Gain&nbsp;</th>
</tr>
</thead>
<tbody>
{% for asset in assets %}
<tr>
<td>{{ asset.account }}</td>
<td>{{ asset.commodity_name() }}</td>
<td class="text-end">{{ asset.format('hide') }}</td>
<td style="border-left-width:1px">{{ asset.acquisition_date.strftime('%Y-%m-%d') }}</td>
<td class="text-end">{{ asset.as_cost().format() }}</td>
<td style="border-left-width:1px">{{ asset.disposal_date.strftime('%Y-%m-%d') if asset.disposal_date else '' }}</td>
<td class="text-end">{{ asset.disposal_value.format() if asset.disposal_value else '' }}</td>
<td style="border-left-width:1px" class="text-end">{% if asset.disposal_date %}{{ (asset.disposal_value - asset.as_cost()).format_accounting() }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -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(',', '&#x202F;'))
elif len(self.commodity) == 1:
return Markup('{0}{1:,.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', '&#x202F;'))

View File

@ -14,6 +14,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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)

View File

@ -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 @@
<td class="text-end"><i>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</i></td>
<td>{{ posting.account }}</td>
{% if commodity_detail %}
<td class="text-end">{{ posting.amount().format(True) if posting.quantity >= 0 else '' }}</td>
<td class="text-end">{{ (posting.amount()|abs).format(True) if posting.quantity < 0 else '' }}</td>
<td class="text-end">{{ posting.amount().format('force') if posting.quantity >= 0 else '' }}</td>
<td class="text-end">{{ (posting.amount()|abs).format('force') if posting.quantity < 0 else '' }}</td>
{% else %}
<td class="text-end">{{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }}</td>
<td class="text-end">{{ (posting.amount()|abs).as_cost().format() if posting.quantity < 0 else '' }}</td>

View File

@ -54,8 +54,8 @@
<td class="text-end"><i>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</i></td>
<td>{{ posting.account }}</td>
{% if commodity_detail %}
<td class="text-end">{{ posting.amount().format(True) if posting.quantity >= 0 else '' }}</td>
<td class="text-end">{{ (posting.amount()|abs).format(True) if posting.quantity < 0 else '' }}</td>
<td class="text-end">{{ posting.amount().format('force') if posting.quantity >= 0 else '' }}</td>
<td class="text-end">{{ (posting.amount()|abs).format('force') if posting.quantity < 0 else '' }}</td>
{% else %}
<td class="text-end">{{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }}</td>
<td class="text-end">{{ (posting.amount()|abs).as_cost().format() if posting.quantity < 0 else '' }}</td>

View File

@ -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 @@
<td></td>
<td></td>
<td>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</td>
<td class="text-end">{{ (posting.amount()|abs).format(True) }}</td>
<td class="text-end">{{ (amount|abs).format(True) }}</td>
<td class="text-end">{{ (posting.amount()|abs).format('force') }}</td>
<td class="text-end">{{ (amount|abs).format('force') }}</td>
<td>{{ 'Dr' if amount.quantity >= 0 else 'Cr' }}</td>
</tr>
{% else %}
@ -69,7 +69,7 @@
<td></td>
<td></td>
<td></td>
<td class="text-end">{{ (amount|abs).format(True) }}</td>
<td class="text-end">{{ (amount|abs).format('force') }}</td>
<td>{{ 'Dr' if amount.quantity >= 0 else 'Cr' }}</td>
</tr>
{% endfor %}