Skip to content
Closed
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
17 changes: 16 additions & 1 deletion policyengine_uk/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,19 @@
is_person=True,
)

entities = [Household, BenUnit, Person]
Firm = build_entity(
key="firm",
plural="firms",
label="Firm",
doc="A business entity that may be subject to VAT and other business taxes.",
roles=[
{
"key": "owner",
"plural": "owners",
"label": "Owner",
"doc": "A person who owns or has an interest in the firm.",
}
],
)

entities = [Household, BenUnit, Person, Firm]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: VAT deregistration threshold - firms can deregister if turnover falls below this
values:
2017-04-01: 83000
2024-04-01: 88000
metadata:
unit: currency-GBP
reference:
- title: HMRC VAT registration thresholds
href: https://www.gov.uk/vat-registration-thresholds
label: VAT deregistration threshold
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: VAT registration threshold - firms with taxable turnover above this must register for VAT
values:
2017-04-01: 85000
2024-04-01: 90000
metadata:
unit: currency-GBP
reference:
- title: HMRC VAT registration thresholds
href: https://www.gov.uk/vat-registration-thresholds
label: VAT registration threshold
9 changes: 9 additions & 0 deletions policyengine_uk/parameters/gov/hmrc/vat/zero_rate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
description: Zero-rated VAT (0% rate) applies to essential items like most food, children's clothes, books
values:
1973-04-01: 0.0
metadata:
unit: /1
reference:
- title: HMRC VAT rates
href: https://www.gov.uk/vat-rates
label: VAT zero rate
9 changes: 5 additions & 4 deletions policyengine_uk/tax_benefit_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from policyengine_core.variables import Variable

# PolicyEngine UK imports
from policyengine_uk.entities import BenUnit, Household, Person
from policyengine_uk.entities import BenUnit, Household, Person, Firm
from policyengine_uk.parameters.gov.contrib.create_private_pension_uprating import (
add_private_pension_uprating_factor,
)
Expand Down Expand Up @@ -108,16 +108,17 @@ def __init__(self):
self.variables = {}

# Create copies of entity classes to avoid modifying originals
person, benunit, household = (
person, benunit, household, firm = (
copy.copy(Person),
copy.copy(BenUnit),
copy.copy(Household),
copy.copy(Firm),
)

# Set up entities
self.entities = [person, benunit, household]
self.entities = [person, benunit, household, firm]
self.person_entity = person
self.group_entities = [benunit, household]
self.group_entities = [benunit, household, firm]
self.group_entity_keys = [entity.key for entity in self.group_entities]

# Link entities to this tax-benefit system
Expand Down
66 changes: 66 additions & 0 deletions policyengine_uk/tests/entities/test_firm_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import pytest
from policyengine_uk import CountryTaxBenefitSystem


def test_firm_entity_exists():
"""Test that the Firm entity is defined in the tax-benefit system."""
system = CountryTaxBenefitSystem()

# Find Firm entity in the list
firm_entity = None
for entity in system.entities:
if entity.key == "firm":
firm_entity = entity
break

assert firm_entity is not None, "Firm entity not found"
assert firm_entity.key == "firm"
assert firm_entity.plural == "firms"
assert firm_entity.label == "Firm"


def test_firm_entity_has_owner_role():
"""Test that the Firm entity has an owner role linking to Person."""
system = CountryTaxBenefitSystem()

# Find Firm entity
firm_entity = None
for entity in system.entities:
if entity.key == "firm":
firm_entity = entity
break

assert firm_entity is not None, "Firm entity not found"

# Check that the entity has roles
assert len(firm_entity.roles) > 0

# Find the owner role
owner_role = None
for role in firm_entity.roles:
if role.key == "owner":
owner_role = role
break

assert owner_role is not None
assert owner_role.plural == "owners"
assert owner_role.label == "Owner"


def test_firm_entity_documentation():
"""Test that the Firm entity has proper documentation."""
system = CountryTaxBenefitSystem()

# Find Firm entity
firm_entity = None
for entity in system.entities:
if entity.key == "firm":
firm_entity = entity
break

assert firm_entity is not None, "Firm entity not found"
assert firm_entity.doc is not None
assert (
"business" in firm_entity.doc.lower()
or "firm" in firm_entity.doc.lower()
)
113 changes: 113 additions & 0 deletions policyengine_uk/tests/policy/firm_vat/test_firm_vat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import pytest
import numpy as np
from policyengine_uk import CountryTaxBenefitSystem


def test_firm_turnover_variable():
"""Test that firm_turnover variable exists and works correctly."""
system = CountryTaxBenefitSystem()

# Check variable exists
assert "firm_turnover" in system.variables

variable = system.variables["firm_turnover"]
assert variable.value_type == float
assert variable.entity.key == "firm"
assert variable.label == "Firm turnover"


def test_firm_vat_registered_variable():
"""Test that firm_vat_registered variable exists and calculates correctly."""
system = CountryTaxBenefitSystem()

# Check variable exists
assert "firm_vat_registered" in system.variables

variable = system.variables["firm_vat_registered"]
assert variable.value_type == bool
assert variable.entity.key == "firm"


def test_vat_registration_threshold():
"""Test VAT registration threshold of £90,000."""
from policyengine_uk.simulation import Simulation

# Test firm below threshold
sim_below = Simulation(
situation={
"firms": {"test_firm": {"firm_turnover": {"2024": 85_000}}},
"people": {"p": {}},
"benunits": {"b": {"members": ["p"]}},
"households": {"h": {"members": ["p"]}},
}
)
vat_registered = sim_below.calculate("firm_vat_registered", "2024")
assert vat_registered[0] == False

# Test firm above threshold
sim_above = Simulation(
situation={
"firms": {"test_firm": {"firm_turnover": {"2024": 95_000}}},
"people": {"p": {}},
"benunits": {"b": {"members": ["p"]}},
"households": {"h": {"members": ["p"]}},
}
)
vat_registered = sim_above.calculate("firm_vat_registered", "2024")
assert vat_registered[0] == True


def test_firm_vat_on_sales():
"""Test VAT calculation on firm sales."""
from policyengine_uk.simulation import Simulation

simulation = Simulation(
situation={
"firms": {
"firm_1": {
"firm_turnover": {"2024": 100_000},
"firm_standard_rated_supplies": {"2024": 80_000},
"firm_reduced_rated_supplies": {"2024": 10_000},
"firm_zero_rated_supplies": {"2024": 10_000},
}
},
"people": {"p": {}},
"benunits": {"b": {"members": ["p"]}},
"households": {"h": {"members": ["p"]}},
}
)

vat_on_sales = simulation.calculate("firm_vat_on_sales", "2024")
# Standard rate 20% on £80,000 = £16,000
# Reduced rate 5% on £10,000 = £500
# Zero rate 0% on £10,000 = £0
# Total = £16,500
expected = 16_500
assert np.isclose(vat_on_sales[0], expected, rtol=0.01)


def test_firm_net_vat_liability():
"""Test net VAT liability calculation (output VAT - input VAT)."""
from policyengine_uk.simulation import Simulation

simulation = Simulation(
situation={
"firms": {
"firm_1": {
"firm_turnover": {"2024": 100_000},
"firm_standard_rated_supplies": {"2024": 100_000},
"firm_vat_on_purchases": {"2024": 5_000},
}
},
"people": {"p": {}},
"benunits": {"b": {"members": ["p"]}},
"households": {"h": {"members": ["p"]}},
}
)

net_vat = simulation.calculate("firm_net_vat_liability", "2024")
# Output VAT: 20% of £100,000 = £20,000
# Input VAT: £5,000
# Net liability: £20,000 - £5,000 = £15,000
expected = 15_000
assert np.isclose(net_vat[0], expected, rtol=0.01)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from policyengine_uk.model_api import *


class firm_net_vat_liability(Variable):
value_type = float
entity = Firm
label = "Net VAT liability"
definition_period = YEAR
unit = GBP
documentation = "Net VAT liability (output VAT minus input VAT)"

def formula(firm, period, parameters):
vat_registered = firm("firm_vat_registered", period)
output_vat = firm("firm_vat_on_sales", period)
input_vat = firm("firm_vat_on_purchases", period)

net_vat = output_vat - input_vat

# Only registered firms pay/reclaim VAT
return net_vat * vat_registered
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from policyengine_uk.model_api import *


class firm_reduced_rated_supplies(Variable):
value_type = float
entity = Firm
label = "Reduced-rated supplies"
definition_period = YEAR
unit = GBP
documentation = "Value of firm's supplies subject to reduced VAT rate"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from policyengine_uk.model_api import *


class firm_standard_rated_supplies(Variable):
value_type = float
entity = Firm
label = "Standard-rated supplies"
definition_period = YEAR
unit = GBP
documentation = "Value of firm's supplies subject to standard VAT rate"
10 changes: 10 additions & 0 deletions policyengine_uk/variables/gov/hmrc/vat/firm/firm_turnover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from policyengine_uk.model_api import *


class firm_turnover(Variable):
value_type = float
entity = Firm
label = "Firm turnover"
definition_period = YEAR
unit = GBP
documentation = "Annual turnover of the firm from all business activities"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from policyengine_uk.model_api import *


class firm_vat_on_purchases(Variable):
value_type = float
entity = Firm
label = "VAT on purchases"
definition_period = YEAR
unit = GBP
documentation = "Total VAT paid on firm's purchases (input VAT)"
30 changes: 30 additions & 0 deletions policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_sales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from policyengine_uk.model_api import *


class firm_vat_on_sales(Variable):
value_type = float
entity = Firm
label = "VAT on sales"
definition_period = YEAR
unit = GBP
documentation = "Total VAT charged on firm's sales (output VAT)"

def formula(firm, period, parameters):
vat_registered = firm("firm_vat_registered", period)

if not vat_registered.any():
return firm.empty_array()

vat_params = parameters(period).gov.hmrc.vat

standard_supplies = firm("firm_standard_rated_supplies", period)
reduced_supplies = firm("firm_reduced_rated_supplies", period)
zero_supplies = firm("firm_zero_rated_supplies", period)

standard_vat = standard_supplies * vat_params.standard_rate
reduced_vat = reduced_supplies * vat_params.reduced_rate
zero_vat = zero_supplies * vat_params.zero_rate

total_vat = standard_vat + reduced_vat + zero_vat

return total_vat * vat_registered
14 changes: 14 additions & 0 deletions policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_registered.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from policyengine_uk.model_api import *


class firm_vat_registered(Variable):
value_type = bool
entity = Firm
label = "Firm is VAT registered"
definition_period = YEAR
documentation = "Whether the firm is registered for VAT"

def formula(firm, period, parameters):
turnover = firm("firm_turnover", period)
threshold = parameters(period).gov.hmrc.vat.registration_threshold
return turnover > threshold
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from policyengine_uk.model_api import *


class firm_zero_rated_supplies(Variable):
value_type = float
entity = Firm
label = "Zero-rated supplies"
definition_period = YEAR
unit = GBP
documentation = "Value of firm's supplies subject to zero VAT rate"
Loading