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 = []
|
||||
|
||||
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):
|
||||
return self.commodity[:self.commodity.index('{')].strip()
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
<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/multi-new" class="btn btn-outline-primary"><i class="bi bi-plus-lg"></i> Multiple CGT adjustments</a>
|
||||
</div>
|
||||
|
||||
<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 datetime import datetime
|
||||
from math import copysign
|
||||
|
||||
@app.route('/tax/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)
|
||||
|
||||
@app.route('/tax/cgt-adjustments/new', methods=['GET', 'POST'])
|
||||
@ -71,6 +72,92 @@ def cgt_adjustment_edit():
|
||||
|
||||
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')
|
||||
def cgt_assets():
|
||||
# Find all CGT asset accounts
|
||||
|
Loading…
Reference in New Issue
Block a user