fix(cma): entity isolation batch3 (cost abc write endpoints + allocations/stats reads + plan kr_id scope)
- cost.py: POST /abc/activities 按会话账套写入 entity_id;POST /abc/allocate 校验 activity_id 归属本账套(跨账套 404)并透传 entity_id;GET /abc/allocations 加账套过滤 - cost_engine.py: allocate_cost(entity_id=None) 按账套过滤作业中心 + 显式写入 AbcAllocation.entity_id(不传维持原语义) - action_plans.py: update_plan 的 kr_id 加账套校验(跨账套 404,对齐 create_plan 既有写法);plan_stats join KPIDefinition 按账套过滤(口径与 list_plans 一致) - tests: 新增 tests/test_cross_entity_isolation_batch3_20260918.py(10 例;改前 7 failed/3 passed → 改后 10 passed) - 回归: 1184 passed,1 xfailed → 1194 passed,1 xfailed(无新增失败)
This commit is contained in:
@@ -262,7 +262,8 @@ def update_plan(
|
||||
if data["kr_id"] is None:
|
||||
plan.kr_id = None
|
||||
else:
|
||||
kr_ent = db.query(KR).filter(KR.id == data["kr_id"]).first()
|
||||
# 账套隔离: KR 必须属于当前企业(对齐同文件 create_plan:158 既有写法;2026-09-18 batch3)
|
||||
kr_ent = db.query(KR).filter(KR.id == data["kr_id"], KR.entity_id == entity_id).first()
|
||||
if not kr_ent:
|
||||
raise HTTPException(404, "关联KR不存在")
|
||||
plan.kr_id = kr_ent.id
|
||||
@@ -299,9 +300,19 @@ def delete_plan(plan_id: int, db: Session = Depends(get_db), entity_id: int = De
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def plan_stats(db: Session = Depends(get_db), current_user: User = Depends(require_auth)):
|
||||
"""行动计划统计"""
|
||||
query = db.query(ActionPlan)
|
||||
def plan_stats(db: Session = Depends(get_db), current_user: User = Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""行动计划统计(账套隔离: join KPI 按企业过滤, 2026-09-18 batch3)
|
||||
|
||||
口径说明(为何改):本端点原为全局统计(仅 business 角色按 assignee 过滤)→ 非 e1 账套
|
||||
用户看到的是含其它账套计划的合计,与同文件列表端点 list_plans(join KPIDefinition 按账套)
|
||||
的可见集不一致。现按同文件既有先例(update_plan/delete_plan 的 join(KPIDefinition))过滤,
|
||||
保证「统计数 = 列表可见行数」。数据侧存在 action_plans 行 entity_id 与 KPI 归属不一致的
|
||||
存量行(batch2 报告 §六.2),统一以 KPI 归属为准与列表口径一致。
|
||||
"""
|
||||
query = (db.query(ActionPlan)
|
||||
.join(KPIDefinition, KPIDefinition.id == ActionPlan.kpi_id)
|
||||
.filter(KPIDefinition.entity_id == entity_id))
|
||||
if current_user.role == "business":
|
||||
query = query.filter(
|
||||
(ActionPlan.assignee == current_user.username) |
|
||||
|
||||
+23
-7
@@ -180,6 +180,12 @@ def create_actual_cost(data: dict, db: Session = Depends(get_db),
|
||||
# GET /abc/activities 原为全表取(生产 10 行全属 entity2)→ 酣客会话可见博海 ABC 作业中心;
|
||||
# 现按会话账套过滤(AbcActivity.entity_id)。
|
||||
# OpenCode 多租户审查 2026-09-07 的「单实体演示数据暂不强制隔离」豁免就此撤销。
|
||||
# 账套隔离(2026-09-18,project-20260918-entity-isolation-batch3):batch2 同族残留收口
|
||||
# - POST /abc/activities 原签名无账套依赖、不写归属 → 落模型默认 entity_id=1;现按会话账套写入
|
||||
# - POST /abc/allocate 原无账套依赖、activity_id 不校验归属、分配记录落默认 1;
|
||||
# 现校验 activity_id 归属本账套(跨账套 → 404,文案对齐同文件先例)
|
||||
# 并把 entity_id 透传 cost_engine.allocate_cost 显式写入 AbcAllocation
|
||||
# - GET /abc/allocations 原查询只按 period/product_code → 现加 entity_id 过滤(读型同族)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/abc/activities")
|
||||
@@ -193,9 +199,11 @@ def list_abc_activities(db: Session = Depends(get_db), entity_id: int = Depends(
|
||||
|
||||
|
||||
@router.post("/abc/activities")
|
||||
def create_abc_activity(data: dict, db: Session = Depends(get_db)):
|
||||
"""创建ABC作业中心"""
|
||||
def create_abc_activity(data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""创建ABC作业中心(账套隔离:按会话账套写入归属)"""
|
||||
act = AbcActivity(
|
||||
entity_id=entity_id,
|
||||
activity_code=data["activity_code"],
|
||||
activity_name=data["activity_name"],
|
||||
activity_desc=data.get("activity_desc"),
|
||||
@@ -211,14 +219,21 @@ def create_abc_activity(data: dict, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/abc/allocate")
|
||||
def do_allocate(data: dict, db: Session = Depends(get_db)):
|
||||
"""执行ABC成本分配"""
|
||||
def do_allocate(data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""执行ABC成本分配(账套隔离:activity_id 须属本账套,跨账套 → 404)"""
|
||||
act = (db.query(AbcActivity)
|
||||
.filter(AbcActivity.id == data["activity_id"], AbcActivity.entity_id == entity_id)
|
||||
.first())
|
||||
if not act:
|
||||
raise HTTPException(404, "作业中心不存在")
|
||||
result = allocate_cost(
|
||||
activity_id=data["activity_id"],
|
||||
period=data.get("period", datetime.now().strftime("%Y-%m")),
|
||||
product_code=data["product_code"],
|
||||
product_name=data.get("product_name", ""),
|
||||
driver_consumed=data["driver_consumed"],
|
||||
entity_id=entity_id,
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -226,9 +241,10 @@ def do_allocate(data: dict, db: Session = Depends(get_db)):
|
||||
@router.get("/abc/allocations")
|
||||
def list_allocations(period: Optional[str] = Query(None),
|
||||
product_code: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)):
|
||||
"""查询ABC分配记录"""
|
||||
query = db.query(AbcAllocation)
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""查询ABC分配记录(账套隔离:只返回本账套)"""
|
||||
query = db.query(AbcAllocation).filter(AbcAllocation.entity_id == entity_id)
|
||||
if period:
|
||||
query = query.filter(AbcAllocation.period == period)
|
||||
if product_code:
|
||||
|
||||
@@ -158,11 +158,21 @@ def calc_driver_rate(activity_id: int) -> dict:
|
||||
|
||||
|
||||
def allocate_cost(activity_id: int, period: str, product_code: str,
|
||||
product_name: str, driver_consumed: float) -> dict:
|
||||
"""按动因分配成本到产品"""
|
||||
product_name: str, driver_consumed: float,
|
||||
entity_id: Optional[int] = None) -> dict:
|
||||
"""按动因分配成本到产品
|
||||
|
||||
账套隔离(2026-09-18,project-20260918-entity-isolation-batch3):
|
||||
- entity_id 非 None 时,作业中心查询按本账套过滤(跨账套 activity_id 查不到 → 返回错误字典,
|
||||
端点侧已先行 404);
|
||||
- 新建 AbcAllocation 显式写入 entity_id(不传则维持原语义:不写归属,落模型默认)。
|
||||
"""
|
||||
db = get_session_local()()
|
||||
try:
|
||||
act = db.query(AbcActivity).filter(AbcActivity.id == activity_id).first()
|
||||
q_act = db.query(AbcActivity).filter(AbcActivity.id == activity_id)
|
||||
if entity_id is not None:
|
||||
q_act = q_act.filter(AbcActivity.entity_id == entity_id)
|
||||
act = q_act.first()
|
||||
if not act or act.driver_rate == 0:
|
||||
# 自动计算分配率
|
||||
if act and act.driver_volume > 0:
|
||||
@@ -171,7 +181,7 @@ def allocate_cost(activity_id: int, period: str, product_code: str,
|
||||
if not act or act.driver_rate == 0:
|
||||
return {"error": "分配率未设置"}
|
||||
allocated = round(driver_consumed * act.driver_rate, 2)
|
||||
alloc = AbcAllocation(
|
||||
alloc_kwargs = dict(
|
||||
period=period,
|
||||
activity_id=activity_id,
|
||||
product_code=product_code,
|
||||
@@ -179,6 +189,9 @@ def allocate_cost(activity_id: int, period: str, product_code: str,
|
||||
driver_consumed=driver_consumed,
|
||||
allocated_cost=allocated,
|
||||
)
|
||||
if entity_id is not None:
|
||||
alloc_kwargs["entity_id"] = entity_id
|
||||
alloc = AbcAllocation(**alloc_kwargs)
|
||||
db.add(alloc)
|
||||
db.commit()
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user