Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2326.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Stop relieving a miscellaneous loss against other income in Income Tax, adjusted net income, total income and Universal Credit earnings: ITA 2007 s. 152(4) allows it only against miscellaneous income.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# A miscellaneous loss is relieved only against miscellaneous income
# (ITA 2007 s. 152(4)). 2026-27: personal allowance £12,570, basic rate 20%
# to £50,270, higher rate 40%.

- name: Case 1, miscellaneous income is taxed with other income.
period: 2026
absolute_error_margin: 0.01
input:
age: 45
employment_income: 30_000
miscellaneous_income: 5_000
output:
taxable_miscellaneous_income: 5_000
# (35,000 - 12,570) x 20%.
income_tax: 4_486

- name: Case 2, a miscellaneous loss does not reduce tax on pay.
period: 2026
absolute_error_margin: 0.01
input:
age: 45
employment_income: 50_000
miscellaneous_income: -10_000
output:
taxable_miscellaneous_income: 0
# (50,000 - 12,570) x 20%, as with no loss.
income_tax: 7_486

- name: Case 3, nor adjusted net income.
period: 2026
absolute_error_margin: 0.01
input:
age: 45
employment_income: 110_000
miscellaneous_income: -20_000
output:
taxable_miscellaneous_income: 0
adjusted_net_income: 110_000
# Personal allowance 12,570 - (110,000 - 100,000) / 2 = 7,570; taxable
# 102,430: 37,700 x 20% + 64,730 x 40% = 33,432.
income_tax: 33_432

- name: Case 4, total income ignores a miscellaneous loss.
period: 2026
absolute_error_margin: 0.01
input:
age: 70
private_pension_income: 40_000
miscellaneous_income: -10_000
output:
total_income: 40_000

- name: Case 5, a miscellaneous loss does not reduce Universal Credit earnings.
period: 2026
absolute_error_margin: 0.01
input:
age: 30
employment_income: 30_000
miscellaneous_income: -10_000
output:
# 30,000 less income tax (30,000 - 12,570) x 20% = 3,486 and primary
# Class 1 (30,000 - 12,570) x 8% = 1,394.40.
uc_individual_earned_income_before_mif: 30_000 - 3_486 - 1_394.40
72 changes: 72 additions & 0 deletions policyengine_uk/tests/test_miscellaneous_loss_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""A miscellaneous loss is relieved only against miscellaneous income
(ITA 2007 s. 152(4)).

Invariants:

1. For any pay, savings, dividends and pension income, a miscellaneous loss
leaves Income Tax, adjusted net income and total income exactly as they are
with no miscellaneous income.
2. For any pay, positive miscellaneous income is taxable in full.
"""

from hypothesis import HealthCheck, example, given, settings
from hypothesis import strategies as st

from policyengine_uk import Simulation


def _calc(misc, pay, savings, dividends, pension):
situation = {
"people": {
"p": {
"age": {"2026": 50},
"employment_income": {"2026": pay},
"savings_interest_income": {"2026": savings},
"dividend_income": {"2026": dividends},
"private_pension_income": {"2026": pension},
"miscellaneous_income": {"2026": misc},
}
},
"benunits": {"b": {"members": ["p"]}},
"households": {"h": {"members": ["p"], "country": {"2026": "ENGLAND"}}},
}
sim = Simulation(situation=situation)
return {
name: float(sim.calculate(name, 2026)[0])
for name in (
"income_tax",
"adjusted_net_income",
"total_income",
"taxable_miscellaneous_income",
)
}


amounts = st.floats(min_value=0, max_value=150_000, allow_nan=False)


@settings(max_examples=25, deadline=None, suppress_health_check=[HealthCheck.too_slow])
@example(loss=20_000, pay=110_000, savings=0, dividends=0, pension=0)
@given(
loss=st.floats(min_value=1, max_value=500_000, allow_nan=False),
pay=amounts,
savings=st.floats(min_value=0, max_value=20_000),
dividends=st.floats(min_value=0, max_value=20_000),
pension=st.floats(min_value=0, max_value=40_000),
)
def test_a_miscellaneous_loss_changes_nothing_else(
loss, pay, savings, dividends, pension
):
with_loss = _calc(-loss, pay, savings, dividends, pension)
without = _calc(0, pay, savings, dividends, pension)
assert with_loss["taxable_miscellaneous_income"] == 0
assert abs(with_loss["income_tax"] - without["income_tax"]) < 0.01
assert abs(with_loss["adjusted_net_income"] - without["adjusted_net_income"]) < 0.01
assert abs(with_loss["total_income"] - without["total_income"]) < 0.01


@settings(max_examples=6, deadline=None, suppress_health_check=[HealthCheck.too_slow])
@given(income=st.floats(min_value=0, max_value=100_000, allow_nan=False), pay=amounts)
def test_positive_miscellaneous_income_is_taxable_in_full(income, pay):
result = _calc(income, pay, 0, 0, 0)
assert abs(result["taxable_miscellaneous_income"] - income) < 0.01
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ def formula(person, period, parameters):
# The deductions are the person's own and come off only their own
# earnings, so one partner's tax, NI or pension contributions never
# reduce the other partner's earned income.
earnings_components = ["employment_income", "miscellaneous_income"]
# Miscellaneous income as income tax counts it, so a loss does not
# reduce pay (and matches uc_income_tax_on_earnings). Whether it is
# earnings at all is issue #1961.
earnings_components = ["employment_income", "taxable_miscellaneous_income"]
bi = parameters(period).gov.contrib.ubi_center.basic_income
if bi.interactions.include_in_means_tests:
earnings_components.append("basic_income")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,31 @@ class taxable_miscellaneous_income(Variable):
value_type = float
entity = Person
label = "Amount of miscellaneous income that is taxable"
definition_period = YEAR
reference = dict(
title="Income Tax Act (Trading and Other Income) 2005, s. 574",
href="https://www.legislation.gov.uk/ukpga/2005/5/section/574",
documentation = (
"Miscellaneous income charged to income tax. On a claim, a loss on a "
"miscellaneous transaction is relieved only against relevant "
"miscellaneous income: income under the same section 1016 provision as "
"the loss (ITA 2007 s. 152(4)-(5)). It is never set against other "
"income, so a net loss counts as nil here. The model holds one net "
"figure per person, so it assumes the items netted inside "
"miscellaneous_income can be relieved against each other; that can "
"undertax a person with a gain under one provision and a loss under "
"another. The input also covers income from sources outside section "
"1016, some of which carry no loss relief at all. Carrying a loss "
"forward against later years' miscellaneous income is not modelled."
)
definition_period = YEAR
reference = [
dict(
title="Income Tax (Trading and Other Income) Act 2005 s. 574",
href="https://www.legislation.gov.uk/ukpga/2005/5/section/574",
),
dict(
title="Income Tax Act 2007 s. 152",
href="https://www.legislation.gov.uk/ukpga/2007/3/section/152",
),
]
unit = GBP

adds = ["miscellaneous_income"]
def formula(person, period, parameters):
return max_(0, person("miscellaneous_income", period))
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ class total_income(Variable):
"property_income",
"savings_interest_income",
"dividend_income",
"miscellaneous_income",
# A miscellaneous loss is never part of total income (ITA 2007 s. 152(4)).
"taxable_miscellaneous_income",
]
Loading