dodil/erp-brick-fixed-assets
DODIL ERP brick: fixed assets
tests/test_revaluation_transfers.py
"""FA-18 (transfer between companies), FA-19 (revaluation), FA-20 (impairment and its reversal)."""
from decimal import Decimal as D
import pytest
from conftest import ACCOUNTANT, APPROVER, acquire, api, create_asset, must, on_day, setup_policy
SEP = "2026-09-30T09:00:00+00:00"
SHA = "a" * 64
@pytest.fixture
def world(gl):
setup_policy(classes=("VEH", "BLDG"), start="2023-01-01")
return gl
def run(period, when):
with on_day(when):
r = must(api("POST", "/depreciation-runs", ACCOUNTANT, {"company_code": "UK01", "period_id": period}))
return must(api("POST", f"/depreciation-runs/{r['run_id']}/approve", APPROVER))
def values(asset_id, area="01"):
return next(v for v in must(api("GET", f"/assets/{asset_id}", ACCOUNTANT))["values"] if v["area_code"] == area)
def building(world):
with on_day("2026-09-01T09:00:00+00:00"):
b = create_asset("bldg", cls="BLDG", description="Warehouse")
must(acquire(b["asset_id"], "500000.00", value_date="2026-04-01", ref="pur:BLDG-1:1"))
run("2026-09", SEP) # April..September: 6 months of 8,333.33 = 50,000.00
return b
def approve(tx, who=APPROVER):
return api("POST", f"/asset-transactions/{tx}/approve", who)
def propose(asset_id, kind, **body):
return api("POST", f"/assets/{asset_id}/{kind}", ACCOUNTANT, body)
def reval(asset_id, amount, when="2026-09-30", sha=SHA):
return propose(asset_id, "revaluations", area_code="30", revalued_amount=amount, valuation_date=when, evidence_sha256=sha)
def journals(world, event):
return [j for j in world.journals.values() if j["event"] == event]
# ── FA-19 ───────────────────────────────────────────────────────────────────────────────────
def test_a_revaluation_credits_the_reserve_eliminates_depreciation_and_leaves_the_cost_model_area_alone(world):
b = building(world)
with on_day(SEP):
t = must(reval(b["asset_id"], "600000.00"))
assert t["evidence_key"] == f"raw/fixed-assets/valuations/{SHA}.pdf" and t["status"] == "proposed"
assert [a["area_code"] for a in t["areas"]] == ["30"] # one area only
done = must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
(j,) = journals(world, "revalue")
assert j["ledger_id"] == "20" and j["journal_date"] == "2026-09-30"
credit = {ln["account_id"]: D(ln["credit"]) for ln in j["lines"] if "credit" in ln}
debit = {ln["account_id"]: D(ln["debit"]) for ln in j["lines"] if "debit" in ln}
assert credit == {"320": D("150000.00")} # the reserve (other comprehensive income)
assert debit == {"150": D("100000.00"), "151": D("50000.00")} # cost up, accumulated eliminated (IAS 16.35b)
v30, v01 = values(b["asset_id"], "30"), values(b["asset_id"], "01")
assert (v30["cost"], v30["accumulated_depreciation"], v30["net_book_value"], v30["revaluation_reserve"]) == (
"600000.00", "0.00", "600000.00", "150000.00")
assert (v01["cost"], v01["accumulated_depreciation"], v01["revaluation_reserve"]) == ("500000.00", "50000.00", "0.00")
assert done["status"] == "posted"
# future depreciation is on 600,000.00 over the remaining 54 periods = 11,111.11 a month in area 30; area 01 keeps 8,333.33
with on_day("2026-10-31T09:00:00+00:00"):
r = must(api("POST", "/depreciation-runs", ACCOUNTANT, {"company_code": "UK01", "period_id": "2026-10"}))
assert r["totals"] == {"01": "8333.33", "30": "11111.11"}
def test_a_decrease_goes_against_the_assets_own_reserve_first_then_to_profit_or_loss(world):
b = building(world)
with on_day(SEP):
must(api("POST", f"/asset-transactions/{must(reval(b['asset_id'], '600000.00'))['transaction_id']}/approve", APPROVER))
run("2026-10", "2026-10-31T09:00:00+00:00") # NBV 600,000.00 - 11,111.11 = 588,888.89
with on_day("2026-10-31T09:00:00+00:00"):
t = must(reval(b["asset_id"], "400000.00", when="2026-10-31")) # 188,888.89 down: 150,000 reserve, the rest loss
must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
j = journals(world, "revalue")[-1]
debit = {ln["account_id"]: D(ln["debit"]) for ln in j["lines"] if "debit" in ln}
assert debit.get("320") == D("150000.00") and debit.get("772") == D("38888.89"), j["lines"]
v = values(b["asset_id"], "30")
assert v["revaluation_reserve"] == "0.00" and v["net_book_value"] == "400000.00"
def test_a_revaluation_needs_its_report_a_valuation_model_area_and_a_second_person(world):
b = building(world)
with on_day(SEP):
s, body = propose(b["asset_id"], "revaluations", area_code="30", revalued_amount="600000.00", valuation_date="2026-09-30")
assert s == 422 # the valuer's report is required
s, body = propose(b["asset_id"], "revaluations", area_code="30", revalued_amount="600000.00", valuation_date="2026-09-30",
evidence_sha256="zz")
assert s == 422
s, body = propose(b["asset_id"], "revaluations", area_code="01", revalued_amount="600000.00", valuation_date="2026-09-30",
evidence_sha256=SHA)
assert s == 409, body # area 01 is the cost model
s, body = propose(b["asset_id"], "revaluations", area_code="30", revalued_amount="600000.00", valuation_date="2026-10-01",
evidence_sha256=SHA)
assert s == 409, body # the valuation date is a fact: not in the future
t = must(reval(b["asset_id"], "600000.00"))
assert api("POST", f"/asset-transactions/{t['transaction_id']}/approve", ACCOUNTANT)[0] in (403, 409)
# ── FA-20 ───────────────────────────────────────────────────────────────────────────────────
def line(world):
with on_day("2026-11-10T09:00:00+00:00"):
pl = create_asset("line", cls="VEH", description="Production line")
must(acquire(pl["asset_id"], "300000.00", value_date="2026-11-10", ref="pur:LINE-1:1"))
return pl
def test_an_impairment_lowers_the_carrying_amount_and_the_reversal_is_capped(world):
pl = line(world)
run("2026-11", "2026-11-30T09:00:00+00:00") # one month: NBV 295,000.00
with on_day("2026-11-30T09:00:00+00:00"):
t = must(propose(pl["asset_id"], "impairments", amount="115000.00", review_date="2026-11-30", evidence_sha256=SHA,
area_codes=["01"]))
assert t["evidence_key"].endswith(f"{SHA}.pdf")
must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
(j,) = journals(world, "impair")
assert j["ledger_id"] == "10" and {ln["account_id"]: D(ln["debit"]) for ln in j["lines"] if "debit" in ln} == {
"772": D("115000.00")} # charged to impairment loss
v = values(pl["asset_id"])
assert (v["cost"], v["net_book_value"]) == ("300000.00", "180000.00")
assert values(pl["asset_id"], "30")["net_book_value"] == "295000.00" # only the named area
s, b = propose(pl["asset_id"], "impairments", amount="180000.01", review_date="2026-11-30", evidence_sha256=SHA,
area_codes=["01"])
assert s == 409 and "more than the net book value" in str(b), b # never below nothing
# a year on, recovery: capped at what the carrying amount would have been without the impairment (IAS 36.117)
run("2027-10", "2027-10-31T09:00:00+00:00") # 11 more months of the lower figure
carrying = D(values(pl["asset_id"])["net_book_value"])
without = D("300000.00") - D("5000.00") * 12 # Nov 2026 .. Oct 2027, as if never impaired
cap = without - carrying
with on_day("2027-10-31T09:00:00+00:00"):
s, body = propose(pl["asset_id"], "impairments", amount=str(cap + D("0.01")), review_date="2027-10-31",
evidence_sha256=SHA, area_codes=["01"], reversal=True)
assert s == 409 and "at most" in str(body), body
rev = must(propose(pl["asset_id"], "impairments", amount=str(cap), review_date="2027-10-31", evidence_sha256=SHA,
area_codes=["01"], reversal=True))
must(api("POST", f"/asset-transactions/{rev['transaction_id']}/approve", APPROVER))
assert values(pl["asset_id"])["net_book_value"] == str(without) # back to, never above, the unimpaired figure
back = journals(world, "impair")[-1]
assert {ln["account_id"]: D(ln["credit"]) for ln in back["lines"] if "credit" in ln} == {"492": cap} # shown separately
# ── FA-18 ───────────────────────────────────────────────────────────────────────────────────
@pytest.fixture
def group(world):
"""UK02 posts to ITS ledgers (11, 21) through its own areas U2-01 / U2-30; the tax area CA serves both companies."""
from conftest import ACC, OWNER
with on_day("2023-01-01T09:00:00+00:00"):
must(api("POST", "/settings/areas", OWNER, {"area_code": "U2-01", "name": "Book UK02", "purpose": "book", "ledger_id": "11",
"company_code": "UK02", "currency": "GBP"}))
must(api("POST", "/settings/areas", OWNER, {"area_code": "U2-30", "name": "IFRS UK02", "purpose": "ifrs",
"ledger_id": "21", "company_code": "UK02", "currency": "GBP",
"value_model": "revaluation"}))
must(api("POST", "/settings/classes", OWNER, {"class_code": "VEH2", "name": "Vehicles UK02", "default_ca_pool": "main",
"areas": [{"area_code": "U2-01", "key_code": "LIN5"},
{"area_code": "U2-30", "key_code": "LIN5"},
{"area_code": "CA", "key_code": "LIN5"}]}))
for code in ("U2-01", "U2-30"):
must(api("POST", "/settings/account-determination", OWNER, {"class_code": "VEH2", "area_code": code,
**{**ACC, "cost_account_id": 250}}))
return world
# The spec moves the forklift "on 1 November" with 9,600.00 accumulated: the figures are those after October's run, so the move
# is entered on 31 October (the last day it was UK01's); depreciation must be posted up to the transfer's period (the gate).
def forklift(world):
with on_day("2026-10-01T09:00:00+00:00"):
f = create_asset("fork", cls="VEH", description="Forklift", cost_centre="WH-1")
must(acquire(f["asset_id"], "24000.00", value_date="2024-11-01", ref="pur:FORK-1:1"))
run("2026-10", "2026-10-31T09:00:00+00:00") # Nov 2024 .. Oct 2026 = 24 months of 400.00 = 9,600.00
return f
def test_a_transfer_retires_in_one_company_and_acquires_in_the_other_each_to_its_own_ledger(group):
f = forklift(group)
assert values(f["asset_id"])["accumulated_depreciation"] == "9600.00"
with on_day("2026-10-31T09:00:00+00:00"):
t = must(api("POST", f"/assets/{f['asset_id']}/transfers", ACCOUNTANT,
{"transfer_date": "2026-10-31", "to_company_code": "UK02", "to_class_code": "VEH2", "variant": "gross"}))
assert t["status"] == "proposed" and not any(j["event"] == "transfer" for j in group.journals.values())
done = must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
assert done["status"] == "posted"
out = {j["ledger_id"]: j for j in journals(group, "transfer") if j["company_code"] == "UK01"}
into = {j["ledger_id"]: j for j in journals(group, "transfer") if j["company_code"] == "UK02"}
assert sorted(out) == ["10", "20"] and sorted(into) == ["11", "21"] # each company to ITS ledgers
lines = {(ln["account_id"], "debit" if "debit" in ln else "credit"): D(ln.get("debit") or ln["credit"])
for ln in out["10"]["lines"]}
assert lines == {("150", "credit"): D("24000.00"), ("151", "debit"): D("9600.00"), ("2900", "debit"): D("14400.00")}
lines = {(ln["account_id"], "debit" if "debit" in ln else "credit"): D(ln.get("debit") or ln["credit"])
for ln in into["11"]["lines"]}
assert lines == {("250", "debit"): D("24000.00"), ("151", "credit"): D("9600.00"), ("2900", "credit"): D("14400.00")}
assert group.balance(2900) == 0 # the clearing nets to zero across the group
new = next(x for x in must(api("GET", "/assets", ACCOUNTANT))["assets"] if x["company_code"] == "UK02")
nv = values(new["asset_id"], "U2-01")
assert (nv["cost"], nv["accumulated_depreciation"], nv["net_book_value"]) == ("24000.00", "9600.00", "14400.00")
assert values(f["asset_id"])["cost"] == "0.00"
got = must(api("GET", f"/assets/{new['asset_id']}", ACCOUNTANT))
assert {a["area_code"]: a["life_periods"] for a in got["areas"]} == {"U2-01": 60, "U2-30": 60, "CA": 60}
# depreciation continues in UK02 on what is left: 14,400.00 over the remaining 36 periods = 400.00 a month
with on_day("2026-11-30T09:00:00+00:00"):
r = must(api("POST", "/depreciation-runs", ACCOUNTANT, {"company_code": "UK02", "period_id": "2026-11"}))
assert r["totals"] == {"U2-01": "400.00", "U2-30": "400.00"}
def test_the_net_variant_carries_the_net_book_value_as_cost_and_a_closed_period_in_either_company_stops_it(group):
f = forklift(group)
with on_day("2026-10-31T09:00:00+00:00"):
t = must(api("POST", f"/assets/{f['asset_id']}/transfers", ACCOUNTANT,
{"transfer_date": "2026-10-31", "to_company_code": "UK02", "to_class_code": "VEH2", "variant": "net"}))
group.closed.add(("11", "2026-10")) # UK02's ledger closed for the period
s, b = api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER)
assert s == 409 and "period_open" in str(b), b
assert not journals(group, "transfer") # nothing half posted
group.closed.clear()
must(api("POST", f"/asset-transactions/{t['transaction_id']}/approve", APPROVER))
new = next(x for x in must(api("GET", "/assets", ACCOUNTANT))["assets"] if x["company_code"] == "UK02")
nv = values(new["asset_id"], "U2-01")
assert (nv["cost"], nv["accumulated_depreciation"]) == ("14400.00", "0.00")
def test_a_move_inside_one_company_is_not_a_transfer(group):
f = forklift(group)
other = create_asset("fork2", cls="VEH", description="Second forklift")
with on_day("2026-10-31T09:00:00+00:00"):
s, b = api("POST", f"/assets/{f['asset_id']}/transfers", ACCOUNTANT,
{"transfer_date": "2026-10-31", "to_asset_id": other["asset_id"]})
assert s == 409 and "master change" in str(b)
def test_a_company_is_gated_only_on_its_own_ledgers(group):
"""UK02's ledger 11 being closed must not stop UK01's depreciation: an area names one ledger of one company."""
group.closed.add(("11", "2026-10"))
f = forklift(group) # posts UK01's October run
assert values(f["asset_id"])["accumulated_depreciation"] == "9600.00"