DrCr/libdrcr/plugins/austax/austax.luau

355 lines
9.2 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 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