From bbb91ad1e7d837e1b9ae82284684bf3b4552f0af Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 1 Jun 2025 22:58:31 +1000 Subject: [PATCH] austax: Basic implementation in Lua --- libdrcr/plugins/austax/account_kinds.luau | 68 +++ libdrcr/plugins/austax/austax.luau | 284 +++++++++++- libdrcr/plugins/austax/tax_tables.luau | 49 +++ libdrcr/plugins/libdrcr.luau | 58 ++- libdrcr/src/austax/mod.rs | 512 ---------------------- libdrcr/src/main.rs | 20 +- libdrcr/src/plugin.rs | 8 + 7 files changed, 468 insertions(+), 531 deletions(-) create mode 100644 libdrcr/plugins/austax/account_kinds.luau create mode 100644 libdrcr/plugins/austax/tax_tables.luau delete mode 100644 libdrcr/src/austax/mod.rs 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, ))?;