feat: 多租户隔离P2批2 — 预算/成本/费用/预警规则/BI报表加entity_id
- 12表加entity_id列(预算/偏差/规则/成本4表/费用2表/BI2表/驱动预算) - 模型: BudgetPlan/StandardCost/ActualCost/AbcActivity/AbcAllocation/DriverFactorBudget/BiReport/Template/BudgetDeviationAlert/ExpenseRule/Reimbursement/AlertRule - API隔离: budget plans / cost standard+actual / expenses rules+reimb / bi_reports list / alert_rules list 按token企业过滤 - 回填: kpi_id关联按KPI归属, 无关联默认酣客(entity=1); 当前数据全归酣客 - 验证: import+全端点200+pytest 451 passed
This commit is contained in:
@@ -11,6 +11,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.database import get_db, Base
|
from app.database import get_db, Base
|
||||||
|
from app.deps import get_entity_id
|
||||||
from app.auth_middleware import require_auth, require_role
|
from app.auth_middleware import require_auth, require_role
|
||||||
from app.models import KPIDefinition, KPIValue, KPIAlert, OperationLog
|
from app.models import KPIDefinition, KPIValue, KPIAlert, OperationLog
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class AlertRule(Base):
|
|||||||
"""预警规则配置"""
|
"""预警规则配置"""
|
||||||
__tablename__ = "alert_rules"
|
__tablename__ = "alert_rules"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
kpi_id = Column(Integer, nullable=False, comment="关联KPI ID")
|
kpi_id = Column(Integer, nullable=False, comment="关联KPI ID")
|
||||||
rule_type = Column(String(30), nullable=False, comment="static/dynamic/trend_up/trend_down")
|
rule_type = Column(String(30), nullable=False, comment="static/dynamic/trend_up/trend_down")
|
||||||
trigger_on = Column(String(20), default="actual", comment="actual/forecast/both — 实际值/预测值/两者触发")
|
trigger_on = Column(String(20), default="actual", comment="actual/forecast/both — 实际值/预测值/两者触发")
|
||||||
@@ -64,9 +66,10 @@ def list_alert_rules(
|
|||||||
rule_type: Optional[str] = None,
|
rule_type: Optional[str] = None,
|
||||||
enabled: Optional[int] = None,
|
enabled: Optional[int] = None,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id),
|
||||||
):
|
):
|
||||||
"""列出所有预警规则"""
|
"""列出所有预警规则(账套隔离: 按token企业, 2026-08-23 P2)"""
|
||||||
query = db.query(AlertRule)
|
query = db.query(AlertRule).filter(AlertRule.entity_id == entity_id)
|
||||||
if kpi_id:
|
if kpi_id:
|
||||||
query = query.filter(AlertRule.kpi_id == kpi_id)
|
query = query.filter(AlertRule.kpi_id == kpi_id)
|
||||||
if rule_type:
|
if rule_type:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
from app.deps import get_entity_id
|
||||||
from app.auth_middleware import require_auth, require_role
|
from app.auth_middleware import require_auth, require_role
|
||||||
from app.models import KPIDefinition, KPIValue, BiReportTemplate, BiReport, OperationLog, KPICausality
|
from app.models import KPIDefinition, KPIValue, BiReportTemplate, BiReport, OperationLog, KPICausality
|
||||||
|
|
||||||
@@ -126,9 +127,9 @@ def delete_template(template_id: int, db: Session = Depends(get_db), user=WRITE_
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def list_reports(db: Session = Depends(get_db)):
|
def list_reports(db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||||
"""获取用户保存的报表"""
|
"""获取用户保存的报表(账套隔离: 按token企业, 2026-08-23 P2)"""
|
||||||
reports = db.query(BiReport).order_by(BiReport.updated_at.desc()).all()
|
reports = db.query(BiReport).filter(BiReport.entity_id == entity_id).order_by(BiReport.updated_at.desc()).all()
|
||||||
result = []
|
result = []
|
||||||
for r in reports:
|
for r in reports:
|
||||||
d = {c.name: getattr(r, c.name) for c in BiReport.__table__.columns}
|
d = {c.name: getattr(r, c.name) for c in BiReport.__table__.columns}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Optional
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
from app.deps import get_entity_id
|
||||||
from app.auth_middleware import require_auth, require_role
|
from app.auth_middleware import require_auth, require_role
|
||||||
from app.models import BudgetPlan, KPIDefinition, OperationLog
|
from app.models import BudgetPlan, KPIDefinition, OperationLog
|
||||||
|
|
||||||
@@ -23,11 +24,12 @@ def list_budget_plans(
|
|||||||
year: Optional[int] = Query(None),
|
year: Optional[int] = Query(None),
|
||||||
version: Optional[str] = Query(None),
|
version: Optional[str] = Query(None),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id),
|
||||||
):
|
):
|
||||||
"""查询预算计划列表"""
|
"""查询预算计划列表"""
|
||||||
query = db.query(BudgetPlan).join(
|
query = db.query(BudgetPlan).join(
|
||||||
KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id
|
KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id
|
||||||
)
|
).filter(KPIDefinition.entity_id == entity_id)
|
||||||
|
|
||||||
if kpi_id:
|
if kpi_id:
|
||||||
query = query.filter(BudgetPlan.kpi_id == kpi_id)
|
query = query.filter(BudgetPlan.kpi_id == kpi_id)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import Optional
|
|||||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
from app.deps import get_entity_id
|
||||||
from app.models import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
from app.models import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
||||||
from app.utils.cost_engine import (
|
from app.utils.cost_engine import (
|
||||||
calc_product_variance, get_cost_overview, get_cost_breakdown,
|
calc_product_variance, get_cost_overview, get_cost_breakdown,
|
||||||
@@ -22,9 +23,10 @@ router = APIRouter(prefix="/api/cma/cost", tags=["成本分析"])
|
|||||||
@router.get("/standard-costs")
|
@router.get("/standard-costs")
|
||||||
def list_standard_costs(product_code: Optional[str] = Query(None),
|
def list_standard_costs(product_code: Optional[str] = Query(None),
|
||||||
cost_type: Optional[str] = Query(None),
|
cost_type: Optional[str] = Query(None),
|
||||||
db: Session = Depends(get_db)):
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id)):
|
||||||
"""查询标准成本卡片"""
|
"""查询标准成本卡片"""
|
||||||
query = db.query(StandardCost).filter(StandardCost.status == "active")
|
query = db.query(StandardCost).filter(StandardCost.status == "active", StandardCost.entity_id == entity_id)
|
||||||
if product_code:
|
if product_code:
|
||||||
query = query.filter(StandardCost.product_code == product_code)
|
query = query.filter(StandardCost.product_code == product_code)
|
||||||
if cost_type:
|
if cost_type:
|
||||||
@@ -86,9 +88,10 @@ def delete_standard_cost(cost_id: int, db: Session = Depends(get_db)):
|
|||||||
@router.get("/actual-costs")
|
@router.get("/actual-costs")
|
||||||
def list_actual_costs(period: Optional[str] = Query(None),
|
def list_actual_costs(period: Optional[str] = Query(None),
|
||||||
product_code: Optional[str] = Query(None),
|
product_code: Optional[str] = Query(None),
|
||||||
db: Session = Depends(get_db)):
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id)):
|
||||||
"""查询实际成本"""
|
"""查询实际成本"""
|
||||||
query = db.query(ActualCost)
|
query = db.query(ActualCost).filter(ActualCost.entity_id == entity_id)
|
||||||
if period:
|
if period:
|
||||||
query = query.filter(ActualCost.period == period)
|
query = query.filter(ActualCost.period == period)
|
||||||
if product_code:
|
if product_code:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
from app.deps import get_entity_id
|
||||||
from app.auth_middleware import require_auth, require_role
|
from app.auth_middleware import require_auth, require_role
|
||||||
from app.models import ExpenseRule, ExpenseReimbursement, OperationLog
|
from app.models import ExpenseRule, ExpenseReimbursement, OperationLog
|
||||||
|
|
||||||
@@ -120,6 +121,7 @@ def list_rules(
|
|||||||
expense_type: str = Query(None),
|
expense_type: str = Query(None),
|
||||||
status: str = Query(None),
|
status: str = Query(None),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id),
|
||||||
_=Depends(require_auth),
|
_=Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""查询费用规则列表"""
|
"""查询费用规则列表"""
|
||||||
@@ -397,6 +399,7 @@ def list_reimbursements(
|
|||||||
applicant: str = Query(None),
|
applicant: str = Query(None),
|
||||||
keyword: str = Query(None),
|
keyword: str = Query(None),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id),
|
||||||
_=Depends(require_auth),
|
_=Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""查询报销单列表(支持状态/类型/申请人/关键字筛选)"""
|
"""查询报销单列表(支持状态/类型/申请人/关键字筛选)"""
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ class BiReportTemplate(Base):
|
|||||||
"""BI报表模板"""
|
"""BI报表模板"""
|
||||||
__tablename__ = "bi_report_templates"
|
__tablename__ = "bi_report_templates"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
name = Column(String(200), nullable=False, comment="模板名称")
|
name = Column(String(200), nullable=False, comment="模板名称")
|
||||||
report_type = Column(String(50), nullable=False, comment="overview/trend/comparison/topn/causality")
|
report_type = Column(String(50), nullable=False, comment="overview/trend/comparison/topn/causality")
|
||||||
config = Column(JSON, nullable=False, comment="报表配置")
|
config = Column(JSON, nullable=False, comment="报表配置")
|
||||||
@@ -346,6 +347,7 @@ class BiReport(Base):
|
|||||||
"""用户保存的BI报表"""
|
"""用户保存的BI报表"""
|
||||||
__tablename__ = "bi_reports"
|
__tablename__ = "bi_reports"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
template_id = Column(Integer, ForeignKey("bi_report_templates.id"), nullable=True)
|
template_id = Column(Integer, ForeignKey("bi_report_templates.id"), nullable=True)
|
||||||
name = Column(String(200), nullable=False, comment="报表名称")
|
name = Column(String(200), nullable=False, comment="报表名称")
|
||||||
config = Column(JSON, nullable=False, comment="报表配置(行/列/值)")
|
config = Column(JSON, nullable=False, comment="报表配置(行/列/值)")
|
||||||
@@ -466,6 +468,7 @@ class BudgetDeviationAlert(Base):
|
|||||||
"""预算偏差预警记录"""
|
"""预算偏差预警记录"""
|
||||||
__tablename__ = "budget_deviation_alerts"
|
__tablename__ = "budget_deviation_alerts"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
|
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
|
||||||
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
|
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
|
||||||
budget_value = Column(Float, nullable=True, comment="预算值")
|
budget_value = Column(Float, nullable=True, comment="预算值")
|
||||||
@@ -583,6 +586,7 @@ class ExpenseRule(Base):
|
|||||||
"""费用规则 — 自动校验报销单的标准"""
|
"""费用规则 — 自动校验报销单的标准"""
|
||||||
__tablename__ = "expense_rules"
|
__tablename__ = "expense_rules"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
rule_name = Column(String(100), nullable=False, comment="规则名称")
|
rule_name = Column(String(100), nullable=False, comment="规则名称")
|
||||||
dimension = Column(String(20), nullable=False, comment="维度: department/person/expense_type")
|
dimension = Column(String(20), nullable=False, comment="维度: department/person/expense_type")
|
||||||
dimension_value = Column(String(100), nullable=True, comment="维度值: 部门名/人员名/费用类型(空=全局)")
|
dimension_value = Column(String(100), nullable=True, comment="维度值: 部门名/人员名/费用类型(空=全局)")
|
||||||
@@ -600,6 +604,7 @@ class ExpenseReimbursement(Base):
|
|||||||
"""费用报销单 — 提交后自动校验规则,超限自动打回"""
|
"""费用报销单 — 提交后自动校验规则,超限自动打回"""
|
||||||
__tablename__ = "expense_reimbursements"
|
__tablename__ = "expense_reimbursements"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
reimb_no = Column(String(50), unique=True, nullable=False, comment="报销单号")
|
reimb_no = Column(String(50), unique=True, nullable=False, comment="报销单号")
|
||||||
applicant = Column(String(100), nullable=False, comment="申请人")
|
applicant = Column(String(100), nullable=False, comment="申请人")
|
||||||
department = Column(String(100), nullable=True, comment="部门")
|
department = Column(String(100), nullable=True, comment="部门")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class BudgetPlan(Base):
|
|||||||
__tablename__ = "budget_plans"
|
__tablename__ = "budget_plans"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
|
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
|
||||||
period = Column(String(20), nullable=False, comment="预算期间 2026-05")
|
period = Column(String(20), nullable=False, comment="预算期间 2026-05")
|
||||||
budget_value = Column(Float, nullable=False, comment="预算值")
|
budget_value = Column(Float, nullable=False, comment="预算值")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class StandardCost(Base):
|
|||||||
__tablename__ = "standard_costs"
|
__tablename__ = "standard_costs"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
product_code = Column(String(50), nullable=False, comment="产品/服务编码")
|
product_code = Column(String(50), nullable=False, comment="产品/服务编码")
|
||||||
product_name = Column(String(200), nullable=False, comment="产品/服务名称")
|
product_name = Column(String(200), nullable=False, comment="产品/服务名称")
|
||||||
cost_type = Column(String(20), nullable=False, comment="成本类型: material/labor/overhead")
|
cost_type = Column(String(20), nullable=False, comment="成本类型: material/labor/overhead")
|
||||||
@@ -30,6 +31,7 @@ class ActualCost(Base):
|
|||||||
__tablename__ = "actual_costs"
|
__tablename__ = "actual_costs"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
period = Column(String(20), nullable=False, comment="期间 2026-05")
|
period = Column(String(20), nullable=False, comment="期间 2026-05")
|
||||||
product_code = Column(String(50), nullable=False, comment="产品/服务编码")
|
product_code = Column(String(50), nullable=False, comment="产品/服务编码")
|
||||||
product_name = Column(String(200), nullable=False, comment="产品/服务名称")
|
product_name = Column(String(200), nullable=False, comment="产品/服务名称")
|
||||||
@@ -47,6 +49,7 @@ class AbcActivity(Base):
|
|||||||
__tablename__ = "abc_activities"
|
__tablename__ = "abc_activities"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
activity_code = Column(String(50), unique=True, nullable=False, comment="作业编码")
|
activity_code = Column(String(50), unique=True, nullable=False, comment="作业编码")
|
||||||
activity_name = Column(String(200), nullable=False, comment="作业名称")
|
activity_name = Column(String(200), nullable=False, comment="作业名称")
|
||||||
activity_desc = Column(Text, nullable=True, comment="作业描述")
|
activity_desc = Column(Text, nullable=True, comment="作业描述")
|
||||||
@@ -65,6 +68,7 @@ class AbcAllocation(Base):
|
|||||||
__tablename__ = "abc_allocations"
|
__tablename__ = "abc_allocations"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
period = Column(String(20), nullable=False, comment="期间 2026-05")
|
period = Column(String(20), nullable=False, comment="期间 2026-05")
|
||||||
activity_id = Column(Integer, ForeignKey("abc_activities.id"), nullable=False)
|
activity_id = Column(Integer, ForeignKey("abc_activities.id"), nullable=False)
|
||||||
product_code = Column(String(50), nullable=False, comment="产品/服务编码")
|
product_code = Column(String(50), nullable=False, comment="产品/服务编码")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class DriverFactorBudget(Base):
|
|||||||
__tablename__ = "driver_factor_budgets"
|
__tablename__ = "driver_factor_budgets"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)")
|
||||||
name = Column(String(200), nullable=False, comment="预算项名称")
|
name = Column(String(200), nullable=False, comment="预算项名称")
|
||||||
industry = Column(String(50), default="general", comment="行业标签")
|
industry = Column(String(50), default="general", comment="行业标签")
|
||||||
template_id = Column(Integer, nullable=True, comment="关联模板ID")
|
template_id = Column(Integer, nullable=True, comment="关联模板ID")
|
||||||
|
|||||||
Reference in New Issue
Block a user