austax: Basic CGT asset register
This commit is contained in:
parent
66b3ddff54
commit
0ebbebc43b
@ -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
33
austax/models.py
Normal 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()
|
60
austax/templates/cgt_assets.html
Normal file
60
austax/templates/cgt_assets.html
Normal 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 </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 %}
|
@ -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(',', ' '))
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user