dodil/erp-brick-fixed-assets
DODIL ERP brick: fixed assets
tests/test_allowances.py
"""FA-21 (capital allowances per pool for a chargeable period) and FA-22 (the controller approves, the computation is filed and frozen)."""
import pytest
from conftest import (
ACCOUNTANT,
APPROVER,
OWNER,
ROLES,
TAX,
acquire,
api,
create_asset,
must,
on_day,
person,
setup_policy,
)
SHA = "b" * 64
P = "/ca-computations/UK01/2026-04-01/2027-03-31"
@pytest.fixture
def world(gl):
setup_policy(classes=("VEH", "PLANT"), start="2023-01-01")
with on_day("2023-01-01T09:00:00+00:00"):
for k, v in (("aia_limit", "1000000"), ("wda_main_pct", "18"), ("wda_special_pct", "6"), ("small_pool_limit", "1000"),
("sba_pct", "3"), ("fya_full_expensing_pct", "100"), ("fya_50_pct", "50")):
must(api("POST", "/settings/ca-rates", OWNER, {"rate_key": k, "value": v, "legislation_ref": "CAA 2001"}))
return gl
def register(world):
"""plant 120,000.00 (main) and a car 30,000.00 (special rate) bought in the period; an old plant of 15,000.00 sold for 8,000.00."""
with on_day("2024-01-02T09:00:00+00:00"):
old = create_asset("old", cls="PLANT", description="Old press")
must(acquire(old["asset_id"], "15000.00", value_date="2024-01-01", ref="pur:OLD-1:1"))
with on_day("2026-07-02T09:00:00+00:00"):
plant = create_asset("plant", cls="PLANT", description="New line")
must(acquire(plant["asset_id"], "120000.00", value_date="2026-06-15", ref="pur:PL-1:1"))
car = create_asset("car", cls="VEH", description="Company car", is_car=True, ca_pool="special_rate")
must(acquire(car["asset_id"], "30000.00", value_date="2026-07-01", ref="pur:CAR-1:1"))
with on_day("2026-09-30T09:00:00+00:00"):
r = must(api("POST", "/depreciation-runs", ACCOUNTANT, {"company_code": "UK01", "period_id": "2026-09"}))
must(api("POST", f"/depreciation-runs/{r['run_id']}/approve", APPROVER))
with on_day("2026-09-15T09:00:00+00:00"):
pass
return old, plant, car
def retire_old(old, when="2026-09-30"):
with on_day(when + "T10:00:00+00:00"):
t = must(api("POST", f"/assets/{old['asset_id']}/retirements", ACCOUNTANT,
{"retirement_date": when, "percent": "100", "proceeds": "8000.00"}))
must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
def draft(opening=True, start="2026-04-01", end="2027-03-31", who=TAX):
body = {"company_code": "UK01", "period_start": start, "period_end": end}
if opening:
body["opening_pools"] = [{"pool": "main", "balance": "50000.00"}, {"pool": "special_rate", "balance": "10000.00"}]
return must(api("POST", "/ca-computations", who, body))
def claim(path, asset, kind, who=TAX, amount=None):
return api("POST", path + "/claims", who, {"claims": [{"asset_id": asset["asset_id"], "claim": kind,
**({"amount": amount} if amount else {})}]})
def prepared(world):
old, plant, car = register(world)
retire_old(old)
draft()
must(claim(P, plant, "aia"))
must(claim(P, car, "pool"))
return old, plant, car, must(api("POST", P + "/prepare", TAX))
# ── FA-21 ───────────────────────────────────────────────────────────────────────────────────
def test_the_computation_is_worked_out_from_the_register_per_pool(world):
_, _, _, c = prepared(world)
assert c["totals"]["aia"] == "120000.00" and c["totals"]["total_allowances"] == "129960.00"
main, special = c["pools"]["main"], c["pools"]["special_rate"]
assert (main["opening"], main["disposals"], main["wda"], main["closing"]) == ("50000.00", "8000.00", "7560.00", "34440.00")
assert (special["opening"], special["additions"], special["wda"], special["closing"]) == (
"10000.00", "30000.00", "2400.00", "37600.00")
assert c["disposals"][0]["disposal_value"] == "8000.00" # the lower of proceeds (8,000.00) and cost (15,000.00)
assert c["status"] == "prepared"
def test_a_car_cannot_take_aia_and_every_addition_needs_a_claim(world):
old, plant, car = register(world)
draft()
s, b = claim(P, car, "aia")
assert s == 409 and "cannot take the annual investment allowance" in str(b) # names the rule
must(claim(P, plant, "aia"))
s, b = api("POST", P + "/prepare", TAX)
assert s == 409 and "claims_complete" in str(b), b # the car has no claim yet
def test_aia_cannot_exceed_the_limit_pro_rated_for_a_short_period(world):
old, plant, car = register(world)
short = "/ca-computations/UK01/2026-04-01/2026-09-30" # 6 months: limit 500,000.00
big = create_asset_big(world)
must(api("POST", "/ca-computations", TAX, {"company_code": "UK01", "period_start": "2026-04-01", "period_end": "2026-09-30",
"opening_pools": [{"pool": "main", "balance": "0.00"}]}))
must(claim(short, plant, "aia"))
must(claim(short, car, "pool"))
must(claim(short, big, "aia"))
s, b = api("POST", short + "/prepare", TAX)
assert s == 409 and "aia_within_limit" in str(b), b # 120,000 + 450,000 > 500,000
def create_asset_big(world):
with on_day("2026-08-02T09:00:00+00:00"):
big = create_asset("big", cls="PLANT", description="Plant hall")
must(acquire(big["asset_id"], "450000.00", value_date="2026-08-01", ref="pur:BIG-1:1"))
return big
def test_a_small_pool_may_be_written_off(world):
old, plant, car = register(world)
retire_old(old)
must(api("POST", "/ca-computations", TAX, {"company_code": "UK01", "period_start": "2026-04-01", "period_end": "2027-03-31",
"opening_pools": [{"pool": "main", "balance": "1500.00"}]}))
must(claim(P, plant, "aia"))
must(claim(P, car, "pool"))
c = must(api("POST", P + "/prepare", TAX))
# 1,500 - 8,000 disposal: the pool goes negative -> a balancing charge, never a negative closing pool
assert c["pools"]["main"]["closing"] == "0.00" and c["pools"]["main"]["balancing_charge"] == "6500.00"
# ── FA-22 ───────────────────────────────────────────────────────────────────────────────────
DUAL = ROLES["tax_preparer"] + ROLES["finance_approver"]
@pytest.mark.parametrize("who", [person("tax_preparer@uk.example", DUAL, sub="other-login"),
person("another.person@uk.example", DUAL, sub="tax_preparer@uk.example")])
def test_the_preparer_cannot_approve_their_own_under_any_identity(world, who):
prepared(world)
s, b = api("POST", P + "/approve", who)
assert s == 409 and "second" in str(b).lower(), b
assert api("POST", P + "/approve", ACCOUNTANT)[0] == 403 # the accountant has no approval right
assert api("POST", P + "/approve", TAX)[0] == 403
c = must(api("POST", P + "/approve", APPROVER))
assert c["status"] == "approved" and c["approved_by_email"] == "finance_approver@uk.example"
def test_filing_freezes_it_a_later_transaction_lands_in_the_next_period_as_a_named_adjustment(world):
old, plant, car, _ = prepared(world)
must(api("POST", P + "/approve", APPROVER))
f = must(api("POST", P + "/file", APPROVER, {"return_reference": "CT600-2027"}))
assert f["status"] == "filed" and f["return_reference"] == "CT600-2027"
assert api("POST", P + "/return", APPROVER, {"reason": "second thoughts"})[0] == 409 # a filed one never moves
assert api("POST", P + "/claims", TAX, {"claims": [{"asset_id": plant["asset_id"], "claim": "pool"}]})[0] == 409
with on_day("2026-11-20T10:00:00+00:00"):
t = must(api("POST", f"/assets/{plant['asset_id']}/retirements", ACCOUNTANT,
{"retirement_date": "2026-11-19", "percent": "100", "proceeds": "90000.00"}))
must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
again = must(api("GET", P, TAX))
assert again["totals"] == f["totals"] and again["pools"] == f["pools"] # it did not move
nxt = must(api("POST", "/ca-computations", TAX, {"company_code": "UK01", "period_start": "2027-04-01",
"period_end": "2028-03-31"}))
assert nxt["opening"] == {"main": "34440.00", "special_rate": "37600.00"} # closing pools are the next opening
assert nxt["status"] == "draft"
assert [a["transaction_id"] for a in nxt["adjustments"]] == [t["transaction_id"]] # named, with its own period
assert nxt["adjustments"][0]["from_period"] == "2026-04-01 to 2027-03-31"
assert nxt["disposals"] == [{"asset_id": plant["asset_id"], "proceeds": "90000.00", "cost": "120000.00"}]