From 1afc48f21843ad4771027100bcbb5d4470a85d2d Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 7 Jan 2023 14:07:58 +1100 Subject: [PATCH] austax: Record CGT cost adjustments --- austax/__init__.py | 56 +---------- austax/models.py | 30 ++++++ austax/templates/cgt_adjustments.html | 56 +++++++++++ austax/templates/cgt_adjustments_edit.html | 71 +++++++++++++ austax/templates/cgt_assets.html | 7 +- austax/views.py | 110 +++++++++++++++++++++ drcr/plugins.py | 3 + drcr/templates/index.html | 3 + drcr/views.py | 4 +- 9 files changed, 284 insertions(+), 56 deletions(-) create mode 100644 austax/templates/cgt_adjustments.html create mode 100644 austax/templates/cgt_adjustments_edit.html create mode 100644 austax/views.py diff --git a/austax/__init__.py b/austax/__init__.py index b88e43a..df24417 100644 --- a/austax/__init__.py +++ b/austax/__init__.py @@ -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 diff --git a/austax/models.py b/austax/models.py index 2b844d8..987cee8 100644 --- a/austax/models.py +++ b/austax/models.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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, '$') diff --git a/austax/templates/cgt_adjustments.html b/austax/templates/cgt_adjustments.html new file mode 100644 index 0000000..454e126 --- /dev/null +++ b/austax/templates/cgt_adjustments.html @@ -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 . +#} + +{% extends 'base.html' %} +{% block title %}CGT adjustments{% endblock %} + +{% block content %} +

CGT adjustments

+ + + + + + + + + + + + + + + + + + {% for cgt_adjustment in cgt_adjustments %} + + + + + + + + + + + {% endfor %} + +
AccountAssetUnitsAcquisition dateAcquisition valueAdjustment dateDescriptionCost adjustment 
{{ cgt_adjustment.account }}{{ cgt_adjustment.asset().commodity_name() }}{{ cgt_adjustment.asset().format('hide') }}{{ cgt_adjustment.acquisition_date.strftime('%Y-%m-%d') }}{{ cgt_adjustment.asset().as_cost().format() }}{{ cgt_adjustment.dt.strftime('%Y-%m-%d') }}{{ cgt_adjustment.description }}{{ cgt_adjustment.cost_adjustment_amount().format_accounting() }}
+{% endblock %} diff --git a/austax/templates/cgt_adjustments_edit.html b/austax/templates/cgt_adjustments_edit.html new file mode 100644 index 0000000..0db5b13 --- /dev/null +++ b/austax/templates/cgt_adjustments_edit.html @@ -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 . +#} + +{% extends 'base.html' %} +{% block title %}{{ 'Edit' if adjustment else 'New' }} CGT adjustment{% endblock %} + +{% block content %} +

{{ 'Edit' if adjustment else 'New' }} CGT adjustment

+ +
+

CGT asset

+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +

CGT adjustment

+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+{% endblock %} diff --git a/austax/templates/cgt_assets.html b/austax/templates/cgt_assets.html index 112107a..d10ba02 100644 --- a/austax/templates/cgt_assets.html +++ b/austax/templates/cgt_assets.html @@ -28,6 +28,7 @@ Acquisition + Adjustment Disposal @@ -37,6 +38,8 @@ Units Date Value + b/f  + {{ eofy_date.year }} Date Value Gain  @@ -50,9 +53,11 @@ {{ asset.format('hide') }} {{ asset.acquisition_date.strftime('%Y-%m-%d') }} {{ asset.as_cost().format() }} + {{ asset.cost_adjustment().format_accounting() if asset.cost_adjustments }} + {{ asset.disposal_date.strftime('%Y-%m-%d') if asset.disposal_date else '' }} {{ asset.disposal_value.format() if asset.disposal_value else '' }} - {% if asset.disposal_date %}{{ (asset.disposal_value - asset.as_cost()).format_accounting() }}{% endif %} + {% if asset.disposal_date %}{{ asset.gain().format_accounting() }}{% endif %} {% endfor %} diff --git a/austax/views.py b/austax/views.py new file mode 100644 index 0000000..dca4968 --- /dev/null +++ b/austax/views.py @@ -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 . + +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) diff --git a/drcr/plugins.py b/drcr/plugins.py index fa48622..a530c78 100644 --- a/drcr/plugins.py +++ b/drcr/plugins.py @@ -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(): diff --git a/drcr/templates/index.html b/drcr/templates/index.html index 8407861..68ba10c 100644 --- a/drcr/templates/index.html +++ b/drcr/templates/index.html @@ -25,6 +25,9 @@
  • Statement lines
  • Balance assertions
  • Chart of accounts
  • + {% for report in data_sources %} +
  • {{ report[1] }}
  • + {% endfor %}

    General reports

    diff --git a/drcr/views.py b/drcr/views.py index 7e256f5..c084717 100644 --- a/drcr/views.py +++ b/drcr/views.py @@ -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():