DrCr/austax/reports.py
RunasSudo 18392d6917
Basic caching of running balances
About 60% performance improvement on balance report
2024-11-09 23:38:56 +11:00

272 lines
9.9 KiB
Python

# 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 <https://www.gnu.org/licenses/>.
from drcr import AMOUNT_DPS
from drcr.database import db
from drcr.models import AccountConfiguration, Amount, Metadata, Transaction, TrialBalancer, reporting_commodity
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
from .util import assert_aud
@assert_aud
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)), reporting_commodity())
@assert_aud
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, reporting_commodity())
if taxable_income.quantity <= 4500000:
return Amount(70000 - 0.05 * (taxable_income.quantity - 3750000), reporting_commodity())
if taxable_income.quantity <= 6666700:
return Amount(32500 - int(0.015 * (taxable_income.quantity - 4500000)), reporting_commodity())
return Amount(0, reporting_commodity())
@assert_aud
def medicare_levy(year, taxable_income):
lower_threshold, upper_threshold = medicare_levy_threshold[year]
if taxable_income.quantity < lower_threshold * 100:
return Amount(0, reporting_commodity())
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, reporting_commodity())
# Normal Medicare levy
return Amount(int(taxable_income.quantity * 0.02), reporting_commodity())
@assert_aud
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, reporting_commodity())
@assert_aud
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, reporting_commodity())
@assert_aud
def tax_summary_report():
# Get trial balance
balancer = TrialBalancer.from_cached(start_date=sofy_date(), end_date=eofy_date())
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, reporting_commodity())),
id='rfb_taxable'
),
Calculated(
'Grossed-up value',
lambda _: Amount(report.by_id('rfb_taxable').amount.quantity * fbt_grossup[eofy_date().year], reporting_commodity()),
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),
auto_hide=True
),
Calculated(
'Medicare levy surcharge',
lambda _: medicare_levy_surcharge(eofy_date().year, report.by_id('taxable').amount, report.by_id('rfb_grossedup').amount),
auto_hide=True
),
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), auto_hide=True),
Subtotal('Total tax offsets', id='offsets')
],
auto_hide=True
),
Spacer(),
Section(
entries=[
Calculated(
'Taxable value of reportable fringe benefits',
lambda _: report.by_id('rfb_taxable').amount,
auto_hide=True
),
Calculated(
'Grossed-up value',
lambda _: report.by_id('rfb_grossedup').amount,
auto_hide=True
),
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,
auto_hide=True
)
],
auto_hide=True
),
Spacer(),
Section(
title='PAYG withheld amounts',
entries=entries_for_kind(account_configurations, accounts, 'austax.paygw') + [Subtotal('Total withheld amounts', id='paygw')],
auto_hide=True
),
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