diff --git a/austax/__init__.py b/austax/__init__.py
new file mode 100644
index 0000000..b85fa16
--- /dev/null
+++ b/austax/__init__.py
@@ -0,0 +1,58 @@
+# 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 render_template
+
+from drcr.models import Posting, Transaction
+import drcr.plugins
+from drcr.webapp import app
+
+from .reports import tax_summary_report
+
+from datetime import datetime
+
+def plugin_init():
+ drcr.plugins.advanced_reports.append(('/tax/summary', 'Tax summary'))
+
+ drcr.plugins.account_kinds.append(('austax.income1', 'Salary or wages (1)'))
+ drcr.plugins.account_kinds.append(('austax.income5', 'Australian Government allowances and payments (5)'))
+ drcr.plugins.account_kinds.append(('austax.d4', 'Work-related self-education expenses (D4)'))
+ drcr.plugins.account_kinds.append(('austax.d5', 'Other work-related expenses (D5)'))
+
+ drcr.plugins.transaction_providers.append(make_tax_transactions)
+
+@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
+
+ # Get EOFY date
+ dt = datetime.now().replace(month=6, day=30)
+ if dt < datetime.now():
+ dt = dt.replace(year=dt.year + 1)
+
+ return [Transaction(
+ dt=dt,
+ description='Estimated tax payable',
+ postings=[
+ Posting(account='Income Tax', quantity=tax_amount.quantity, commodity='$'),
+ Posting(account='Income Tax Control', quantity=-tax_amount.quantity, commodity='$')
+ ]
+ )]
diff --git a/austax/reports.py b/austax/reports.py
new file mode 100644
index 0000000..1a6c28d
--- /dev/null
+++ b/austax/reports.py
@@ -0,0 +1,116 @@
+# 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 drcr.database import db
+from drcr.models import AccountConfiguration, Amount, Transaction, TrialBalancer
+from drcr.reports import Calculated, Report, Section, Spacer, Subtotal, entries_for_kind
+
+def base_income_tax(taxable_income):
+ income = taxable_income.quantity
+
+ if income <= 1820000:
+ return Amount(0, '$')
+ if income <= 4500000:
+ return Amount(int((income - 1820000) * 0.19), '$')
+ if income <= 12000000:
+ return Amount(int(509200 + (income - 4500000) * 0.325), '$')
+ if income <= 18000000:
+ return Amount(int(2946700 + (income - 12000000) * 0.37), '$')
+ return Amount(int(5166700 + (income - 18000000) * 0.45), '$')
+
+def medicare_levy(taxable_income):
+ if taxable_income.quantity < 2920700:
+ raise NotImplementedError('Medicare levy reduction is not implemented')
+
+ return Amount(int(taxable_income.quantity * 0.02), '$')
+
+def tax_summary_report():
+ # Get trial balance
+ balancer = TrialBalancer()
+ #balancer.apply_transactions(all_transactions())
+ balancer.apply_transactions(db.session.scalars(db.select(Transaction)).all())
+
+ accounts = dict(sorted(balancer.accounts.items()))
+
+ # Get account configurations
+ account_configurations = AccountConfiguration.get_all_kinds()
+
+ report = Report(title='Tax summary')
+ report.entries = [
+ Section(
+ entries=[
+ Section(
+ title='Salary or wages (1)',
+ entries=entries_for_kind(account_configurations, accounts, 'austax.income1', neg=True, floor=100) + [Subtotal('Total item 1', id='income1')]
+ ),
+ Spacer(),
+ Section(
+ title='Australian Government allowances and payments (5)',
+ entries=entries_for_kind(account_configurations, accounts, 'austax.income5', neg=True) + [Subtotal('Total item 5', id='income5', floor=100)]
+ ),
+ Spacer(),
+ Calculated(
+ 'Total assessable income',
+ lambda r: r.by_id('income1').amount + r.by_id('income5').amount,
+ id='assessable',
+ heading=True,
+ bordered=True
+ ),
+ Spacer(),
+ Section(
+ title='Work-related self-education expenses (D4)',
+ entries=entries_for_kind(account_configurations, accounts, 'austax.d4') + [Subtotal('Total item D4', id='d4', floor=100)]
+ ),
+ Spacer(),
+ Section(
+ title='Other work-related expenses (D5)',
+ entries=entries_for_kind(account_configurations, accounts, 'austax.d5') + [Subtotal('Total item D5', id='d5', floor=100)]
+ ),
+ Spacer(),
+ Calculated(
+ 'Total deductions',
+ lambda r: r.by_id('d4').amount + r.by_id('d5').amount,
+ id='deductions',
+ heading=True,
+ bordered=True
+ ),
+ Spacer(),
+ Calculated(
+ 'Taxable income',
+ lambda r: r.by_id('assessable').amount - r.by_id('deductions').amount,
+ id='taxable',
+ heading=True,
+ bordered=True
+ ),
+ Section(
+ entries=[
+ Calculated(
+ 'Income tax',
+ lambda _: base_income_tax(report.by_id('taxable').amount)
+ ),
+ Calculated(
+ 'Medicare levy',
+ lambda _: medicare_levy(report.by_id('taxable').amount)
+ ),
+ Subtotal(id='total_tax', visible=False)
+ ]
+ )
+ ]
+ )
+ ]
+ report.calculate()
+
+ return report
diff --git a/drcr/config.py.example b/drcr/config.py.example
index fe7b711..1bc714f 100644
--- a/drcr/config.py.example
+++ b/drcr/config.py.example
@@ -1,6 +1 @@
-TAX_MAPPING = {
- 'Government allowances': [],
- 'Other work-related expenses': [],
- 'Salary and wages': [],
- 'Work-related self-education expenses': []
-}
+PLUGINS = ['austax']
diff --git a/drcr/reports.py b/drcr/reports.py
index cb79160..caa1602 100644
--- a/drcr/reports.py
+++ b/drcr/reports.py
@@ -77,18 +77,21 @@ class Entry:
pass
class Subtotal:
- def __init__(self, text=None, *, id=None, bordered=False):
+ def __init__(self, text=None, *, id=None, visible=True, bordered=False, floor=0):
self.text = text
self.id = id
+ self.visible = visible
self.bordered = bordered
+ self.floor = floor
self.amount = None
def calculate(self, parent):
- self.amount = Amount(
- sum(e.amount.quantity for e in parent.entries if isinstance(e, Entry)),
- '$'
- )
+ amount = sum(e.amount.quantity for e in parent.entries if isinstance(e, Entry))
+ if self.floor:
+ amount = (amount // self.floor) * self.floor
+
+ self.amount = Amount(amount, '$')
class Calculated(Entry):
def __init__(self, text=None, calc=None, **kwargs):
@@ -112,12 +115,16 @@ def validate_accounts(accounts, account_configurations):
if n != 1:
raise Exception('Account "{}" mapped to {} account types (expected 1)'.format(account, n))
-def entries_for_kind(account_configurations, accounts, kind, neg=False):
- return [
- Entry(text=account_name, amount=-amount if neg else amount, link='/account-transactions?account=' + account_name)
- for account_name, amount in accounts.items()
- if kind in account_configurations.get(account_name, []) and amount.quantity != 0
- ]
+def entries_for_kind(account_configurations, accounts, kind, neg=False, floor=0):
+ entries = []
+ for account_name, amount in accounts.items():
+ if kind in account_configurations.get(account_name, []) and amount.quantity != 0:
+ if neg:
+ amount = -amount
+ if floor:
+ amount.quantity = (amount.quantity // floor) * floor
+ entries.append(Entry(text=account_name, amount=amount, link='/account-transactions?account=' + account_name))
+ return entries
def balance_sheet_report():
# Get trial balance
diff --git a/drcr/tax/__init__.py b/drcr/tax/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/drcr/tax/aus_tax.py b/drcr/tax/aus_tax.py
deleted file mode 100644
index ec73b7f..0000000
--- a/drcr/tax/aus_tax.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# 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 ..config import TAX_MAPPING
-from ..models import Amount, Posting, Transaction, TrialBalancer
-
-from datetime import datetime
-
-def taxable_income():
- balancer = TrialBalancer()
- balancer.apply_transactions(Transaction.query.all())
-
- result = Amount(0, '$')
-
- for account in TAX_MAPPING['Salary and wages']:
- result.quantity += int(balancer.accounts[account].quantity / 100) * 100
- for account in TAX_MAPPING['Government allowances']:
- result.quantity += int(balancer.accounts[account].quantity / 100) * 100
- for account in TAX_MAPPING['Work-related self-education expenses']:
- result.quantity += int(balancer.accounts[account].quantity / 100) * 100
- for account in TAX_MAPPING['Other work-related expenses']:
- result.quantity += int(balancer.accounts[account].quantity / 100) * 100
-
- return result
-
-def base_income_tax(taxable_income):
- income = -taxable_income.as_cost().quantity
-
- if income <= 1820000:
- return Amount(0, '$')
- if income <= 4500000:
- return Amount(int((income - 1820000) * 0.19), '$')
- if income <= 12000000:
- return Amount(int(509200 + (income - 4500000) * 0.325), '$')
- if income <= 18000000:
- return Amount(int(2946700 + (income - 12000000) * 0.37), '$')
- return Amount(int(5166700 + (income - 18000000) * 0.45), '$')
-
-def calculate_tax(taxable_income):
- income = -taxable_income.as_cost().quantity
- medicare_levy = int(income * 0.02)
-
- return Amount(base_income_tax(taxable_income).quantity + medicare_levy, '$')
-
-def tax_transaction(taxable_income):
- tax = calculate_tax(taxable_income)
-
- # Get EOFY date
- dt = datetime.now().replace(month=6, day=30)
- if dt < datetime.now():
- dt = dt.replace(year=dt.year + 1)
-
- return Transaction(
- dt=dt,
- description='Estimated tax payable',
- postings=[
- Posting(account='Income Tax', quantity=tax.quantity, commodity='$'),
- Posting(account='Income Tax Control', quantity=-tax.quantity, commodity='$')
- ]
- )
diff --git a/drcr/tax/views.py b/drcr/tax/views.py
deleted file mode 100644
index 90ad061..0000000
--- a/drcr/tax/views.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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 render_template
-
-from ..config import TAX_MAPPING
-from ..models import Amount, TrialBalancer
-from ..webapp import all_transactions, app
-from .aus_tax import base_income_tax, calculate_tax
-
-@app.route('/tax/summary')
-def tax_summary():
- # Get trial balance and validate COA
- balancer = TrialBalancer()
- balancer.apply_transactions(all_transactions())
-
- return render_template(
- 'tax/summary.html',
- accounts=balancer.accounts,
- base_income_tax=base_income_tax, calculate_tax=calculate_tax,
- running_total=Amount(0, '$'),
- TAX_MAPPING=TAX_MAPPING
- )
diff --git a/drcr/templates/report.html b/drcr/templates/report.html
index 020cfd5..d7fa897 100644
--- a/drcr/templates/report.html
+++ b/drcr/templates/report.html
@@ -19,10 +19,12 @@
{% block title %}{{ report.title }}{% endblock %}
{% macro render_section(section) %}
-
- {{ section.title }} |
- |
-
+ {% if section.title %}
+
+ {{ section.title }} |
+ |
+
+ {% endif %}
{% for entry in section.entries %}
{{ render_entry(entry) }}
{% endfor %}
@@ -32,10 +34,12 @@
{% if entry.__class__.__name__ == 'Section' %}
{{ render_section(entry) }}
{% elif entry.__class__.__name__ == 'Subtotal' %}
-
- {{ entry.text }} |
- {{ entry.amount.format_accounting() }} |
-
+ {% if entry.visible %}
+
+ {{ entry.text }} |
+ {{ entry.amount.format_accounting() }} |
+
+ {% endif %}
{% elif entry.__class__.__name__ == 'Spacer' %}
|
{% else %}
diff --git a/drcr/templates/tax/summary.html b/drcr/templates/tax/summary.html
deleted file mode 100644
index 4f6186e..0000000
--- a/drcr/templates/tax/summary.html
+++ /dev/null
@@ -1,120 +0,0 @@
-{# 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 %}Tax summary{% endblock %}
-
-{% macro fmtbal(amount, rnd=1, mul=1) %}
- {# FIXME: Honour AMOUNT_DPS #}
- {% if amount.quantity * mul >= 0 %}
- {{ '{:,.2f}'.format((amount.quantity|abs / rnd)|round(0, 'floor') * rnd / 100) }}
- {% else %}
- ({{ '{:,.2f}'.format((amount.quantity|abs / rnd)|round(0, 'floor') * rnd / 100) }})
- {% endif %}
-{% endmacro %}
-
-{% macro acctrow(name, rnd=1, mul=1) %}
-
- {{ name }} |
- {{ fmtbal(accounts[name], rnd, mul) }} |
-
- {% set rnd_qty = (accounts[name].quantity|abs / rnd)|round(0, 'floor') * rnd * accounts[name].quantity/(accounts[name].quantity|abs) %}
- {% set _ = running_total.__setattr__('quantity', running_total.quantity + rnd_qty) %}
-{% endmacro %}
-
-{% block content %}
- Tax summary
-
-
-
-
- |
- $ |
-
-
-
-
- Salary and wages (1) |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in TAX_MAPPING['Salary and wages'] if account in accounts %}
- {{ acctrow(account, 100, -1) }}
- {% endfor %}
-
- |
-
-
- Government allowances (5) |
-
- {% for account in TAX_MAPPING['Government allowances'] if account in accounts %}
- {{ acctrow(account, 100, -1) }}
- {% endfor %}
-
- |
-
-
- Total assessable income |
- {{ fmtbal(running_total, 1, -1) }} |
-
- {% set taxable_income = running_total.quantity %}
-
- |
-
-
- Work-related self-education expenses (D4) |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in TAX_MAPPING['Work-related self-education expenses'] if account in accounts %}
- {{ acctrow(account, 100) }}
- {% endfor %}
-
- |
-
-
- Other work-related expenses (D5) |
-
- {% for account in TAX_MAPPING['Other work-related expenses'] if account in accounts %}
- {{ acctrow(account, 100) }}
- {% endfor %}
-
- |
-
-
- Total deductions |
- {{ fmtbal(running_total) }} |
-
- {% set taxable_income = taxable_income + running_total.quantity %}
-
- |
-
-
- Taxable income |
- {% set _ = running_total.__setattr__('quantity', taxable_income) %}
- {{ fmtbal(running_total, 1, -1) }} |
-
-
- Income tax |
- {{ fmtbal(base_income_tax(running_total)) }} |
-
-
- Medicare levy |
- {% set _ = running_total.__setattr__('quantity', taxable_income * 0.02) %}
- {{ fmtbal(running_total, 1, -1) }} |
-
-
-
-{% endblock %}
diff --git a/drcr/templates/transactions.html b/drcr/templates/transactions.html
index 6291d72..ba2a3be 100644
--- a/drcr/templates/transactions.html
+++ b/drcr/templates/transactions.html
@@ -22,6 +22,7 @@
Account transactions
diff --git a/drcr/webapp.py b/drcr/webapp.py
index 7984c2f..68a371c 100644
--- a/drcr/webapp.py
+++ b/drcr/webapp.py
@@ -21,7 +21,6 @@ from .database import db
from .models import Transaction
from .plugins import init_plugins, transaction_providers
from .statements.models import StatementLine
-from .tax import aus_tax
import time
@@ -49,7 +48,6 @@ from .journal import views
from .statements import views
init_plugins()
-from .tax import views
@app.cli.command('initdb')
def initdb():