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
+
+
+
+
+
+
+ Account |
+ Asset |
+ Units |
+ Acquisition date |
+ Acquisition value |
+ Adjustment date |
+ Description |
+ Cost adjustment |
+
+
+
+ {% for cgt_adjustment in cgt_adjustments %}
+
+ {{ 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() }} |
+
+ {% endfor %}
+
+
+{% 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
+
+
+{% 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():