# DrCr: Web-based double-entry bookkeeping framework # Copyright (C) 2022–2024 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 import AMOUNT_DPS from drcr.database import db from drcr.models import AccountConfiguration, Amount, Metadata, Transaction, TrialBalancer from drcr.reports import Calculated, Report, Section, Spacer, Subtotal, entries_for_kind from drcr.webapp import eofy_date, sofy_date from .tax_tables import base_tax, medicare_levy_threshold, medicare_levy_surcharge_single, repayment_rates, fbt_grossup def base_income_tax(year, taxable_income): """Get the amount of base income tax""" for i, (upper_limit, flat_amount, marginal_rate) in enumerate(base_tax[year]): if upper_limit is None or taxable_income.quantity <= upper_limit * (10**AMOUNT_DPS): lower_limit = base_tax[year][i - 1][0] or 0 return Amount(flat_amount * (10**AMOUNT_DPS) + marginal_rate * (taxable_income.quantity - lower_limit * (10**AMOUNT_DPS)), '$') def lito(taxable_income, total_tax): """Get the amount of low income tax offset""" if taxable_income.quantity <= 3750000: # LITO is non-refundable # FIXME: This will not work if we implement multiple non-refundable tax offsets if total_tax.quantity <= 70000: return total_tax return Amount(70000, '$') if taxable_income.quantity <= 4500000: return Amount(70000 - 0.05 * (taxable_income.quantity - 3750000), '$') if taxable_income.quantity <= 6666700: return Amount(32500 - int(0.015 * (taxable_income.quantity - 4500000)), '$') return Amount(0, '$') def medicare_levy(year, taxable_income): lower_threshold, upper_threshold = medicare_levy_threshold[year] if taxable_income.quantity < lower_threshold * 100: return Amount(0, '$') if taxable_income.quantity < upper_threshold * 100: # Medicare levy is 10% of the amount above the lower threshold return Amount((taxable_income - lower_threshold * 100) * 0.1, '$') # Normal Medicare levy return Amount(int(taxable_income.quantity * 0.02), '$') def medicare_levy_surcharge(year, taxable_income, rfb_grossedup): mls_income = taxable_income + rfb_grossedup for i, (upper_limit, rate) in enumerate(medicare_levy_surcharge_single[year]): if upper_limit is None or mls_income.quantity <= upper_limit * (10**AMOUNT_DPS): return Amount(rate * mls_income.quantity, '$') def study_loan_repayment(year, taxable_income, rfb_grossedup): """Get the amount of mandatory study loan repayment""" repayment_income = taxable_income + rfb_grossedup for upper_limit, rate in repayment_rates[year]: if upper_limit is None or repayment_income.quantity <= upper_limit * (10**AMOUNT_DPS): return Amount(rate * repayment_income.quantity, '$') def tax_summary_report(): # Get trial balance balancer = TrialBalancer() balancer.apply_transactions(db.session.scalars(db.select(Transaction).where((Transaction.dt >= sofy_date()) & (Transaction.dt <= eofy_date())).options(db.selectinload(Transaction.postings))).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')], auto_hide=True ), 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)], auto_hide=True ), Spacer(), Section( title='Gross interest (10)', entries=entries_for_kind(account_configurations, accounts, 'austax.income10', neg=True) + [Subtotal('Total item 10', id='income10', floor=100)], auto_hide=True ), Spacer(), Section( title='Partnerships and trusts (13)', entries=entries_for_kind(account_configurations, accounts, 'austax.income13', neg=True, floor=100) + [Subtotal('Total item 13', id='income13', floor=100)], auto_hide=True ), Spacer(), #Section( # title='Net capital gains (18)', # entries=entries_for_kind(account_configurations, accounts, 'austax.income18', neg=True) + [Subtotal('Total item 18', id='income18', floor=100)] #), #Spacer(), Section( title='Foreign source income and foreign assets or property (20)', entries=entries_for_kind(account_configurations, accounts, 'austax.income20', neg=True, floor=100) + [Subtotal('Total item 20', id='income20', floor=100)], auto_hide=True ), Spacer(), Calculated( 'Total assessable income', lambda r: r.by_id('income1').amount + r.by_id('income5').amount + r.by_id('income10').amount + r.by_id('income13').amount + r.by_id('income20').amount, id='assessable', heading=True, bordered=True ), Spacer(), Section( title='Work-related travel expenses (D2)', entries=entries_for_kind(account_configurations, accounts, 'austax.d2') + [Subtotal('Total item D2', id='d2', floor=100)], auto_hide=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)], auto_hide=True ), 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)], auto_hide=True ), Spacer(), Section( title='Gifts or donations (D9)', entries=entries_for_kind(account_configurations, accounts, 'austax.d9') + [Subtotal('Total item D9', id='d9', floor=100)], auto_hide=True ), Spacer(), Section( title='Other deductions (D15)', entries=entries_for_kind(account_configurations, accounts, 'austax.d15') + [Subtotal('Total item D15', id='d15', floor=100)], auto_hide=True ), Spacer(), Calculated( 'Total deductions', lambda r: r.by_id('d2').amount + r.by_id('d4').amount + r.by_id('d5').amount + r.by_id('d9').amount + r.by_id('d15').amount, id='deductions', heading=True, bordered=True ), Spacer(), Calculated( 'Net taxable income', lambda r: r.by_id('assessable').amount - r.by_id('deductions').amount, id='taxable', heading=True, bordered=True ), Spacer(), Section( entries=[ Calculated( 'Taxable value of reportable fringe benefits', lambda _: -sum((e.amount for e in entries_for_kind(account_configurations, accounts, 'austax.rfb')), Amount(0, '$')), id='rfb_taxable' ), Calculated( 'Grossed-up value', lambda _: Amount(report.by_id('rfb_taxable').amount.quantity * fbt_grossup[eofy_date().year], '$'), id='rfb_grossedup' ) ], visible=False # Precompute RFB amount as this is required for MLS ), Section( entries=[ Calculated( 'Base income tax', lambda _: base_income_tax(eofy_date().year, report.by_id('taxable').amount) ), Calculated( 'Medicare levy', lambda _: medicare_levy(eofy_date().year, report.by_id('taxable').amount) ), Calculated( 'Medicare levy surcharge', lambda _: medicare_levy_surcharge(eofy_date().year, report.by_id('taxable').amount, report.by_id('rfb_grossedup').amount) ), Subtotal('Total income tax', id='total_tax', bordered=True) ] ), Spacer(), Section( title='Tax offsets', entries=entries_for_kind(account_configurations, accounts, 'austax.offset', neg=True) + [ Calculated('Low income tax offset', lambda _: lito(report.by_id('taxable').amount, report.by_id('total_tax').amount)), Subtotal('Total tax offsets', id='offsets') ] ), Spacer(), Section( entries=[ Calculated( 'Taxable value of reportable fringe benefits', lambda _: report.by_id('rfb_taxable').amount ), Calculated( 'Grossed-up value', lambda _: report.by_id('rfb_grossedup').amount ), Calculated( 'Mandatory study loan repayment', lambda _: study_loan_repayment(eofy_date().year, report.by_id('taxable').amount, report.by_id('rfb_grossedup').amount), id='loan_repayment', heading=True ) ] ), Spacer(), Section( title='PAYG withheld amounts', entries=entries_for_kind(account_configurations, accounts, 'austax.paygw') + [Subtotal('Total withheld amounts', id='paygw')] ), Spacer(), Calculated( 'ATO liability payable (refundable)', lambda _: report.by_id('total_tax').amount - report.by_id('offsets').amount - report.by_id('paygw').amount + report.by_id('loan_repayment').amount, heading=True, bordered=True ) ] ) ] report.calculate() return report