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 drcr.models import AccountConfiguration, Amount, Posting, Transaction, TrialBalancer
from drcr.models import AccountConfiguration, 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
from . import views
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/summary', 'Tax summary'))
@ -39,55 +38,6 @@ def plugin_init():
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():
report = tax_summary_report()
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
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from drcr.database import db
from drcr.models import Amount
class CGTAsset(Amount):
@ -26,8 +27,37 @@ class CGTAsset(Amount):
self.disposal_date = None
self.disposal_value = None
self.cost_adjustments = []
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()
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-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-bottom-width:0;border-left-width:1px"></th>
</tr>
@ -37,6 +38,8 @@
<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" class="text-end">b/f&nbsp;</th>
<th class="text-end">{{ eofy_date.year }}</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>
@ -50,9 +53,11 @@
<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" 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 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>
{% endfor %}
</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
data_sources = [] # list of tuplet (url, label)
advanced_reports = [] # list of tuplet (url, label)
account_kinds = [
# list of tuplet (id, label)
('drcr.asset', 'Asset'),
@ -30,6 +32,7 @@ account_kinds = [
('drcr.expense', 'Expense'),
('drcr.equity', 'Equity')
]
transaction_providers = [] # list of callable
def init_plugins():

View File

@ -25,6 +25,9 @@
<li><a href="/statement-lines">Statement lines</a></li>
<li><a href="/balance-assertions">Balance assertions</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>
<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 .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 .webapp import all_transactions, app
@ -26,7 +26,7 @@ from itertools import groupby
@app.route('/')
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')
def chart_of_accounts():