austax: Basic implementation in Lua
This commit is contained in:
parent
1d3aa269b7
commit
bbb91ad1e7
68
libdrcr/plugins/austax/account_kinds.luau
Normal file
68
libdrcr/plugins/austax/account_kinds.luau
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
--!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 income_types = {
|
||||||
|
{'income1', 'Salary or wages', '1'},
|
||||||
|
{'income2', 'Allowances, earnings, tips, director\'s fees etc.', '2'},
|
||||||
|
{'income3', 'Employer lump sum payments', '3'},
|
||||||
|
{'income4', 'Employment termination payments', '4'},
|
||||||
|
{'income5', 'Australian Government allowances and payments', '5'},
|
||||||
|
{'income6', 'Australian Government pensions and allowances', '6'},
|
||||||
|
{'income7', 'Australian annuities and superannuation income streams', '7'},
|
||||||
|
{'income8', 'Australian superannuation lump sum payments', '8'},
|
||||||
|
{'income9', 'Attributed personal services income', '9'},
|
||||||
|
{'income10', 'Gross interest', '10'},
|
||||||
|
{'income11', 'Dividends', '11'},
|
||||||
|
{'income12', 'Employee share schemes', '12'},
|
||||||
|
{'income13', 'Partnerships and trusts', '13'},
|
||||||
|
{'income14', 'Personal services income', '14'},
|
||||||
|
{'income15', 'Net income or loss from business', '15'},
|
||||||
|
{'income16', 'Deferred non-commercial business losses', '16'},
|
||||||
|
{'income17', 'Net farm management deposits or repayments', '17'},
|
||||||
|
{'income18', 'Capital gains', '18'},
|
||||||
|
{'income19', 'Foreign entities', '19'},
|
||||||
|
{'income20', 'Foreign source income and foreign assets or property', '20'},
|
||||||
|
{'income21', 'Rent', '21'},
|
||||||
|
{'income22', 'Bonuses from life insurance companies and friendly societies', '22'},
|
||||||
|
{'income23', 'Forestry managed investment scheme income', '23'},
|
||||||
|
{'income24', 'Other income', '24'},
|
||||||
|
}
|
||||||
|
|
||||||
|
local deduction_types = {
|
||||||
|
{'d1', 'Work-related car expenses', 'D1'},
|
||||||
|
{'d2', 'Work-related travel expenses', 'D2'},
|
||||||
|
{'d3', 'Work-related clothing, laundry and dry cleaning expenses', 'D3'},
|
||||||
|
{'d4', 'Work-related self-education expenses', 'D4'},
|
||||||
|
{'d5', 'Other work-related expenses', 'D5'},
|
||||||
|
{'d6', 'Low value pool deduction', 'D6'},
|
||||||
|
{'d7', 'Interest deductions', 'D7'},
|
||||||
|
{'d8', 'Dividend deductions', 'D8'},
|
||||||
|
{'d9', 'Gifts or donations', 'D9'},
|
||||||
|
{'d10', 'Cost of managing tax affairs', 'D10'},
|
||||||
|
{'d11', 'Deductible amount of undeducted purchase price of a foreign pension or annuity', 'D11'},
|
||||||
|
{'d12', 'Personal superannuation contributions', 'D12'},
|
||||||
|
{'d13', 'Deduction for project pool', 'D13'},
|
||||||
|
{'d14', 'Forestry managed investment scheme deduction', 'D14'},
|
||||||
|
{'d15', 'Other deductions', 'D15'},
|
||||||
|
}
|
||||||
|
|
||||||
|
local account_kinds = {
|
||||||
|
income_types = income_types,
|
||||||
|
deduction_types = deduction_types,
|
||||||
|
}
|
||||||
|
|
||||||
|
return account_kinds
|
@ -17,6 +17,32 @@
|
|||||||
|
|
||||||
local libdrcr = require('../libdrcr')
|
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)
|
function requires(args, context)
|
||||||
return {
|
return {
|
||||||
{
|
{
|
||||||
@ -28,8 +54,7 @@ function requires(args, context)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function after_init_graph(args, steps, add_dependency, context)
|
function after_init_graph(args, steps, add_dependency, context)
|
||||||
for i = 1, #steps do
|
for _, other in ipairs(steps) do
|
||||||
local other = steps[i]
|
|
||||||
if other.name == 'AllTransactionsExceptEarningsToEquity' then
|
if other.name == 'AllTransactionsExceptEarningsToEquity' then
|
||||||
-- AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
|
-- AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
|
||||||
-- TODO: Only in applicable years
|
-- TODO: Only in applicable years
|
||||||
@ -50,26 +75,269 @@ function after_init_graph(args, steps, add_dependency, context)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function execute(args, context, get_product)
|
function execute(args, context, kinds_for_account, get_product)
|
||||||
print('Stub: CombineOrdinaryTransactions.execute')
|
-- Get balances for current year
|
||||||
|
|
||||||
local product = get_product({
|
local product = get_product({
|
||||||
name = 'CombineOrdinaryTransactions',
|
name = 'CombineOrdinaryTransactions',
|
||||||
kind = 'BalancesBetween',
|
kind = 'BalancesBetween',
|
||||||
args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } }
|
args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } }
|
||||||
})
|
})
|
||||||
|
assert(product.BalancesBetween ~= nil)
|
||||||
|
local balances = product.BalancesBetween.balances
|
||||||
|
|
||||||
print(libdrcr.repr(product))
|
-- 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 {
|
return {
|
||||||
[{ name = 'CalculateIncomeTax', kind = 'Transactions', args = 'VoidArgs' }] = {
|
[{ name = 'CalculateIncomeTax', kind = 'Transactions', args = 'VoidArgs' }] = {
|
||||||
Transactions = {
|
Transactions = {
|
||||||
transactions = {}
|
transactions = transactions
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[{ name = 'CalculateIncomeTax', kind = 'DynamicReport', args = 'VoidArgs' }] = {
|
||||||
|
DynamicReport = report
|
||||||
|
},
|
||||||
}
|
}
|
||||||
end
|
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 = {
|
local plugin: libdrcr.Plugin = {
|
||||||
name = 'austax',
|
name = 'austax',
|
||||||
reporting_steps = {
|
reporting_steps = {
|
||||||
|
49
libdrcr/plugins/austax/tax_tables.luau
Normal file
49
libdrcr/plugins/austax/tax_tables.luau
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
--!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/>.
|
||||||
|
|
||||||
|
-- Base income tax
|
||||||
|
-- https://www.ato.gov.au/rates/individual-income-tax-rates/
|
||||||
|
-- Maps each financial year to list of (upper limit (INclusive), flat amount, marginal rate)
|
||||||
|
local base_tax = {
|
||||||
|
[2025] = {
|
||||||
|
{18200, 0, 0},
|
||||||
|
{45000, 0, 0.16},
|
||||||
|
{135000, 4288, 0.30},
|
||||||
|
{190000, 31288, 0.37},
|
||||||
|
{math.huge, 51638, 0.45}
|
||||||
|
},
|
||||||
|
[2024] = {
|
||||||
|
{18200, 0, 0},
|
||||||
|
{45000, 0, 0.19},
|
||||||
|
{120000, 5092, 0.325},
|
||||||
|
{180000, 29467, 0.37},
|
||||||
|
{math.huge, 51667, 0.45}
|
||||||
|
},
|
||||||
|
[2023] = {
|
||||||
|
{18200, 0, 0},
|
||||||
|
{45000, 0, 0.19},
|
||||||
|
{120000, 5092, 0.325},
|
||||||
|
{180000, 29467, 0.37},
|
||||||
|
{math.huge, 51667, 0.45}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local tax_tables = {
|
||||||
|
base_tax = base_tax,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tax_tables
|
@ -44,10 +44,39 @@ export type ReportingStep = {
|
|||||||
execute: (
|
execute: (
|
||||||
ReportingStepArgs,
|
ReportingStepArgs,
|
||||||
ReportingContext,
|
ReportingContext,
|
||||||
|
{[string]: {string}}, -- kinds_for_account
|
||||||
(ReportingProductId) -> ReportingProduct -- get_product
|
(ReportingProductId) -> ReportingProduct -- get_product
|
||||||
) -> {[ReportingProductId]: ReportingProduct},
|
) -> {[ReportingProductId]: ReportingProduct},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
------------------
|
||||||
|
-- Dynamic reports
|
||||||
|
|
||||||
|
export type DynamicReport = {
|
||||||
|
title: string,
|
||||||
|
columns: {string},
|
||||||
|
entries: {DynamicReportEntry},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DynamicReportEntry = 'Spacer' | { Section: Section } | { Row: Row }
|
||||||
|
|
||||||
|
export type Section = {
|
||||||
|
text: string | nil,
|
||||||
|
id: string | nil,
|
||||||
|
visible: boolean,
|
||||||
|
entries: {DynamicReportEntry},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Row = {
|
||||||
|
text: string,
|
||||||
|
quantity: {number},
|
||||||
|
id: string | nil,
|
||||||
|
visible: boolean,
|
||||||
|
link: string | nil,
|
||||||
|
heading: boolean,
|
||||||
|
bordered: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
-------------------------
|
-------------------------
|
||||||
-- libdrcr internal types
|
-- libdrcr internal types
|
||||||
|
|
||||||
@ -55,6 +84,7 @@ export type ReportingContext = {
|
|||||||
sofy_date: string,
|
sofy_date: string,
|
||||||
eofy_date: string,
|
eofy_date: string,
|
||||||
reporting_commodity: string,
|
reporting_commodity: string,
|
||||||
|
dps: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Accounting types
|
-- Accounting types
|
||||||
@ -87,7 +117,6 @@ export type ReportingProduct = {
|
|||||||
|
|
||||||
export type BalancesAt = any
|
export type BalancesAt = any
|
||||||
export type BalancesBetween = any
|
export type BalancesBetween = any
|
||||||
export type DynamicReport = any
|
|
||||||
export type Transactions = { transactions: {Transaction} }
|
export type Transactions = { transactions: {Transaction} }
|
||||||
|
|
||||||
export type ReportingProductId = {
|
export type ReportingProductId = {
|
||||||
@ -120,6 +149,33 @@ export type MultipleDateStartDateEndArgs = { dates: {DateStartDateEndArgs} }
|
|||||||
|
|
||||||
local libdrcr = {}
|
local libdrcr = {}
|
||||||
|
|
||||||
|
function libdrcr.arr_contains(haystack: {any}, needle: any): boolean
|
||||||
|
for _, element in ipairs(haystack) do
|
||||||
|
if element == needle then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function libdrcr.date_to_dt(date: string): string
|
||||||
|
return date .. ' 00:00:00.000000'
|
||||||
|
end
|
||||||
|
|
||||||
|
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)')
|
||||||
|
|
||||||
|
local year = tonumber(year_str)
|
||||||
|
local month = tonumber(month_str)
|
||||||
|
local day = tonumber(day_str)
|
||||||
|
|
||||||
|
assert(year ~= nil)
|
||||||
|
assert(month ~= nil)
|
||||||
|
assert(day ~= nil)
|
||||||
|
|
||||||
|
return year, month, day
|
||||||
|
end
|
||||||
|
|
||||||
function libdrcr.repr(value: any): string
|
function libdrcr.repr(value: any): string
|
||||||
local result = ''
|
local result = ''
|
||||||
if type(value) == 'table' then
|
if type(value) == 'table' then
|
||||||
|
@ -1,512 +0,0 @@
|
|||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//! Implements Australian individual income tax calculations
|
|
||||||
|
|
||||||
// TODO: Ideally this would be separated into its own plugin
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::account_config::kinds_for_account;
|
|
||||||
use crate::model::transaction::{Posting, Transaction, TransactionWithPostings};
|
|
||||||
use crate::reporting::calculator::ReportingGraphDependencies;
|
|
||||||
use crate::reporting::dynamic_report::{
|
|
||||||
entries_for_kind, DynamicReport, DynamicReportEntry, Row, Section,
|
|
||||||
};
|
|
||||||
use crate::reporting::executor::ReportingExecutionError;
|
|
||||||
use crate::reporting::steps::AllTransactionsExceptEarningsToEquityBalances;
|
|
||||||
use crate::reporting::types::{
|
|
||||||
BalancesBetween, DateStartDateEndArgs, ReportingContext, ReportingProductId,
|
|
||||||
ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId,
|
|
||||||
Transactions, VoidArgs,
|
|
||||||
};
|
|
||||||
use crate::util::sofy_from_eofy;
|
|
||||||
use crate::{QuantityInt, INCOME_TAX, INCOME_TAX_CONTROL};
|
|
||||||
|
|
||||||
// Constants and tax calculations
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const INCOME_TYPES: &[(&str, &str, &str)] = &[
|
|
||||||
("income1", "Salary or wages", "1"),
|
|
||||||
("income2", "Allowances, earnings, tips, director's fees etc.", "2"),
|
|
||||||
("income3", "Employer lump sum payments", "3"),
|
|
||||||
("income4", "Employment termination payments", "4"),
|
|
||||||
("income5", "Australian Government allowances and payments", "5"),
|
|
||||||
("income6", "Australian Government pensions and allowances", "6"),
|
|
||||||
("income7", "Australian annuities and superannuation income streams", "7"),
|
|
||||||
("income8", "Australian superannuation lump sum payments", "8"),
|
|
||||||
("income9", "Attributed personal services income", "9"),
|
|
||||||
("income10", "Gross interest", "10"),
|
|
||||||
("income11", "Dividends", "11"),
|
|
||||||
("income12", "Employee share schemes", "12"),
|
|
||||||
("income13", "Partnerships and trusts", "13"),
|
|
||||||
("income14", "Personal services income", "14"),
|
|
||||||
("income15", "Net income or loss from business", "15"),
|
|
||||||
("income16", "Deferred non-commercial business losses", "16"),
|
|
||||||
("income17", "Net farm management deposits or repayments", "17"),
|
|
||||||
("income18", "Capital gains", "18"),
|
|
||||||
("income19", "Foreign entities", "19"),
|
|
||||||
("income20", "Foreign source income and foreign assets or property", "20"),
|
|
||||||
("income21", "Rent", "21"),
|
|
||||||
("income22", "Bonuses from life insurance companies and friendly societies", "22"),
|
|
||||||
("income23", "Forestry managed investment scheme income", "23"),
|
|
||||||
("income24", "Other income", "24"),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const DEDUCTION_TYPES: &[(&str, &str, &str)] = &[
|
|
||||||
("d1", "Work-related car expenses", "D1"),
|
|
||||||
("d2", "Work-related travel expenses", "D2"),
|
|
||||||
("d3", "Work-related clothing, laundry and dry cleaning expenses", "D3"),
|
|
||||||
("d4", "Work-related self-education expenses", "D4"),
|
|
||||||
("d5", "Other work-related expenses", "D5"),
|
|
||||||
("d6", "Low value pool deduction", "D6"),
|
|
||||||
("d7", "Interest deductions", "D7"),
|
|
||||||
("d8", "Dividend deductions", "D8"),
|
|
||||||
("d9", "Gifts or donations", "D9"),
|
|
||||||
("d10", "Cost of managing tax affairs", "D10"),
|
|
||||||
("d11", "Deductible amount of undeducted purchase price of a foreign pension or annuity", "D11"),
|
|
||||||
("d12", "Personal superannuation contributions", "D12"),
|
|
||||||
("d13", "Deduction for project pool", "D13"),
|
|
||||||
("d14", "Forestry managed investment scheme deduction", "D14"),
|
|
||||||
("d15", "Other deductions", "D15"),
|
|
||||||
];
|
|
||||||
|
|
||||||
fn get_grossedup_rfb(taxable_value: QuantityInt) -> QuantityInt {
|
|
||||||
// FIXME: May vary from year to year
|
|
||||||
((taxable_value as f64) * 2.0802) as QuantityInt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_base_income_tax(net_taxable: QuantityInt) -> QuantityInt {
|
|
||||||
// FIXME: May vary from year to year
|
|
||||||
if net_taxable <= 18200_00 {
|
|
||||||
0
|
|
||||||
} else if net_taxable <= 45000_00 {
|
|
||||||
(0.16 * (net_taxable - 18200_00) as f64) as QuantityInt
|
|
||||||
} else if net_taxable <= 135000_00 {
|
|
||||||
4288_00 + (0.30 * (net_taxable - 45000_00) as f64) as QuantityInt
|
|
||||||
} else if net_taxable <= 190000_00 {
|
|
||||||
31288_00 + (0.37 * (net_taxable - 135000_00) as f64) as QuantityInt
|
|
||||||
} else {
|
|
||||||
51638_00 + (0.45 * (net_taxable - 190000_00) as f64) as QuantityInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn get_medicare_levy(net_taxable: QuantityInt) -> QuantityInt {
|
|
||||||
// todo!()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn get_medicare_levy_surcharge(
|
|
||||||
// net_taxable: QuantityInt,
|
|
||||||
// rfb_grossedup: QuantityInt,
|
|
||||||
// ) -> QuantityInt {
|
|
||||||
// todo!()
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
|
|
||||||
pub fn register_lookup_fns(context: &mut ReportingContext) {
|
|
||||||
CalculateIncomeTax::register_lookup_fn(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates income tax
|
|
||||||
///
|
|
||||||
/// [Transactions] product represents income tax charge for the year.
|
|
||||||
/// [DynamicReport] product represents the tax summary report.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CalculateIncomeTax {}
|
|
||||||
|
|
||||||
impl CalculateIncomeTax {
|
|
||||||
fn register_lookup_fn(context: &mut ReportingContext) {
|
|
||||||
context.register_lookup_fn(
|
|
||||||
"CalculateIncomeTax".to_string(),
|
|
||||||
vec![ReportingProductKind::Transactions],
|
|
||||||
Self::takes_args,
|
|
||||||
Self::from_args,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
|
|
||||||
args.is::<VoidArgs>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
|
|
||||||
Box::new(CalculateIncomeTax {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CalculateIncomeTax {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_fmt(format_args!("{}", self.id()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ReportingStep for CalculateIncomeTax {
|
|
||||||
fn id(&self) -> ReportingStepId {
|
|
||||||
ReportingStepId {
|
|
||||||
name: "CalculateIncomeTax".to_string(),
|
|
||||||
product_kinds: vec![
|
|
||||||
ReportingProductKind::DynamicReport,
|
|
||||||
ReportingProductKind::Transactions,
|
|
||||||
],
|
|
||||||
args: Box::new(VoidArgs {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
|
|
||||||
// CalculateIncomeTax depends on CombineOrdinaryTransactions
|
|
||||||
vec![ReportingProductId {
|
|
||||||
name: "CombineOrdinaryTransactions".to_string(),
|
|
||||||
kind: ReportingProductKind::BalancesBetween,
|
|
||||||
args: Box::new(DateStartDateEndArgs {
|
|
||||||
date_start: sofy_from_eofy(context.eofy_date),
|
|
||||||
date_end: context.eofy_date.clone(),
|
|
||||||
}),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn after_init_graph(
|
|
||||||
&self,
|
|
||||||
steps: &Vec<Box<dyn ReportingStep>>,
|
|
||||||
dependencies: &mut ReportingGraphDependencies,
|
|
||||||
_context: &ReportingContext,
|
|
||||||
) {
|
|
||||||
for other in steps {
|
|
||||||
if let Some(other) =
|
|
||||||
other.downcast_ref::<AllTransactionsExceptEarningsToEquityBalances>()
|
|
||||||
{
|
|
||||||
// AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
|
|
||||||
dependencies.add_dependency(
|
|
||||||
other.id(),
|
|
||||||
ReportingProductId {
|
|
||||||
name: self.id().name,
|
|
||||||
kind: other.product_kind,
|
|
||||||
args: if other.product_kind == ReportingProductKind::Transactions {
|
|
||||||
Box::new(VoidArgs {})
|
|
||||||
} else {
|
|
||||||
other.id().args
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
context: &ReportingContext,
|
|
||||||
_steps: &Vec<Box<dyn ReportingStep>>,
|
|
||||||
_dependencies: &ReportingGraphDependencies,
|
|
||||||
products: &RwLock<ReportingProducts>,
|
|
||||||
) -> Result<ReportingProducts, ReportingExecutionError> {
|
|
||||||
let products = products.read().await;
|
|
||||||
|
|
||||||
// Get balances for current year
|
|
||||||
let balances = &products
|
|
||||||
.get_or_err(&ReportingProductId {
|
|
||||||
name: "CombineOrdinaryTransactions".to_string(),
|
|
||||||
kind: ReportingProductKind::BalancesBetween,
|
|
||||||
args: Box::new(DateStartDateEndArgs {
|
|
||||||
date_start: sofy_from_eofy(context.eofy_date),
|
|
||||||
date_end: context.eofy_date.clone(),
|
|
||||||
}),
|
|
||||||
})?
|
|
||||||
.downcast_ref::<BalancesBetween>()
|
|
||||||
.unwrap()
|
|
||||||
.balances;
|
|
||||||
|
|
||||||
// Get taxable income and deduction accounts
|
|
||||||
let kinds_for_account =
|
|
||||||
kinds_for_account(context.db_connection.get_account_configurations().await);
|
|
||||||
|
|
||||||
// Generate tax summary report
|
|
||||||
let mut report = DynamicReport {
|
|
||||||
title: "Tax summary".to_string(),
|
|
||||||
columns: vec!["$".to_string()],
|
|
||||||
entries: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add income entries
|
|
||||||
let mut total_income: QuantityInt = 0;
|
|
||||||
|
|
||||||
for (code, label, number) in INCOME_TYPES {
|
|
||||||
let entries;
|
|
||||||
if *code == "income1" {
|
|
||||||
// Special case for salary or wages - round each separately
|
|
||||||
entries = entries_for_kind_floor(
|
|
||||||
&format!("austax.{}", code),
|
|
||||||
true,
|
|
||||||
&vec![balances],
|
|
||||||
&kinds_for_account,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entries = entries_for_kind(
|
|
||||||
&format!("austax.{}", code),
|
|
||||||
true,
|
|
||||||
&vec![balances],
|
|
||||||
&kinds_for_account,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if entries.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut section = Section {
|
|
||||||
text: Some(format!("{} ({})", label, number)),
|
|
||||||
id: None,
|
|
||||||
visible: true,
|
|
||||||
entries,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add subtotal row
|
|
||||||
let subtotal = floor_quantity(section.subtotal(&report), 100);
|
|
||||||
total_income += subtotal[0];
|
|
||||||
|
|
||||||
section.entries.push(
|
|
||||||
Row {
|
|
||||||
text: format!("Total item {}", number),
|
|
||||||
quantity: subtotal,
|
|
||||||
id: Some(format!("total_{}", code)),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: true,
|
|
||||||
bordered: true,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
report.entries.push(section.into());
|
|
||||||
report.entries.push(DynamicReportEntry::Spacer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total assessable income
|
|
||||||
report.entries.push(
|
|
||||||
Row {
|
|
||||||
text: "Total assessable income".to_string(),
|
|
||||||
quantity: vec![total_income],
|
|
||||||
id: Some("total_income".to_string()),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: true,
|
|
||||||
bordered: true,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
report.entries.push(DynamicReportEntry::Spacer);
|
|
||||||
|
|
||||||
// Add deduction entries
|
|
||||||
let mut total_deductions: QuantityInt = 0;
|
|
||||||
|
|
||||||
for (code, label, number) in DEDUCTION_TYPES {
|
|
||||||
let entries = entries_for_kind(
|
|
||||||
&format!("austax.{}", code),
|
|
||||||
false,
|
|
||||||
&vec![balances],
|
|
||||||
&kinds_for_account,
|
|
||||||
);
|
|
||||||
|
|
||||||
if entries.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut section = Section {
|
|
||||||
text: Some(format!("{} ({})", label, number)),
|
|
||||||
id: None,
|
|
||||||
visible: true,
|
|
||||||
entries,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add subtotal row
|
|
||||||
let subtotal = floor_quantity(section.subtotal(&report), 100);
|
|
||||||
total_deductions += subtotal[0];
|
|
||||||
|
|
||||||
section.entries.push(
|
|
||||||
Row {
|
|
||||||
text: format!("Total item {}", number),
|
|
||||||
quantity: subtotal,
|
|
||||||
id: Some(format!("total_{}", code)),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: true,
|
|
||||||
bordered: true,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
report.entries.push(section.into());
|
|
||||||
report.entries.push(DynamicReportEntry::Spacer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total deductions
|
|
||||||
report.entries.push(
|
|
||||||
Row {
|
|
||||||
text: "Total deductions".to_string(),
|
|
||||||
quantity: vec![total_deductions],
|
|
||||||
id: Some("total_deductions".to_string()),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: true,
|
|
||||||
bordered: true,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
report.entries.push(DynamicReportEntry::Spacer);
|
|
||||||
|
|
||||||
// Net taxable income
|
|
||||||
let net_taxable = total_income - total_deductions;
|
|
||||||
report.entries.push(
|
|
||||||
Row {
|
|
||||||
text: "Net taxable income".to_string(),
|
|
||||||
quantity: vec![net_taxable],
|
|
||||||
id: Some("net_taxable".to_string()),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: true,
|
|
||||||
bordered: true,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
report.entries.push(DynamicReportEntry::Spacer);
|
|
||||||
|
|
||||||
// Precompute RFB amount as this is required for MLS
|
|
||||||
let rfb_taxable = balances
|
|
||||||
.iter()
|
|
||||||
.filter(|(acc, _)| {
|
|
||||||
kinds_for_account
|
|
||||||
.get(*acc)
|
|
||||||
.map(|kinds| kinds.iter().any(|k| k == "austax.rfb"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.map(|(_, bal)| *bal)
|
|
||||||
.sum();
|
|
||||||
let _rfb_grossedup = get_grossedup_rfb(rfb_taxable);
|
|
||||||
|
|
||||||
// Base income tax row
|
|
||||||
let tax_base = get_base_income_tax(net_taxable);
|
|
||||||
report.entries.push(
|
|
||||||
Row {
|
|
||||||
text: "Base income tax".to_string(),
|
|
||||||
quantity: vec![tax_base],
|
|
||||||
id: Some("tax_base".to_string()),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: false,
|
|
||||||
bordered: false,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Total income tax row
|
|
||||||
let tax_total = tax_base;
|
|
||||||
report.entries.push(
|
|
||||||
Row {
|
|
||||||
text: "Total income tax".to_string(),
|
|
||||||
quantity: vec![tax_total],
|
|
||||||
id: Some("tax_total".to_string()),
|
|
||||||
visible: true,
|
|
||||||
link: None,
|
|
||||||
heading: true,
|
|
||||||
bordered: true,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate income tax transaction
|
|
||||||
let transactions = Transactions {
|
|
||||||
transactions: vec![TransactionWithPostings {
|
|
||||||
transaction: Transaction {
|
|
||||||
id: None,
|
|
||||||
dt: context
|
|
||||||
.db_connection
|
|
||||||
.metadata()
|
|
||||||
.eofy_date
|
|
||||||
.and_hms_opt(0, 0, 0)
|
|
||||||
.unwrap(),
|
|
||||||
description: "Estimated income tax".to_string(),
|
|
||||||
},
|
|
||||||
postings: vec![
|
|
||||||
Posting {
|
|
||||||
id: None,
|
|
||||||
transaction_id: None,
|
|
||||||
description: None,
|
|
||||||
account: INCOME_TAX.to_string(),
|
|
||||||
quantity: tax_total,
|
|
||||||
commodity: context.db_connection.metadata().reporting_commodity.clone(),
|
|
||||||
quantity_ascost: Some(tax_total),
|
|
||||||
},
|
|
||||||
Posting {
|
|
||||||
id: None,
|
|
||||||
transaction_id: None,
|
|
||||||
description: None,
|
|
||||||
account: INCOME_TAX_CONTROL.to_string(),
|
|
||||||
quantity: -tax_total,
|
|
||||||
commodity: context.db_connection.metadata().reporting_commodity.clone(),
|
|
||||||
quantity_ascost: Some(tax_total),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store products
|
|
||||||
let mut result = ReportingProducts::new();
|
|
||||||
result.insert(
|
|
||||||
ReportingProductId {
|
|
||||||
name: self.id().name,
|
|
||||||
kind: ReportingProductKind::Transactions,
|
|
||||||
args: Box::new(VoidArgs {}),
|
|
||||||
},
|
|
||||||
Box::new(transactions),
|
|
||||||
);
|
|
||||||
result.insert(
|
|
||||||
ReportingProductId {
|
|
||||||
name: self.id().name,
|
|
||||||
kind: ReportingProductKind::DynamicReport,
|
|
||||||
args: Box::new(VoidArgs {}),
|
|
||||||
},
|
|
||||||
Box::new(report),
|
|
||||||
);
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call [entries_for_kind] then round results down to next multiple of `floor`
|
|
||||||
fn entries_for_kind_floor(
|
|
||||||
kind: &str,
|
|
||||||
invert: bool,
|
|
||||||
balances: &Vec<&HashMap<String, QuantityInt>>,
|
|
||||||
kinds_for_account: &HashMap<String, Vec<String>>,
|
|
||||||
floor: QuantityInt,
|
|
||||||
) -> Vec<DynamicReportEntry> {
|
|
||||||
let mut entries_for_kind = entries_for_kind(kind, invert, balances, kinds_for_account);
|
|
||||||
entries_for_kind.iter_mut().for_each(|e| match e {
|
|
||||||
DynamicReportEntry::Row(row) => row
|
|
||||||
.quantity
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|v| *v = (*v / floor) * floor),
|
|
||||||
_ => unreachable!(),
|
|
||||||
});
|
|
||||||
entries_for_kind
|
|
||||||
}
|
|
||||||
|
|
||||||
fn floor_quantity(mut quantity: Vec<QuantityInt>, floor: QuantityInt) -> Vec<QuantityInt> {
|
|
||||||
quantity.iter_mut().for_each(|v| *v = (*v / floor) * floor);
|
|
||||||
quantity
|
|
||||||
}
|
|
@ -111,17 +111,17 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// let result = products
|
let result = products
|
||||||
// .get_or_err(&ReportingProductId {
|
.get_or_err(&ReportingProductId {
|
||||||
// name: "CalculateIncomeTax".to_string(),
|
name: "CalculateIncomeTax".to_string(),
|
||||||
// kind: ReportingProductKind::DynamicReport,
|
kind: ReportingProductKind::DynamicReport,
|
||||||
// args: ReportingStepArgs::VoidArgs,
|
args: ReportingStepArgs::VoidArgs,
|
||||||
// })
|
})
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// println!("Tax summary:");
|
|
||||||
// println!("{:?}", result);
|
|
||||||
|
|
||||||
|
println!("Tax summary:");
|
||||||
|
println!("{:?}", result);
|
||||||
|
|
||||||
let result = products
|
let result = products
|
||||||
.get_or_err(&ReportingProductId {
|
.get_or_err(&ReportingProductId {
|
||||||
name: "AllTransactionsExceptEarningsToEquity".to_string(),
|
name: "AllTransactionsExceptEarningsToEquity".to_string(),
|
||||||
|
@ -24,6 +24,7 @@ use mlua::{FromLua, Function, Lua, LuaSerdeExt, Table, Value};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::account_config::kinds_for_account;
|
||||||
use crate::reporting::calculator::ReportingGraphDependencies;
|
use crate::reporting::calculator::ReportingGraphDependencies;
|
||||||
use crate::reporting::dynamic_report::DynamicReport;
|
use crate::reporting::dynamic_report::DynamicReport;
|
||||||
use crate::reporting::executor::ReportingExecutionError;
|
use crate::reporting::executor::ReportingExecutionError;
|
||||||
@ -178,6 +179,7 @@ struct LuaReportingContext {
|
|||||||
#[serde(with = "crate::serde::naivedate_to_js")]
|
#[serde(with = "crate::serde::naivedate_to_js")]
|
||||||
pub eofy_date: NaiveDate,
|
pub eofy_date: NaiveDate,
|
||||||
pub reporting_commodity: String,
|
pub reporting_commodity: String,
|
||||||
|
pub dps: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LuaReportingContext {
|
impl LuaReportingContext {
|
||||||
@ -186,6 +188,7 @@ impl LuaReportingContext {
|
|||||||
sofy_date: sofy_from_eofy(context.eofy_date),
|
sofy_date: sofy_from_eofy(context.eofy_date),
|
||||||
eofy_date: context.eofy_date,
|
eofy_date: context.eofy_date,
|
||||||
reporting_commodity: context.reporting_commodity.clone(),
|
reporting_commodity: context.reporting_commodity.clone(),
|
||||||
|
dps: context.db_connection.metadata().dps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,6 +316,10 @@ impl ReportingStep for PluginReportingStep {
|
|||||||
_dependencies: &ReportingGraphDependencies,
|
_dependencies: &ReportingGraphDependencies,
|
||||||
products: &RwLock<ReportingProducts>,
|
products: &RwLock<ReportingProducts>,
|
||||||
) -> Result<ReportingProducts, ReportingExecutionError> {
|
) -> Result<ReportingProducts, ReportingExecutionError> {
|
||||||
|
// Pre-compute some context for Lua
|
||||||
|
let kinds_for_account =
|
||||||
|
kinds_for_account(context.db_connection.get_account_configurations().await);
|
||||||
|
|
||||||
let products = products.read().await;
|
let products = products.read().await;
|
||||||
|
|
||||||
// Load plugin
|
// Load plugin
|
||||||
@ -338,6 +345,7 @@ impl ReportingStep for PluginReportingStep {
|
|||||||
let result_table = plugin_step.execute.call::<Table>((
|
let result_table = plugin_step.execute.call::<Table>((
|
||||||
lua.to_value(&self.args).unwrap(),
|
lua.to_value(&self.args).unwrap(),
|
||||||
lua.to_value(&LuaReportingContext::from(context)).unwrap(),
|
lua.to_value(&LuaReportingContext::from(context)).unwrap(),
|
||||||
|
lua.to_value(&kinds_for_account).unwrap(),
|
||||||
get_product,
|
get_product,
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user