austax: Record CGT cost adjustments

This commit is contained in:
RunasSudo 2023-01-07 14:07:58 +11:00
parent 0ebbebc43b
commit 1afc48f218
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 284 additions and 56 deletions

View File

@ -16,16 +16,15 @@
from flask import render_template from flask import render_template
from drcr.models import AccountConfiguration, Amount, Posting, Transaction, TrialBalancer from drcr.models import AccountConfiguration, 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 .models import CGTAsset
from .reports import eofy_date, tax_summary_report from .reports import eofy_date, tax_summary_report
from . import views
def plugin_init(): def plugin_init():
drcr.plugins.data_sources.append(('/tax/cgt-adjustments', 'CGT adjustments'))
drcr.plugins.advanced_reports.append(('/tax/cgt-assets', 'CGT assets')) 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'))
@ -39,55 +38,6 @@ def plugin_init():
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')
def tax_summary():
report = tax_summary_report()
return render_template('report.html', report=report)
def make_tax_transactions(): def make_tax_transactions():
report = tax_summary_report() report = tax_summary_report()
tax_amount = report.by_id('total_tax').amount tax_amount = report.by_id('total_tax').amount

View File

@ -14,6 +14,7 @@
# 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 drcr.database import db
from drcr.models import Amount from drcr.models import Amount
class CGTAsset(Amount): class CGTAsset(Amount):
@ -26,8 +27,37 @@ class CGTAsset(Amount):
self.disposal_date = None self.disposal_date = None
self.disposal_value = None self.disposal_value = None
self.cost_adjustments = []
def __repr__(self): def __repr__(self):
return '<{}: {} [{:%Y-%m-%d}]>'.format(self.__class__.__name__, self.format(True), self.acquisition_date) return '<{}: {} [{:%Y-%m-%d}]>'.format(self.__class__.__name__, self.format(True), self.acquisition_date)
def commodity_name(self): def commodity_name(self):
return self.commodity[:self.commodity.index('{')].strip() return self.commodity[:self.commodity.index('{')].strip()
def cost_adjustment(self):
# TODO: brought forward vs current period
return Amount(sum(a.cost_adjustment for a in self.cost_adjustments), '$')
def gain(self):
return self.disposal_value - (self.as_cost() + self.cost_adjustment())
class CGTCostAdjustment(db.Model):
__tablename__ = 'austax_cgt_cost_adjustments'
id = db.Column(db.Integer, primary_key=True)
quantity = db.Column(db.Integer)
commodity = db.Column(db.String)
account = db.Column(db.String)
acquisition_date = db.Column(db.DateTime)
dt = db.Column(db.DateTime)
description = db.Column(db.String)
cost_adjustment = db.Column(db.Integer)
def asset(self):
return CGTAsset(self.quantity, self.commodity, self.account, self.acquisition_date)
def cost_adjustment_amount(self):
return Amount(self.cost_adjustment, '$')

View File

@ -0,0 +1,56 @@
{# 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 adjustments{% endblock %}
{% block content %}
<h1 class="h2 my-4">CGT adjustments</h1>
<div class="mb-2">
<a href="/tax/cgt-adjustments/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> New CGT adjustment</a>
</div>
<table class="table">
<thead>
<tr>
<th>Account</th>
<th>Asset</th>
<th class="text-end">Units</th>
<th>Acquisition date</th>
<th class="text-end">Acquisition value</th>
<th>Adjustment date</th>
<th>Description</th>
<th class="text-end">Cost adjustment&nbsp;</th>
</tr>
</thead>
<tbody>
{% for cgt_adjustment in cgt_adjustments %}
<tr>
<td>{{ cgt_adjustment.account }}</td>
<td>{{ cgt_adjustment.asset().commodity_name() }}</td>
<td class="text-end">{{ cgt_adjustment.asset().format('hide') }}</td>
<td>{{ cgt_adjustment.acquisition_date.strftime('%Y-%m-%d') }}</td>
<td class="text-end">{{ cgt_adjustment.asset().as_cost().format() }}</td>
<td>{{ cgt_adjustment.dt.strftime('%Y-%m-%d') }}</td>
<td>{{ cgt_adjustment.description }}</td>
<td class="text-end">{{ cgt_adjustment.cost_adjustment_amount().format_accounting() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,71 @@
{# 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 %}{{ 'Edit' if adjustment else 'New' }} CGT adjustment{% endblock %}
{% block content %}
<h1 class="h2 my-4">{{ 'Edit' if adjustment else 'New' }} CGT adjustment</h1>
<form method="POST">
<h2 class="h3">CGT asset</h2>
<div class="row mb-2">
<label class="col-sm-2 col-form-label">Acquisition date</label>
<div class="col-sm-10">
<input type="date" class="form-control" name="acquisition_date" value="{{ adjustment.acquisition_date.strftime('%Y-%m-%d') if adjustment }}">
</div>
</div>
<div class="row mb-2">
<label class="col-sm-2 col-form-label">Account</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="account" value="{{ adjustment.account if adjustment }}">
</div>
</div>
<div class="row mb-2">
<label class="col-sm-2 col-form-label">Asset</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="asset" value="{{ adjustment.asset().quantity_string() if adjustment }}">
</div>
</div>
<h2 class="h3 mt-4">CGT adjustment</h2>
<div class="row mb-2">
<label class="col-sm-2 col-form-label">Adjustment date</label>
<div class="col-sm-10">
<input type="date" class="form-control" name="dt" value="{{ adjustment.dt.strftime('%Y-%m-%d') if adjustment }}">
</div>
</div>
<div class="row mb-2">
<label class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="description" value="{{ adjustment.description if adjustment }}">
</div>
</div>
<div class="row mb-4">
<label class="col-sm-2 col-form-label">Cost adjustment</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="cost_adjustment" step="0.01" value="{{ adjustment.cost_adjustment_amount().quantity_string() if adjustment }}">
</div>
</div>
<div class="text-end">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
{% endblock %}

View File

@ -28,6 +28,7 @@
<th style="border-bottom-width:0"></th> <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">Acquisition</th>
<th style="border-left-width:1px" colspan="2">Adjustment</th>
<th style="border-left-width:1px" colspan="2">Disposal</th> <th style="border-left-width:1px" colspan="2">Disposal</th>
<th style="border-bottom-width:0;border-left-width:1px"></th> <th style="border-bottom-width:0;border-left-width:1px"></th>
</tr> </tr>
@ -37,6 +38,8 @@
<th class="text-end">Units</th> <th class="text-end">Units</th>
<th style="border-left-width:1px">Date</th> <th style="border-left-width:1px">Date</th>
<th class="text-end">Value</th> <th class="text-end">Value</th>
<th style="border-left-width:1px" class="text-end">b/f&nbsp;</th>
<th class="text-end">{{ eofy_date.year }}</th>
<th style="border-left-width:1px">Date</th> <th style="border-left-width:1px">Date</th>
<th class="text-end">Value</th> <th class="text-end">Value</th>
<th style="border-left-width:1px" class="text-end">Gain&nbsp;</th> <th style="border-left-width:1px" class="text-end">Gain&nbsp;</th>
@ -50,9 +53,11 @@
<td class="text-end">{{ asset.format('hide') }}</td> <td class="text-end">{{ asset.format('hide') }}</td>
<td style="border-left-width:1px">{{ asset.acquisition_date.strftime('%Y-%m-%d') }}</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 class="text-end">{{ asset.as_cost().format() }}</td>
<td style="border-left-width:1px" class="text-end">{{ asset.cost_adjustment().format_accounting() if asset.cost_adjustments }}</td>
<td></td>
<td style="border-left-width:1px">{{ asset.disposal_date.strftime('%Y-%m-%d') if asset.disposal_date else '' }}</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 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> <td style="border-left-width:1px" class="text-end">{% if asset.disposal_date %}{{ asset.gain().format_accounting() }}{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

110
austax/views.py Normal file
View File

@ -0,0 +1,110 @@
# 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 flask import redirect, render_template, request
from drcr.models import AccountConfiguration, Amount, Posting, Transaction
from drcr.database import db
from drcr.plugins import render_plugin_template
from drcr.webapp import app
from .models import CGTAsset, CGTCostAdjustment
from .reports import eofy_date, tax_summary_report
from datetime import datetime
@app.route('/tax/cgt-adjustments')
def cgt_adjustments():
adjustments = db.session.scalars(db.select(CGTCostAdjustment)).all()
return render_plugin_template('austax', 'cgt_adjustments.html', cgt_adjustments=adjustments)
@app.route('/tax/cgt-adjustments/new', methods=['GET', 'POST'])
def cgt_adjustment_new():
if request.method == 'GET':
return render_plugin_template('austax', 'cgt_adjustments_edit.html', adjustment=None)
asset = Amount.parse(request.form['asset'])
adjustment = CGTCostAdjustment(
quantity=asset.quantity,
commodity=asset.commodity,
account=request.form['account'],
acquisition_date=datetime.strptime(request.form['acquisition_date'], '%Y-%m-%d'),
dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'),
description=request.form['description'],
cost_adjustment=Amount.parse(request.form['cost_adjustment']).quantity
)
db.session.add(adjustment)
db.session.commit()
return redirect('/tax/cgt-adjustments')
@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
# Process CGT adjustments
for cost_adjustment in db.session.scalars(db.select(CGTCostAdjustment)).all():
asset = next((a for a in assets if a.quantity == cost_adjustment.quantity and a.commodity == cost_adjustment.commodity and a.account == cost_adjustment.account and a.acquisition_date == cost_adjustment.acquisition_date), None)
if asset is None:
raise Exception('No matching CGT asset for {}'.format(repr(cost_adjustment.asset())))
asset.cost_adjustments.append(cost_adjustment)
return render_plugin_template('austax', 'cgt_assets.html', assets=assets, eofy_date=eofy_date())
@app.route('/tax/summary')
def tax_summary():
report = tax_summary_report()
return render_template('report.html', report=report)

View File

@ -21,7 +21,9 @@ from .webapp import app
import importlib import importlib
data_sources = [] # list of tuplet (url, label)
advanced_reports = [] # list of tuplet (url, label) advanced_reports = [] # list of tuplet (url, label)
account_kinds = [ account_kinds = [
# list of tuplet (id, label) # list of tuplet (id, label)
('drcr.asset', 'Asset'), ('drcr.asset', 'Asset'),
@ -30,6 +32,7 @@ account_kinds = [
('drcr.expense', 'Expense'), ('drcr.expense', 'Expense'),
('drcr.equity', 'Equity') ('drcr.equity', 'Equity')
] ]
transaction_providers = [] # list of callable transaction_providers = [] # list of callable
def init_plugins(): def init_plugins():

View File

@ -25,6 +25,9 @@
<li><a href="/statement-lines">Statement lines</a></li> <li><a href="/statement-lines">Statement lines</a></li>
<li><a href="/balance-assertions">Balance assertions</a></li> <li><a href="/balance-assertions">Balance assertions</a></li>
<li><a href="/chart-of-accounts">Chart of accounts</a></li> <li><a href="/chart-of-accounts">Chart of accounts</a></li>
{% for report in data_sources %}
<li><a href="{{ report[0] }}">{{ report[1] }}</a></li>
{% endfor %}
</ul> </ul>
<h1 class="h2 my-4">General reports</h1> <h1 class="h2 my-4">General reports</h1>

View File

@ -18,7 +18,7 @@ from flask import redirect, render_template, request
from .database import db from .database import db
from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer
from .plugins import account_kinds, advanced_reports from .plugins import account_kinds, advanced_reports, data_sources
from .reports import balance_sheet_report, income_statement_report from .reports import balance_sheet_report, income_statement_report
from .webapp import all_transactions, app from .webapp import all_transactions, app
@ -26,7 +26,7 @@ from itertools import groupby
@app.route('/') @app.route('/')
def index(): def index():
return render_template('index.html', advanced_reports=advanced_reports) return render_template('index.html', data_sources=data_sources, advanced_reports=advanced_reports)
@app.route('/chart-of-accounts') @app.route('/chart-of-accounts')
def chart_of_accounts(): def chart_of_accounts():