Basic income tax estimation
This commit is contained in:
parent
315ff158c3
commit
4f845eaaea
@ -27,6 +27,7 @@ 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, CalculatableDynamicReport, CalculatableDynamicReportEntry,
|
||||
@ -40,7 +41,39 @@ use crate::reporting::types::{
|
||||
Transactions, VoidArgs,
|
||||
};
|
||||
use crate::util::sofy_from_eofy;
|
||||
use crate::QuantityInt;
|
||||
use crate::{QuantityInt, INCOME_TAX, INCOME_TAX_CONTROL};
|
||||
|
||||
// Constants and tax calculations
|
||||
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) {
|
||||
@ -158,6 +191,18 @@ impl ReportingStep for CalculateIncomeTax {
|
||||
let kinds_for_account =
|
||||
kinds_for_account(context.db_connection.get_account_configurations().await);
|
||||
|
||||
// Pre-compute taxable value of reportable fringe benefits (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();
|
||||
|
||||
// Generate tax summary report
|
||||
let report = CalculatableDynamicReport::new(
|
||||
"Tax summary".to_string(),
|
||||
@ -603,15 +648,132 @@ impl ReportingStep for CalculateIncomeTax {
|
||||
bordered: true,
|
||||
},
|
||||
}),
|
||||
// Precompute RFB amount as this is required for MLS
|
||||
CalculatableDynamicReportEntry::LiteralRow(LiteralRow {
|
||||
text: "Taxable value of reportable fringe benefits".to_string(),
|
||||
quantity: vec![rfb_taxable],
|
||||
id: Some("rfb_taxable".to_string()),
|
||||
visible: false,
|
||||
auto_hide: false,
|
||||
link: None,
|
||||
heading: false,
|
||||
bordered: false,
|
||||
}),
|
||||
CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||
calculate_fn: |report| LiteralRow {
|
||||
text: "Grossed-up value".to_string(),
|
||||
quantity: vec![get_grossedup_rfb(
|
||||
report.quantity_for_id("rfb_taxable").unwrap()[0],
|
||||
)],
|
||||
id: Some("rfb_grossedup".to_string()),
|
||||
visible: false,
|
||||
auto_hide: false,
|
||||
link: None,
|
||||
heading: false,
|
||||
bordered: false,
|
||||
},
|
||||
}),
|
||||
CalculatableDynamicReportEntry::Spacer,
|
||||
CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||
calculate_fn: |report| LiteralRow {
|
||||
text: "Base income tax".to_string(),
|
||||
quantity: vec![get_base_income_tax(
|
||||
report.quantity_for_id("net_taxable").unwrap()[0],
|
||||
)],
|
||||
id: Some("tax_base".to_string()),
|
||||
visible: true,
|
||||
auto_hide: false,
|
||||
link: None,
|
||||
heading: false,
|
||||
bordered: false,
|
||||
},
|
||||
}),
|
||||
// CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||
// calculate_fn: |report| LiteralRow {
|
||||
// text: "Medicare levy".to_string(),
|
||||
// quantity: vec![get_medicare_levy(
|
||||
// report.quantity_for_id("net_taxable").unwrap()[0],
|
||||
// )],
|
||||
// id: Some("tax_ml".to_string()),
|
||||
// visible: true,
|
||||
// auto_hide: true,
|
||||
// link: None,
|
||||
// heading: false,
|
||||
// bordered: false,
|
||||
// },
|
||||
// }),
|
||||
// CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||
// calculate_fn: |report| LiteralRow {
|
||||
// text: "Medicare levy".to_string(),
|
||||
// quantity: vec![get_medicare_levy_surcharge(
|
||||
// report.quantity_for_id("net_taxable").unwrap()[0],
|
||||
// report.quantity_for_id("rfb_grossedup").unwrap()[0],
|
||||
// )],
|
||||
// id: Some("tax_mls".to_string()),
|
||||
// visible: true,
|
||||
// auto_hide: true,
|
||||
// link: None,
|
||||
// heading: false,
|
||||
// bordered: false,
|
||||
// },
|
||||
// }),
|
||||
CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
|
||||
calculate_fn: |report| LiteralRow {
|
||||
text: "Total income tax".to_string(),
|
||||
quantity: vec![
|
||||
report.quantity_for_id("tax_base").unwrap()[0], // + report.quantity_for_id("tax_ml").map(|v| v[0]).unwrap_or(0)
|
||||
// + report.quantity_for_id("tax_mls").map(|v| v[0]).unwrap_or(0),
|
||||
],
|
||||
id: Some("total_tax".to_string()),
|
||||
visible: true,
|
||||
auto_hide: false,
|
||||
link: None,
|
||||
heading: true,
|
||||
bordered: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
let mut report: DynamicReport = report.calculate();
|
||||
report.auto_hide();
|
||||
|
||||
let total_tax = report.quantity_for_id("total_tax").unwrap()[0];
|
||||
|
||||
// Generate income tax transaction
|
||||
let transactions = Transactions {
|
||||
transactions: Vec::new(), // FIXME
|
||||
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: total_tax,
|
||||
commodity: context.db_connection.metadata().reporting_commodity.clone(),
|
||||
quantity_ascost: Some(total_tax),
|
||||
},
|
||||
Posting {
|
||||
id: None,
|
||||
transaction_id: None,
|
||||
description: None,
|
||||
account: INCOME_TAX_CONTROL.to_string(),
|
||||
quantity: -total_tax,
|
||||
commodity: context.db_connection.metadata().reporting_commodity.clone(),
|
||||
quantity_ascost: Some(total_tax),
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
// Store products
|
||||
|
@ -12,4 +12,6 @@ pub type QuantityInt = i64;
|
||||
// Magic strings
|
||||
// TODO: Make this configurable
|
||||
pub const CURRENT_YEAR_EARNINGS: &'static str = "Current Year Earnings";
|
||||
pub const INCOME_TAX: &'static str = "Income Tax";
|
||||
pub const INCOME_TAX_CONTROL: &'static str = "Income Tax Control";
|
||||
pub const RETAINED_EARNINGS: &'static str = "Retained Earnings";
|
||||
|
@ -224,6 +224,59 @@ impl DynamicReport {
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
|
||||
/// Look up [DynamicReportEntry] by id
|
||||
///
|
||||
/// Returns a cloned copy of the [DynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow.
|
||||
pub fn by_id(&self, id: &str) -> Option<DynamicReportEntry> {
|
||||
// Manually iterate over self.entries rather than self.entries()
|
||||
// To catch the situation where entry is already mutably borrowed
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
DynamicReportEntry::Section(section) => {
|
||||
if let Some(i) = §ion.id {
|
||||
if i == id {
|
||||
return Some(entry.clone());
|
||||
}
|
||||
}
|
||||
if let Some(e) = section.by_id(id) {
|
||||
return Some(match e {
|
||||
DynamicReportEntry::Section(section) => {
|
||||
DynamicReportEntry::Section(section.clone())
|
||||
}
|
||||
DynamicReportEntry::LiteralRow(row) => {
|
||||
DynamicReportEntry::LiteralRow(row.clone())
|
||||
}
|
||||
DynamicReportEntry::Spacer => DynamicReportEntry::Spacer,
|
||||
});
|
||||
}
|
||||
}
|
||||
DynamicReportEntry::LiteralRow(row) => {
|
||||
if let Some(i) = &row.id {
|
||||
if i == id {
|
||||
return Some(entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
DynamicReportEntry::Spacer => (),
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Return the quantities for the [LiteralRow] with the given id
|
||||
pub fn quantity_for_id(&self, id: &str) -> Option<Vec<QuantityInt>> {
|
||||
if let Some(entry) = self.by_id(id) {
|
||||
if let DynamicReportEntry::LiteralRow(row) = entry {
|
||||
Some(row.quantity)
|
||||
} else {
|
||||
panic!("Called quantity_for_id on non-LiteralRow");
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportingProduct for DynamicReport {}
|
||||
|
@ -19,21 +19,25 @@
|
||||
|
||||
<template>
|
||||
<template v-if="literalRow">
|
||||
<tr :class="literalRow.bordered ? 'border-y border-gray-300' : null">
|
||||
<component :is="literalRow.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': literalRow.heading }">
|
||||
<a :href="literalRow.link as string" class="hover:text-blue-700 hover:underline" v-if="literalRow.link !== null">{{ literalRow.text }}</a>
|
||||
<template v-if="literalRow.link === null">{{ literalRow.text }}</template>
|
||||
</component>
|
||||
<component :is="literalRow.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': literalRow.heading }" v-html="(cell !== 0 || literalRow.heading) ? ppBracketed(cell, literalRow.link ?? undefined) : ''" v-for="cell of literalRow.quantity">
|
||||
</component>
|
||||
</tr>
|
||||
<template v-if="literalRow.visible">
|
||||
<tr :class="literalRow.bordered ? 'border-y border-gray-300' : null">
|
||||
<component :is="literalRow.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': literalRow.heading }">
|
||||
<a :href="literalRow.link as string" class="hover:text-blue-700 hover:underline" v-if="literalRow.link !== null">{{ literalRow.text }}</a>
|
||||
<template v-if="literalRow.link === null">{{ literalRow.text }}</template>
|
||||
</component>
|
||||
<component :is="literalRow.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': literalRow.heading }" v-html="(cell !== 0 || literalRow.heading) ? ppBracketed(cell, literalRow.link ?? undefined) : ''" v-for="cell of literalRow.quantity">
|
||||
</component>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="section">
|
||||
<tr v-if="section.text !== null">
|
||||
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">{{ section.text }}</th>
|
||||
<th></th><!-- FIXME: Have correct colspan -->
|
||||
</tr>
|
||||
<DynamicReportEntryComponent :entry="child" v-for="child of section.entries" />
|
||||
<template v-if="section.visible">
|
||||
<tr v-if="section.text !== null">
|
||||
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">{{ section.text }}</th>
|
||||
<th></th><!-- FIXME: Have correct colspan -->
|
||||
</tr>
|
||||
<DynamicReportEntryComponent :entry="child" v-for="child of section.entries" />
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="entry == 'Spacer'">
|
||||
<tr><td colspan="2" class="py-0.5"> </td></tr><!-- FIXME: Have correct colspan -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user