548 lines
14 KiB
Plaintext
548 lines
14 KiB
Plaintext
--!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 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
|
|
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),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
-- Mandatory study loan repayment
|
|
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
|