austax: Record CGT cost adjustments
This commit is contained in:
parent
0ebbebc43b
commit
1afc48f218
@ -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
|
||||
|
@ -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):
|
||||
@ -25,9 +26,38 @@ 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, '$')
|
||||
|
56
austax/templates/cgt_adjustments.html
Normal file
56
austax/templates/cgt_adjustments.html
Normal 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 </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 %}
|
71
austax/templates/cgt_adjustments_edit.html
Normal file
71
austax/templates/cgt_adjustments_edit.html
Normal 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 %}
|
@ -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 </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 </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
110
austax/views.py
Normal 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)
|
@ -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():
|
||||
|
@ -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>
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user