diff --git a/backend/tests/test_ai_suggestions.py b/backend/tests/test_ai_suggestions.py index ac8db3e8..31334421 100644 --- a/backend/tests/test_ai_suggestions.py +++ b/backend/tests/test_ai_suggestions.py @@ -231,9 +231,12 @@ class TestRuleSuggestions: create_test_user(db) token = get_token_for_user(client) kpi = create_test_kpi(db, target_value=100.0) - db.add(KPIValue(kpi_id=kpi.id, period="2026-08", actual_value=200.0)) - db.add(BudgetPlan(entity_id=1, kpi_id=kpi.id, period="2026-08", budget_value=100.0, - budget_year=2026, budget_month=8, status="active")) + # 规则引擎按「当月」取预算(ai_analysis.py: _generate_rule_suggestions 的 period) + # 原用例写死 2026-08(编写时的当月),跨月后与实现错配 → 恒 0 条(2026-09 跨月实证) + period = datetime.now().strftime("%Y-%m") + db.add(KPIValue(kpi_id=kpi.id, period=period, actual_value=200.0)) + db.add(BudgetPlan(entity_id=1, kpi_id=kpi.id, period=period, budget_value=100.0, + budget_year=int(period[:4]), budget_month=int(period[5:7]), status="active")) db.commit() resp = client.get("/api/cma/ai/dashboard-analysis", headers=auth_header(token)) diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index 55e27569..61ae818d 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -3,6 +3,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi +from datetime import datetime from app.models.budget_plan import BudgetPlan from app.models import KPIValue, SystemConfig, CashPlan @@ -341,14 +342,21 @@ class TestBudgetRollForward: data = resp.json() assert "滚动预算已延展" in data["message"] assert len(data["rolled_kpis"]) == 1 - # 新增月份 = 当前月(08) + 12 = 明年08 - assert data["rolled_kpis"][0]["added_period"].startswith("2027-") + # 新增月份 = 当前月 + rolling_months(12)(实现口径见 budget.py:budget_roll_forward) + # 用例原写死 2026-08+12=2027-08,跨月后与实现错配(2026-09 跨月实证) + now = datetime.now() + exp_year, exp_month = now.year, now.month + 12 + while exp_month > 12: + exp_month -= 12 + exp_year += 1 + expected_period = f"{exp_year}-{exp_month:02d}" + assert data["rolled_kpis"][0]["added_period"] == expected_period # 最早月(2026-01)被删除 from app.models import BudgetPlan as BP periods = [p.period for p in db.query(BP).filter(BP.kpi_id == kpi.id).all()] assert "2026-01" not in periods - assert "2027-08" in periods + assert expected_period in periods class TestBudgetComparison: diff --git a/backend/tests/test_budget_tech_improve.py b/backend/tests/test_budget_tech_improve.py index 8719e0ed..0a6d8d2d 100644 --- a/backend/tests/test_budget_tech_improve.py +++ b/backend/tests/test_budget_tech_improve.py @@ -11,17 +11,36 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session +import datetime from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi from app.models import ( BudgetPlan, KPIValue, BudgetDeviationAlert, KPIAlert, - KPIValueSource, KPIValueCollectLog, + KPIValueSource, KPIValueCollectLog, VoucherDetail, BudgetZeroBasedItem, BudgetDerivationRule, CashPlanClassifyRule, CashPlanUnclassified, CashPlan, ScenarioSuggestion, KPIDefinition, ) +def _seed_vouchers(db, period="2026-06", entity_id=1, credit=1500.0): + """给自动归集造源数据(voucher_details 贷方发生额)。 + + 采集器口径(d71c7fe「自动归集空汇总不写0」):源表无记录 → 返回 None 并记 skipped, + 不落 0 值 KPIValue。故本类用例必须先造源数据,否则 collected 恒为 0。 + """ + y, m = int(period[:4]), int(period[5:7]) + db.add(VoucherDetail( + entity_id=entity_id, voucher_no="COLLECT-001", + voucher_date=datetime.datetime(y, m, 15), + subject_code="6001", subject_name="主营业务收入", + debit_amount=0.0, credit_amount=credit, + summary="自动归集单测", carry_forward=0, period=period, + )) + db.flush() + + + class TestP1AlertAttribution: """P1-③ 告警归因: 告警从'差多少'到'差在哪+怎么办'""" @@ -114,6 +133,7 @@ class TestP1ValueCollect: create_test_user(db) token = get_token_for_user(client) kpi = create_test_kpi(db, kpi_code="COLLECT_KPI") + _seed_vouchers(db) # 源数据(无源数据采集器按设计跳过) # 建映射 resp = client.post(f"{self.BASE}/value-sources", headers=auth_header(token), json={ @@ -135,9 +155,11 @@ class TestP1ValueCollect: "aggregate": "sum", "filter_rule": {"direction": "credit"}, "period_field": "period", + "period": "2026-06", # 显式期间(不传则默认当月,与造数期间错配 → value=None) }) assert test_resp.status_code == 200 assert test_resp.json()["value"] is not None + assert test_resp.json()["value"] == 1500.0 # 贷方发生额合计(unit_conversion=1) # 手动触发采集 run_resp = client.post(f"{self.BASE}/value-collect/run", headers=auth_header(token), @@ -169,6 +191,7 @@ class TestP1ValueCollect: create_test_user(db) token = get_token_for_user(client) kpi = create_test_kpi(db, kpi_code="COLLECT_IDEMP") + _seed_vouchers(db) client.post(f"{self.BASE}/value-sources", headers=auth_header(token), json={ "kpi_id": kpi.id, "source_table": "voucher_details", "source_field": "credit_amount", "aggregate": "sum", @@ -188,6 +211,7 @@ class TestP1ValueCollect: create_test_user(db) token = get_token_for_user(client) kpi = create_test_kpi(db, kpi_code="COLLECT_LOG") + _seed_vouchers(db) client.post(f"{self.BASE}/value-sources", headers=auth_header(token), json={ "kpi_id": kpi.id, "source_table": "voucher_details", "source_field": "credit_amount", "aggregate": "sum", diff --git a/backend/tests/test_cash.py b/backend/tests/test_cash.py index 6daaae5b..3fdc1820 100644 --- a/backend/tests/test_cash.py +++ b/backend/tests/test_cash.py @@ -234,7 +234,9 @@ class TestVoucherImport: buf.seek(0) return buf - def _upload(self, client, token, buf, fname="test_vouchers.xlsx"): + def _upload(self, client, token, buf, fname="vouchers_import.xlsx"): + """注意: 文件名不得含 test/示例/sample/demo 等测试标识——导入端点有生产防线 + (d71c7fe「Excel导入示例文件防线」),含标识直接 400,与三条校验规则的用例无关。""" return client.post( f"{BASE}/import/vouchers", headers=auth_header(token), files={"file": (fname, buf, @@ -315,3 +317,4 @@ class TestVoucherImport: buf = self._xlsx([["2026-08-01", "记-001", 100, 0]], cols=["日期", "凭证号", "借方金额", "贷方金额"]) resp = self._upload(client, token, buf) assert resp.status_code == 400 + assert "缺少必要列" in resp.json()["detail"] # 断言是列校验拦下,而非文件名防线 diff --git a/backend/tests/test_roadmap_r2r5.py b/backend/tests/test_roadmap_r2r5.py index 8b7b48a1..26798b7a 100644 --- a/backend/tests/test_roadmap_r2r5.py +++ b/backend/tests/test_roadmap_r2r5.py @@ -16,6 +16,15 @@ from scripts.opportunity_detector import ( from scripts.closed_loop_check import check_entity, build_report +def _cur_period() -> str: + """当前月字符串。 + + detect_budget_headroom 的口径是「当月预算执行率<70%」(实现内取 datetime.now()), + 用例里硬编码月份会在跨月后与实现错配、恒判 0 条(2026-09 跨月实证:原写死 2026-08)。 + """ + return datetime.now().strftime("%Y-%m") + + def _kpi(db, code, target=100.0, **kw): return create_test_kpi(db, kpi_code=code, target_value=target, **kw) @@ -68,9 +77,10 @@ class TestOpportunityR2: def test_budget_headroom(self, db): """当月预算执行率<70% → 预算余量机会""" + p = _cur_period() kpi = _kpi(db, "OPP_04", target=1000.0) - _value(db, kpi.id, "2026-08", 300.0) - _budget(db, kpi.id, "2026-08", 1000.0) + _value(db, kpi.id, p, 300.0) + _budget(db, kpi.id, p, 1000.0) db.commit() out = detect_budget_headroom(db, 1) assert len(out) == 1 @@ -78,18 +88,20 @@ class TestOpportunityR2: def test_budget_headroom_negative_skip(self, db): """实际值为负(现金流异常)不误判为余量""" + p = _cur_period() kpi = _kpi(db, "OPP_05", target=1000.0) - _value(db, kpi.id, "2026-08", -500.0) - _budget(db, kpi.id, "2026-08", 1000.0) + _value(db, kpi.id, p, -500.0) + _budget(db, kpi.id, p, 1000.0) db.commit() assert detect_budget_headroom(db, 1) == [] def test_budget_headroom_dedup(self, db): """同KPI同期间多版本预算只取一条""" + p = _cur_period() kpi = _kpi(db, "OPP_06", target=1000.0) - _value(db, kpi.id, "2026-08", 300.0) - _budget(db, kpi.id, "2026-08", 1000.0) - b2 = _budget(db, kpi.id, "2026-08", 2000.0) + _value(db, kpi.id, p, 300.0) + _budget(db, kpi.id, p, 1000.0) + b2 = _budget(db, kpi.id, p, 2000.0) b2.version = "v2.0" db.commit() assert len(detect_budget_headroom(db, 1)) == 1