"""Budget commitment layer — open-PO forward exposure folded into budget-vs-actual (committed / remaining / commitment-aware forecast). Budget/actual/run-rate forecast were already shipped; this pins the NEW commitment leg: open_commitments derivation + its integration into budget_vs_actual. """ from __future__ import annotations from decimal import Decimal from uuid import uuid4 import pytest from app.core.coordinators import finance_governance_coordinator as gov from app.domains.finance import budget_service from app.domains.finance.models import FinanceBudget # ─── _commit_matches (pure) ───────────────────────────────────────── def test_commit_matches_scope(): b = FinanceBudget(fiscal_year=2026, amount_usd=Decimal("1000"), department="研发", cost_center=None) assert budget_service._commit_matches({"department": "研发", "cost_center": "X"}, b) assert not budget_service._commit_matches({"department": "销售", "cost_center": None}, b) # #1667: a budget with NO dept/cost_center scope (company-level or # cost_type-only) has no PO axis to match on — matching every open PO # would inflate its committed / forecast. It carries no commitments. b_all = FinanceBudget(fiscal_year=2026, amount_usd=Decimal("1"), department=None, cost_center=None) assert not budget_service._commit_matches({"department": "任意", "cost_center": None}, b_all) # A cost_center-scoped budget still matches on cost_center alone. b_cc = FinanceBudget(fiscal_year=2026, amount_usd=Decimal("1"), department=None, cost_center="CC-1") assert budget_service._commit_matches({"department": "x", "cost_center": "CC-1"}, b_cc) assert not budget_service._commit_matches({"department": "x", "cost_center": "CC-2"}, b_cc) # ─── open_commitments (DB, USD POs to avoid FX/clock coupling) ────── def _person(): from app.identity.person_models import IdentityPerson return IdentityPerson( source_system="test", hris_person_id=f"P{uuid4().hex[:6]}", employee_no=f"E{uuid4().hex[:6]}", work_email=f"{uuid4().hex[:8]}@test.local", display_name="PO Applicant", employment_status="active", ) def _po(applicant_id, *, status, payment_status, amount, dept="研发"): from app.domains.procurement.models import ProcurementOrder return ProcurementOrder( order_number=f"PO-{uuid4().hex[:8]}", order_type="purchase", status=status, currency="USD", total_amount=Decimal(str(amount)), total_cny=None, department=dept, cost_center="CC-1", applicant_id=applicant_id, payment_status=payment_status, receipt_status="pending", ) @pytest.mark.asyncio async def test_open_commitments_includes_only_firm_unpaid(db_session): db = db_session p = _person() db.add(p) await db.flush() db.add(_po(p.id, status="approved", payment_status="pending", amount=1000)) # received = already recognised as actual spend → NOT a forward commitment db.add(_po(p.id, status="received", payment_status="partial", amount=500)) db.add(_po(p.id, status="completed", payment_status="paid", amount=9999)) # actual → excluded db.add(_po(p.id, status="draft", payment_status="pending", amount=8888)) # not firm → excluded db.add(_po(p.id, status="approved", payment_status="paid", amount=7777)) # paid → excluded await db.flush() res = await gov.open_commitments(db) # Only the approved-and-unpaid PO is a forward commitment (received is # already actual spend in the ledger — excluded to avoid double-count). assert res["count"] == 1 assert res["total_usd"] == "1000.00" # ─── budget_vs_actual commitment integration (monkeypatched) ──────── @pytest.mark.asyncio async def test_budget_vs_actual_folds_commitments(db_session, monkeypatch): db = db_session db.add(FinanceBudget(fiscal_year=2026, amount_usd=Decimal("10000.00"), department="研发", cost_center=None, owner_person_id=None)) await db.flush() async def _fake_commitments(_db): return {"commitments": [ {"department": "研发", "cost_center": None, "amount_usd": "2000.00"}, {"department": "销售", "cost_center": None, "amount_usd": "5000.00"}, # other scope ], "total_usd": "7000.00", "count": 2} monkeypatch.setattr(gov, "open_commitments", _fake_commitments) res = await budget_service.budget_vs_actual( db, 2026, as_of_period="2026-05", start_month=4, ) item = next(i for i in res["items"] if i["department"] == "研发") assert item["committed_usd"] == "2000.00" # No actuals → remaining = 10000 - 0 - 2000. assert item["remaining_usd"] == "8000.00" # Commitment-aware forecast floors at ytd(0)+committed(2000). assert item["forecast_year_end_usd"] == "2000.00" assert res["total_committed_usd"] == "2000.00" # only the matching one assert res["total_remaining_usd"] == "8000.00" @pytest.mark.asyncio async def test_company_level_budget_folds_no_commitments(db_session, monkeypatch): """#1667: a company-level budget (no dept, no cost_center) must NOT pull in every open PO as committed — that inflated committed/forecast and mis-fired status='over'.""" db = db_session db.add(FinanceBudget(fiscal_year=2026, amount_usd=Decimal("10000.00"), department=None, cost_center=None, owner_person_id=None)) await db.flush() async def _fake_commitments(_db): return {"commitments": [ {"department": "研发", "cost_center": "CC-1", "amount_usd": "3000.00"}, {"department": "销售", "cost_center": "CC-2", "amount_usd": "5000.00"}, ], "total_usd": "8000.00", "count": 2} monkeypatch.setattr(gov, "open_commitments", _fake_commitments) res = await budget_service.budget_vs_actual( db, 2026, as_of_period="2026-05", start_month=4, ) item = res["items"][0] assert item["committed_usd"] == "0.00" # no PO axis → no commitments assert item["remaining_usd"] == "10000.00" # full headroom, not eaten by POs assert item["status"] != "over"