dodil/erp-brick-fixed-assets
DODIL ERP brick: fixed assets
tests/test_settings.py
"""FA-01 and FA-02: the controls on the people who value assets — areas and the ledger each posts to, keys, classes, account
determination — are DATED rows set by the owner alone, never edited, prospective."""
from decimal import Decimal
from conftest import ACC, ACCOUNTANT, APPROVER, OWNER, api, create_asset, must, on_day, person, setup_policy
def test_the_owner_records_a_class_from_tomorrow_and_an_asset_created_then_takes_its_defaults(gl):
setup_policy()
must(api("POST", "/settings/keys", OWNER, {"key_code": "LIN3", "method": "straight_line", "life_periods": 36,
"valid_from": "2026-11-21"}))
got = must(api("POST", "/settings/classes", OWNER, {
"class_code": "IT", "name": "IT equipment", "valid_from": "2026-11-21", "default_ca_pool": "main",
"areas": [{"area_code": "01", "key_code": "LIN3"}, {"area_code": "30", "key_code": "LIN4"},
{"area_code": "CA", "key_code": "LIN3"}]}))
assert got["valid_from"] == "2026-11-21" and len(got["areas"]) == 3
for area in ("01", "30"):
r = must(api("POST", "/settings/account-determination", OWNER, {
"class_code": "IT", "area_code": area, "valid_from": "2026-11-21", **{**ACC, "cost_account_id": "160",
"accumulated_depreciation_account_id": "161",
"depreciation_expense_account_id": "7710",
"gain_account_id": "4910",
"loss_account_id": "7910",
"revaluation_reserve_account_id": "3200",
"impairment_account_id": "7720"}}))
assert r["cost_account_id"] == "160" and r["valid_from"] == "2026-11-21"
status, body = api("POST", "/assets", ACCOUNTANT, {"request_id": "r1", "company_code": "UK01", "class_code": "IT",
"description": "Laptop"})
assert status == 409 and "class_in_force" in body["detail"] # not in force until tomorrow
with on_day("2026-11-21T09:00:00+00:00"):
made = create_asset("r2", cls="IT", description="Laptop")
keys = {a["area_code"]: (a["key_code"], a["life_periods"]) for a in made["areas"]}
assert keys == {"01": ("LIN3", 36), "30": ("LIN4", 48), "CA": ("LIN3", 36)}
def test_the_accountant_who_tries_a_control_is_refused_and_so_is_whoever_a_control_governs():
body = {"area_code": "01", "name": "Book", "purpose": "book"}
assert api("POST", "/settings/areas", ACCOUNTANT, body) == (403, {"detail": "missing permission: fa:settings:controls"})
assert api("POST", "/settings/areas", APPROVER, body)[0] == 403 # settings:write is day-to-day only
# fa:settings:write cannot change a control, whatever the key
s, b = api("POST", "/settings/day-to-day", APPROVER, {"setting_key": "capitalisation_threshold", "value": "1"})
assert s == 403 and "control" in b["detail"]
# a person who holds the controls AND a role a control governs is a configuration error, refused at configuration
both = person("both@uk.example", ["fa:settings:controls", "fa:asset:write"])
s, b = api("POST", "/settings/controls", both, {"setting_key": "capitalisation_threshold", "value": "250.00"})
assert s == 403 and "never held by whoever it controls" in b["detail"]
approver_owner = person("x@uk.example", ["fa:settings:controls", "fa:depreciation:approve"])
assert api("POST", "/settings/keys", approver_owner, {"key_code": "K", "method": "straight_line"})[0] == 403
tax_owner = person("t@uk.example", ["fa:settings:controls", "fa:capital_allowance:write"])
assert api("POST", "/settings/ca-rates", tax_owner, {"rate_key": "aia_limit", "value": "1"})[0] == 403
def test_an_area_naming_a_ledger_gl_does_not_have_is_refused_naming_it_and_the_tax_area_takes_none(gl):
s, b = api("POST", "/settings/areas", OWNER, {"area_code": "01", "name": "Book", "purpose": "book", "ledger_id": "99",
"company_code": "UK01"})
assert s == 409 and "no ledger 99" in b["detail"]
s, b = api("POST", "/settings/areas", OWNER, {"area_code": "CA", "name": "Tax", "purpose": "tax", "ledger_id": "10",
"company_code": "UK01"})
assert s == 409 and "tax area never posts" in b["detail"]
s, b = api("POST", "/settings/areas", OWNER, {"area_code": "CA", "name": "Tax", "purpose": "tax"})
assert s == 200 and b["posting"] == "none" and b["ledger_id"] is None
must(api("POST", "/settings/areas", OWNER, {"area_code": "01", "name": "Book", "purpose": "book", "ledger_id": "10",
"company_code": "UK01"}))
s, b = api("POST", "/settings/areas", OWNER, {"area_code": "02", "name": "Other", "purpose": "book", "ledger_id": "10",
"company_code": "UK01"})
assert s == 409 and "already posts to ledger 10" in b["detail"] # one area per ledger
s, b = api("POST", "/settings/areas", OWNER, {"area_code": "03", "name": "No gl", "purpose": "book", "ledger_id": "10"})
assert s == 409 and "company_code" in b["detail"]
def test_without_gl_an_area_with_a_ledger_cannot_be_checked():
s, b = api("POST", "/settings/areas", OWNER, {"area_code": "01", "name": "Book", "purpose": "book", "ledger_id": "10",
"company_code": "UK01"})
assert s == 409 and "gl is not installed" in b["detail"]
def test_a_dated_row_is_never_edited_and_a_later_one_starts_after_today(gl):
setup_policy()
again = {"area_code": "01", "name": "Book", "purpose": "book", "ledger_id": "10", "company_code": "UK01",
"currency": "GBP", "valid_from": "2026-01-01"}
assert must(api("POST", "/settings/areas", OWNER, again))["created"] is False # the same row again: a no-op
s, b = api("POST", "/settings/areas", OWNER, {**again, "name": "Renamed"})
assert s == 409 and "never edited" in b["detail"]
s, b = api("POST", "/settings/areas", OWNER, {**again, "name": "Renamed", "valid_from": "2026-11-20"})
assert s == 409 and "after today" in b["detail"] # a row dated today is refused
s, b = api("POST", "/settings/areas", OWNER, {**again, "name": "Renamed", "valid_from": "2026-11-21"})
assert s == 200 and b["created"] is True
def test_a_key_is_a_dated_row_with_its_method_rate_and_conventions_and_a_rate_outside_0_100_is_a_422(gl):
got = must(api("POST", "/settings/keys", OWNER, {
"key_code": "DB25", "method": "declining_then_straight", "rate_pct": "25", "life_periods": 60,
"acquisition_convention": "full_month", "retirement_convention": "next_month", "stop_at": "scrap",
"base": "net_book_value"}))
assert (got["method"], got["ret_conv"], got["stop_at"]) == ("declining_then_straight", "next_month", "scrap")
assert Decimal(got["rate_pct"]) == 25
s, b = api("POST", "/settings/keys", OWNER, {"key_code": "BAD", "method": "declining_balance", "rate_pct": "101"})
assert s == 422 and "rate_pct" in str(b)
s, b = api("POST", "/settings/keys", OWNER, {"key_code": "BAD", "method": "straight_line", "life_periods": 0})
assert s == 422 and "life_periods" in str(b)
s, b = api("POST", "/settings/keys", OWNER, {"key_code": "BAD", "method": "declining_balance"})
assert s == 409 and "rate_pct" in b["detail"]
s, b = api("POST", "/settings/keys", OWNER, {"key_code": "BAD", "method": "mystery"})
assert s == 409 and "method is one of" in b["detail"]
# superseded by a new dated row, prospectively; the old one is untouched
s, b = api("POST", "/settings/keys", OWNER, {"key_code": "DB25", "method": "declining_then_straight", "rate_pct": "20",
"valid_from": "2026-12-01"})
assert s == 200 and Decimal(b["rate_pct"]) == 20
shown = must(api("GET", "/settings", OWNER, {"on": "2026-11-25"}))
assert shown["keys"][0]["rate_pct"] == "25.0000"
assert must(api("GET", "/settings", OWNER, {"on": "2026-12-02"}))["keys"][0]["rate_pct"] == "20.0000"
def test_the_controls_section_is_shown_to_the_owner_only(gl):
setup_policy()
assert "areas" in must(api("GET", "/settings", OWNER))
seen = must(api("GET", "/settings", APPROVER))
assert "areas" not in seen and "keys" not in seen and list(seen["settings"]) == ["ai.ai_suggest_asset_class.mode"]
def test_controls_are_validated_and_warned(gl):
s, b = api("POST", "/settings/controls", OWNER, {"setting_key": "asset_number_format", "value": "no-sequence"})
assert s == 409
s, b = api("POST", "/settings/controls", OWNER, {"setting_key": "transfer_variant_default", "value": "sideways"})
assert s == 409
s, b = api("POST", "/settings/controls", OWNER, {"setting_key": "depreciation_needs_approval", "value": "false"})
assert s == 200 and "no longer needs a second person" in b["warning"]
s, b = api("POST", "/settings/controls", OWNER, {"setting_key": "capitalisation_threshold", "value": "-5"})
assert s == 409
assert api("POST", "/settings/controls", OWNER, {"setting_key": "nonsense", "value": "x"})[0] == 409
def test_an_ai_step_mode_is_a_day_to_day_setting(gl):
s, b = api("POST", "/settings/day-to-day", APPROVER, {"setting_key": "ai.ai_suggest_asset_class.mode", "value": "off"})
assert s == 200 and b["value"] == "off"
assert api("POST", "/settings/day-to-day", APPROVER, {"setting_key": "ai.ai_suggest_asset_class.mode",
"value": "auto", "valid_from": "2026-12-01"})[0] == 409