feat(cost): 成本模型加研发归集维度 research_flag/project_type (cma-rd-dimension-20260905)
- StandardCost/ActualCost 增加 research_flag(int,idx)/project_type(varchar30,idx) project_type: platform_dev平台研发/client_delivery客户交付/internal内部管理 - 生产库两表 ALTER 完成, 存量 29+134 行默认 internal(保守防误归研发) - API: create/update 支持写入维度, 未传默认 client_delivery(录入默认), research_flag 未显式传入按 project_type 联动(platform_dev=1); create/update/delete 绑定 entity_id(token优先, 防跨账套) - API: standard/actual 列表支持 project_type/research_flag 过滤 - 前端 CostDashboard: 表单加归集类型下拉(默认客户交付+研发提示), 列表加归集类型列(标签), 工具栏按归集类型过滤 - tests: test_cost.py +6 用例(默认值/联动/过滤/更新) - scripts: verify_cost_research_dimension_e2e.py 生产E2E(标记+清理+回查)
This commit is contained in:
+55
-6
@@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.deps import get_entity_id
|
||||
from app.models import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
||||
from app.models.cost_model import RESEARCH_PROJECT_TYPES
|
||||
from app.utils.cost_engine import (
|
||||
calc_product_variance, get_cost_overview, get_cost_breakdown,
|
||||
calc_driver_rate, allocate_cost
|
||||
@@ -16,6 +17,25 @@ logger = logging.getLogger("cma.cost")
|
||||
router = APIRouter(prefix="/api/cma/cost", tags=["成本分析"])
|
||||
|
||||
|
||||
def _resolve_research_dims(data: dict, current=None) -> tuple:
|
||||
"""解析研发归集维度(research_flag/project_type)。
|
||||
- project_type 缺省:新建=client_delivery(录入默认),编辑=保留现值
|
||||
- research_flag 未显式传入 → 按 project_type 联动(platform_dev=研发1,其余=0)
|
||||
- 显式传入 research_flag 尊重原值(可覆盖联动)
|
||||
"""
|
||||
if current is not None:
|
||||
project_type = data.get("project_type") or current.project_type or "client_delivery"
|
||||
else:
|
||||
project_type = data.get("project_type") or "client_delivery"
|
||||
if project_type not in RESEARCH_PROJECT_TYPES:
|
||||
raise HTTPException(400, f"project_type 非法: {project_type},可选 {', '.join(RESEARCH_PROJECT_TYPES)}")
|
||||
if data.get("research_flag") is not None:
|
||||
research_flag = int(bool(data.get("research_flag")))
|
||||
else:
|
||||
research_flag = 1 if project_type == "platform_dev" else 0
|
||||
return project_type, research_flag
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 标准成本卡片 CRUD
|
||||
# ============================================================
|
||||
@@ -23,6 +43,8 @@ router = APIRouter(prefix="/api/cma/cost", tags=["成本分析"])
|
||||
@router.get("/standard-costs")
|
||||
def list_standard_costs(product_code: Optional[str] = Query(None),
|
||||
cost_type: Optional[str] = Query(None),
|
||||
project_type: Optional[str] = Query(None),
|
||||
research_flag: Optional[int] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""查询标准成本卡片"""
|
||||
@@ -31,14 +53,23 @@ def list_standard_costs(product_code: Optional[str] = Query(None),
|
||||
query = query.filter(StandardCost.product_code == product_code)
|
||||
if cost_type:
|
||||
query = query.filter(StandardCost.cost_type == cost_type)
|
||||
if project_type:
|
||||
query = query.filter(StandardCost.project_type == project_type)
|
||||
if research_flag is not None:
|
||||
query = query.filter(StandardCost.research_flag == research_flag)
|
||||
items = query.order_by(StandardCost.product_code, StandardCost.cost_type).all()
|
||||
return {"data": items}
|
||||
|
||||
|
||||
@router.post("/standard-costs")
|
||||
def create_standard_cost(data: dict, db: Session = Depends(get_db)):
|
||||
def create_standard_cost(data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""创建标准成本卡片"""
|
||||
project_type, research_flag = _resolve_research_dims(data)
|
||||
sc = StandardCost(
|
||||
entity_id=entity_id,
|
||||
project_type=project_type,
|
||||
research_flag=research_flag,
|
||||
product_code=data["product_code"],
|
||||
product_name=data.get("product_name", ""),
|
||||
cost_type=data["cost_type"],
|
||||
@@ -56,24 +87,31 @@ def create_standard_cost(data: dict, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.put("/standard-costs/{cost_id}")
|
||||
def update_standard_cost(cost_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
def update_standard_cost(cost_id: int, data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""修改标准成本卡片"""
|
||||
sc = db.query(StandardCost).filter(StandardCost.id == cost_id).first()
|
||||
sc = db.query(StandardCost).filter(StandardCost.id == cost_id,
|
||||
StandardCost.entity_id == entity_id).first()
|
||||
if not sc:
|
||||
raise HTTPException(404, "标准成本记录不存在")
|
||||
for k in ("product_code", "product_name", "cost_type", "item_name",
|
||||
"standard_quantity", "unit", "standard_price", "version", "remark"):
|
||||
if k in data:
|
||||
setattr(sc, k, data[k])
|
||||
project_type, research_flag = _resolve_research_dims(data, current=sc)
|
||||
sc.project_type = project_type
|
||||
sc.research_flag = research_flag
|
||||
sc.standard_cost = round(sc.standard_quantity * sc.standard_price, 2)
|
||||
db.commit()
|
||||
return {"message": "已更新"}
|
||||
|
||||
|
||||
@router.delete("/standard-costs/{cost_id}")
|
||||
def delete_standard_cost(cost_id: int, db: Session = Depends(get_db)):
|
||||
def delete_standard_cost(cost_id: int, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""删除标准成本卡片"""
|
||||
sc = db.query(StandardCost).filter(StandardCost.id == cost_id).first()
|
||||
sc = db.query(StandardCost).filter(StandardCost.id == cost_id,
|
||||
StandardCost.entity_id == entity_id).first()
|
||||
if not sc:
|
||||
raise HTTPException(404, "标准成本记录不存在")
|
||||
sc.status = "archived"
|
||||
@@ -88,6 +126,8 @@ def delete_standard_cost(cost_id: int, db: Session = Depends(get_db)):
|
||||
@router.get("/actual-costs")
|
||||
def list_actual_costs(period: Optional[str] = Query(None),
|
||||
product_code: Optional[str] = Query(None),
|
||||
project_type: Optional[str] = Query(None),
|
||||
research_flag: Optional[int] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""查询实际成本"""
|
||||
@@ -96,14 +136,23 @@ def list_actual_costs(period: Optional[str] = Query(None),
|
||||
query = query.filter(ActualCost.period == period)
|
||||
if product_code:
|
||||
query = query.filter(ActualCost.product_code == product_code)
|
||||
if project_type:
|
||||
query = query.filter(ActualCost.project_type == project_type)
|
||||
if research_flag is not None:
|
||||
query = query.filter(ActualCost.research_flag == research_flag)
|
||||
items = query.order_by(ActualCost.period.desc(), ActualCost.product_code).all()
|
||||
return {"data": items}
|
||||
|
||||
|
||||
@router.post("/actual-costs")
|
||||
def create_actual_cost(data: dict, db: Session = Depends(get_db)):
|
||||
def create_actual_cost(data: dict, db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id)):
|
||||
"""录入实际成本"""
|
||||
project_type, research_flag = _resolve_research_dims(data)
|
||||
ac = ActualCost(
|
||||
entity_id=entity_id,
|
||||
project_type=project_type,
|
||||
research_flag=research_flag,
|
||||
period=data["period"],
|
||||
product_code=data["product_code"],
|
||||
product_name=data.get("product_name", ""),
|
||||
|
||||
Reference in New Issue
Block a user