austax: Implement helper utility to distribute CGT cost adjustment across multiple assets

This commit is contained in:
RunasSudo 2023-01-07 15:39:04 +11:00
parent 9ad5b7642e
commit a4faca5ea8
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 159 additions and 2 deletions

View File

@ -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()

View File

@ -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">

View 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 %}

View File

@ -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