# 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 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