austax: Basic CGT asset register
This commit is contained in:
parent
66b3ddff54
commit
0ebbebc43b
@ -16,14 +16,17 @@
|
|||||||
|
|
||||||
from flask import render_template
|
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
|
from drcr.database import db
|
||||||
import drcr.plugins
|
import drcr.plugins
|
||||||
|
from drcr.plugins import render_plugin_template
|
||||||
from drcr.webapp import app
|
from drcr.webapp import app
|
||||||
|
|
||||||
|
from .models import CGTAsset
|
||||||
from .reports import eofy_date, tax_summary_report
|
from .reports import eofy_date, tax_summary_report
|
||||||
|
|
||||||
def plugin_init():
|
def plugin_init():
|
||||||
|
drcr.plugins.advanced_reports.append(('/tax/cgt-assets', 'CGT assets'))
|
||||||
drcr.plugins.advanced_reports.append(('/tax/summary', 'Tax summary'))
|
drcr.plugins.advanced_reports.append(('/tax/summary', 'Tax summary'))
|
||||||
|
|
||||||
drcr.plugins.account_kinds.append(('austax.income1', 'Salary or wages (1)'))
|
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.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.d5', 'Other work-related expenses (D5)'))
|
||||||
drcr.plugins.account_kinds.append(('austax.paygw', 'PAYG withheld amounts'))
|
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)
|
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')
|
@app.route('/tax/summary')
|
||||||
def tax_summary():
|
def tax_summary():
|
||||||
report = tax_summary_report()
|
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:]
|
commodity = amount_str[amount_str.index(' ')+1:]
|
||||||
return Amount(quantity, commodity)
|
return Amount(quantity, commodity)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{}: {}>'.format(self.__class__.__name__, self.format('force'))
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return Amount(abs(self.quantity), self.commodity)
|
return Amount(abs(self.quantity), self.commodity)
|
||||||
|
|
||||||
@ -108,8 +111,11 @@ class Amount:
|
|||||||
def __sub__(self, other):
|
def __sub__(self, other):
|
||||||
return self + (-other)
|
return self + (-other)
|
||||||
|
|
||||||
def format(self, force_commodity=False):
|
def format(self, commodity='non_reporting'):
|
||||||
if self.commodity == '$' and not force_commodity:
|
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(',', ' '))
|
return Markup('{:,.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
|
||||||
elif len(self.commodity) == 1:
|
elif len(self.commodity) == 1:
|
||||||
return Markup('{0}{1:,.{dps}f}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
|
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
|
# 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/>.
|
# 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
|
from .webapp import app
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
@ -33,3 +36,6 @@ def init_plugins():
|
|||||||
for plugin in app.config['PLUGINS']:
|
for plugin in app.config['PLUGINS']:
|
||||||
module = importlib.import_module(plugin)
|
module = importlib.import_module(plugin)
|
||||||
module.plugin_init()
|
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
|
{# 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
|
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
|
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 class="text-end"><i>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</i></td>
|
||||||
<td>{{ posting.account }}</td>
|
<td>{{ posting.account }}</td>
|
||||||
{% if commodity_detail %}
|
{% if commodity_detail %}
|
||||||
<td class="text-end">{{ posting.amount().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(True) if posting.quantity < 0 else '' }}</td>
|
<td class="text-end">{{ (posting.amount()|abs).format('force') if posting.quantity < 0 else '' }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="text-end">{{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }}</td>
|
<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>
|
<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 class="text-end"><i>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</i></td>
|
||||||
<td>{{ posting.account }}</td>
|
<td>{{ posting.account }}</td>
|
||||||
{% if commodity_detail %}
|
{% if commodity_detail %}
|
||||||
<td class="text-end">{{ posting.amount().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(True) if posting.quantity < 0 else '' }}</td>
|
<td class="text-end">{{ (posting.amount()|abs).format('force') if posting.quantity < 0 else '' }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="text-end">{{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }}</td>
|
<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>
|
<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
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -59,8 +59,8 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</td>
|
<td>{{ 'Dr' if posting.quantity >= 0 else 'Cr' }}</td>
|
||||||
<td class="text-end">{{ (posting.amount()|abs).format(True) }}</td>
|
<td class="text-end">{{ (posting.amount()|abs).format('force') }}</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>
|
<td>{{ 'Dr' if amount.quantity >= 0 else 'Cr' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -69,7 +69,7 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<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>
|
<td>{{ 'Dr' if amount.quantity >= 0 else 'Cr' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Loading…
Reference in New Issue
Block a user