diff --git a/austax/models.py b/austax/models.py
index 987cee8..d2f0566 100644
--- a/austax/models.py
+++ b/austax/models.py
@@ -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()
diff --git a/austax/templates/cgt_adjustments.html b/austax/templates/cgt_adjustments.html
index 5880592..5c6f5c5 100644
--- a/austax/templates/cgt_adjustments.html
+++ b/austax/templates/cgt_adjustments.html
@@ -23,6 +23,7 @@
diff --git a/austax/templates/cgt_adjustments_multinew.html b/austax/templates/cgt_adjustments_multinew.html
new file mode 100644
index 0000000..8fb6820
--- /dev/null
+++ b/austax/templates/cgt_adjustments_multinew.html
@@ -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 .
+#}
+
+{% extends 'base.html' %}
+{% block title %}Multiple CGT adjustments{% endblock %}
+
+{% block content %}
+ Multiple CGT adjustments
+
+
+{% endblock %}
diff --git a/austax/views.py b/austax/views.py
index 3f947c0..fe0454e 100644
--- a/austax/views.py
+++ b/austax/views.py
@@ -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