fix(cma): entity isolation batch4 (cash plan writes, budget ownership, read leaks, okr template)

P0-A cash.py: scope api_update_plan / api_delete_plan / api_complete_plan /
api_register_payment to session entity (cross-entity id -> 404, aligning with
the file's existing get_entity_id endpoints).
P0-B budget.py: write entity_id on BudgetPlan creation + require the KPI to
belong to the session entity; scope /comparison, /comparison/kpi/{id} and
/deviation-report reads to the session entity.
P1 notifications.py: explicit field whitelist (name/config/enabled) on
update_channel to close mass-assignment.
P1 read leaks scoped to session entity: bot_kpis list, data_quality check/stats
(KPI calibre only; quality-log table has no entity_id column), kpis causality
chain + associate-map (KPI and map ownership), dashboard my-kpis,
customer_dashboard list, kpi_governance audit, budget_generate candidates,
driver_budget history (+ entity_id on its write path), expenses stats
(+ entity_id on its write path).
P2 okr_templates.py: write entity_id on the StrategicMap created by apply.

tests: tests/test_cross_entity_isolation_batch4_20260921.py (30 cases;
RED before fix = 22 failed, GREEN after = 30 passed; full suite 1302 passed /
1 xfailed / 0 failed).
This commit is contained in:
Hermes CI Fix
2026-09-21 09:37:38 +08:00
parent 8c5853f64b
commit a0762b367f
14 changed files with 749 additions and 44 deletions
+11 -2
View File
@@ -343,6 +343,7 @@ def submit_reimbursement(
data: dict,
db: Session = Depends(get_db),
current_user=Depends(require_auth),
entity_id: int = Depends(get_entity_id),
):
"""提交报销单 → 自动校验规则(超限自动打回)"""
applicant = data.get("applicant") or (current_user.name if hasattr(current_user, "name") else current_user.username)
@@ -366,6 +367,7 @@ def submit_reimbursement(
expense_date = None
reimb = ExpenseReimbursement(
entity_id=entity_id, # 账套隔离 2026-09-21:写侧先写归属(否则读侧过滤误杀本账套行)
reimb_no="BX" + datetime.now().strftime("%Y%m%d%H%M%S") + f"{random.randint(100, 999)}",
applicant=applicant,
department=data.get("department", ""),
@@ -537,6 +539,7 @@ def resubmit_reimbursement(
def expense_stats(
period: str = Query(None, description="期间 YYYY-MM,默认当前月"),
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
_=Depends(require_auth),
):
"""费用审核看板统计: 状态计数 / 费用类型统计 / 超限预警 / 本月总额"""
@@ -544,7 +547,9 @@ def expense_stats(
period = period or now.strftime("%Y-%m")
counts = {"pending": 0, "approved": 0, "rejected": 0, "returned": 0, "total": 0}
for st, cnt in db.query(ExpenseReimbursement.status, func.count(ExpenseReimbursement.id)).group_by(
for st, cnt in db.query(ExpenseReimbursement.status, func.count(ExpenseReimbursement.id)).filter(
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
).group_by(
ExpenseReimbursement.status
).all():
if st in counts:
@@ -560,6 +565,7 @@ def expense_stats(
)
.filter(
ExpenseReimbursement.status.in_(["pending", "approved"]),
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
func.date_format(ExpenseReimbursement.expense_date, "%Y-%m") == period,
)
.group_by(ExpenseReimbursement.expense_type)
@@ -583,6 +589,7 @@ def expense_stats(
.filter(
ExpenseReimbursement.status == "returned",
ExpenseReimbursement.check_result == "fail",
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
)
.order_by(ExpenseReimbursement.id.desc())
.limit(50)
@@ -604,7 +611,9 @@ def expense_stats(
for r in over_limit_rows
]
recent_rows = db.query(ExpenseReimbursement).order_by(ExpenseReimbursement.id.desc()).limit(10).all()
recent_rows = db.query(ExpenseReimbursement).filter(
ExpenseReimbursement.entity_id == entity_id, # 账套隔离 2026-09-21
).order_by(ExpenseReimbursement.id.desc()).limit(10).all()
recent = [_reimb_to_dict(r) for r in recent_rows]
# 本月预算使用率(招待费15万/月 管理费91万/月)