Compare commits
8 Commits
b993c23909
...
5616a13195
Author | SHA1 | Date | |
---|---|---|---|
5616a13195 | |||
6ae2106a8a | |||
ff7af2f06e | |||
1a5167acf5 | |||
a0071cf120 | |||
023d1035c5 | |||
1ca9c3cbe8 | |||
1d6c6e1da3 |
@ -15,6 +15,8 @@
|
||||
-- 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/>.
|
||||
|
||||
-- NB: Content of this file is also parsed by frontend src/plugins/austax/account_kinds.ts
|
||||
-- Check the syntax is compatible with that parser!
|
||||
local income_types = {
|
||||
{'income1', 'Salary or wages', '1'},
|
||||
{'income2', 'Allowances, earnings, tips, director\'s fees etc.', '2'},
|
||||
@ -42,6 +44,8 @@ local income_types = {
|
||||
{'income24', 'Other income', '24'},
|
||||
}
|
||||
|
||||
-- NB: Content of this file is also parsed by frontend src/plugins/austax/account_kinds.ts
|
||||
-- Check the syntax is compatible with that parser!
|
||||
local deduction_types = {
|
||||
{'d1', 'Work-related car expenses', 'D1'},
|
||||
{'d2', 'Work-related travel expenses', 'D2'},
|
||||
|
@ -1,354 +0,0 @@
|
||||
--!strict
|
||||
-- DrCr: Web-based double-entry bookkeeping framework
|
||||
-- Copyright (C) 2022-2025 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/>.
|
||||
|
||||
local libdrcr = require('../libdrcr')
|
||||
|
||||
local account_kinds = require('../austax/account_kinds')
|
||||
local tax_tables = require('../austax/tax_tables')
|
||||
|
||||
function get_base_income_tax(net_taxable: number, context: libdrcr.ReportingContext): number
|
||||
local year, _, _ = libdrcr.parse_date(context.eofy_date)
|
||||
local base_tax_table = tax_tables.base_tax[year]
|
||||
|
||||
for i, row in ipairs(base_tax_table) do
|
||||
local upper_limit = row[1] * (10 ^ context.dps)
|
||||
local flat_amount = row[2] * (10 ^ context.dps)
|
||||
local marginal_rate = row[3]
|
||||
|
||||
-- Lower limit is the upper limit of the preceding bracket
|
||||
local lower_limit = 0
|
||||
if i > 1 then
|
||||
lower_limit = base_tax_table[i - 1][1] * (10 ^ context.dps)
|
||||
end
|
||||
|
||||
if net_taxable <= upper_limit then
|
||||
return flat_amount + marginal_rate * (net_taxable - lower_limit)
|
||||
end
|
||||
end
|
||||
|
||||
error('Taxable income not within any tax bracket')
|
||||
end
|
||||
|
||||
function requires(args, context)
|
||||
return {
|
||||
{
|
||||
name = 'CombineOrdinaryTransactions',
|
||||
kind = 'BalancesBetween',
|
||||
args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } },
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function after_init_graph(args, steps, add_dependency, context)
|
||||
for _, other in ipairs(steps) do
|
||||
if other.name == 'AllTransactionsExceptEarningsToEquity' then
|
||||
-- AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
|
||||
-- TODO: Only in applicable years
|
||||
|
||||
local other_args: libdrcr.ReportingStepArgs
|
||||
if other.product_kinds[1] == 'Transactions' then
|
||||
other_args = 'VoidArgs'
|
||||
else
|
||||
other_args = other.args
|
||||
end
|
||||
|
||||
add_dependency(other, {
|
||||
name = 'CalculateIncomeTax',
|
||||
kind = other.product_kinds[1],
|
||||
args = other_args,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function execute(args, context, kinds_for_account, get_product)
|
||||
-- Get balances for current year
|
||||
local product = get_product({
|
||||
name = 'CombineOrdinaryTransactions',
|
||||
kind = 'BalancesBetween',
|
||||
args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } }
|
||||
})
|
||||
assert(product.BalancesBetween ~= nil)
|
||||
local balances = product.BalancesBetween.balances
|
||||
|
||||
-- Generate tax summary report
|
||||
local report: libdrcr.DynamicReport = {
|
||||
title = 'Tax summary',
|
||||
columns = {'$'},
|
||||
entries = {},
|
||||
}
|
||||
|
||||
-- Add income entries
|
||||
local total_income = 0
|
||||
|
||||
for _, income_type in ipairs(account_kinds.income_types) do
|
||||
local code, label, number = unpack(income_type)
|
||||
|
||||
local entries
|
||||
if code == 'income1' then
|
||||
-- Special case for salary or wages - round each separately
|
||||
entries = entries_for_kind_floor('austax.' .. code, true, balances, kinds_for_account, 100)
|
||||
else
|
||||
entries = entries_for_kind('austax.' .. code, true, balances, kinds_for_account)
|
||||
end
|
||||
|
||||
if #entries == 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
local section: libdrcr.Section = {
|
||||
text = label .. ' (' .. number .. ')',
|
||||
id = nil,
|
||||
visible = true,
|
||||
entries = entries,
|
||||
}
|
||||
|
||||
-- Add subtotal row
|
||||
local subtotal = math.floor(entries_subtotal(entries) / 100) * 100
|
||||
total_income += subtotal
|
||||
|
||||
table.insert(section.entries, { Row = {
|
||||
text = 'Total item ' .. number,
|
||||
quantity = {subtotal},
|
||||
id = 'total_' .. code,
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, { Section = section })
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- Total assessable income
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total assessable income',
|
||||
quantity = {total_income},
|
||||
id = 'total_income',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Add deduction entries
|
||||
local total_deductions = 0
|
||||
|
||||
for _, deduction_type in ipairs(account_kinds.deduction_types) do
|
||||
local code, label, number = unpack(deduction_type)
|
||||
|
||||
local entries = entries_for_kind('austax.' .. code, false, balances, kinds_for_account)
|
||||
|
||||
if #entries == 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
local section: libdrcr.Section = {
|
||||
text = label .. ' (' .. number .. ')',
|
||||
id = nil,
|
||||
visible = true,
|
||||
entries = entries,
|
||||
}
|
||||
|
||||
-- Add subtotal row
|
||||
local subtotal = math.floor(entries_subtotal(entries) / 100) * 100
|
||||
total_deductions += subtotal
|
||||
|
||||
table.insert(section.entries, { Row = {
|
||||
text = 'Total item ' .. number,
|
||||
quantity = {subtotal},
|
||||
id = 'total_' .. code,
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, { Section = section })
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- Total deductions
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total deductions',
|
||||
quantity = {total_deductions},
|
||||
id = 'total_deductions',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Net taxable income
|
||||
local net_taxable = total_income - total_deductions
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Net taxable income',
|
||||
quantity = {net_taxable},
|
||||
id = 'net_taxable',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Base income tax row
|
||||
local tax_base = get_base_income_tax(net_taxable, context)
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Base income tax',
|
||||
quantity = {tax_base},
|
||||
id = 'tax_base',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
|
||||
-- Total income tax row
|
||||
local tax_total = tax_base
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total income tax',
|
||||
quantity = {tax_total},
|
||||
id = 'tax_total',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
|
||||
-- Generate income tax transaction
|
||||
local transactions: {libdrcr.Transaction} = {
|
||||
{
|
||||
id = nil,
|
||||
dt = libdrcr.date_to_dt(context.eofy_date),
|
||||
description = 'Estimated income tax',
|
||||
postings = {
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = 'Income Tax',
|
||||
quantity = tax_total,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = tax_total,
|
||||
},
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = 'Income Tax Control',
|
||||
quantity = -tax_total,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = -tax_total,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[{ name = 'CalculateIncomeTax', kind = 'Transactions', args = 'VoidArgs' }] = {
|
||||
Transactions = {
|
||||
transactions = transactions
|
||||
}
|
||||
},
|
||||
[{ name = 'CalculateIncomeTax', kind = 'DynamicReport', args = 'VoidArgs' }] = {
|
||||
DynamicReport = report
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function entries_for_kind(kind: string, invert: boolean, balances:{ [string]: number }, kinds_for_account:{ [string]: {string} }): {libdrcr.DynamicReportEntry}
|
||||
-- Get accounts of specified kind
|
||||
local accounts = {}
|
||||
for account, kinds in pairs(kinds_for_account) do
|
||||
if libdrcr.arr_contains(kinds, kind) then
|
||||
table.insert(accounts, account)
|
||||
end
|
||||
end
|
||||
table.sort(accounts)
|
||||
|
||||
local entries = {}
|
||||
for _, account in ipairs(accounts) do
|
||||
local quantity = balances[account] or 0
|
||||
if invert then
|
||||
quantity = -quantity
|
||||
end
|
||||
|
||||
-- Do not show if all quantities are zero
|
||||
if quantity == 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
-- Some exceptions for the link
|
||||
local link: string | nil
|
||||
if account == 'Current Year Earnings' then
|
||||
link = '/income-statement'
|
||||
elseif account == 'Retained Earnings' then
|
||||
link = nil
|
||||
else
|
||||
link = '/transactions/' .. account
|
||||
end
|
||||
|
||||
local row: libdrcr.Row = {
|
||||
text = account,
|
||||
quantity = {quantity},
|
||||
id = nil,
|
||||
visible = true,
|
||||
link = link,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}
|
||||
table.insert(entries, { Row = row })
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
-- Call `entries_for_kind` then round results down to next multiple of `floor`
|
||||
function entries_for_kind_floor(kind: string, invert: boolean, balances:{ [string]: number }, kinds_for_account:{ [string]: {string} }, floor: number): {libdrcr.DynamicReportEntry}
|
||||
local entries = entries_for_kind(kind, invert, balances, kinds_for_account)
|
||||
for _, entry in ipairs(entries) do
|
||||
local row = (entry :: { Row: libdrcr.Row }).Row
|
||||
row.quantity[1] = math.floor(row.quantity[1] / floor) * floor
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function entries_subtotal(entries: {libdrcr.DynamicReportEntry}): number
|
||||
local subtotal = 0
|
||||
for _, entry in ipairs(entries) do
|
||||
local row = (entry :: { Row: libdrcr.Row }).Row
|
||||
subtotal += row.quantity[1]
|
||||
end
|
||||
return subtotal
|
||||
end
|
||||
|
||||
local plugin: libdrcr.Plugin = {
|
||||
name = 'austax',
|
||||
reporting_steps = {
|
||||
{
|
||||
name = 'CalculateIncomeTax',
|
||||
product_kinds = {'DynamicReport', 'Transactions'},
|
||||
requires = requires,
|
||||
after_init_graph = after_init_graph,
|
||||
execute = execute,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return plugin
|
129
libdrcr/plugins/austax/calc.luau
Normal file
129
libdrcr/plugins/austax/calc.luau
Normal file
@ -0,0 +1,129 @@
|
||||
--!strict
|
||||
-- DrCr: Web-based double-entry bookkeeping framework
|
||||
-- Copyright (C) 2022-2025 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/>.
|
||||
|
||||
local libdrcr = require('../libdrcr')
|
||||
local tax_tables = require('../austax/tax_tables')
|
||||
|
||||
local calc = {}
|
||||
|
||||
-- Get the amount of base income tax
|
||||
function calc.base_income_tax(net_taxable: number, context: libdrcr.ReportingContext): number
|
||||
local year, _, _ = libdrcr.parse_date(context.eofy_date)
|
||||
local base_tax_table = tax_tables.base_tax[year]
|
||||
|
||||
for i, row in ipairs(base_tax_table) do
|
||||
local upper_limit = row[1] * (10 ^ context.dps)
|
||||
local flat_amount = row[2] * (10 ^ context.dps)
|
||||
local marginal_rate = row[3]
|
||||
|
||||
-- Lower limit is the upper limit of the preceding bracket
|
||||
local lower_limit = 0
|
||||
if i > 1 then
|
||||
lower_limit = base_tax_table[i - 1][1] * (10 ^ context.dps)
|
||||
end
|
||||
|
||||
if net_taxable <= upper_limit then
|
||||
return flat_amount + math.floor(marginal_rate * (net_taxable - lower_limit))
|
||||
end
|
||||
end
|
||||
|
||||
error('Taxable income not within any tax bracket')
|
||||
end
|
||||
|
||||
-- Get the amount of low income tax offset
|
||||
-- https://www.ato.gov.au/forms-and-instructions/low-and-middle-income-earner-tax-offsets
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/itaa1997240/s61.115.html
|
||||
function calc.lito(net_taxable: number, tax_total: number, context: libdrcr.ReportingContext): number
|
||||
if net_taxable <= 37500 * (10 ^ context.dps) then
|
||||
-- LITO is non-refundable
|
||||
-- FIXME: This will not work if we implement multiple non-refundable tax offsets
|
||||
if tax_total <= 700 * (10 ^ context.dps) then
|
||||
return tax_total
|
||||
else
|
||||
return 700 * (10 ^ context.dps)
|
||||
end
|
||||
elseif net_taxable <= 45000 * (10 ^ context.dps) then
|
||||
return 700 * (10 ^ context.dps) - math.floor(0.05 * (net_taxable - 37500 * (10 ^ context.dps)))
|
||||
elseif net_taxable <= 66667 * (10 ^ context.dps) then
|
||||
return 325 * (10 ^ context.dps) - math.floor(0.015 * (net_taxable - 45000 * (10 ^ context.dps)))
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the amount of Medicare levy
|
||||
function calc.medicare_levy(net_taxable: number, context: libdrcr.ReportingContext): number
|
||||
local year, _, _ = libdrcr.parse_date(context.eofy_date)
|
||||
local threshold_table = tax_tables.medicare_levy_threshold[year]
|
||||
local lower_threshold = threshold_table[1] * (10 ^ context.dps)
|
||||
local upper_threshold = threshold_table[2] * (10 ^ context.dps)
|
||||
|
||||
if net_taxable < lower_threshold then
|
||||
return 0
|
||||
elseif net_taxable < upper_threshold then
|
||||
-- Medicare levy is 10% of the amount above the lower threshold
|
||||
return math.floor((net_taxable - lower_threshold) * 0.1)
|
||||
else
|
||||
-- Normal Medicare levy
|
||||
return math.floor(net_taxable * 0.02)
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the amount of Medicare levy surcharge
|
||||
function calc.medicare_levy_surcharge(net_taxable: number, rfb_grossedup: number, context: libdrcr.ReportingContext): number
|
||||
local mls_income = net_taxable + rfb_grossedup
|
||||
|
||||
local year, _, _ = libdrcr.parse_date(context.eofy_date)
|
||||
local mls_table = tax_tables.medicare_levy_surcharge_single[year]
|
||||
|
||||
for _, row in ipairs(mls_table) do
|
||||
local upper_limit = row[1] * (10 ^ context.dps)
|
||||
local rate = row[2]
|
||||
|
||||
if mls_income <= upper_limit then
|
||||
return math.floor(rate * mls_income)
|
||||
end
|
||||
end
|
||||
|
||||
error('MLS income not within any MLS bracket')
|
||||
end
|
||||
|
||||
-- Calculate the grossed-up reportable fringe benefit
|
||||
function calc.rfb_grossup(rfb_taxable: number, context: libdrcr.ReportingContext): number
|
||||
return math.floor(rfb_taxable * tax_tables.fbt_grossup)
|
||||
end
|
||||
|
||||
-- Get the amount of mandatory study loan repayment
|
||||
function calc.study_loan_repayment(net_taxable: number, rfb_grossedup: number, context: libdrcr.ReportingContext): number
|
||||
local repayment_income = net_taxable + rfb_grossedup
|
||||
|
||||
local year, _, _ = libdrcr.parse_date(context.eofy_date)
|
||||
local repayment_table = tax_tables.study_loan_repayment_rates[year]
|
||||
|
||||
for _, row in ipairs(repayment_table) do
|
||||
local upper_limit = row[1] * (10 ^ context.dps)
|
||||
local rate = row[2]
|
||||
|
||||
if repayment_income < upper_limit then
|
||||
return math.floor(rate * repayment_income)
|
||||
end
|
||||
end
|
||||
|
||||
error('HELP repayment income not within any repayment bracket')
|
||||
end
|
||||
|
||||
return calc
|
28
libdrcr/plugins/austax/plugin.luau
Normal file
28
libdrcr/plugins/austax/plugin.luau
Normal file
@ -0,0 +1,28 @@
|
||||
--!strict
|
||||
-- DrCr: Web-based double-entry bookkeeping framework
|
||||
-- Copyright (C) 2022-2025 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/>.
|
||||
|
||||
local libdrcr = require('../libdrcr')
|
||||
local reporting = require('../austax/reporting')
|
||||
|
||||
local plugin: libdrcr.Plugin = {
|
||||
name = 'austax',
|
||||
reporting_steps = {
|
||||
reporting.CalculateIncomeTax
|
||||
},
|
||||
}
|
||||
|
||||
return plugin
|
617
libdrcr/plugins/austax/reporting.luau
Normal file
617
libdrcr/plugins/austax/reporting.luau
Normal file
@ -0,0 +1,617 @@
|
||||
--!strict
|
||||
-- DrCr: Web-based double-entry bookkeeping framework
|
||||
-- Copyright (C) 2022-2025 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/>.
|
||||
|
||||
-----------------
|
||||
-- Flags
|
||||
|
||||
-- true = Spread income tax expense over monthly transactions
|
||||
-- false = Charge income tax expense in one transaction at end of financial year
|
||||
local charge_tax_monthly = true
|
||||
|
||||
-----------------
|
||||
-- Reporting code
|
||||
|
||||
local libdrcr = require('../libdrcr')
|
||||
local account_kinds = require('../austax/account_kinds')
|
||||
local calc = require('../austax/calc')
|
||||
|
||||
-- Account constants
|
||||
local CURRENT_YEAR_EARNINGS = 'Current Year Earnings'
|
||||
local HELP = 'HELP'
|
||||
local INCOME_TAX = 'Income Tax'
|
||||
local INCOME_TAX_CONTROL = 'Income Tax Control'
|
||||
local RETAINED_EARNINGS = 'Retained Earnings'
|
||||
|
||||
local reporting = {}
|
||||
|
||||
-- This ReportingStep calculates income tax
|
||||
--
|
||||
-- Generates the tax summary DynamicReport, and adds Transactions reconciling income tax expense, PAYG withholding and study loan repayments.
|
||||
reporting.CalculateIncomeTax = {
|
||||
name = 'CalculateIncomeTax',
|
||||
product_kinds = {'DynamicReport', 'Transactions'},
|
||||
} :: libdrcr.ReportingStep
|
||||
|
||||
function reporting.CalculateIncomeTax.requires(args, context)
|
||||
return {
|
||||
{
|
||||
name = 'CombineOrdinaryTransactions',
|
||||
kind = 'BalancesBetween',
|
||||
args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } },
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function reporting.CalculateIncomeTax.after_init_graph(args, steps, add_dependency, context)
|
||||
for _, other in ipairs(steps) do
|
||||
if other.name == 'AllTransactionsExceptEarningsToEquity' then
|
||||
-- AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
|
||||
-- TODO: Only in applicable years
|
||||
|
||||
local other_args: libdrcr.ReportingStepArgs
|
||||
if other.product_kinds[1] == 'Transactions' then
|
||||
other_args = 'VoidArgs'
|
||||
else
|
||||
other_args = other.args
|
||||
end
|
||||
|
||||
add_dependency(other, {
|
||||
name = 'CalculateIncomeTax',
|
||||
kind = other.product_kinds[1],
|
||||
args = other_args,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function reporting.CalculateIncomeTax.execute(args, context, kinds_for_account, get_product)
|
||||
-- Get balances for current year
|
||||
local product = get_product({
|
||||
name = 'CombineOrdinaryTransactions',
|
||||
kind = 'BalancesBetween',
|
||||
args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } }
|
||||
})
|
||||
assert(product.BalancesBetween ~= nil)
|
||||
local balances = product.BalancesBetween.balances
|
||||
|
||||
-- Generate tax summary report
|
||||
local report: libdrcr.DynamicReport = {
|
||||
title = 'Tax summary',
|
||||
columns = {'$'},
|
||||
entries = {},
|
||||
}
|
||||
|
||||
-- Add income entries
|
||||
local total_income = 0
|
||||
|
||||
for _, income_type in ipairs(account_kinds.income_types) do
|
||||
local code, label, number = unpack(income_type)
|
||||
|
||||
local entries
|
||||
if code == 'income1' then
|
||||
-- Special case for salary or wages - round each separately
|
||||
entries = entries_for_kind_floor('austax.' .. code, true, balances, kinds_for_account, 100)
|
||||
else
|
||||
entries = entries_for_kind('austax.' .. code, true, balances, kinds_for_account)
|
||||
end
|
||||
|
||||
if #entries == 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
local section: libdrcr.Section = {
|
||||
text = label .. ' (' .. number .. ')',
|
||||
id = nil,
|
||||
visible = true,
|
||||
entries = entries,
|
||||
}
|
||||
|
||||
-- Add subtotal row
|
||||
local subtotal = math.floor(entries_subtotal(entries) / 100) * 100
|
||||
total_income += subtotal
|
||||
|
||||
table.insert(section.entries, { Row = {
|
||||
text = 'Total item ' .. number,
|
||||
quantity = {subtotal},
|
||||
id = 'total_' .. code,
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = false,
|
||||
}})
|
||||
table.insert(report.entries, { Section = section })
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- Total assessable income
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total assessable income',
|
||||
quantity = {total_income},
|
||||
id = 'total_income',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Add deduction entries
|
||||
local total_deductions = 0
|
||||
|
||||
for _, deduction_type in ipairs(account_kinds.deduction_types) do
|
||||
local code, label, number = unpack(deduction_type)
|
||||
|
||||
local entries = entries_for_kind('austax.' .. code, false, balances, kinds_for_account)
|
||||
|
||||
if #entries == 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
local section: libdrcr.Section = {
|
||||
text = label .. ' (' .. number .. ')',
|
||||
id = nil,
|
||||
visible = true,
|
||||
entries = entries,
|
||||
}
|
||||
|
||||
-- Add subtotal row
|
||||
local subtotal = math.floor(entries_subtotal(entries) / 100) * 100
|
||||
total_deductions += subtotal
|
||||
|
||||
table.insert(section.entries, { Row = {
|
||||
text = 'Total item ' .. number,
|
||||
quantity = {subtotal},
|
||||
id = 'total_' .. code,
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = false,
|
||||
}})
|
||||
table.insert(report.entries, { Section = section })
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- Total deductions
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total deductions',
|
||||
quantity = {total_deductions},
|
||||
id = 'total_deductions',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Net taxable income
|
||||
local net_taxable = total_income - total_deductions
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Net taxable income',
|
||||
quantity = {net_taxable},
|
||||
id = 'net_taxable',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Base income tax row
|
||||
local tax_base = calc.base_income_tax(net_taxable, context)
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Base income tax',
|
||||
quantity = {tax_base},
|
||||
id = 'tax_base',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
|
||||
-- Medicare levy row
|
||||
local tax_ml = calc.medicare_levy(net_taxable, context)
|
||||
if tax_ml ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Medicare levy',
|
||||
quantity = {tax_ml},
|
||||
id = 'tax_ml',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
end
|
||||
|
||||
-- Precompute RFB amount as this is required for MLS
|
||||
local rfb_taxable = 0
|
||||
for account, kinds in pairs(kinds_for_account) do
|
||||
if libdrcr.arr_contains(kinds, 'austax.rfb') then
|
||||
rfb_taxable -= balances[account] or 0 -- Invert as income = credit balances
|
||||
end
|
||||
end
|
||||
local rfb_grossedup = calc.rfb_grossup(rfb_taxable, context)
|
||||
|
||||
-- Medicare levy surcharge row
|
||||
local tax_mls = calc.medicare_levy_surcharge(net_taxable, rfb_grossedup, context)
|
||||
if tax_mls ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Medicare levy surcharge',
|
||||
quantity = {tax_mls},
|
||||
id = 'tax_mls',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
end
|
||||
|
||||
-- Total income tax row
|
||||
local tax_total = tax_base + tax_ml + tax_mls
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total income tax',
|
||||
quantity = {tax_total},
|
||||
id = 'tax_total',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
|
||||
-- Low income tax offset row
|
||||
local offset_lito = calc.lito(net_taxable, tax_total, context)
|
||||
if offset_lito ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Low income tax offset',
|
||||
quantity = {offset_lito},
|
||||
id = nil,
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
end
|
||||
|
||||
-- Total tax offsets row
|
||||
local offset_total = offset_lito
|
||||
if offset_total ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total tax offsets',
|
||||
quantity = {offset_total},
|
||||
id = nil,
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = false,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- Calculate mandatory study loan repayment
|
||||
local study_loan_repayment = calc.study_loan_repayment(net_taxable, rfb_grossedup, context)
|
||||
|
||||
-- Mandatory study loan repayment section
|
||||
if study_loan_repayment ~= 0 then
|
||||
-- Taxable value of reportable fringe benefits row
|
||||
if rfb_taxable ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Taxable value of reportable fringe benefits',
|
||||
quantity = {rfb_taxable},
|
||||
id = 'rfb_taxable',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
end
|
||||
|
||||
-- Grossed-up value row
|
||||
if rfb_grossedup ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Grossed-up value',
|
||||
quantity = {rfb_grossedup},
|
||||
id = 'rfb_grossedup',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}})
|
||||
end
|
||||
|
||||
-- Mandatory study loan repayment row
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Mandatory study loan repayment',
|
||||
quantity = {study_loan_repayment},
|
||||
id = 'study_loan_repayment',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = false,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- Add PAYGW entries
|
||||
local total_paygw = 0
|
||||
local entries = entries_for_kind('austax.paygw', false, balances, kinds_for_account)
|
||||
|
||||
if #entries ~= 0 then
|
||||
local section: libdrcr.Section = {
|
||||
text = 'PAYG withheld amounts',
|
||||
id = nil,
|
||||
visible = true,
|
||||
entries = entries,
|
||||
}
|
||||
table.insert(report.entries, { Section = section })
|
||||
total_paygw = math.floor(entries_subtotal(entries) / 100) * 100
|
||||
end
|
||||
|
||||
-- Total PAYGW row
|
||||
if total_paygw ~= 0 then
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'Total withheld amounts',
|
||||
quantity = {total_paygw},
|
||||
id = 'total_paygw',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = false,
|
||||
}})
|
||||
table.insert(report.entries, 'Spacer')
|
||||
end
|
||||
|
||||
-- ATO liability row
|
||||
local ato_payable = tax_total - offset_total - total_paygw + study_loan_repayment
|
||||
table.insert(report.entries, { Row = {
|
||||
text = 'ATO liability payable (refundable)',
|
||||
quantity = {ato_payable},
|
||||
id = 'ato_payable',
|
||||
visible = true,
|
||||
link = nil,
|
||||
heading = true,
|
||||
bordered = true,
|
||||
}})
|
||||
|
||||
-- Generate income tax transactions
|
||||
local transactions: {libdrcr.Transaction} = {}
|
||||
|
||||
-- Estimated tax payable
|
||||
if charge_tax_monthly then
|
||||
-- Charge income tax expense in parts, one per month
|
||||
local monthly_tax = math.floor((tax_total - offset_total) / 12)
|
||||
local last_month_tax = (tax_total - offset_total) - 11 * monthly_tax -- To account for rounding errors
|
||||
|
||||
-- Some ad hoc calendar code
|
||||
local eofy_year, eofy_month, _ = libdrcr.parse_date(context.eofy_date)
|
||||
local last_day_of_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } -- Leap years handled below
|
||||
|
||||
for month = 1, 12 do
|
||||
local this_year, this_month_tax
|
||||
if month == eofy_month then
|
||||
this_year = eofy_year
|
||||
this_month_tax = last_month_tax
|
||||
elseif month < eofy_month then
|
||||
this_year = eofy_year
|
||||
this_month_tax = monthly_tax
|
||||
else
|
||||
this_year = eofy_year - 1
|
||||
this_month_tax = monthly_tax
|
||||
end
|
||||
|
||||
local this_day = last_day_of_month[month]
|
||||
|
||||
-- Check for leap year
|
||||
if month == 2 and (this_year % 4 == 0) and (this_year % 100 ~= 0 or this_year % 400 == 0) then
|
||||
this_day = 29
|
||||
end
|
||||
|
||||
-- Charge monthly tax
|
||||
table.insert(transactions, {
|
||||
id = nil,
|
||||
dt = libdrcr.date_to_dt(libdrcr.format_date(this_year, month, this_day)),
|
||||
description = 'Estimated income tax',
|
||||
postings = {
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = INCOME_TAX,
|
||||
quantity = this_month_tax,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = this_month_tax,
|
||||
},
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = INCOME_TAX_CONTROL,
|
||||
quantity = -this_month_tax,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = -this_month_tax,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
else
|
||||
-- Charge income tax expense in one transaction at EOFY
|
||||
table.insert(transactions, {
|
||||
id = nil,
|
||||
dt = libdrcr.date_to_dt(context.eofy_date),
|
||||
description = 'Estimated income tax',
|
||||
postings = {
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = INCOME_TAX,
|
||||
quantity = (tax_total - offset_total),
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = (tax_total - offset_total),
|
||||
},
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = INCOME_TAX_CONTROL,
|
||||
quantity = -(tax_total - offset_total),
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = -(tax_total - offset_total),
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
-- Mandatory study loan repayment
|
||||
table.insert(transactions, {
|
||||
id = nil,
|
||||
dt = libdrcr.date_to_dt(context.eofy_date),
|
||||
description = 'Mandatory study loan repayment payable',
|
||||
postings = {
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = HELP,
|
||||
quantity = study_loan_repayment,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = study_loan_repayment,
|
||||
},
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = INCOME_TAX_CONTROL,
|
||||
quantity = -study_loan_repayment,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = -study_loan_repayment,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
-- Transfer PAYGW balances to Income Tax Control
|
||||
for account, kinds in pairs(kinds_for_account) do
|
||||
if libdrcr.arr_contains(kinds, 'austax.paygw') then
|
||||
local balance = balances[account] or 0
|
||||
if balance ~= 0 then
|
||||
table.insert(transactions, {
|
||||
id = nil,
|
||||
dt = libdrcr.date_to_dt(context.eofy_date),
|
||||
description = 'PAYG withheld amounts',
|
||||
postings = {
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = INCOME_TAX_CONTROL,
|
||||
quantity = balance,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = balance,
|
||||
},
|
||||
{
|
||||
id = nil,
|
||||
transaction_id = nil,
|
||||
description = nil,
|
||||
account = account,
|
||||
quantity = -balance,
|
||||
commodity = context.reporting_commodity,
|
||||
quantity_ascost = -balance,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
[{ name = 'CalculateIncomeTax', kind = 'Transactions', args = 'VoidArgs' }] = {
|
||||
Transactions = {
|
||||
transactions = transactions
|
||||
}
|
||||
},
|
||||
[{ name = 'CalculateIncomeTax', kind = 'DynamicReport', args = 'VoidArgs' }] = {
|
||||
DynamicReport = report
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function entries_for_kind(kind: string, invert: boolean, balances:{ [string]: number }, kinds_for_account:{ [string]: {string} }): {libdrcr.DynamicReportEntry}
|
||||
-- Get accounts of specified kind
|
||||
local accounts = {}
|
||||
for account, kinds in pairs(kinds_for_account) do
|
||||
if libdrcr.arr_contains(kinds, kind) then
|
||||
table.insert(accounts, account)
|
||||
end
|
||||
end
|
||||
table.sort(accounts)
|
||||
|
||||
local entries = {}
|
||||
for _, account in ipairs(accounts) do
|
||||
local quantity = balances[account] or 0
|
||||
if invert then
|
||||
quantity = -quantity
|
||||
end
|
||||
|
||||
-- Do not show if all quantities are zero
|
||||
if quantity == 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
-- Some exceptions for the link
|
||||
local link: string | nil
|
||||
if account == CURRENT_YEAR_EARNINGS then
|
||||
link = '/income-statement'
|
||||
elseif account == RETAINED_EARNINGS then
|
||||
link = nil
|
||||
else
|
||||
link = '/transactions/' .. account
|
||||
end
|
||||
|
||||
local row: libdrcr.Row = {
|
||||
text = account,
|
||||
quantity = {quantity},
|
||||
id = nil,
|
||||
visible = true,
|
||||
link = link,
|
||||
heading = false,
|
||||
bordered = false,
|
||||
}
|
||||
table.insert(entries, { Row = row })
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
-- Call `entries_for_kind` then round results down to next multiple of `floor`
|
||||
function entries_for_kind_floor(kind: string, invert: boolean, balances:{ [string]: number }, kinds_for_account:{ [string]: {string} }, floor: number): {libdrcr.DynamicReportEntry}
|
||||
local entries = entries_for_kind(kind, invert, balances, kinds_for_account)
|
||||
for _, entry in ipairs(entries) do
|
||||
local row = (entry :: { Row: libdrcr.Row }).Row
|
||||
row.quantity[1] = math.floor(row.quantity[1] / floor) * floor
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function entries_subtotal(entries: {libdrcr.DynamicReportEntry}): number
|
||||
local subtotal = 0
|
||||
for _, entry in ipairs(entries) do
|
||||
local row = (entry :: { Row: libdrcr.Row }).Row
|
||||
subtotal += row.quantity[1]
|
||||
end
|
||||
return subtotal
|
||||
end
|
||||
|
||||
return reporting
|
@ -15,10 +15,14 @@
|
||||
-- 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/>.
|
||||
|
||||
local tax_tables = {}
|
||||
|
||||
-- Base income tax
|
||||
-- https://www.ato.gov.au/rates/individual-income-tax-rates/
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/itra1986174/sch7.html
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/itra1986174/s3.html (tax-free threshold)
|
||||
-- Maps each financial year to list of (upper limit (INclusive), flat amount, marginal rate)
|
||||
local base_tax = {
|
||||
tax_tables.base_tax = {
|
||||
[2025] = {
|
||||
{18200, 0, 0},
|
||||
{45000, 0, 0.16},
|
||||
@ -42,8 +46,114 @@ local base_tax = {
|
||||
}
|
||||
}
|
||||
|
||||
local tax_tables = {
|
||||
base_tax = base_tax,
|
||||
-- FBT type 1 gross-up factor
|
||||
-- https://www.ato.gov.au/rates/fbt/#GrossupratesforFBT
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/fbtaa1986312/s5b.html
|
||||
tax_tables.fbt_grossup = 2.0802
|
||||
|
||||
-- Medicare levy thresholds
|
||||
-- https://www.ato.gov.au/Individuals/Medicare-and-private-health-insurance/Medicare-levy/Medicare-levy-reduction/Medicare-levy-reduction-for-low-income-earners/
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/mla1986131/s3.html
|
||||
-- Maps each financial year to list of (lower threshold, upper threshold)
|
||||
tax_tables.medicare_levy_threshold = {
|
||||
[2025] = {27222, 34027},
|
||||
[2024] = {26000, 32500},
|
||||
[2023] = {24276, 30345},
|
||||
[2022] = {23365, 29207}
|
||||
}
|
||||
|
||||
-- Medicare levy surcharge rates (singles)
|
||||
-- https://www.ato.gov.au/individuals-and-families/medicare-and-private-health-insurance/medicare-levy-surcharge/medicare-levy-surcharge-income-thresholds-and-rates
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdb/au/legis/cth/consol_act/mla1986131/s8b.html
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/phia2007248/s22.35.html
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/phia2007248/s22.45.html
|
||||
-- Maps each financial year to list of (upper limit (INclusive), MLS rate)
|
||||
-- FIXME: Only supports singles
|
||||
tax_tables.medicare_levy_surcharge_single = {
|
||||
[2025] = {
|
||||
{97000, 0},
|
||||
{113000, 0.01},
|
||||
{151000, 0.0125},
|
||||
{math.huge, 0.015}
|
||||
},
|
||||
[2024] = {
|
||||
{93000, 0},
|
||||
{108000, 0.01},
|
||||
{144000, 0.0125},
|
||||
{math.huge, 0.015}
|
||||
}
|
||||
}
|
||||
|
||||
-- Study and training loan (HELP, etc.) repayment thresholds and rates
|
||||
-- https://www.ato.gov.au/Rates/HELP,-TSL-and-SFSS-repayment-thresholds-and-rates/
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/hesa2003271/s154.20.html
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/hesa2003271/s154.25.html
|
||||
-- https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/cth/consol_act/hesa2003271/s154.30.html
|
||||
-- Maps each financial year to list of (upper limit (EXclusive), repayment rate)
|
||||
tax_tables.study_loan_repayment_rates = {
|
||||
[2025] = {
|
||||
{54435, 0},
|
||||
{62851, 0.01},
|
||||
{66621, 0.02},
|
||||
{70619, 0.025},
|
||||
{74856, 0.03},
|
||||
{79347, 0.035},
|
||||
{84108, 0.04},
|
||||
{89155, 0.045},
|
||||
{94504, 0.05},
|
||||
{100175, 0.055},
|
||||
{106186, 0.06},
|
||||
{112557, 0.065},
|
||||
{119310, 0.07},
|
||||
{126468, 0.075},
|
||||
{134057, 0.08},
|
||||
{142101, 0.085},
|
||||
{150627, 0.09},
|
||||
{159664, 0.095},
|
||||
{math.huge, 0.1}
|
||||
},
|
||||
[2024] = {
|
||||
{51550, 0},
|
||||
{59519, 0.01},
|
||||
{63090, 0.02},
|
||||
{66876, 0.025},
|
||||
{70889, 0.03},
|
||||
{75141, 0.035},
|
||||
{79650, 0.04},
|
||||
{84430, 0.045},
|
||||
{89495, 0.05},
|
||||
{94866, 0.055},
|
||||
{100558, 0.06},
|
||||
{106591, 0.065},
|
||||
{112986, 0.07},
|
||||
{119765, 0.075},
|
||||
{126951, 0.08},
|
||||
{134569, 0.085},
|
||||
{142643, 0.09},
|
||||
{151201, 0.095},
|
||||
{math.huge, 0.1}
|
||||
},
|
||||
[2023] = {
|
||||
{48361, 0},
|
||||
{55837, 0.01},
|
||||
{59187, 0.02},
|
||||
{62739, 0.025},
|
||||
{66503, 0.03},
|
||||
{70493, 0.035},
|
||||
{74723, 0.04},
|
||||
{79207, 0.045},
|
||||
{83959, 0.05},
|
||||
{88997, 0.055},
|
||||
{94337, 0.06},
|
||||
{99997, 0.065},
|
||||
{105997, 0.07},
|
||||
{112356, 0.075},
|
||||
{119098, 0.08},
|
||||
{126244, 0.085},
|
||||
{133819, 0.09},
|
||||
{141848, 0.095},
|
||||
{math.huge, 0.1}
|
||||
}
|
||||
}
|
||||
|
||||
return tax_tables
|
||||
|
@ -149,6 +149,7 @@ export type MultipleDateStartDateEndArgs = { dates: {DateStartDateEndArgs} }
|
||||
|
||||
local libdrcr = {}
|
||||
|
||||
-- Returns true if array haystack contains needle
|
||||
function libdrcr.arr_contains(haystack: {any}, needle: any): boolean
|
||||
for _, element in ipairs(haystack) do
|
||||
if element == needle then
|
||||
@ -158,10 +159,17 @@ function libdrcr.arr_contains(haystack: {any}, needle: any): boolean
|
||||
return false
|
||||
end
|
||||
|
||||
-- Converts a date string (YYYY-MM-DD) into datetime string (YYYY-MM-DD HH:MM:SS.xxxxxx) for database
|
||||
function libdrcr.date_to_dt(date: string): string
|
||||
return date .. ' 00:00:00.000000'
|
||||
end
|
||||
|
||||
-- Formats the date as date string (YYYY-MM-DD)
|
||||
function libdrcr.format_date(year: number, month: number, day: number): string
|
||||
return string.format('%04d-%02d-%02d', year, month, day)
|
||||
end
|
||||
|
||||
-- Parses the date string (YYYY-MM-DD) into components
|
||||
function libdrcr.parse_date(date: string): (number, number, number)
|
||||
local year_str, month_str, day_str = string.match(date, '(%d%d%d%d)-(%d%d)-(%d%d)')
|
||||
|
||||
@ -176,6 +184,7 @@ function libdrcr.parse_date(date: string): (number, number, number)
|
||||
return year, month, day
|
||||
end
|
||||
|
||||
-- Convert the Lua value to string recursively
|
||||
function libdrcr.repr(value: any): string
|
||||
local result = ''
|
||||
if type(value) == 'table' then
|
||||
|
@ -13,6 +13,4 @@ pub type QuantityInt = i64;
|
||||
// Magic strings
|
||||
// TODO: Make this configurable
|
||||
pub const CURRENT_YEAR_EARNINGS: &'static str = "Current Year Earnings";
|
||||
pub const INCOME_TAX: &'static str = "Income Tax";
|
||||
pub const INCOME_TAX_CONTROL: &'static str = "Income Tax Control";
|
||||
pub const RETAINED_EARNINGS: &'static str = "Retained Earnings";
|
||||
|
@ -39,7 +39,7 @@ async fn main() {
|
||||
let mut context = ReportingContext::new(
|
||||
db_connection,
|
||||
"plugins".to_string(),
|
||||
vec!["austax.austax".to_string()],
|
||||
vec!["austax.plugin".to_string()],
|
||||
NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
|
||||
"$".to_string(),
|
||||
);
|
||||
|
@ -764,14 +764,17 @@ impl ReportingStep for UpdateBalancesBetween {
|
||||
let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id());
|
||||
let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build
|
||||
|
||||
if matches!(
|
||||
balances_between_product.args,
|
||||
ReportingStepArgs::DateStartDateEndArgs(_)
|
||||
) {
|
||||
// Directly depends on BalanceBetween -> Transaction with appropriate date
|
||||
// Do not need to add extra dependencies
|
||||
} else {
|
||||
// Depends on BalanceBetween with appropriate date
|
||||
let mut is_already_transitive_dependency = false;
|
||||
if let ReportingStepArgs::DateStartDateEndArgs(args) = &balances_between_product.args {
|
||||
if *args == self.args {
|
||||
// Directly depends on BalanceBetween -> Transaction with appropriate date
|
||||
// Do not need to add extra dependencies
|
||||
is_already_transitive_dependency = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !is_already_transitive_dependency {
|
||||
// Add dependency on BalanceBetween with appropriate date
|
||||
dependencies.add_dependency(
|
||||
self.id(),
|
||||
ReportingProductId {
|
||||
|
35
package.json
35
package.json
@ -10,28 +10,29 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@tauri-apps/plugin-sql": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "~2.2.2",
|
||||
"@tauri-apps/plugin-fs": "~2.3.0",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"@tauri-apps/plugin-sql": "~2.2.0",
|
||||
"@tauri-apps/plugin-store": "~2.2.0",
|
||||
"clusterize.js": "^1.0.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "4"
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/clusterize.js": "^0.18.3",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.3.1",
|
||||
"vue-tsc": "^2.0.22"
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^5.4.19",
|
||||
"vue-tsc": "^2.2.10"
|
||||
}
|
||||
}
|
||||
|
875
pnpm-lock.yaml
generated
875
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2256
src-tauri/Cargo.lock
generated
2256
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,7 @@ serde_json = "1"
|
||||
sqlx = { version = "0.8", features = ["json", "time"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
||||
tauri-plugin-store = "2"
|
||||
|
@ -12,6 +12,9 @@
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-show",
|
||||
"dialog:default",
|
||||
"fs:default",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-resource-read-recursive",
|
||||
"shell:allow-open",
|
||||
"sql:default",
|
||||
"sql:allow-execute",
|
||||
|
@ -85,6 +85,7 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_sql::Builder::new().build())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
|
@ -44,7 +44,7 @@ fn prepare_reporting_context(context: &mut ReportingContext) {
|
||||
|
||||
fn get_plugins() -> Vec<String> {
|
||||
// FIXME: Dynamically get this
|
||||
vec!["austax.austax".to_string()]
|
||||
vec!["austax.plugin".to_string()]
|
||||
}
|
||||
|
||||
pub(crate) async fn get_report(
|
||||
|
@ -213,7 +213,7 @@
|
||||
VALUES ($1, $2)`,
|
||||
[newTransaction.dt, newTransaction.description]
|
||||
);
|
||||
newTransaction.id = result.lastInsertId;
|
||||
newTransaction.id = result.lastInsertId!;
|
||||
} else {
|
||||
// Update existing transaction
|
||||
await dbTransaction.execute(
|
||||
|
@ -44,7 +44,7 @@ async function initApp() {
|
||||
{ path: '/statement-lines/import', name: 'import-statement', component: () => import('./pages/ImportStatementView.vue') },
|
||||
{ path: '/transactions/:account', name: 'transactions', component: () => import('./pages/TransactionsView.vue') },
|
||||
{ path: '/trial-balance', name: 'trial-balance', component: () => import('./reports/TrialBalanceReport.vue') },
|
||||
{ path: '/austax/tax-summary', name: 'tax-summary', component: () => import('./austax/TaxSummaryReport.vue') },
|
||||
{ path: '/austax/tax-summary', name: 'tax-summary', component: () => import('./plugins/austax/TaxSummaryReport.vue') },
|
||||
];
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||
Copyright (C) 2022-2025 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
|
||||
@ -57,20 +57,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { accountKinds } from '../registry.ts';
|
||||
import { drcrAccountKinds, getAccountKinds } from '../registry.ts';
|
||||
import { db } from '../db.ts';
|
||||
import DropdownBox from '../components/DropdownBox.vue';
|
||||
|
||||
const accountKindsMap = new Map(accountKinds);
|
||||
const accountKindsByModule = [...Map.groupBy(accountKinds, (k) => k[0].split('.')[0]).entries()];
|
||||
const accountKinds = ref([...drcrAccountKinds]);
|
||||
const accountKindsMap = computed(() => new Map(accountKinds.value));
|
||||
const accountKindsByModule = computed(() => [...Map.groupBy(accountKinds.value, (k) => k[0].split('.')[0]).entries()]);
|
||||
|
||||
const accounts = ref(new Map<string, string[]>());
|
||||
const selectedAccounts = ref([]);
|
||||
const selectedAccountKind = ref(accountKinds[0]);
|
||||
const selectedAccountKind = ref(drcrAccountKinds[0]);
|
||||
|
||||
async function load() {
|
||||
async function loadAccountConfigurations() {
|
||||
const session = await db.load();
|
||||
|
||||
const accountKindsRaw: {account: string, kind: string | null}[] = await session.select(
|
||||
@ -88,7 +89,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
async function loadAccountKinds() {
|
||||
accountKinds.value = await getAccountKinds();
|
||||
}
|
||||
|
||||
loadAccountConfigurations();
|
||||
loadAccountKinds();
|
||||
|
||||
async function addAccountType() {
|
||||
// Associate selected accounts with the selected account kind
|
||||
@ -109,7 +115,7 @@
|
||||
|
||||
// Reload data
|
||||
accounts.value.clear();
|
||||
await load();
|
||||
await loadAccountConfigurations();
|
||||
}
|
||||
|
||||
async function removeAccountType() {
|
||||
@ -131,6 +137,6 @@
|
||||
|
||||
// Reload data
|
||||
accounts.value.clear();
|
||||
await load();
|
||||
await loadAccountConfigurations();
|
||||
}
|
||||
</script>
|
||||
|
@ -25,7 +25,7 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
|
||||
import { DynamicReport } from '../reports/base.ts';
|
||||
import { DynamicReport } from '../../reports/base.ts';
|
||||
|
||||
const report = ref(null as DynamicReport | null);
|
||||
|
75
src/plugins/austax/account_kinds.ts
Normal file
75
src/plugins/austax/account_kinds.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022-2025 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/>.
|
||||
*/
|
||||
|
||||
import { resolveResource } from '@tauri-apps/api/path';
|
||||
import { readTextFile } from '@tauri-apps/plugin-fs';
|
||||
|
||||
// Dodgy implementation to parse Lua table as JSON
|
||||
function parseLua(luaArray: string): any[] {
|
||||
luaArray = luaArray.trim();
|
||||
|
||||
// Remove surrounding { and }
|
||||
if (!luaArray.startsWith('{') || !luaArray.endsWith('}')) {
|
||||
throw new Error('Unparseable Lua array');
|
||||
}
|
||||
luaArray = luaArray.substring(1, luaArray.length - 1).trim();
|
||||
|
||||
if (luaArray.endsWith(',')) {
|
||||
// Remove trailing comma as this is invalid JSON
|
||||
luaArray = luaArray.substring(0, luaArray.length - 1);
|
||||
}
|
||||
|
||||
// Replace Lua {...} with JSON [...]
|
||||
luaArray = luaArray.replaceAll('{', '[').replaceAll('}', ']');
|
||||
|
||||
// Replace single quotes with double quotes
|
||||
luaArray = luaArray.replaceAll(/(?<!\\)'/g, '"');
|
||||
luaArray = luaArray.replaceAll("\\'", "'");
|
||||
|
||||
return JSON.parse('[' + luaArray + ']');
|
||||
}
|
||||
|
||||
export async function getAccountKinds(): Promise<[string, string][]> {
|
||||
// Read contents of account_kinds.luau
|
||||
const luaFilePath = await resolveResource('plugins/austax/account_kinds.luau');
|
||||
const luaFileContent = await readTextFile(luaFilePath);
|
||||
|
||||
const accountKinds: [string, string][] = [];
|
||||
|
||||
// Parse income_types
|
||||
const incomeTypesLua = luaFileContent.match(/local income_types = ({.*?\n})\n/s)![1];
|
||||
const incomeTypes = parseLua(incomeTypesLua);
|
||||
for (const [code, name, number] of incomeTypes) {
|
||||
accountKinds.push(['austax.' + code, name + ' (' + number + ')']);
|
||||
}
|
||||
|
||||
// Parse deduction_types
|
||||
const deductionTypesLua = luaFileContent.match(/local deduction_types = ({.*?\n})\n/s)![1];
|
||||
const deductionTypes = parseLua(deductionTypesLua);
|
||||
for (const [code, name, number] of deductionTypes) {
|
||||
accountKinds.push(['austax.' + code, name + ' (' + number + ')']);
|
||||
}
|
||||
|
||||
// Hard-coded types
|
||||
accountKinds.push(['austax.offset', 'Tax offset']);
|
||||
accountKinds.push(['austax.paygw', 'PAYG withheld amounts']);
|
||||
accountKinds.push(['austax.cgtasset', 'CGT asset']);
|
||||
accountKinds.push(['austax.rfb', 'Reportable fringe benefit']);
|
||||
|
||||
return accountKinds;
|
||||
}
|
@ -16,10 +16,21 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const accountKinds: [string, string][] = [
|
||||
import * as austax from './plugins/austax/account_kinds.ts';
|
||||
|
||||
export const drcrAccountKinds: [string, string][] = [
|
||||
['drcr.asset', 'Asset'],
|
||||
['drcr.liability', 'Liability'],
|
||||
['drcr.income', 'Income'],
|
||||
['drcr.expense', 'Expense'],
|
||||
['drcr.equity', 'Equity']
|
||||
];
|
||||
|
||||
export async function getAccountKinds() {
|
||||
const accountKinds = [...drcrAccountKinds];
|
||||
|
||||
// FIXME: Make this customisable
|
||||
accountKinds.push(...await austax.getAccountKinds());
|
||||
|
||||
return accountKinds;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user