austax: Implement helper utility to distribute CGT cost adjustment across multiple assets
This commit is contained in:
parent
9ad5b7642e
commit
a4faca5ea8
@ -30,7 +30,7 @@ class CGTAsset(Amount):
|
|||||||
self.cost_adjustments = []
|
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('force'), 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()
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<div class="mb-2">
|
<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>
|
<a href="/tax/cgt-adjustments/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> New CGT adjustment</a>
|
||||||
|
<a href="/tax/cgt-adjustments/multi-new" class="btn btn-outline-primary"><i class="bi bi-plus-lg"></i> Multiple CGT adjustments</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
69
austax/templates/cgt_adjustments_multinew.html
Normal file
69
austax/templates/cgt_adjustments_multinew.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{# 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 %}Multiple CGT adjustments{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="h2 my-4">Multiple CGT adjustments</h1>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<h2 class="h3">CGT assets</h2>
|
||||||
|
|
||||||
|
<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="{{ account or '' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<label class="col-sm-2 col-form-label">Commodity</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" name="commodity" value="{{ commodity or '' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
The total cost adjustment will be distributed proportionally across all matching CGT assets.
|
||||||
|
</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="{{ dt or '' }}">
|
||||||
|
</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="{{ description or '' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<label class="col-sm-2 col-form-label">Total cost adjustment</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="number" class="form-control" name="cost_adjustment" step="0.01" value="{{ cost_adjustment or '' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -25,10 +25,11 @@ from .models import CGTAsset, CGTCostAdjustment
|
|||||||
from .reports import eofy_date, tax_summary_report
|
from .reports import eofy_date, tax_summary_report
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from math import copysign
|
||||||
|
|
||||||
@app.route('/tax/cgt-adjustments')
|
@app.route('/tax/cgt-adjustments')
|
||||||
def cgt_adjustments():
|
def cgt_adjustments():
|
||||||
adjustments = db.session.scalars(db.select(CGTCostAdjustment)).all()
|
adjustments = db.session.scalars(db.select(CGTCostAdjustment).order_by(CGTCostAdjustment.dt, CGTCostAdjustment.account)).all()
|
||||||
return render_plugin_template('austax', 'cgt_adjustments.html', cgt_adjustments=adjustments)
|
return render_plugin_template('austax', 'cgt_adjustments.html', cgt_adjustments=adjustments)
|
||||||
|
|
||||||
@app.route('/tax/cgt-adjustments/new', methods=['GET', 'POST'])
|
@app.route('/tax/cgt-adjustments/new', methods=['GET', 'POST'])
|
||||||
@ -71,6 +72,92 @@ def cgt_adjustment_edit():
|
|||||||
|
|
||||||
return redirect('/tax/cgt-adjustments')
|
return redirect('/tax/cgt-adjustments')
|
||||||
|
|
||||||
|
@app.route('/tax/cgt-adjustments/multi-new', methods=['GET', 'POST'])
|
||||||
|
def cgt_adjustment_multinew():
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_plugin_template(
|
||||||
|
'austax', 'cgt_adjustments_multinew.html',
|
||||||
|
account=None,
|
||||||
|
commodity=None,
|
||||||
|
dt=None,
|
||||||
|
description=None,
|
||||||
|
cost_adjustment=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Preview mode?
|
||||||
|
|
||||||
|
total_adjustment = Amount.parse(request.form['cost_adjustment']).quantity
|
||||||
|
|
||||||
|
# Get all postings to the CGT asset account
|
||||||
|
cgt_postings = db.session.scalars(
|
||||||
|
db.select(Posting)
|
||||||
|
.where(Posting.account == request.form['account'])
|
||||||
|
.join(Posting.transaction)
|
||||||
|
.order_by(Transaction.dt)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Process postings to determine final balances
|
||||||
|
assets = []
|
||||||
|
|
||||||
|
for posting in cgt_postings:
|
||||||
|
if posting.commodity[:posting.commodity.index('{')].strip() != request.form['commodity']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
assets.remove(asset)
|
||||||
|
|
||||||
|
# Distribute total adjustment across matching assets
|
||||||
|
total_quantity = sum(a.quantity for a in assets)
|
||||||
|
cgt_adjustments = {}
|
||||||
|
|
||||||
|
for asset in assets:
|
||||||
|
cgt_adjustments[asset] = total_adjustment * asset.quantity / total_quantity
|
||||||
|
|
||||||
|
# Round up as many as required to equal the total adjustment
|
||||||
|
rounding_shortfall = abs(total_adjustment) - sum(int(abs(v)) for v in cgt_adjustments.values())
|
||||||
|
largest_remainders = [(k, abs(v) - int(abs(v))) for k, v in cgt_adjustments.items()]
|
||||||
|
largest_remainders.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
for asset, _ in largest_remainders[:rounding_shortfall]:
|
||||||
|
adjustment = cgt_adjustments[asset]
|
||||||
|
adjustment = copysign(int(abs(adjustment)) + 1, adjustment)
|
||||||
|
cgt_adjustments[asset] = adjustment
|
||||||
|
|
||||||
|
# Round others down
|
||||||
|
for asset, adjustment in cgt_adjustments.items():
|
||||||
|
cgt_adjustments[asset] = copysign(int(abs(adjustment)), adjustment)
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
assert sum(v for v in cgt_adjustments.values()) == total_adjustment
|
||||||
|
|
||||||
|
# Add adjustments
|
||||||
|
for asset, adjustment in cgt_adjustments.items():
|
||||||
|
adjustment = CGTCostAdjustment(
|
||||||
|
quantity=asset.quantity,
|
||||||
|
commodity=asset.commodity,
|
||||||
|
account=asset.account,
|
||||||
|
acquisition_date=asset.acquisition_date,
|
||||||
|
dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'),
|
||||||
|
description=request.form['description'],
|
||||||
|
cost_adjustment=adjustment
|
||||||
|
)
|
||||||
|
db.session.add(adjustment)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect('/tax/cgt-adjustments')
|
||||||
|
|
||||||
@app.route('/tax/cgt-assets')
|
@app.route('/tax/cgt-assets')
|
||||||
def cgt_assets():
|
def cgt_assets():
|
||||||
# Find all CGT asset accounts
|
# Find all CGT asset accounts
|
||||||
|
Loading…
Reference in New Issue
Block a user