144 lines
5.5 KiB
Python
144 lines
5.5 KiB
Python
# 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/>.
|
|
|
|
from flask import render_template
|
|
|
|
from drcr.models import AccountConfiguration, Amount, 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
|
|
|
|
def plugin_init():
|
|
drcr.plugins.advanced_reports.append(('/tax/cgt-assets', 'CGT assets'))
|
|
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.income10', 'Gross interest (10)'))
|
|
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.account_kinds.append(('austax.paygw', 'PAYG withheld amounts'))
|
|
drcr.plugins.account_kinds.append(('austax.cgtasset', 'CGT asset'))
|
|
|
|
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
|
|
|
|
# Get EOFY date
|
|
dt = eofy_date()
|
|
|
|
# Estimated tax payable
|
|
transactions = [Transaction(
|
|
dt=dt,
|
|
description='Estimated income tax',
|
|
postings=[
|
|
Posting(account='Income Tax', quantity=tax_amount.quantity, commodity='$'),
|
|
Posting(account='Income Tax Control', quantity=-tax_amount.quantity, commodity='$')
|
|
]
|
|
)]
|
|
|
|
# Mandatory study loan repayment
|
|
loan_repayment = report.by_id('loan_repayment').amount
|
|
if loan_repayment.quantity != 0:
|
|
transactions.append(Transaction(
|
|
dt=dt,
|
|
description='Mandatory study loan repayment payable',
|
|
postings=[
|
|
Posting(account='HELP', quantity=loan_repayment.quantity, commodity='$'), # FIXME: Correct account
|
|
Posting(account='Income Tax Control', quantity=-loan_repayment.quantity, commodity='$')
|
|
]
|
|
))
|
|
|
|
# Get trial balance
|
|
balancer = TrialBalancer()
|
|
balancer.apply_transactions(db.session.scalars(db.select(Transaction).options(db.selectinload(Transaction.postings))).all())
|
|
|
|
accounts = dict(sorted(balancer.accounts.items()))
|
|
|
|
# Get account configurations
|
|
account_configurations = AccountConfiguration.get_all_kinds()
|
|
|
|
# PAYG withholding
|
|
for account_name, kinds in account_configurations.items():
|
|
if 'austax.paygw' in kinds:
|
|
if accounts[account_name].quantity != 0:
|
|
# Transfer balance to Income Tax Control
|
|
transactions.append(Transaction(
|
|
dt=dt,
|
|
description='PAYG withheld amounts',
|
|
postings=[
|
|
Posting(account='Income Tax Control', quantity=accounts[account_name].quantity, commodity='$'),
|
|
Posting(account=account_name, quantity=-accounts[account_name].quantity, commodity='$')
|
|
]
|
|
))
|
|
|
|
return transactions
|