"""业务管理接口(需求 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))