- 编号 YW{日期}-{序号}:MAX+FOR UPDATE 并发安全 + 1062 兜底重试(no_generator 三单据共用)
- 明细全量替换重算五项汇总冗余;销售额服务层按数量×单价固化不信任前端
- 状态机终态拦截、终止必填原因、创建写首条留痕;归属变更同事务留痕防旁路
- 毛利润视图按行实时计算(不含运费 #2),阈值按 asOf 取 M1 快照,率输出
Fraction 分数字符串同 M1 预览口径;成本缺失行标注无法计算
- 入参契约统一为蛇形直出(撤回 customer/business 的驼峰 alias,与前端
businessRoles.ts 模式对齐)
Co-Authored-By: Claude Code <noreply@anthropic.com>
178 lines
6.1 KiB
Python
178 lines
6.1 KiB
Python
"""业务管理接口(需求 2.2.1/2.2.4)。
|
||
|
||
校验规则(编号生成、状态机、归属留痕、汇总重算)见 `business_service`
|
||
模块 docstring;基础数据属业务操作,登录即可访问。
|
||
"""
|
||
|
||
from datetime import date
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, Depends, Path, Query
|
||
|
||
from app.api.deps import DbSession, RequestMetaDep, get_current_user
|
||
from app.core.response import ApiResponse, PageQuery, PageResult, ok
|
||
from app.models import User
|
||
from app.schemas.business import (
|
||
BusinessCreateIn,
|
||
BusinessDetailOut,
|
||
BusinessOut,
|
||
BusinessOwnershipIn,
|
||
BusinessQuery,
|
||
BusinessStatusIn,
|
||
BusinessStatusLogOut,
|
||
BusinessUpdateIn,
|
||
GrossProfitOut,
|
||
OwnershipChangeLogOut,
|
||
)
|
||
from app.services import business_service
|
||
|
||
# 基础数据是业务操作:登录即可,不要求管理员(区别于系统管理的 AdminUser)
|
||
CurrentUser = Annotated[User, Depends(get_current_user)]
|
||
|
||
router = APIRouter(prefix="/businesses", tags=["业务管理"])
|
||
|
||
|
||
@router.get("", response_model=ApiResponse[PageResult[BusinessOut]], summary="业务列表")
|
||
def list_businesses(
|
||
session: DbSession,
|
||
_: CurrentUser,
|
||
query: Annotated[BusinessQuery, Query()],
|
||
) -> ApiResponse[PageResult[BusinessOut]]:
|
||
"""分页查询业务。编号/公司名称模糊,可按状态/客户/归属人员/日期范围筛选。"""
|
||
return ok(business_service.list_businesses(session, query))
|
||
|
||
|
||
@router.post("", response_model=ApiResponse[BusinessOut], summary="创建业务")
|
||
def create_business(
|
||
session: DbSession,
|
||
current_user: CurrentUser,
|
||
payload: BusinessCreateIn,
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[BusinessOut]:
|
||
"""创建业务 + 明细行(≥1)。业务编号自动生成;创建写首条状态留痕。"""
|
||
return ok(
|
||
business_service.create_business(session, payload, operator=current_user, meta=meta),
|
||
message="业务已创建",
|
||
)
|
||
|
||
|
||
@router.get("/{business_id}", response_model=ApiResponse[BusinessDetailOut], summary="业务详情")
|
||
def get_business(
|
||
session: DbSession,
|
||
_: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
) -> ApiResponse[BusinessDetailOut]:
|
||
"""业务详情,附产品明细行,供编辑抽屉一次回显。"""
|
||
return ok(business_service.get_detail(session, business_id))
|
||
|
||
|
||
@router.put("/{business_id}", response_model=ApiResponse[BusinessOut], summary="编辑业务")
|
||
def update_business(
|
||
session: DbSession,
|
||
current_user: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
payload: BusinessUpdateIn,
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[BusinessOut]:
|
||
"""编辑业务(明细全量替换并重算汇总)。归属不在此端点调整,走 /ownership。"""
|
||
return ok(
|
||
business_service.update_business(
|
||
session, business_id, payload, operator=current_user, meta=meta
|
||
),
|
||
message="已保存",
|
||
)
|
||
|
||
|
||
@router.delete("/{business_id}", response_model=ApiResponse[None], summary="删除业务")
|
||
def delete_business(
|
||
session: DbSession,
|
||
current_user: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[None]:
|
||
"""删除业务。有发货或回款记录时禁删,应改为已终止。"""
|
||
business_service.delete_business(session, business_id, operator=current_user, meta=meta)
|
||
return ok(message="业务已删除")
|
||
|
||
|
||
@router.put("/{business_id}/status", response_model=ApiResponse[BusinessOut], summary="状态流转")
|
||
def change_status(
|
||
session: DbSession,
|
||
current_user: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
payload: BusinessStatusIn,
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[BusinessOut]:
|
||
"""状态流转:执行中→已完成/已终止;终态不可再变更;终止必填原因。"""
|
||
return ok(
|
||
business_service.change_status(
|
||
session, business_id, payload, operator=current_user, meta=meta
|
||
),
|
||
message="状态已变更",
|
||
)
|
||
|
||
|
||
@router.put("/{business_id}/ownership", response_model=ApiResponse[BusinessOut], summary="调整归属")
|
||
def change_ownership(
|
||
session: DbSession,
|
||
current_user: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
payload: BusinessOwnershipIn,
|
||
meta: RequestMetaDep,
|
||
) -> ApiResponse[BusinessOut]:
|
||
"""调整负责/协助营销人员,同事务写归属变更留痕(不回溯历史)。"""
|
||
return ok(
|
||
business_service.change_ownership(
|
||
session, business_id, payload, operator=current_user, meta=meta
|
||
),
|
||
message="归属已调整",
|
||
)
|
||
|
||
|
||
@router.get(
|
||
"/{business_id}/ownership-logs",
|
||
response_model=ApiResponse[PageResult[OwnershipChangeLogOut]],
|
||
summary="归属变更记录",
|
||
)
|
||
def list_ownership_logs(
|
||
session: DbSession,
|
||
_: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
query: Annotated[PageQuery, Query()],
|
||
) -> ApiResponse[PageResult[OwnershipChangeLogOut]]:
|
||
"""某业务的归属变更留痕(分页,按时间倒序)。"""
|
||
return ok(business_service.list_ownership_logs(session, business_id, query))
|
||
|
||
|
||
@router.get(
|
||
"/{business_id}/status-logs",
|
||
response_model=ApiResponse[PageResult[BusinessStatusLogOut]],
|
||
summary="状态流转记录",
|
||
)
|
||
def list_status_logs(
|
||
session: DbSession,
|
||
_: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
query: Annotated[PageQuery, Query()],
|
||
) -> ApiResponse[PageResult[BusinessStatusLogOut]]:
|
||
"""某业务的状态流转时间线(分页,按时间正序,从创建起展示)。"""
|
||
return ok(business_service.list_status_logs(session, business_id, query))
|
||
|
||
|
||
@router.get(
|
||
"/{business_id}/gross-profit",
|
||
response_model=ApiResponse[GrossProfitOut],
|
||
summary="毛利润数据视图",
|
||
)
|
||
def gross_profit(
|
||
session: DbSession,
|
||
_: CurrentUser,
|
||
business_id: Annotated[int, Path(ge=1)],
|
||
as_of: Annotated[date | None, Query()] = None,
|
||
) -> ApiResponse[GrossProfitOut]:
|
||
"""按产品逐行实时计算毛利润与折算额(不含运费口径,对照表 #2)。
|
||
|
||
阈值按 asOf(默认今日)取参数快照;M3 核算实际按回款到账日取参。
|
||
"""
|
||
return ok(business_service.gross_profit(session, business_id, as_of))
|