Files
PayrollManagementSystem/backend/app/api/v1/business/businesses.py
T
liuwenxin b8920c0d28 fix(T+): 补齐指导成本与读取服务保护
补充业务详情指导成本入口、来源版本和历史封存保护,接入月份锁并完善权限复核。
同步接入读取服务运行状态到页面,补充前端控制、回归测试和服务端只读连接授权校验。

定向隔离测试:89 项通过,1 条既有 Starlette 弃用警告。
2026-11-05 19:32:20 +08:00

264 lines
10 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, PayrollWriter, 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.schemas.business_source import (
GuidanceConfirmIn,
SalesRelationConfirmIn,
SalesRelationQuery,
SalesRelationReleaseIn,
SourceDocumentsQuery,
SourceMaterialsIn,
SourcePriceConfirmIn,
)
from app.services import business_service, business_source
# 基础数据是业务操作:登录即可,不要求管理员(区别于系统管理的 AdminUser)
CurrentUser = Annotated[User, Depends(get_current_user)]
router = APIRouter(prefix="/businesses", tags=["业务管理"])
@router.get("/{business_id}/readiness")
def source_readiness(session: DbSession, _: CurrentUser, business_id: Annotated[int, Path(ge=1)]):
return ok(business_source.readiness(session, business_id))
@router.get("/{business_id}/source-documents")
def source_documents(session: DbSession, _: CurrentUser, business_id: Annotated[int, Path(ge=1)],
query: Annotated[SourceDocumentsQuery, Query()]):
return ok(business_source.documents(session, business_id, query, query.entity))
@router.get("/{business_id}/source-quantities")
def source_quantities(session: DbSession, _: CurrentUser, business_id: Annotated[int, Path(ge=1)],
query: Annotated[PageQuery, Query()]):
return ok(business_source.source_quantities(session, business_id, query))
@router.get("/{business_id}/source-documents/{document_id}/details")
def source_document_details(session: DbSession, _: CurrentUser, business_id: Annotated[int, Path(ge=1)],
document_id: Annotated[int, Path(ge=1)], query: Annotated[PageQuery, Query()]):
return ok(business_source.document_details(session, business_id, document_id, query))
@router.put("/{business_id}/source-documents/{document_id}/missing-price")
def confirm_source_price(session: DbSession, actor: PayrollWriter, business_id: Annotated[int, Path(ge=1)],
document_id: Annotated[int, Path(ge=1)], payload: SourcePriceConfirmIn, meta: RequestMetaDep):
from app.services.authorization_service import require_permission
require_permission(session, actor, "payroll:prepare")
return ok(business_source.confirm_missing_source_price(session, business_id, document_id, payload, actor, meta))
@router.put("/{business_id}/guidance")
def confirm_guidance(session: DbSession, actor: PayrollWriter, business_id: Annotated[int, Path(ge=1)],
payload: GuidanceConfirmIn, meta: RequestMetaDep):
"""从本地发货明细登记来源产品的发货月份指导成本。"""
from app.services.authorization_service import require_permission
require_permission(session, actor, "payroll:prepare")
require_permission(session, actor, "tplus:facts")
return ok(business_source.confirm_guidance(session, business_id, payload, actor, meta))
@router.get("/{business_id}/sales-relation-candidates")
def sales_relation_candidates(session: DbSession, _: CurrentUser, business_id: Annotated[int, Path(ge=1)],
query: Annotated[SalesRelationQuery, Query()]):
return ok(business_source.sales_relation_candidates(session, business_id, query, query.shipment_page, query.order_page))
@router.put("/{business_id}/sales-relation")
def confirm_sales_relation(session: DbSession, actor: PayrollWriter,
business_id: Annotated[int, Path(ge=1)],
payload: SalesRelationConfirmIn, meta: RequestMetaDep):
from app.services.authorization_service import require_permission
from app.services.tplus import sales_relations
require_permission(session, actor, "payroll:prepare")
return ok(sales_relations.confirm(session, business_id, payload, actor, meta))
@router.put("/{business_id}/sales-relation/release")
def release_sales_relation(session: DbSession, actor: PayrollWriter,
business_id: Annotated[int, Path(ge=1)],
payload: SalesRelationReleaseIn, meta: RequestMetaDep):
from app.services.authorization_service import require_permission
from app.services.tplus import sales_relations
require_permission(session, actor, "payroll:prepare")
return ok(sales_relations.release(session, business_id, payload, actor, meta))
@router.put("/{business_id}/source-materials")
def source_materials(session: DbSession, actor: PayrollWriter, business_id: Annotated[int, Path(ge=1)],
payload: SourceMaterialsIn, meta: RequestMetaDep):
return ok(business_source.save_materials(session, business_id, payload, actor, meta))
@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: PayrollWriter,
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: PayrollWriter,
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: PayrollWriter,
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: PayrollWriter,
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: PayrollWriter,
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))