dodil/erp-brick-ar
DODIL ERP brick: accounts receivable
tests/test_v4b.py
"""Spec v4, slice B: AR-42 .. AR-47 (and the slice-A leftovers). Given/When/Then from the signed stories; one test per rule a story states.
Business 'today' is 2026-11-20."""
import json
import xml.etree.ElementTree as ET
from decimal import Decimal
from conftest import (
APPROVER,
BILLER,
ROLES,
SALES,
SYSTEM,
account_map,
api,
approved_credit,
billing,
control,
hand_draft,
issue,
issued,
line,
must,
on_day,
person,
tool,
tools,
)
from sqlalchemy.exc import IntegrityError
from brick_ar.application import use_cases as uc
D = Decimal
GLN = "0088:5060012349998"
def setting(key, value):
return must(api("POST", "/settings", APPROVER, {"key": key, "value": value}))
def peppol_world(world, provider="peppol", **profile):
"""Customer 7311 on Peppol, our own details set, the access point composed."""
for k, v in (("supplier_name", "Dodil Ltd"), ("supplier_address", "1 Mill Rd, London, GB"), ("supplier_country", "GB"),
("einvoice_sender_id", "0088:5060000000001"), ("einvoice_provider", provider)):
setting(k, v)
return billing(einvoice_channel="peppol", peppol_participant_id=GLN, bill_to_address="2 High St, Leeds, GB", **profile)
def posted(world, doc_id):
must(api("POST", f"/documents/{doc_id}/post", SYSTEM))
def journals_of(world, doc_id):
return [j for j in world.gl.journals.values() if j["source_document"] == doc_id]
# ── AR-42: a recurring invoice with no order ──────────────────────────────────────────────────────
def desk(**kw):
body = {"bp_id": 7311, "currency": "GBP", "description": "Desk rental", "unit_price": "450.00", "starts_on": "2026-12-01", "periods": 12,
"tax_category": "standard", **kw}
return api("POST", "/schedules/manual", BILLER, body)
def test_ar_42_a_manual_schedule_is_keyed_man_uuid_and_runs_on_the_same_engine(world):
billing()
s = must(desk())
assert s["schedule_id"].startswith("man:") and s["schedule_id"].endswith("|1")
assert s["source_kind"] == "manual" and s["status"] == "active" and s["periods"] == 12 and s["next_billing_date"].startswith("2026-12-01")
with on_day("2026-12-01"):
raised = must(api("POST", "/schedules/raise-due", SYSTEM, {}))
assert raised["raised"] == [f"rec:{s['schedule_id']}:1"]
d = must(api("GET", f"/invoices/rec:{s['schedule_id']}:1", BILLER))
assert d["lifecycle"] == "draft" and d["lines"][0]["service_from"] == "2026-12-01" and d["lines"][0]["service_to"] == "2026-12-31"
def test_ar_42_the_draft_issues_itself_only_when_the_owner_names_manual(world):
billing()
must(control("auto_issue_sources", "manual"))
s = must(desk())
with on_day("2026-12-01"):
must(api("POST", "/schedules/raise-due", SYSTEM, {}))
assert must(api("GET", f"/invoices/rec:{s['schedule_id']}:1", BILLER))["lifecycle"] == "issued"
def test_ar_42_it_starts_today_or_later_never_earlier(world):
billing()
status, body = desk(starts_on="2026-11-19")
assert status == 409 and "starts_on" in body["detail"]
must(desk(starts_on="2026-11-20")) # today is fine
def test_ar_42_finished_after_period_12_and_stopped_with_a_reason_dated_today(world):
billing()
s = must(desk(periods=2))
with on_day("2027-03-01"):
assert len(must(api("POST", "/schedules/raise-due", SYSTEM, {}))["raised"]) == 2
assert must(api("POST", "/schedules/finish", SYSTEM, {}))["finished"] == [s["schedule_id"]]
assert must(api("POST", "/schedules/raise-due", SYSTEM, {}))["raised"] == []
t = must(desk())
stopped = must(api("POST", "/schedules/stop", SYSTEM, {"source_id": t["source_id"], "reason": "customer left"}))
assert stopped["stopped_on"] == "2026-11-20"
got = must(api("GET", f"/schedules/{t['schedule_id']}", BILLER))
assert got["status"] == "stopped" and got["stop_reason"] == "customer left"
def test_ar_42_the_billing_profile_is_a_gate_and_the_tool_exists(world):
status, body = desk()
assert status == 409 and "billing_profile" in body["detail"]
billing()
kind, got = tool(BILLER, "create_recurring_invoice", {"bp_id": 7311, "currency": "GBP", "description": "Desk", "unit_price": "450.00",
"starts_on": "2026-12-01", "periods": 12})
assert kind == "ok" and got["source_kind"] == "manual"
assert api("POST", "/schedules/manual", SALES, {"bp_id": 7311, "currency": "GBP", "description": "x", "unit_price": "1", "starts_on": "2026-12-01", "periods": 1})[0] == 403
# ── AR-46: deferred revenue ───────────────────────────────────────────────────────────────────────
def annual(world):
"""Issued 2026-10-01 for 2026-10-01 .. 2027-09-30 at 12,000.00 net, posted to the ledger."""
with on_day("2026-10-01"):
account_map()
billing()
d = hand_draft([line("Annual support", "1", "12000.00", service_from="2026-10-01", service_to="2027-09-30")])
inv = issue(d["invoice_id"], world=world)
posted(world, inv["invoice_id"])
return inv
def release(period, day, who=SYSTEM):
with on_day(day):
return api("POST", "/revenue/release", who, {"period_id": period})
def test_ar_46_issue_posts_the_service_period_to_deferred_revenue(world):
inv = annual(world)
(j,) = journals_of(world, inv["invoice_id"])
assert {"purpose": "deferred_revenue", "credit": "12000.00", "memo": j["lines"][1]["memo"]} == j["lines"][1]
assert not any(ln.get("account_id") == "4000" for ln in j["lines"]) # none went straight to revenue
def test_ar_46_october_releases_31_over_365_once_and_twice_adds_nothing(world):
inv = annual(world)
first = must(release("2026-10", "2026-11-01"))
assert [r["amount"] for r in first["released"]] == ["1019.18"] and first["posted"] == 1
again = must(release("2026-10", "2026-11-01"))
assert again["released"] == [] and again["already_released"] == 1 and again["posted"] == 0
(j,) = journals_of(world, f"rel:{inv['invoice_id']}:1:2026-10")
assert j["event"] == "approve" and j["journal_date"] == "2026-10-31"
assert j["lines"][0]["purpose"] == "deferred_revenue" and j["lines"][0]["debit"] == "1019.18"
assert j["lines"][1]["purpose"] == "revenue" and j["lines"][1]["credit"] == "1019.18"
def test_ar_46_the_last_period_takes_the_remainder_so_the_line_releases_exactly_its_net(world):
annual(world)
got = must(release("2027-09", "2027-10-01")) # a catch-up run releases every month through September
assert len(got["released"]) == 12 and sum(D(r["amount"]) for r in got["released"]) == D("12000.00")
assert uc.repository # (the monthly amounts are the days over 365: Feb 28/365 etc.)
assert [r["amount"] for r in got["released"]][:2] == ["1019.18", "986.30"]
def test_ar_46_a_credit_note_takes_back_what_is_not_yet_released_first(world):
inv = annual(world)
must(release("2026-10", "2026-11-01"))
with on_day("2026-11-20"):
c = approved_credit(world, inv["invoice_id"], lines=[{"description": "Support credit", "quantity": "1", "unit_price": "3000.00"}])
posted(world, c["credit_id"])
(j,) = journals_of(world, c["credit_id"])
assert {"purpose": "deferred_revenue", "debit": "3000.00", "memo": j["lines"][1]["memo"]} == j["lines"][1]
got = must(release("2027-09", "2027-10-01"))
assert sum(D(r["amount"]) for r in got["released"]) == D("12000.00") - D("3000.00") - D("1019.18")
def test_ar_46_lines_without_a_service_period_post_straight_to_revenue(world):
account_map()
i = issued(world)
posted(world, i["invoice_id"])
(j,) = journals_of(world, i["invoice_id"])
assert any(ln.get("account_id") == "4000" for ln in j["lines"]) and not any("purpose" in ln for ln in j["lines"])
def test_ar_46_a_period_that_has_not_ended_is_refused_and_a_person_may_not_release(world):
annual(world)
assert release("2026-11", "2026-11-20")[0] == 409
assert release("2026-10", "2026-11-01", BILLER)[0] == 403
assert release("2026-1x", "2026-11-01")[0] == 422
def test_ar_46_an_invoice_with_released_revenue_cannot_be_voided(world):
inv = annual(world)
must(release("2026-10", "2026-11-01"))
status, body = api("POST", f"/invoices/{inv['invoice_id']}/void", APPROVER, {"reason": "mistake"})
assert status == 409 and "no_released_revenue" in body["detail"]
def test_ar_46_the_switch_off_posts_straight_to_revenue(world):
account_map()
billing()
must(control("defer_service_revenue", False, valid_from="2026-12-01"))
with on_day("2026-12-02"):
d = hand_draft([line("Annual support", "1", "1200.00", service_from="2026-12-02", service_to="2027-12-01")])
inv = issue(d["invoice_id"], world=world)
posted(world, inv["invoice_id"])
assert not any("purpose" in ln for j in journals_of(world, inv["invoice_id"]) for ln in j["lines"])
# ── AR-47: cost centre on revenue ────────────────────────────────────────────────────────────────
def test_ar_47_revenue_lines_carry_their_cost_centre_and_receivable_and_vat_go_unsplit(world):
account_map()
billing()
d = hand_draft([line("Consulting", "1", "6000.00", cost_centre="CC-CONS"), line("Training", "1", "4000.00", cost_centre="CC-TRAIN")])
inv = issue(d["invoice_id"], world=world)
posted(world, inv["invoice_id"])
(j,) = journals_of(world, inv["invoice_id"])
by = {(ln["account_id"], ln.get("cost_centre")): ln for ln in j["lines"]}
assert by[("1100", None)]["debit"] == "12000.00" and by[("2200", None)]["credit"] == "2000.00"
assert by[("4000", "CC-CONS")]["credit"] == "6000.00" and by[("4000", "CC-TRAIN")]["credit"] == "4000.00"
def test_ar_47_a_line_with_none_takes_the_customers_default_else_the_gate_where_the_ledger_requires_one(world):
account_map()
billing(default_cost_centre="CC-MAIN")
must(control("cost_centre_required", True))
d = hand_draft([line("Consulting", "1", "100.00")])
inv = issue(d["invoice_id"], world=world)
assert inv["lines"][0]["cost_centre"] == "CC-MAIN"
billing(8000)
d2 = hand_draft([line("Consulting", "1", "100.00")], bp=8000)
status, body = api("POST", f"/invoices/{d2['invoice_id']}/issue", BILLER, {})
assert status == 409 and "cost_centre" in body["detail"]
assert next(g for g in must(api("GET", f"/invoices/{d2['invoice_id']}/readiness", BILLER))["gates"] if g["gate"] == "cost_centre")["met"] is False
# ── AR-43: the Peppol e-invoice ──────────────────────────────────────────────────────────────────
def test_ar_43_a_valid_invoice_goes_as_ubl_bis3_with_the_message_id_recorded(world):
peppol_world(world)
i = issued(world)
got = must(api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {}))
(sent,) = world.peppol.sent
root = ET.fromstring(sent["xml"])
ns = {"cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
"cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"}
assert root.find("cbc:CustomizationID", ns).text == "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
assert root.tag.endswith("}Invoice") and root.find("cbc:ID", ns).text == i["invoice_no"]
assert root.find("cac:InvoiceLine/cbc:InvoicedQuantity", ns).get("unitCode") == "MON"
assert root.find("cac:AccountingCustomerParty/cac:Party/cbc:EndpointID", ns).get("schemeID") == "0088"
assert root.find("cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:ID", ns).text == "S"
assert sent["receiver_id"] == GLN and sent["document_type"] == "invoice"
assert got["send"]["message_id"] == "MSG-1" and got["send"]["format"] == "ubl" and "Invoice" in got["send"]["record"]
assert got["sends"][0]["channel"] == "peppol" and got["einvoice_message_id"] == "MSG-1"
def test_ar_43_a_failed_rule_refuses_the_issue_before_a_number_is_claimed_then_it_issues_and_delivers(world):
peppol_world(world) # SC-AR-12
d = hand_draft([{"description": "Hosting", "quantity": "2", "unit_price": "100.00", "tax_category": "standard"}])
status, body = api("POST", f"/invoices/{d['invoice_id']}/issue", BILLER, {})
assert status == 409 and "einvoice_valid" in body["detail"] and "BR-23" in body["detail"] and "line 1 unit code" in body["detail"]
assert must(api("GET", f"/invoices/{d['invoice_id']}", BILLER))["invoice_no"] == "" # nothing claimed
must(api("POST", f"/invoices/{d['invoice_id']}/edit", BILLER, {"lines": [line("Hosting", "2", "100.00")]}))
inv = issue(d["invoice_id"], world=world)
assert inv["invoice_no"].endswith("0001") # the first number: no gap was burned
must(api("POST", f"/invoices/{d['invoice_id']}/send", BILLER, {}))
must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "delivered"}))
must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "AP"}))
got = must(api("GET", f"/invoices/{d['invoice_id']}", BILLER))
assert [s["status"] for s in got["einvoice"]["statuses"]] == ["delivered", "AP"]
def test_ar_43_the_send_itself_refuses_a_document_that_fails_a_rule_and_sends_nothing(world):
peppol_world(world)
i = issued(world)
setting("supplier_name", "") # the seller name goes missing from tomorrow
with on_day("2026-11-21"):
status, body = api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {})
assert status == 409 and "BR-06" in body["detail"] and world.peppol.sent == []
def test_ar_43_no_provider_composed_is_a_named_gate_and_a_down_provider_is_a_502(world):
peppol_world(world, provider="")
i = issued(world)
status, body = api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {})
assert status == 409 and "einvoice_provider" in body["detail"]
setting("einvoice_provider", "peppol")
world.peppol.down = True
with on_day("2026-11-21"):
assert api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {})[0] == 502
return
world.peppol.down = True
assert api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {})[0] == 502
def test_ar_43_a_credit_note_is_a_ubl_creditnote_the_same_way(world):
peppol_world(world)
i = issued(world)
c = approved_credit(world, i["invoice_id"], full=True)
must(api("POST", f"/credit-notes/{c['credit_id']}/send", BILLER, {}))
root = ET.fromstring(world.peppol.sent[-1]["xml"])
assert root.tag.endswith("}CreditNote") and world.peppol.sent[-1]["document_type"] == "credit_note"
assert must(api("POST", f"/credit-notes/{c['credit_id']}/send", BILLER, {}))["already"] is True
def test_ar_43_a_peppol_customer_needs_a_participant_id_and_an_email_customer_is_unchanged(world):
status, body = api("POST", "/customers/7311/billing", APPROVER, {"bill_to_name": "A", "bill_to_address": "x", "invoice_email": "a@b.c",
"terms_id": "NET30", "currency": "GBP", "einvoice_channel": "peppol"})
assert status == 409 and "participant id" in body["detail"]
i = issued(world)
got = must(api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {}))
assert got["send"]["address"] == "ap@acme.example" and world.peppol.sent == []
# ── AR-44: the access point reports ──────────────────────────────────────────────────────────────
def sent_invoice(world):
peppol_world(world)
i = issued(world)
must(api("POST", f"/invoices/{i['invoice_id']}/send", BILLER, {}))
return i
def test_ar_44_delivered_then_a_rejection_with_its_reason_shows_on_the_invoice_which_is_unchanged(world):
i = sent_invoice(world)
must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "delivered", "at": "2026-11-20T08:00:00Z"}))
got = must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "RE", "reason": "PO number missing", "at": "2026-11-20T09:00:00+00:00"}))
assert [s["status"] for s in got["statuses"]] == ["delivered", "RE"] and got["rejected"] == "PO number missing"
inv = must(api("GET", f"/invoices/{i['invoice_id']}", BILLER))
assert inv["lifecycle"] == "issued" and inv["total"] == i["total"] and inv["invoice_no"] == i["invoice_no"]
assert inv["einvoice"]["rejection"] == "PO number missing"
assert json.loads(inv["sends"][0]["statuses_json"])[1] == {"status": "RE", "at": "2026-11-20T09:00:00", "reason": "PO number missing"}
def test_ar_44_a_duplicate_notification_adds_nothing(world):
sent_invoice(world)
a = must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "AB"}))
b = must(api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "AB"}))
assert a["statuses"] == b["statuses"] and len(b["statuses"]) == 1
def test_ar_44_an_unknown_message_a_bad_status_and_a_person_are_refused(world):
sent_invoice(world)
assert api("POST", "/einvoices/NOPE/status", SYSTEM, {"status": "AB"})[0] == 404
assert api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "WAT"})[0] == 422
assert api("POST", "/einvoices/MSG-1/status", BILLER, {"status": "AB"})[0] == 403
def test_ar_44_a_lost_status_claim_is_a_409_never_swallowed(world, monkeypatch):
sent_invoice(world)
real = uc.Repository if hasattr(uc, "Repository") else None
from brick_ar.infrastructure import tables
def boom(self, *a, **k):
raise IntegrityError("INSERT INTO ar_einvoice_status", {}, Exception("duplicate key value violates unique constraint (23505)"))
monkeypatch.setattr(tables.EinvoiceRepository, "add_status", boom)
assert api("POST", "/einvoices/MSG-1/status", SYSTEM, {"status": "AB"})[0] == 409
del real
# ── AR-45: the doubtful-debt provision ───────────────────────────────────────────────────────────
def aged(world):
"""Three issued invoices across the buckets: 1,200.00 gross (net 1,000.00) at 31-60, 2,400.00 (net 2,000.00) at 61-90, and 600.00
(net 500.00) in dispute at 90+. Returns their ids."""
account_map()
ids = []
for bp, price, bucket, disputed in ((7311, "1000.00", "31-60", False), (7312, "2000.00", "61-90", False), (7313, "500.00", "90+", True)):
billing(bp)
inv = issue(hand_draft([line("Work", "1", price)], bp=bp)["invoice_id"], world=world)
world.payments.aged[bucket].append({"invoice_id": inv["invoice_id"], "bp_id": bp, "outstanding": inv["total"], "days_past_due": 40,
"disputed": disputed})
ids.append(inv["invoice_id"])
for bucket, rate in (("0-30", "0"), ("31-60", "2"), ("61-90", "10"), ("90+", "50"), ("specific", "100")):
must(api("POST", "/provision-rates", APPROVER, {"bucket": bucket, "rate_pct": rate}))
return ids
def propose(period="2026-11", day="2026-12-01", who=BILLER):
with on_day(day):
return api("POST", "/provisions", who, {"period_id": period})
def approve(run_id, who=APPROVER, day="2026-12-01"):
with on_day(day):
return api("POST", f"/provisions/{run_id}/approve", who, {})
def test_ar_45_the_run_lists_each_bucket_and_each_disputed_invoice_net_of_vat(world):
ids = aged(world)
run = must(propose())
assert run["run_id"] == "prov::2026-11".replace("::", ":default:") and run["lifecycle"] == "proposed" and run["as_at"] == "2026-11-30"
by = {(ln["kind"], ln["bucket"]): ln for ln in run["lines"]}
assert by[("bucket", "31-60")]["outstanding_net"] == "1000.00" and by[("bucket", "31-60")]["provision"] == "20.00"
assert by[("bucket", "61-90")]["outstanding_net"] == "2000.00" and by[("bucket", "61-90")]["provision"] == "200.00"
assert by[("bucket", "90+")]["outstanding_net"] == "0.00" # the disputed one is its own line
(sp,) = [ln for ln in run["lines"] if ln["kind"] == "specific"]
assert sp["invoice_id"] == ids[2] and sp["outstanding_net"] == "500.00" and sp["rate_pct"] == "100.0000" and sp["provision"] == "500.00"
assert run["total"] == "720.00" and run["movement"] == "720.00" and run["previous_total"] == "0.00"
def test_ar_45_approval_by_another_person_posts_the_movement_once_dated_the_period_end(world):
aged(world)
run = must(propose())
done = must(approve(run["run_id"]))
assert done["lifecycle"] == "approved" and done["posting"]["posted"] is True
again = must(approve(run["run_id"]))
assert again["posting"]["posted"] is True
(j,) = [x for x in world.gl.journals.values() if x["source_document"] == run["run_id"]]
assert j["event"] == "approve" and j["journal_date"] == "2026-11-30" and j["currency"] == "GBP"
assert j["lines"][0]["purpose"] == "doubtful_debt_expense" and j["lines"][0]["debit"] == "720.00"
assert j["lines"][1]["purpose"] == "doubtful_debt_allowance" and j["lines"][1]["credit"] == "720.00"
assert world.gl.calls.count(j["journal_id"]) == 1 # the repeat never reached the ledger again
def test_ar_45_next_month_posts_only_the_movement_and_a_fall_reverses_the_sides(world):
ids = aged(world)
must(approve(must(propose())["run_id"]))
world.payments.aged_out.add(ids[1]) # the 2,400 invoice is paid in December
with on_day("2027-01-02"):
run = must(api("POST", "/provisions", BILLER, {"period_id": "2026-12"}))
assert run["previous_total"] == "720.00" and run["total"] == "520.00" and run["movement"] == "-200.00"
must(api("POST", f"/provisions/{run['run_id']}/approve", APPROVER, {}))
j = next(x for x in world.gl.journals.values() if x["source_document"] == run["run_id"])
assert j["lines"][0] == {"purpose": "doubtful_debt_expense", "memo": j["lines"][0]["memo"], "credit": "200.00"}
assert j["lines"][1]["debit"] == "200.00"
def test_ar_45_the_proposer_may_not_approve_by_email_or_by_sub_where_second_approver_is_on(world):
aged(world)
must(control("second_approver", True))
both = ROLES["biller"] + ROLES["approver"]
run = must(propose(who=person("pat@shop.example", both)))
status, body = approve(run["run_id"], person("pat@shop.example", both))
assert status == 409 and "proposer" in body["detail"]
other_email = person("someone-else@shop.example", both) # same sub, another email
other_email["X-Dodil-User"] = json.dumps({"sub": "pat@shop.example", "email": "someone-else@shop.example", "app_roles": []})
assert approve(run["run_id"], other_email)[0] == 409
assert must(approve(run["run_id"]))["lifecycle"] == "approved" # the approver persona is another person
def test_ar_45_the_month_must_have_ended_and_a_rejection_can_be_proposed_again(world):
aged(world)
assert propose(day="2026-11-30")[0] == 409
assert propose(period="nope")[0] == 422
run = must(propose())
assert api("POST", f"/provisions/{run['run_id']}/reject", APPROVER, {"reason": ""})[0] in (409, 422)
with on_day("2026-12-01"):
assert must(api("POST", f"/provisions/{run['run_id']}/reject", APPROVER, {"reason": "rates are stale"}))["lifecycle"] == "rejected"
assert must(propose())["lifecycle"] == "proposed"
def test_ar_45_a_closed_ledger_period_blocks_approval_and_a_refusing_ledger_leaves_it_to_repeat(world):
aged(world)
run = must(propose())
world.gl.closed_before = "2026-11-30"
status, body = approve(run["run_id"])
assert status == 409 and "period_open" in body["detail"]
world.gl.closed_before = None
world.install_down = world.remove("gl") # the ledger is away: approved, not posted
got = must(approve(run["run_id"]))
assert got["lifecycle"] == "approved" and got["posting"]["posted"] is False
world.install("gl")
assert must(approve(run["run_id"]))["posting"]["posted"] is True
def test_ar_45_rates_are_dated_rows_a_person_cannot_run_the_machine_and_the_tools_exist(world):
must(api("POST", "/provision-rates", APPROVER, {"bucket": "31-60", "rate_pct": "2"}))
assert api("POST", "/provision-rates", APPROVER, {"bucket": "31-60", "rate_pct": "3", "valid_from": "2026-11-20"})[0] == 409
must(api("POST", "/provision-rates", APPROVER, {"bucket": "31-60", "rate_pct": "3", "valid_from": "2026-12-01"}))
assert len(must(api("GET", "/provision-rates", BILLER))["rates"]) == 2
assert api("POST", "/provisions", SALES, {"period_id": "2026-10"})[0] == 403
names = tools(BILLER)
assert {"create_recurring_invoice", "propose_doubtful_provision", "approve_doubtful_provision", "release_deferred_revenue",
"record_einvoice_status", "send_credit_einvoice", "hand_over_credit_note"} <= set(names)
# ── slice A leftovers: AR-41 credits ─────────────────────────────────────────────────────────────
def ic_invoice(world):
account_map()
billing(intercompany_company_code="UK02")
must(control("default_company_code", "UK01"))
i = issued(world)
return i
def test_ar_41_a_credit_note_against_an_intercompany_invoice_is_handed_over_once_with_negative_amounts(world):
i = ic_invoice(world)
c = approved_credit(world, i["invoice_id"], full=True)
assert c["handed_to_partner"] is True
bill = world.pur.bills[f"ic:{c['credit_id']}"]
assert bill["company_code"] == "UK02" and bill["totals"]["gross"] == f"-{c['total']}" and bill["lines"][0]["net_amount"].startswith("-")
again = must(api("POST", f"/credit-notes/{c['credit_id']}/hand-over", SYSTEM, {}))
assert again["already"] is True and len(world.pur.bills) == 2 # the invoice's bill and the credit's
def test_ar_41_intercompany_open_lists_the_credit_as_a_negative_document(world):
i = ic_invoice(world)
c = approved_credit(world, i["invoice_id"], full=True)
got = must(api("GET", "/intercompany/open", SYSTEM, {"company_code": "UK01", "trading_partner": "UK02"}))
docs = {d.get("credit_id") or d["invoice_id"]: d for d in got["documents"]}
assert docs[c["credit_id"]]["total"] == f"-{c['total']}" and docs[i["invoice_id"]]["total"] == i["total"]