diff --git a/libdrcr/plugins/austax/account_kinds.luau b/libdrcr/plugins/austax/account_kinds.luau
new file mode 100644
index 0000000..3fca6ad
--- /dev/null
+++ b/libdrcr/plugins/austax/account_kinds.luau
@@ -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 .
+
+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
diff --git a/libdrcr/plugins/austax/austax.luau b/libdrcr/plugins/austax/austax.luau
index 29aaf7b..e7bfc83 100644
--- a/libdrcr/plugins/austax/austax.luau
+++ b/libdrcr/plugins/austax/austax.luau
@@ -17,6 +17,32 @@
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 {
{
@@ -28,8 +54,7 @@ function requires(args, context)
end
function after_init_graph(args, steps, add_dependency, context)
- for i = 1, #steps do
- local other = steps[i]
+ for _, other in ipairs(steps) do
if other.name == 'AllTransactionsExceptEarningsToEquity' then
-- AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
-- TODO: Only in applicable years
@@ -50,26 +75,269 @@ function after_init_graph(args, steps, add_dependency, context)
end
end
-function execute(args, context, get_product)
- print('Stub: CombineOrdinaryTransactions.execute')
-
+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
- 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 {
[{ name = 'CalculateIncomeTax', kind = 'Transactions', args = 'VoidArgs' }] = {
Transactions = {
- 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 = {
diff --git a/libdrcr/plugins/austax/tax_tables.luau b/libdrcr/plugins/austax/tax_tables.luau
new file mode 100644
index 0000000..434b0a3
--- /dev/null
+++ b/libdrcr/plugins/austax/tax_tables.luau
@@ -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 .
+
+-- 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
diff --git a/libdrcr/plugins/libdrcr.luau b/libdrcr/plugins/libdrcr.luau
index 7287c71..472dcc3 100644
--- a/libdrcr/plugins/libdrcr.luau
+++ b/libdrcr/plugins/libdrcr.luau
@@ -44,10 +44,39 @@ export type ReportingStep = {
execute: (
ReportingStepArgs,
ReportingContext,
+ {[string]: {string}}, -- kinds_for_account
(ReportingProductId) -> ReportingProduct -- get_product
) -> {[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
@@ -55,6 +84,7 @@ export type ReportingContext = {
sofy_date: string,
eofy_date: string,
reporting_commodity: string,
+ dps: number,
}
-- Accounting types
@@ -87,7 +117,6 @@ export type ReportingProduct = {
export type BalancesAt = any
export type BalancesBetween = any
-export type DynamicReport = any
export type Transactions = { transactions: {Transaction} }
export type ReportingProductId = {
@@ -120,6 +149,33 @@ export type MultipleDateStartDateEndArgs = { dates: {DateStartDateEndArgs} }
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
local result = ''
if type(value) == 'table' then
diff --git a/libdrcr/src/austax/mod.rs b/libdrcr/src/austax/mod.rs
deleted file mode 100644
index 1449dab..0000000
--- a/libdrcr/src/austax/mod.rs
+++ /dev/null
@@ -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 .
-*/
-
-//! 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) -> bool {
- args.is::()
- }
-
- fn from_args(_args: Box) -> Box {
- 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 {
- // 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>,
- dependencies: &mut ReportingGraphDependencies,
- _context: &ReportingContext,
- ) {
- for other in steps {
- if let Some(other) =
- other.downcast_ref::()
- {
- // 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>,
- _dependencies: &ReportingGraphDependencies,
- products: &RwLock,
- ) -> Result {
- 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::()
- .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>,
- kinds_for_account: &HashMap>,
- floor: QuantityInt,
-) -> Vec {
- 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, floor: QuantityInt) -> Vec {
- quantity.iter_mut().for_each(|v| *v = (*v / floor) * floor);
- quantity
-}
diff --git a/libdrcr/src/main.rs b/libdrcr/src/main.rs
index cbec874..7c3fb64 100644
--- a/libdrcr/src/main.rs
+++ b/libdrcr/src/main.rs
@@ -111,17 +111,17 @@ async fn main() {
.await
.unwrap();
- // let result = products
- // .get_or_err(&ReportingProductId {
- // name: "CalculateIncomeTax".to_string(),
- // kind: ReportingProductKind::DynamicReport,
- // args: ReportingStepArgs::VoidArgs,
- // })
- // .unwrap();
-
- // println!("Tax summary:");
- // println!("{:?}", result);
+ let result = products
+ .get_or_err(&ReportingProductId {
+ name: "CalculateIncomeTax".to_string(),
+ kind: ReportingProductKind::DynamicReport,
+ args: ReportingStepArgs::VoidArgs,
+ })
+ .unwrap();
+ println!("Tax summary:");
+ println!("{:?}", result);
+
let result = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
diff --git a/libdrcr/src/plugin.rs b/libdrcr/src/plugin.rs
index 00b0ad4..b9b53db 100644
--- a/libdrcr/src/plugin.rs
+++ b/libdrcr/src/plugin.rs
@@ -24,6 +24,7 @@ use mlua::{FromLua, Function, Lua, LuaSerdeExt, Table, Value};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
+use crate::account_config::kinds_for_account;
use crate::reporting::calculator::ReportingGraphDependencies;
use crate::reporting::dynamic_report::DynamicReport;
use crate::reporting::executor::ReportingExecutionError;
@@ -178,6 +179,7 @@ struct LuaReportingContext {
#[serde(with = "crate::serde::naivedate_to_js")]
pub eofy_date: NaiveDate,
pub reporting_commodity: String,
+ pub dps: u32,
}
impl LuaReportingContext {
@@ -186,6 +188,7 @@ impl LuaReportingContext {
sofy_date: sofy_from_eofy(context.eofy_date),
eofy_date: context.eofy_date,
reporting_commodity: context.reporting_commodity.clone(),
+ dps: context.db_connection.metadata().dps,
}
}
}
@@ -313,6 +316,10 @@ impl ReportingStep for PluginReportingStep {
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
+ // 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;
// Load plugin
@@ -338,6 +345,7 @@ impl ReportingStep for PluginReportingStep {
let result_table = plugin_step.execute.call::((
lua.to_value(&self.args).unwrap(),
lua.to_value(&LuaReportingContext::from(context)).unwrap(),
+ lua.to_value(&kinds_for_account).unwrap(),
get_product,
))?;