diff --git a/changelog.d/trading-loss-relief.added.md b/changelog.d/trading-loss-relief.added.md new file mode 100644 index 0000000000..05460b68df --- /dev/null +++ b/changelog.d/trading-loss-relief.added.md @@ -0,0 +1 @@ +Trade loss relief against general income (ITA 2007 s.64): a trading loss (`trading_loss`) now reduces net income for the loss-making year, within the s.24A cap of the greater of £50,000 and 25% of adjusted total income (s.24A(8): total income less relieved pension contributions). Beyond non-savings income it comes off savings, then dividends, in the model's allowance order (#2106). New variables `net_income_before_trade_loss_relief`, `trade_loss_relief_against_general_income`, `trade_loss_relief_against_savings_and_dividends`, `income_tax_relief_cap`, `adjusted_total_income`, `ni_class_4_profits` and the input `ni_class_4_losses_brought_forward` (SSCBA 1992 Sch 2 para 3(4)(b)). diff --git a/changelog.d/trading-loss-relief.changed.md b/changelog.d/trading-loss-relief.changed.md new file mode 100644 index 0000000000..37d675777c --- /dev/null +++ b/changelog.d/trading-loss-relief.changed.md @@ -0,0 +1 @@ +`trading_loss` is now an uprated input read by each programme's own rule: Class 4 and Class 2 NICs deduct the relief from trading profits only (SSCBA 1992 Sch 2 para 3, s.11(3)); tax credit income deducts the claimants' losses (SI 2002/2006 reg 3(1) Step 4); UC sets a loss against the person's other trades' profits but never employed earnings (UC Regs 2013 reg 57(2)); the other means tests never offset it; market income and HBAI household income count it as negative income. `loss_relief` now holds only losses brought forward (s.83); it no longer adds the previous year's `trading_loss`, which relief against general income may already have used. diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index fe3072c0c7..1af9f17e96 100644 --- a/policyengine_uk/data/uprating_indices.yaml +++ b/policyengine_uk/data/uprating_indices.yaml @@ -108,6 +108,7 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - sublet_income gov.economic_assumptions.yoy_growth.obr.per_capita.mixed_income: - self_employment_income +- trading_loss gov.economic_assumptions.yoy_growth.obr.private_pension_index: - private_pension_income gov.economic_assumptions.yoy_growth.ons.population: diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/bases/taxable_self_employment_income_deductions.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/bases/taxable_self_employment_income_deductions.yaml index 9b7c33a1d3..3a919715bf 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/bases/taxable_self_employment_income_deductions.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/bases/taxable_self_employment_income_deductions.yaml @@ -9,8 +9,8 @@ metadata: label: Deductions from self-employment income period: year reference: - - title: "Income Tax Act 2007, s. 64" - href: https://www.legislation.gov.uk/ukpga/2007/3/section/64 + - title: "Income Tax Act 2007, s. 83" + href: https://www.legislation.gov.uk/ukpga/2007/3/section/83 - title: "Capital Allowances Act 2001, s. 1" href: https://www.legislation.gov.uk/ukpga/2001/2/section/1 - title: "Income Tax (Trading and Other Income) Act 2005, s. 783A" diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/README.md b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/README.md new file mode 100644 index 0000000000..587ce7e3dc --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/README.md @@ -0,0 +1 @@ +# Cap on certain reliefs diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/amount.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/amount.yaml new file mode 100644 index 0000000000..362b663ca3 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/amount.yaml @@ -0,0 +1,13 @@ +description: Reliefs against general income listed in ITA 2007 s.24A, such as trade loss relief, are limited to the greater of this amount and a share of adjusted total income. +values: + 0000-01-01: .inf + 2013-04-06: 50_000 +metadata: + label: Cap on certain income tax reliefs, fixed amount + period: year + unit: currency-GBP + reference: + - title: Income Tax Act 2007 s. 24A(3) + href: https://www.legislation.gov.uk/ukpga/2007/3/section/24A + - title: Finance Act 2013 Sch. 3 (cap introduced from 2013-14) + href: https://www.legislation.gov.uk/ukpga/2013/29/schedule/3 diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/rate.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/rate.yaml new file mode 100644 index 0000000000..96d7f3ad79 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/cap/rate.yaml @@ -0,0 +1,14 @@ +description: Reliefs against general income listed in ITA 2007 s.24A, such as trade loss relief, are limited to the greater of a fixed amount and this share of adjusted total income. +values: + # No cap before 2013-14: the fixed amount is infinite then. + 0000-01-01: 0 + 2013-04-06: 0.25 +metadata: + label: Cap on certain income tax reliefs, share of adjusted total income + period: year + unit: /1 + reference: + - title: Income Tax Act 2007 s. 24A(3) + href: https://www.legislation.gov.uk/ukpga/2007/3/section/24A + - title: Finance Act 2013 Sch. 3 (cap introduced from 2013-14) + href: https://www.legislation.gov.uk/ukpga/2013/29/schedule/3 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits/tax_credits_applicable_income_trading_loss.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits/tax_credits_applicable_income_trading_loss.yaml new file mode 100644 index 0000000000..ad854d8bf6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits/tax_credits_applicable_income_trading_loss.yaml @@ -0,0 +1,78 @@ +# SI 2002/2006 reg 3(1) Step 4: a trading loss in the year is subtracted from +# the income of Steps 1 to 3, so it reduces the claimants' joint income. + +- name: A trading loss reduces the claimant's other income + period: 2024 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + employment_income: 20_000 + trading_loss: 5_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + tax_credits_applicable_income: 15_000 + +- name: One partner's loss reduces the couple's joint income + period: 2024 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + employment_income: 20_000 + partner: + age: 38 + trading_loss: 8_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + tax_credits_applicable_income: 12_000 + +- name: Income does not go below zero + period: 2024 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + employment_income: 3_000 + trading_loss: 9_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + tax_credits_applicable_income: 0 + +- name: A child's trading loss does not reduce the claimants' income + period: 2024 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + employment_income: 20_000 + child: + age: 15 + trading_loss: 5_000 + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + output: + tax_credits_applicable_income: 20_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_trading_loss.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_trading_loss.yaml new file mode 100644 index 0000000000..3209c128a6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_trading_loss.yaml @@ -0,0 +1,72 @@ +# UC Regs 2013 reg 57(2): Steps 1-2 add the profit or loss of each trade and +# Step 3 makes a negative total nil, so one trade's loss reduces another +# trade's profits but never employed earnings. + +- name: A loss in one trade reduces another trade's profits for UC + period: 2026 + absolute_error_margin: 0 + input: + age: 40 + self_employment_income: 30_000 + trading_loss: 10_000 + uc_is_in_startup_period: true + output: + uc_mif_capped_earned_income: 20_000 + +- name: A loss never reduces employed earnings for UC + period: 2026 + absolute_error_margin: 0 + input: + age: 40 + employment_income: 20_000 + trading_loss: 10_000 + output: + uc_mif_capped_earned_income: 20_000 + +- name: Self-employed earnings for UC are never below nil + period: 2026 + absolute_error_margin: 0 + input: + age: 40 + employment_income: 20_000 + self_employment_income: 5_000 + trading_loss: 10_000 + uc_is_in_startup_period: true + output: + uc_mif_capped_earned_income: 20_000 + +- name: The benefit cap earnings test nets trades too (reg 82 with reg 57(2)) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + self_employment_income: 12_000 + trading_loss: 5_000 + uc_is_in_startup_period: true + income_tax: 0 + national_insurance: 0 + child_1: + age: 1 + child_2: + age: 4 + child_3: + age: 7 + child_4: + age: 10 + benunits: + benunit: + members: [parent, child_1, child_2, child_3, child_4] + households: + household: + members: [parent, child_1, child_2, child_3, child_4] + region: LONDON + rent: 24_000 + tenure_type: RENT_PRIVATELY + output: + uc_trading_losses_set_against_profits: [5_000, 0, 0, 0, 0] + # £7,000 of earnings, below the reg 82 threshold, so the cap applies, + # exactly as for £7,000 of profits and no loss. + is_benefit_cap_exempt_earnings: false + universal_credit: 21_124 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/loss_relief.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/loss_relief.yaml index 8d976948fe..31a1e52134 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/loss_relief.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/loss_relief.yaml @@ -1,80 +1,39 @@ -- name: No trading losses - period: 2024 - input: - trading_loss: - 2023: 0 - 2024: 0 - output: - loss_relief: 0 - -- name: Current year trading loss only - period: 2024 - input: - trading_loss: - 2023: 0 - 2024: 5000 - output: - loss_relief: 5000 - -- name: Previous year trading loss only - period: 2024 - input: - trading_loss: - 2023: 3000 - 2024: 0 - output: - loss_relief: 3000 - -- name: Trading losses in both years - period: 2024 - input: - trading_loss: - 2023: 2500 - 2024: 3500 - output: - loss_relief: 6000 - -- name: High trading losses - period: 2024 - input: - trading_loss: - 2023: 50000 - 2024: 75000 - output: - loss_relief: 125000 +# loss_relief holds trade losses brought forward from earlier years (ITA 2007 +# s.83). A loss made in the year itself is trading_loss, relieved against +# general income instead (see trade_loss_relief_against_general_income.yaml). -- name: Very low positive trading losses +- name: Losses brought forward come off the trade's profits period: 2024 + absolute_error_margin: 0 input: - trading_loss: - 2023: 1 - 2024: 1 + self_employment_income: 10_000 + loss_relief: 4_000 + trading_allowance_deduction: 0 output: - loss_relief: 2 + taxable_self_employment_income: 6_000 -- name: Uneven trading losses +- name: Losses brought forward never reduce other income period: 2024 + absolute_error_margin: 0 input: - trading_loss: - 2023: 1234.56 - 2024: 5678.90 + self_employment_income: 1_000 + employment_income: 20_000 + loss_relief: 5_000 + trading_allowance_deduction: 0 output: - loss_relief: 6913.46 + taxable_self_employment_income: 0 + taxable_employment_income: 20_000 + adjusted_net_income: 20_000 -- name: Large previous year loss, small current year loss +- name: Last year's trading loss is not brought forward automatically period: 2024 + absolute_error_margin: 0 input: + self_employment_income: 10_000 + trading_allowance_deduction: 0 trading_loss: - 2023: 100000 - 2024: 1000 - output: - loss_relief: 101000 - -- name: Small previous year loss, large current year loss - period: 2024 - input: - trading_loss: - 2023: 1000 - 2024: 100000 + 2023: 5_000 + 2024: 0 output: - loss_relief: 101000 \ No newline at end of file + loss_relief: 0 + taxable_self_employment_income: 10_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_general_income.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_general_income.yaml new file mode 100644 index 0000000000..bd5f40143d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_general_income.yaml @@ -0,0 +1,192 @@ +# Trade loss relief against general income: ITA 2007 s.64, limited to net +# income and, from 2013-14, to the greater of £50,000 and 25% of adjusted total +# income (s.24A). 2026-27 rates: Personal Allowance £12,570, basic rate 20% to +# £50,270, dividend ordinary rate 10.75%, dividend allowance £500. + +- name: No trading loss, no relief + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 40_000 + output: + trade_loss_relief_against_general_income: 0 + adjusted_net_income: 40_000 + +- name: A loss within the person's income is relieved in full against their pay + period: 2026 + absolute_error_margin: 0.01 + input: + employment_income: 40_000 + trading_loss: 10_000 + output: + trade_loss_relief_against_general_income: 10_000 + adjusted_net_income: 30_000 + # (30,000 - 12,570) x 20%: the tax on £30,000 of pay. + income_tax: 3_486 + +- name: Relief is limited to net income and the Personal Allowance is wasted + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 8_000 + trading_loss: 20_000 + output: + trade_loss_relief_against_general_income: 8_000 + adjusted_net_income: 0 + income_tax: 0 + +- name: The cap is 25% of adjusted total income above £200,000 + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 300_000 + trading_loss: 200_000 + output: + adjusted_total_income: 300_000 + income_tax_relief_cap: 75_000 + trade_loss_relief_against_general_income: 75_000 + adjusted_net_income: 225_000 + +- name: The cap is never below £50,000 + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 150_000 + trading_loss: 80_000 + output: + income_tax_relief_cap: 50_000 + trade_loss_relief_against_general_income: 50_000 + +- name: Pension contributions come out of adjusted total income + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 300_000 + personal_pension_contributions: 40_000 + trading_loss: 200_000 + output: + adjusted_total_income: 260_000 + income_tax_relief_cap: 65_000 + trade_loss_relief_against_general_income: 65_000 + +- name: Relief lowers adjusted net income below £100,000 and restores the Personal Allowance + period: 2026 + absolute_error_margin: 0.01 + input: + employment_income: 110_000 + trading_loss: 20_000 + output: + adjusted_net_income: 90_000 + personal_allowance: 12_570 + # 37,700 x 20% + (77,430 - 37,700) x 40%: the tax on £90,000 of pay. + income_tax: 23_432 + +- name: With no non-savings income the relief comes off dividends + period: 2026 + absolute_error_margin: 0.01 + input: + dividend_income: 60_000 + trading_loss: 20_000 + output: + trade_loss_relief_against_general_income: 20_000 + trade_loss_relief_against_savings_and_dividends: 20_000 + # (40,000 - 12,570 - 500) x 10.75%: the tax on £40,000 of dividends. + income_tax: 2_894.98 + +- name: Relief takes non-savings income first, then savings, then dividends + period: 2026 + absolute_error_margin: 0.01 + input: + employment_income: 5_000 + savings_interest_income: 10_000 + dividend_income: 30_000 + trading_loss: 20_000 + output: + trade_loss_relief_against_general_income: 20_000 + trade_loss_relief_against_savings_and_dividends: 15_000 + # The £15,000 of relief left after the pay, with the Personal + # Allowance, takes all £10,000 of savings and £17,570 of dividends. + received_allowances_earned_income: 0 + received_allowances_savings_income: 10_000 + received_allowances_dividend_income: 17_570 + # £25,000 of dividends is left: (25,000 - 12,570 - 500) x 10.75%. + income_tax: 1_282.48 + +- name: Adjusted total income is total income after employment expenses + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 300_000 + employment_expenses: 100_000 + trading_loss: 100_000 + output: + adjusted_total_income: 200_000 + income_tax_relief_cap: 50_000 + trade_loss_relief_against_general_income: 50_000 + +- name: Contributions from age 75 get no relief, so they stay in adjusted total income + period: 2026 + absolute_error_margin: 0 + input: + age: 76 + employment_income: 300_000 + personal_pension_contributions: 40_000 + trading_loss: 100_000 + state_pension: 0 + output: + adjusted_total_income: 300_000 + income_tax_relief_cap: 75_000 + trade_loss_relief_against_general_income: 75_000 + +- name: Losses brought forward are added back to total income + period: 2026 + absolute_error_margin: 0 + input: + self_employment_income: 250_000 + loss_relief: 50_000 + trading_loss: 100_000 + output: + # Step 1 total income is the £250,000 of profits: s.83 relief is a Step 2 + # deduction, made inside taxable self-employment income in the model. + adjusted_total_income: 250_000 + income_tax_relief_cap: 62_500 + +- name: Taxable basic income is general income the relief can be set against + period: 2026 + absolute_error_margin: 0.01 + input: + gov.contrib.ubi_center.basic_income.interactions.include_in_taxable_income: true + basic_income: 20_000 + trading_loss: 20_000 + output: + net_income_before_trade_loss_relief: 20_000 + trade_loss_relief_against_general_income: 20_000 + adjusted_net_income: 0 + income_tax: 0 + +- name: Only contributions up to relevant UK earnings, or £3,600, come out of adjusted total income + period: 2026 + absolute_error_margin: 0 + input: + dividend_income: 300_000 + personal_pension_contributions: 40_000 + trading_loss: 200_000 + output: + # No relevant UK earnings, so relief is on £3,600 at most (FA 2004 s.190). + adjusted_total_income: 296_400 + income_tax_relief_cap: 74_100 + trade_loss_relief_against_general_income: 74_100 + +- name: Statutory Maternity Pay is relevant UK earnings for pension relief + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 10_000 + statutory_maternity_pay: 30_000 + dividend_income: 300_000 + personal_pension_contributions: 40_000 + trading_loss: 100_000 + output: + # £40,000 of relevant UK earnings covers the £40,000 contribution. + adjusted_total_income: 300_000 + income_tax_relief_cap: 75_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_trading_loss.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_trading_loss.yaml new file mode 100644 index 0000000000..9628f99574 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_trading_loss.yaml @@ -0,0 +1,59 @@ +# SSCBA 1992 Sch 2 para 3: the trade loss reliefs of ITA 2007 ss.64 and 83 +# reduce Class 4 profits, and para 3(4)(a) applies a loss relieved against +# general income to the profits of any of the person's trades. 2026-27: main +# rate 6% between £12,570 and £50,270. + +- name: A loss in one trade reduces another trade's Class 4 profits + period: 2026 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + trading_loss: 10_000 + output: + ni_class_4_profits: 20_000 + ni_class_4: 445.8 + +- name: Relief set against pay for Income Tax still reduces Class 4 profits + period: 2026 + absolute_error_margin: 0.01 + input: + employment_income: 50_000 + self_employment_income: 20_000 + trading_loss: 15_000 + output: + trade_loss_relief_against_general_income: 15_000 + ni_class_4_profits: 5_000 + ni_class_4: 0 + +- name: Losses brought forward reduce Class 4 profits + period: 2026 + absolute_error_margin: 0.01 + input: + self_employment_income: 20_000 + loss_relief: 5_000 + output: + ni_class_4_profits: 15_000 + ni_class_4: 145.8 + +- name: A loss with no profits pays no Class 4 and never goes below zero + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 40_000 + trading_loss: 10_000 + output: + ni_class_4_profits: 0 + ni_class_4: 0 + +- name: Class 4 carries forward a loss Income Tax set against other income + period: 2026 + absolute_error_margin: 0.01 + input: + self_employment_income: 50_000 + ni_class_4_losses_brought_forward: 20_000 + output: + ni_class_4_profits: 30_000 + # (30,000 - 12,570) x 6%. + ni_class_4: 1_045.8 + # Income Tax is unaffected: £50,000 less the £12,570 Personal Allowance at 20%. + income_tax: 7_486 diff --git a/policyengine_uk/tests/policy/baseline/household/income/trading_loss_household_income.yaml b/policyengine_uk/tests/policy/baseline/household/income/trading_loss_household_income.yaml new file mode 100644 index 0000000000..23595bcb87 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/household/income/trading_loss_household_income.yaml @@ -0,0 +1,42 @@ +# HBAI counts self-employment losses as negative income; household market +# income does the same. 2026-27: £40,000 of pay with a £10,000 trading loss +# pays the Income Tax due on £30,000 (£3,486, against £5,486 without the +# loss), while NI on the pay is unchanged. + +- name: A trading loss is negative market income + period: 2026 + absolute_error_margin: 0 + input: + employment_income: 40_000 + trading_loss: 10_000 + output: + market_income: 30_000 + household_market_income: 30_000 + +- name: HBAI income falls by the loss less the Income Tax it saves + period: 2026 + absolute_error_margin: 1 + input: + people: + with_loss: + age: 40 + employment_income: 40_000 + trading_loss: 10_000 + without_loss: + age: 40 + employment_income: 40_000 + benunits: + benunit_1: + members: [with_loss] + benunit_2: + members: [without_loss] + households: + household_1: + members: [with_loss] + household_2: + members: [without_loss] + output: + income_tax: [3_486, 5_486] + # -10,000 of loss + 2,000 of Income Tax saved. + hbai_household_net_income: [24_320, 32_320] + household_net_income: [24_140, 32_140] diff --git a/policyengine_uk/tests/test_trading_loss_properties.py b/policyengine_uk/tests/test_trading_loss_properties.py new file mode 100644 index 0000000000..4475c4e228 --- /dev/null +++ b/policyengine_uk/tests/test_trading_loss_properties.py @@ -0,0 +1,656 @@ +"""Property-based tests for the trading loss input (trading_loss). + +A trading loss is entered as a positive amount, separate from the profits in +self_employment_income, and each programme reads it by its own rule. +Invariants, for any people and incomes: + +1. Relief definition (ITA 2007 s.64, s.24A): trade loss relief against + general income equals min(loss, max(£50,000, 25% of adjusted total + income), net income). Net income is the adjusted net income of the same + person without the loss (a separate simulation); adjusted total income is + that less the pension contributions given relief (own contributions up to + the greater of £3,600 and pay plus profits, none from age 75). +2. Differential: a person's Income Tax with a loss equals the Income Tax of + the same person without the loss and with their income cut by the relief, + taken from non-savings income first, then savings, then dividends: the + model's fixed allowance order (s.25(2) asks for the order giving the + greatest reduction; #2106). +3. Income Tax never rises with the loss and falls by at most the relief (for + people without savings income: the personal savings allowance steps up by + £500 when someone stops being a higher-rate taxpayer, so with savings + income a small relief can save more than itself). +4. Means tests never set the loss against employed earnings or other + income: with Income Tax and NI held at the values the loss gives, every + means-tested benefit and means-test income is what it is without the loss, + except that UC sets it against the person's other trades' profits (UC Regs + 2013 reg 57(2)), so UC, its benefit cap earnings test and the capped + award equal the same family with those profits cut by the loss. (The model's means tests deduct the year's Income Tax liability, + so otherwise the loss reaches them only through that tax.) +5. Tax credits (SI 2002/2006 reg 3(1) Step 4): the claimants' losses (not a + child's) come off their applicable income, never below zero, and the award + never falls. +6. Household income: market income falls by exactly the household's losses, + and HBAI net income moves only through the loss itself, taxes and + means-tested or passported benefits; every other component is unchanged. +7. Class 4 (SSCBA 1992 Sch 2 para 3): Class 4 profits are self-employment + profits less losses brought forward (for both taxes and for Class 4 only) + and the year's relief, never below zero, so Class 4 never rises with the + loss and someone with no profits pays none whatever their loss. +8. The s.24A cap starts in 2013-14. + +Comparisons allow float32 rounding: the model stores values as float32. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import CountryTaxBenefitSystem, Simulation +from policyengine_uk.variables.household.income.hbai_household_net_income import ( + HBAI_HOUSEHOLD_NET_INCOME_ADDS, + HBAI_HOUSEHOLD_NET_INCOME_SUBTRACTS, +) + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=3, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +REGIONS = ["LONDON", "NORTH_EAST", "SCOTLAND", "WALES"] +# Means tests other than UC, compared with UC held (CTR counts UC as income). +MEANS_TESTED = [ + "housing_benefit", + "council_tax_reduction", + "pension_credit", + "income_support", + "jsa_income", + "esa_income", + "housing_benefit_applicable_income", + "income_support_applicable_income", + "council_tax_reduction_applicable_income", + "pension_credit_income", +] +UC_TESTS = [ + "uc_earned_income", + "universal_credit_pre_benefit_cap", + "is_benefit_cap_exempt_earnings", + "universal_credit", +] +# HBAI components the loss may move: the loss itself, taxes, and benefits +# that are means-tested or passported from a means-tested benefit. +HBAI_CHANNELS = { + "trading_loss", + "income_tax", + "national_insurance", + "student_loan_repayments", + "universal_credit", + "housing_benefit", + "council_tax_benefit", + "pension_credit", + "income_support", + "jsa_income", + "esa_income", + "working_tax_credit", + "child_tax_credit", + "tax_free_childcare", + "free_school_meals", + "free_school_fruit_veg", + "free_school_milk", + "healthy_start_vouchers", + "scottish_child_payment", + "cost_of_living_support_payment", +} + + +def money(high): + return st.one_of( + st.just(0.0), + st.floats(1, high, allow_nan=False, allow_infinity=False), + ) + + +def tolerance(*amounts): + # float32 has a 24-bit significand: a few ulps of the largest amount + # involved, plus a penny. + return 0.01 + 4e-7 * max([abs(float(a)) for a in amounts] + [1]) + + +@st.composite +def taxpayers(draw): + """One working-age person with income of every taxed kind and a loss.""" + return { + "age": draw(st.integers(18, 80)), + "employment_income": draw(money(250_000)), + "private_pension_income": draw(money(60_000)), + "savings_interest_income": draw(money(40_000)), + "dividend_income": draw(money(150_000)), + "property_income": draw(money(40_000)), + "self_employment_income": draw(money(80_000)), + "personal_pension_contributions": draw(money(30_000)), + "trading_loss": draw(money(300_000)), + "state_pension": 0.0, + } + + +def single_people(people, regions): + situation = {"people": {}, "benunits": {}, "households": {}} + for i, (inputs, region) in enumerate(zip(people, regions)): + name = f"p{i}" + situation["people"][name] = {k: {YEAR: v} for k, v in inputs.items()} + situation["benunits"][f"b{i}"] = {"members": [name]} + situation["households"][f"h{i}"] = { + "members": [name], + "region": {YEAR: region}, + } + return situation + + +def calculate(situation, variables, year=YEAR): + sim = Simulation(situation=situation) + return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in variables} + + +def taxpayer(**inputs): + """A taxpayer with every taxpayers() field, zero unless given.""" + base = { + "age": 40, + "employment_income": 0.0, + "private_pension_income": 0.0, + "savings_interest_income": 0.0, + "dividend_income": 0.0, + "property_income": 0.0, + "self_employment_income": 0.0, + "personal_pension_contributions": 0.0, + "trading_loss": 0.0, + "state_pension": 0.0, + } + return {**base, **inputs} + + +# Cases every run checks, alongside the generated ones: relief limited by net +# income, the 25% and £50,000 caps, contributions above relevant earnings, +# no relief from age 75, profits and a loss together, and income spilling +# into savings and dividends. +PINNED_TAXPAYERS = [ + taxpayer(employment_income=20_000, trading_loss=50_000), + taxpayer(employment_income=400_000, trading_loss=300_000), + taxpayer(employment_income=150_000, trading_loss=80_000), + taxpayer( + dividend_income=300_000, + personal_pension_contributions=40_000, + trading_loss=200_000, + ), + taxpayer( + age=78, + employment_income=300_000, + personal_pension_contributions=40_000, + trading_loss=100_000, + ), + taxpayer(self_employment_income=30_000, trading_loss=10_000), + taxpayer( + employment_income=5_000, + savings_interest_income=10_000, + dividend_income=30_000, + trading_loss=20_000, + ), + taxpayer(employment_income=110_000, trading_loss=20_000), +] +PINNED_REGIONS = ["LONDON"] * 8 + ["SCOTLAND"] * 8 + + +@PROPERTY_SETTINGS +@example(PINNED_TAXPAYERS, PINNED_REGIONS) +@given( + st.lists(taxpayers(), min_size=8, max_size=16), + st.lists(st.sampled_from(REGIONS), min_size=16, max_size=16), +) +def test_relief_is_the_loss_within_net_income_and_the_cap(people, regions): + without = [{**p, "trading_loss": 0.0} for p in people] + a = calculate( + single_people(people, regions), + ["trade_loss_relief_against_general_income", "adjusted_net_income"], + ) + b = calculate( + single_people(without, regions), + [ + "adjusted_net_income", + "taxable_employment_income", + "taxable_self_employment_income", + ], + ) + for i, person in enumerate(people): + # Relevant UK earnings (FA 2004 s.189(2)): taxable pay and profits. + earnings = ( + b["taxable_employment_income"][i] + b["taxable_self_employment_income"][i] + ) + relieved = (person["age"] < 75) * min( + person["personal_pension_contributions"], max(3_600.0, earnings) + ) + ati = max(0.0, b["adjusted_net_income"][i] - relieved) + cap = max(50_000.0, 0.25 * ati) + expected = min(person["trading_loss"], cap, b["adjusted_net_income"][i]) + relief = a["trade_loss_relief_against_general_income"][i] + assert abs(relief - expected) <= tolerance(expected, ati), (person, relief) + # Net income falls by exactly the relief. + assert abs( + b["adjusted_net_income"][i] - relief - a["adjusted_net_income"][i] + ) <= tolerance(b["adjusted_net_income"][i]), person + + +@st.composite +def portfolio_holders(draw): + """Pay, pension, savings and dividends only, so the reduced-income twin + differs from the person only in those four amounts.""" + return { + "age": draw(st.integers(18, 64)), + "employment_income": draw(money(250_000)), + "private_pension_income": draw(money(60_000)), + "savings_interest_income": draw(money(40_000)), + "dividend_income": draw(money(150_000)), + "trading_loss": draw(money(300_000)), + "state_pension": 0.0, + } + + +def reduced(person): + """The person's income cut by the relief, in the s.25 order the model + uses, computed outside the model.""" + pay = person["employment_income"] + pension = person["private_pension_income"] + savings = person["savings_interest_income"] + dividends = person["dividend_income"] + total = pay + pension + savings + dividends + relief = min(person["trading_loss"], max(50_000.0, 0.25 * total), total) + left = relief + out = {} + for name, amount in [ + ("employment_income", pay), + ("private_pension_income", pension), + ("savings_interest_income", savings), + ("dividend_income", dividends), + ]: + cut = min(left, amount) + out[name] = amount - cut + left -= cut + return {**person, **out, "trading_loss": 0.0}, relief + + +PINNED_PORTFOLIOS = [ + { + "age": 40, + "employment_income": 5_000.0, + "private_pension_income": 0.0, + "savings_interest_income": 10_000.0, + "dividend_income": 30_000.0, + "trading_loss": 20_000.0, + "state_pension": 0.0, + }, + { + "age": 40, + "employment_income": 0.0, + "private_pension_income": 0.0, + "savings_interest_income": 0.0, + "dividend_income": 60_000.0, + "trading_loss": 20_000.0, + "state_pension": 0.0, + }, + { + "age": 40, + "employment_income": 300_000.0, + "private_pension_income": 0.0, + "savings_interest_income": 0.0, + "dividend_income": 0.0, + "trading_loss": 200_000.0, + "state_pension": 0.0, + }, + { + "age": 40, + "employment_income": 8_000.0, + "private_pension_income": 12_000.0, + "savings_interest_income": 2_000.0, + "dividend_income": 0.0, + "trading_loss": 15_000.0, + "state_pension": 0.0, + }, +] * 2 + + +@PROPERTY_SETTINGS +@example(PINNED_PORTFOLIOS, PINNED_REGIONS) +@given( + st.lists(portfolio_holders(), min_size=8, max_size=16), + st.lists(st.sampled_from(REGIONS), min_size=16, max_size=16), +) +def test_relief_taxes_like_income_cut_in_section_25_order(people, regions): + twins = [reduced(p)[0] for p in people] + variables = ["income_tax", "trade_loss_relief_against_general_income"] + a = calculate(single_people(people, regions), variables) + b = calculate(single_people(twins, regions), ["income_tax"]) + for i, person in enumerate(people): + _, relief = reduced(person) + assert abs(a["trade_loss_relief_against_general_income"][i] - relief) <= ( + tolerance(relief) + ), person + assert abs(a["income_tax"][i] - b["income_tax"][i]) <= 0.5 + tolerance( + a["income_tax"][i], person["employment_income"] + ), (person, a["income_tax"][i], b["income_tax"][i]) + + +@PROPERTY_SETTINGS +@example(PINNED_TAXPAYERS, PINNED_REGIONS, 0.5) +@given( + st.lists(taxpayers(), min_size=8, max_size=16), + st.lists(st.sampled_from(REGIONS), min_size=16, max_size=16), + st.floats(0, 1), +) +def test_income_tax_falls_with_the_loss_by_at_most_the_relief(people, regions, share): + smaller = [{**p, "trading_loss": p["trading_loss"] * share} for p in people] + without = [{**p, "trading_loss": 0.0} for p in people] + variables = ["income_tax", "trade_loss_relief_against_general_income"] + a = calculate(single_people(people, regions), variables) + s = calculate(single_people(smaller, regions), variables) + b = calculate(single_people(without, regions), variables) + for i, person in enumerate(people): + tol = tolerance(b["income_tax"][i], person["employment_income"]) + assert a["income_tax"][i] <= s["income_tax"][i] + tol, person + assert s["income_tax"][i] <= b["income_tax"][i] + tol, person + if person["savings_interest_income"] == 0: + saved = b["income_tax"][i] - a["income_tax"][i] + relief = a["trade_loss_relief_against_general_income"][i] + assert saved <= relief + tol, (person, saved, relief) + + +@st.composite +def families(draw): + """A benefit unit with a head, maybe a partner and children, rent and + council tax, where the adults may have a trading loss.""" + head_age = draw(st.integers(18, 80)) + adults = [head_age] + if draw(st.booleans()): + adults.append(draw(st.integers(18, 80))) + people = [ + { + "age": age, + "employment_income": draw(money(60_000)), + "self_employment_income": draw(money(30_000)), + "private_pension_income": draw(money(20_000)), + "savings_interest_income": draw(money(5_000)), + "trading_loss": draw(money(40_000)), + "hours_worked": draw(st.sampled_from([0.0, 16.0, 35.0])), + } + for age in adults + ] + for _ in range(draw(st.integers(0, 3))): + # A child's own trading loss is never the claimants'. + people.append( + {"age": draw(st.integers(0, 15)), "trading_loss": draw(money(5_000))} + ) + household = { + "rent": draw(money(15_000)), + "council_tax": draw(money(3_000)), + "tenure_type": draw(st.sampled_from(["RENT_PRIVATELY", "RENT_FROM_COUNCIL"])), + "region": draw(st.sampled_from(REGIONS)), + } + return people, household + + +def family_situation(units, overrides=None, benunit_overrides=None): + situation = {"people": {}, "benunits": {}, "households": {}} + for i, (people, household) in enumerate(units): + names = [] + for j, inputs in enumerate(people): + name = f"p{i}_{j}" + values = dict(inputs) + if overrides is not None: + values.update(overrides.get(name, {})) + situation["people"][name] = {k: {YEAR: v} for k, v in values.items()} + names.append(name) + situation["benunits"][f"b{i}"] = { + "members": names, + **{ + k: {YEAR: v} + for k, v in (benunit_overrides or {}).get(f"b{i}", {}).items() + }, + } + situation["households"][f"h{i}"] = { + "members": names, + **{k: {YEAR: v} for k, v in household.items()}, + } + return situation + + +def adult(**inputs): + base = { + "age": 40, + "employment_income": 0.0, + "self_employment_income": 0.0, + "private_pension_income": 0.0, + "savings_interest_income": 0.0, + "trading_loss": 0.0, + "hours_worked": 35.0, + } + return {**base, **inputs} + + +def home(rent, region="LONDON"): + return { + "rent": rent, + "council_tax": 2_000.0, + "tenure_type": "RENT_PRIVATELY", + "region": region, + } + + +# Families every run checks: profits and another trade's loss under the +# benefit cap, a loss beside pay only, a child's loss, and a loss-making +# couple with pay. +PINNED_FAMILIES = [ + ( + [adult(self_employment_income=12_000.0, trading_loss=5_000.0)] + + [{"age": a, "trading_loss": 0.0} for a in (1, 4, 7, 10)], + home(24_000.0), + ), + ( + [adult(employment_income=15_000.0, trading_loss=10_000.0)], + home(9_000.0, "NORTH_EAST"), + ), + ( + [adult(employment_income=20_000.0), {"age": 12, "trading_loss": 4_000.0}], + home(8_000.0, "WALES"), + ), + ( + [ + adult( + employment_income=12_000.0, + self_employment_income=3_000.0, + trading_loss=8_000.0, + ), + adult(age=38, employment_income=9_000.0, trading_loss=2_000.0), + ], + home(11_000.0, "SCOTLAND"), + ), +] + + +@PROPERTY_SETTINGS +@example(PINNED_FAMILIES) +@given(st.lists(families(), min_size=4, max_size=10)) +def test_means_tests_see_the_loss_only_through_tax(units): + with_loss = Simulation(situation=family_situation(units)) + + def values(sim, variable): + return np.asarray(sim.calculate(variable, YEAR), dtype=float) + + names = with_loss.populations["person"].ids + benunits = with_loss.populations["benunit"].ids + tax = {v: values(with_loss, v) for v in ["income_tax", "national_insurance"]} + loss = values(with_loss, "trading_loss") + profits = values(with_loss, "self_employment_income") + mif = np.asarray(with_loss.calculate("uc_mif_applies", YEAR)) + uc = values(with_loss, "universal_credit") + hb = values(with_loss, "housing_benefit") + + def held(k, **extra): + return { + "trading_loss": 0.0, + "income_tax": float(tax["income_tax"][k]), + "national_insurance": float(tax["national_insurance"][k]), + **extra, + } + + # Every means test but UC: the loss reaches them only through the tax. + no_loss = Simulation( + situation=family_situation( + units, + {name: held(k) for k, name in enumerate(names)}, + {b: {"universal_credit": float(uc[k])} for k, b in enumerate(benunits)}, + ) + ) + for variable in MEANS_TESTED: + a, b = values(with_loss, variable), values(no_loss, variable) + assert np.allclose(a, b, atol=0.5), (variable, a, b, units) + # UC: as if the loss had cut the person's other trades' profits. + netted = Simulation( + situation=family_situation( + units, + { + name: held( + k, + self_employment_income=float(profits[k] - min(loss[k], profits[k])), + uc_mif_applies=bool(mif[k]), + ) + for k, name in enumerate(names) + }, + # The cap counts Housing Benefit, which reads profits before any + # loss: hold it at the with-loss value. + {b: {"housing_benefit": float(hb[k])} for k, b in enumerate(benunits)}, + ) + ) + for variable in UC_TESTS: + a, b = values(with_loss, variable), values(netted, variable) + assert np.allclose(a, b, atol=0.5), (variable, a, b, units) + + +@PROPERTY_SETTINGS +@example(PINNED_FAMILIES) +@given(st.lists(families(), min_size=4, max_size=10)) +def test_tax_credit_income_falls_by_the_claimants_losses(units): + year = 2024 + without = [ + ([{**p, "trading_loss": 0.0} if "trading_loss" in p else p for p in people], h) + for people, h in units + ] + + def run(us): + situation = family_situation(us) + for person in situation["people"].values(): + for key, value in list(person.items()): + person[key] = {year: value[YEAR]} + for household in situation["households"].values(): + for key, value in list(household.items()): + if key != "members": + household[key] = {year: value[YEAR]} + sim = Simulation(situation=situation) + return { + v: np.asarray(sim.calculate(v, year), dtype=float) + for v in ["tax_credits_applicable_income", "tax_credits"] + }, sim + + a, sim = run(units) + b, _ = run(without) + members = sim.calculate("is_claimant_or_partner", year) + loss = np.asarray(sim.calculate("trading_loss", year), dtype=float) + benunit_loss = sim.map_result(loss * members, "person", "benunit") + expected = np.maximum(0, b["tax_credits_applicable_income"] - benunit_loss) + # On income-related IS, ESA or JSA the applicable income is nil anyway. + on_exempt = b["tax_credits_applicable_income"] == 0 + assert np.allclose( + a["tax_credits_applicable_income"], np.where(on_exempt, 0, expected), atol=0.5 + ), (a, expected, units) + assert np.all(a["tax_credits"] >= b["tax_credits"] - 0.5), units + + +@PROPERTY_SETTINGS +@example(PINNED_FAMILIES) +@given(st.lists(families(), min_size=4, max_size=10)) +def test_household_income_moves_only_through_its_channels(units): + without = [ + ([{**p, "trading_loss": 0.0} if "trading_loss" in p else p for p in people], h) + for people, h in units + ] + components = sorted( + set(HBAI_HOUSEHOLD_NET_INCOME_ADDS + HBAI_HOUSEHOLD_NET_INCOME_SUBTRACTS) + ) + variables = components + ["household_market_income", "hbai_household_net_income"] + a_sim = Simulation(situation=family_situation(units)) + b_sim = Simulation(situation=family_situation(without)) + + def household_values(sim, variable): + values = np.asarray(sim.calculate(variable, YEAR), dtype=float) + entity = sim.tax_benefit_system.get_variable(variable).entity.key + if entity == "household": + return values + return sim.map_result(values, entity, "household") + + a = {v: household_values(a_sim, v) for v in variables} + b = {v: household_values(b_sim, v) for v in variables} + losses = a["trading_loss"] + assert np.allclose( + a["household_market_income"], b["household_market_income"] - losses, atol=0.5 + ) + for variable in components: + if variable not in HBAI_CHANNELS: + assert np.allclose(a[variable], b[variable], atol=0.5), variable + channels = sum( + (a[v] - b[v]) * (1 if v in HBAI_HOUSEHOLD_NET_INCOME_ADDS else -1) + for v in components + if v in HBAI_CHANNELS + ) + assert np.allclose( + a["hbai_household_net_income"] - b["hbai_household_net_income"], + channels, + atol=1, + ) + + +@PROPERTY_SETTINGS +@example(PINNED_TAXPAYERS, PINNED_REGIONS, [5_000.0] * 16) +@given( + st.lists(taxpayers(), min_size=8, max_size=16), + st.lists(st.sampled_from(REGIONS), min_size=16, max_size=16), + st.lists(money(20_000), min_size=16, max_size=16), +) +def test_class_4_profits_net_of_the_relief(people, regions, brought_forward): + people = [{**p, "loss_relief": b} for p, b in zip(people, brought_forward)] + without = [{**p, "trading_loss": 0.0} for p in people] + variables = [ + "ni_class_4_profits", + "ni_class_4", + "trade_loss_relief_against_general_income", + ] + a = calculate(single_people(people, regions), variables) + b = calculate(single_people(without, regions), variables) + for i, person in enumerate(people): + expected = max( + 0.0, + person["self_employment_income"] + - person["loss_relief"] + - a["trade_loss_relief_against_general_income"][i], + ) + assert abs(a["ni_class_4_profits"][i] - expected) <= tolerance( + person["self_employment_income"] + ), person + assert a["ni_class_4"][i] <= b["ni_class_4"][i] + tolerance( + b["ni_class_4"][i] + ), person + if person["self_employment_income"] == 0: + assert a["ni_class_4"][i] == 0, person + + +def test_cap_starts_in_2013_14(): + cap = CountryTaxBenefitSystem().parameters.gov.hmrc.income_tax.reliefs.cap + assert cap.amount("2013-04-05") == np.inf + assert cap.amount("2013-04-06") == 50_000 + assert cap.rate("2013-04-06") == 0.25 diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py index c27e13c39d..4ecc408af0 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py @@ -26,6 +26,9 @@ def formula(benunit, period, parameters): uc_earned = benunit.sum( benunit.members("employment_income", period) + benunit.members("self_employment_income", period) + # Reg 82 reads earned income as UC calculates it: a trading loss + # nets against other trades' profits (reg 57(2)). + - benunit.members("uc_trading_losses_set_against_profits", period) - benunit.members("income_tax", period) - benunit.members("national_insurance", period) ) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py index 69d9205c9a..47500d0240 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py @@ -26,6 +26,9 @@ def formula(benunit, period, parameters): uc_earned = benunit.sum( benunit.members("employment_income", period) + benunit.members("self_employment_income", period) + # Reg 82 reads earned income as UC calculates it: a trading loss + # nets against other trades' profits (reg 57(2)). + - benunit.members("uc_trading_losses_set_against_profits", period) - benunit.members("income_tax", period) - benunit.members("national_insurance", period) ) diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py index 1c698c1921..61acfe6b6d 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -39,6 +39,13 @@ def formula(benunit, period, parameters): if bi.interactions.include_in_means_tests: STEP_2_COMPONENTS.append("basic_income") income += add_for_members(benunit, period, STEP_2_COMPONENTS, members) + # Step 4: the claimants' trading loss in the year is subtracted from + # the total of the other steps, so one partner's loss reduces the + # joint income. A child's loss is not the claimants'. + claimants = person("is_claimant_or_partner", period) + income = max_( + 0, income - add_for_members(benunit, period, ["trading_loss"], claimants) + ) EXEMPT_BENEFITS = ["income_support", "esa_income", "jsa_income"] on_exempt_benefits = ( add_for_members(benunit, period, EXEMPT_BENEFITS, members) > 0 diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_capped_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_capped_earned_income.py index 03c7deb25c..7e7760bf63 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_capped_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_capped_earned_income.py @@ -18,7 +18,9 @@ def formula(person, period, parameters): bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: INCOME_COMPONENTS.append("basic_income") - personal_gross_earned_income = add(person, period, INCOME_COMPONENTS) + personal_gross_earned_income = add(person, period, INCOME_COMPONENTS) - person( + "uc_trading_losses_set_against_profits", period + ) floor = where( person("uc_mif_applies", period), person("uc_minimum_income_floor", period), diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_trading_losses_set_against_profits.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_trading_losses_set_against_profits.py new file mode 100644 index 0000000000..191debaee3 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_trading_losses_set_against_profits.py @@ -0,0 +1,26 @@ +from policyengine_uk.model_api import * + + +class uc_trading_losses_set_against_profits(Variable): + value_type = float + entity = Person + label = "Trading losses set against other trades' profits for Universal Credit" + documentation = ( + "UC Regs 2013 reg 57(2) Steps 1-2 add together the profit or loss of " + "each of the person's trades, and Step 3 makes a negative total nil: " + "one trade's loss reduces another trade's profits but never employed " + "earnings. Over a year, with profits and losses spread evenly, that is " + "the loss up to the profits. UC earnings and the benefit cap earnings " + "test (reg 82) both deduct it." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Universal Credit Regulations 2013 reg. 57(2)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ) + + def formula(person, period, parameters): + return min_( + person("trading_loss", period), person("self_employment_income", period) + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/adjusted_net_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/adjusted_net_income.py index 3723b6a60a..ed81043887 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/adjusted_net_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/adjusted_net_income.py @@ -13,18 +13,12 @@ class adjusted_net_income(Variable): unit = GBP def formula(person, period, parameters): - adjusted_net_income_components = parameters( - period - ).gov.hmrc.income_tax.adjusted_net_income_components - - # Find adjusted net income - ani = add(person, period, adjusted_net_income_components) - - # For basic income contributions, add basic income - # Modifying param list directly is mutative, hence two-step process - if parameters( - period - ).gov.contrib.ubi_center.basic_income.interactions.include_in_taxable_income: - ani += person("basic_income", period) - - return max_(0, ani) + # Net income after trade loss relief against general income + # (ITA 2007 s.24(1), s.64), which reduces it before the Personal + # Allowance taper and every other adjusted net income test. Includes + # basic income where a reform makes it taxable. + return max_( + 0, + person("net_income_before_trade_loss_relief", period) + - person("trade_loss_relief_against_general_income", period), + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_dividend_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_dividend_income.py index 3ec0cd9fd3..8503f249b8 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_dividend_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_dividend_income.py @@ -17,8 +17,11 @@ def formula(person, period, parameters): received_allowances_savings_income = person( "received_allowances_savings_income", period ) + # Includes any trade loss relief beyond non-savings and savings + # income (ITA 2007 s.25(2)). remaining_allowance = ( all_allowances + + person("trade_loss_relief_against_savings_and_dividends", period) - received_allowances_earned_income - received_allowances_savings_income ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_savings_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_savings_income.py index ba3469a3eb..f970da5855 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_savings_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/received_allowances/received_allowances_savings_income.py @@ -14,7 +14,13 @@ def formula(person, period, parameters): received_allowances_earned_income = person( "received_allowances_earned_income", period ) - remaining_allowance = all_allowances - received_allowances_earned_income + # Trade loss relief beyond non-savings income comes off savings income + # next, then dividends (ITA 2007 s.25(2)). + remaining_allowance = ( + all_allowances + + person("trade_loss_relief_against_savings_and_dividends", period) + - received_allowances_earned_income + ) savings_income = person("taxable_savings_interest_income", period) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/trading_loss.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/trading_loss.py deleted file mode 100644 index 0277b37f86..0000000000 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bases/trading_loss.py +++ /dev/null @@ -1,13 +0,0 @@ -from policyengine_uk.model_api import * - - -class trading_loss(Variable): - value_type = float - entity = Person - label = "Loss from trading in the current year." - reference = dict( - title="Income Tax Act 2007 s. 64", - href="https://www.legislation.gov.uk/ukpga/2007/3/section/64", - ) - definition_period = YEAR - unit = GBP diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/adjusted_total_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/adjusted_total_income.py new file mode 100644 index 0000000000..959562f123 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/adjusted_total_income.py @@ -0,0 +1,69 @@ +from policyengine_uk.model_api import * + + +class adjusted_total_income(Variable): + value_type = float + entity = Person + label = "Adjusted total income" + documentation = ( + "The base of the cap on reliefs against general income (ITA 2007 " + "s.24A(8)). Step 1 is total income (s.23 Step 1): the taxable income " + "components, with losses brought forward (loss_relief, a Step 2 " + "relief the model deducts from trading profits) added back. Steps 3 " + "and 4 deduct the pension contributions given relief (FA 2004 " + "ss.192-194): the person's own contributions up to the greater of " + "their relevant UK earnings (s.189(2): taxable employment income, " + "including benefits and statutory pay, and trading profits) and the " + "basic amount (s.190), none from " + "age 75 (s.188(3)(a)). The annual allowance limits tax-relieved " + "saving through a charge, not the relief itself, so it is not applied " + "here. Step 2 adds back payroll giving, which the model does not have." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Income Tax Act 2007 s. 24A(8)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/24A", + ), + dict( + title="Finance Act 2004 ss. 188-190", + href="https://www.legislation.gov.uk/ukpga/2004/12/section/190", + ), + ] + + def formula(person, period, parameters): + p = parameters(period).gov.hmrc + profits = person("self_employment_income", period) + other_deductions = [ + deduction + for deduction in p.income_tax.bases.taxable_self_employment_income_deductions + if deduction != "loss_relief" + ] + profits_before_carry_forward = max_( + 0, profits - add(person, period, other_deductions) + ) + losses_brought_forward_deducted = profits_before_carry_forward - person( + "taxable_self_employment_income", period + ) + total_income = ( + person("net_income_before_trade_loss_relief", period) + + losses_brought_forward_deducted + ) + # Relevant UK earnings (FA 2004 s.189(2)): employment income chargeable + # to tax, including taxable benefits and statutory payments, and + # trading profits chargeable to tax. + relevant_earnings = ( + person("taxable_employment_income", period) + profits_before_carry_forward + ) + relief_limit = max_( + p.income_tax.reliefs.pension_contribution.basic_amount, relevant_earnings + ) + under_age_limit = ( + person("age", period) < p.pensions.pension_contributions_relief_age_limit + ) + relieved_contributions = ( + min_(person("pension_contributions", period), relief_limit) + * under_age_limit + ) + return max_(0, total_income - relieved_contributions) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/income_tax_relief_cap.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/income_tax_relief_cap.py new file mode 100644 index 0000000000..482ef814fd --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/income_tax_relief_cap.py @@ -0,0 +1,25 @@ +from policyengine_uk.model_api import * + + +class income_tax_relief_cap(Variable): + value_type = float + entity = Person + label = "Cap on certain Income Tax reliefs" + documentation = ( + "The most that the reliefs listed in ITA 2007 s.24A(6), of which the " + "model has trade loss relief against general income, can deduct at " + "Step 2 in the year: '£50,000, or ... if more, 25% of the taxpayer's " + "adjusted total income' (s.24A(5)). Deductions from profits of the " + "loss-making trade itself fall outside the cap (s.24A(7)(b)); in the " + "loss-making year that trade has no profits." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax Act 2007 s. 24A", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/24A", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hmrc.income_tax.reliefs.cap + return max_(p.amount, p.rate * person("adjusted_total_income", period)) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/loss_relief.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/loss_relief.py index ced48ecb1e..0b27ae0104 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/loss_relief.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/loss_relief.py @@ -8,16 +8,19 @@ class loss_relief(Variable): value_type = float entity = Person - label = "Tax relief from trading losses" - definition_period = YEAR + label = "Trade losses brought forward" reference = dict( - title="Income Tax (Trading and Other Income) Act 2005 s. 59", - href="https://www.legislation.gov.uk/ukpga/2005/5/section/59", + title="Income Tax Act 2007 s. 83", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/83", + ) + documentation = ( + "Unrelieved trade losses of earlier years deducted from this year's " + "profits of the same trade (ITA 2007 s.83). Deducted from " + "self-employment income only, and never below zero. A loss made in " + "this year goes in trading_loss, which is relieved against general " + "income instead (s.64); whatever part of it this year's income cannot " + "absorb is carried forward by entering it here in later years." ) - documentation = "Can be set against general income." + definition_period = YEAR unit = GBP - - def formula(person, period, parameters): - current_loss = person("trading_loss", period) - previous_loss = person("trading_loss", period.last_year) - return current_loss + previous_loss + quantity_type = FLOW diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/net_income_before_trade_loss_relief.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/net_income_before_trade_loss_relief.py new file mode 100644 index 0000000000..3631f4c369 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/net_income_before_trade_loss_relief.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class net_income_before_trade_loss_relief(Variable): + value_type = float + entity = Person + label = "Net income before trade loss relief against general income" + documentation = ( + "The components of income left after the Step 2 reliefs the model " + "deducts inside them (ITA 2007 s.23), before trade loss relief against " + "general income: the income that relief can be deducted from " + "(s.25(4)). Includes basic income where a reform makes it taxable." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax Act 2007 s. 23", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/23", + ) + + def formula(person, period, parameters): + p = parameters(period) + income = add( + person, period, p.gov.hmrc.income_tax.adjusted_net_income_components + ) + if p.gov.contrib.ubi_center.basic_income.interactions.include_in_taxable_income: + income += person("basic_income", period) + return income diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_general_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_general_income.py new file mode 100644 index 0000000000..7c2aa67c83 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_general_income.py @@ -0,0 +1,37 @@ +from policyengine_uk.model_api import * + + +class trade_loss_relief_against_general_income(Variable): + value_type = float + entity = Person + label = "Trade loss relief against general income" + documentation = ( + "The part of the year's trading loss deducted in calculating net income " + "for the loss-making year (ITA 2007 s.64(2)(a)): the whole loss, " + "'limited in accordance with sections 24A and 25(4) and (5)' (s.65(1)), " + "so to the s.24A cap and to the net income there is to deduct it from. " + "Nothing lets a claim be restricted to keep the Personal Allowance in " + "use. The model assumes the trade is commercial (s.66), that the person " + "claims, and that the claim is for the loss-making year rather than " + "the year before (s.64(2)(b)); the part it cannot use is carried " + "forward only by entering it in loss_relief in later years (s.83)." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Income Tax Act 2007 s. 64", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/64", + ), + dict( + title="Income Tax Act 2007 s. 24A", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/24A", + ), + ] + + def formula(person, period, parameters): + net_income = max_(0, person("net_income_before_trade_loss_relief", period)) + return min_( + person("trading_loss", period), + min_(person("income_tax_relief_cap", period), net_income), + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_savings_and_dividends.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_savings_and_dividends.py new file mode 100644 index 0000000000..201cc012df --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/trade_loss_relief_against_savings_and_dividends.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class trade_loss_relief_against_savings_and_dividends(Variable): + value_type = float + entity = Person + label = "Trade loss relief deducted from savings and dividend income" + documentation = ( + "The part of trade loss relief against general income left after " + "non-savings income is used up; it joins the allowances applied to " + "savings income and then to dividends. ITA 2007 s.25(2) deducts " + "reliefs and allowances in the order giving the greatest reduction in " + "liability, and from 2027-28 s.25(3A) puts income other than property, " + "savings and dividends first. The model uses its fixed allowance order " + "(non-savings income, property last, then savings, then dividends), " + "which can overcharge where savings are taxed at 0%; that is model-wide " + "and tracked in #2106." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax Act 2007 s. 25", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/25", + ) + + def formula(person, period, parameters): + non_savings_income = ( + person("net_income_before_trade_loss_relief", period) + - person("taxable_savings_interest_income", period) + - person("taxable_dividend_income", period) + ) + relief = person("trade_loss_relief_against_general_income", period) + return max_(0, relief - max_(0, non_savings_income)) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py index a36358bb4d..164402c8ed 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_2/ni_class_2.py @@ -13,6 +13,8 @@ class ni_class_2(Variable): def formula(person, period, parameters): class_2 = parameters(period).gov.hmrc.national_insurance.class_2 - profits = person("self_employment_income", period) + # SSCBA 1992 s.11(3): "relevant profits" are the profits on which + # Class 4 is (or would be) payable, after the Schedule 2 loss reliefs. + profits = person("ni_class_4_profits", period) over_threshold = profits >= class_2.small_profits_threshold return over_threshold * class_2.flat_rate * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py index 6f43033d47..a6ecf2a361 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py @@ -13,8 +13,9 @@ class ni_class_4(Variable): def formula(person, period, parameters): class_4 = parameters(period).gov.hmrc.national_insurance.class_4 # Schedule 2 para 2: Class 4 is charged on the full trading profits; - # Class 1 contributions are not a deduction. - profits = person("self_employment_income", period) + # Class 1 contributions are not a deduction. Trade losses do reduce + # them (para 3), through ni_class_4_profits. + profits = person("ni_class_4_profits", period) add_rate_income = max_( profits - class_4.thresholds.upper_profits_limit, 0, diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_losses_brought_forward.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_losses_brought_forward.py new file mode 100644 index 0000000000..701873abc1 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_losses_brought_forward.py @@ -0,0 +1,21 @@ +from policyengine_uk.model_api import * + + +class ni_class_4_losses_brought_forward(Variable): + value_type = float + entity = Person + label = "Class 4 losses brought forward" + documentation = ( + "Trade losses of earlier years that reduce this year's Class 4 profits " + "but not Income Tax: the excess that SSCBA 1992 Sch 2 para 3(4)(b) " + "carries forward where Income Tax set an earlier year's loss against " + "income other than the trade's profits. Losses brought forward for " + "both Income Tax and Class 4 go in loss_relief instead." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + reference = dict( + title="Social Security Contributions and Benefits Act 1992 Sch. 2 para. 3(4)(b)", + href="https://www.legislation.gov.uk/ukpga/1992/4/schedule/2/paragraph/3", + ) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py index 6f738716ba..74b467363e 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py @@ -12,7 +12,7 @@ class ni_class_4_main(Variable): def formula(person, period, parameters): class_4 = parameters(period).gov.hmrc.national_insurance.class_4 - profits = person("self_employment_income", period) + profits = person("ni_class_4_profits", period) add_rate_income = max_( profits - class_4.thresholds.upper_profits_limit, 0, diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py index 1d9cd4e3ba..b9e61c8aad 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py @@ -35,7 +35,7 @@ def formula(person, period, parameters): # a wrong Case 1 dropped the additional-rate band (#1878). The unused # band is exactly zero at or above the UPL, so that equality is now # decided exactly. - profits = person("self_employment_income", period) + profits = person("ni_class_4_profits", period) unused_main_band = clip(upl - profits.astype(np.float64), 0, step_1) case_1 = (step_4_raw >= 0) & ( main_rate * unused_main_band + class_2_weeks_addition diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_profits.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_profits.py new file mode 100644 index 0000000000..e31a7f3908 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_profits.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class ni_class_4_profits(Variable): + value_type = float + entity = Person + label = "Profits for Class 4 NICs" + documentation = ( + "Self-employment profits after the trade loss deductions Schedule 2 " + "para 3 SSCBA 1992 carries over from Income Tax: losses brought forward " + "(ITA 2007 s.83) and the year's trade loss relief against general " + "income (s.64), which para 3(4)(a) treats as reducing the profits of " + "any of the person's trades for Class 4, whatever income it was set " + "against for Income Tax. Para 3(4)(b) carries forward, for Class 4 " + "only, the part of an earlier loss that Income Tax set against other " + "income: ni_class_4_losses_brought_forward. A loss never reduces other " + "income for Class 4 and the result is never negative. Capital " + "allowances and the trading allowance are not deducted here, as " + "before (#1916)." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Social Security Contributions and Benefits Act 1992 s. 15", + href="https://www.legislation.gov.uk/ukpga/1992/4/section/15", + ), + dict( + title="Social Security Contributions and Benefits Act 1992 Sch. 2 para. 3", + href="https://www.legislation.gov.uk/ukpga/1992/4/schedule/2/paragraph/3", + ), + ] + + def formula(person, period, parameters): + return max_( + 0, + person("self_employment_income", period) + - person("loss_relief", period) + - person("trade_loss_relief_against_general_income", period) + - person("ni_class_4_losses_brought_forward", period), + ) diff --git a/policyengine_uk/variables/household/income/hbai_household_net_income.py b/policyengine_uk/variables/household/income/hbai_household_net_income.py index 5061ab23c1..6edadca113 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -59,6 +59,8 @@ "income_tax", "national_insurance", "student_loan_repayments", + # Self-employment losses count as negative income in HBAI. + "trading_loss", "employee_pension_contributions", "personal_pension_contributions", "maintenance_expenses", diff --git a/policyengine_uk/variables/household/income/household_market_income.py b/policyengine_uk/variables/household/income/household_market_income.py index 3991db6b7c..ec622f987f 100644 --- a/policyengine_uk/variables/household/income/household_market_income.py +++ b/policyengine_uk/variables/household/income/household_market_income.py @@ -26,6 +26,9 @@ class household_market_income(Variable): unit = GBP def formula(person, period, parameters): - total = add(person, period, HOUSEHOLD_MARKET_INCOME_VARIABLES) + # Trading losses are negative self-employment income. + total = add(person, period, HOUSEHOLD_MARKET_INCOME_VARIABLES) - add( + person, period, ["trading_loss"] + ) contrib = parameters(period).gov.contrib.policyengine.economy.gdp_per_capita return total * (contrib + 1) diff --git a/policyengine_uk/variables/household/income/market_income.py b/policyengine_uk/variables/household/income/market_income.py index 9df8de09cf..6ba591808e 100644 --- a/policyengine_uk/variables/household/income/market_income.py +++ b/policyengine_uk/variables/household/income/market_income.py @@ -24,4 +24,8 @@ def formula(person, period, parameters): "maintenance_income", ] income = add(person, period, INCOME_VARIABLES) - return income - person("maintenance_expenses", period) + return ( + income + - person("trading_loss", period) + - person("maintenance_expenses", period) + ) diff --git a/policyengine_uk/variables/input/self_employment_income.py b/policyengine_uk/variables/input/self_employment_income.py index 5a58e87ccd..005de5be5a 100644 --- a/policyengine_uk/variables/input/self_employment_income.py +++ b/policyengine_uk/variables/input/self_employment_income.py @@ -5,7 +5,7 @@ class self_employment_income(Variable): value_type = float entity = Person label = "self-employment income" - documentation = "Income from self-employment profits, including gig work. This should be net of self-employment expenses." + documentation = "Income from self-employment profits, including gig work. This should be net of self-employment expenses and is never negative: enter a trading loss in trading_loss." definition_period = YEAR unit = GBP reference = "Income Tax (Trading and Other Income) Act 2005 s. 1(1)(a)" diff --git a/policyengine_uk/variables/input/trading_loss.py b/policyengine_uk/variables/input/trading_loss.py new file mode 100644 index 0000000000..bc43e15fbb --- /dev/null +++ b/policyengine_uk/variables/input/trading_loss.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class trading_loss(Variable): + value_type = float + entity = Person + label = "trading loss" + documentation = ( + "Losses made in the tax year by the person's trades, professions or " + "vocations, as a positive amount. Profits of their other trades go in " + "self_employment_income, which is never negative. Each programme reads " + "the loss by its own rule: Income Tax deducts it from general income " + "(ITA 2007 s.64, within the s.24A cap); Class 4 NICs deduct it from " + "trading profits only (SSCBA 1992 Sch 2 para 3); tax credits deduct it " + "from the claimants' income (SI 2002/2006 reg 3(1) Step 4); means tests " + "never set it against employed earnings or other income: UC sets it " + "against the person's other trades' profits (UC Regs 2013 reg 57(2)), " + "and HB, CTR, Pension Credit and the legacy benefits not even that (HB " + "Regs 2006 reg 38(10)); HBAI household income counts it as negative " + "income. Losses brought forward from earlier years go in loss_relief." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + uprating = "gov.economic_assumptions.indices.obr.per_capita.mixed_income" + reference = dict( + title="Income Tax Act 2007 s. 64", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/64", + )