diff --git a/src/inventory/api/purchase_demand_routes.py b/src/inventory/api/purchase_demand_routes.py index 8f83464..8bd87a2 100644 --- a/src/inventory/api/purchase_demand_routes.py +++ b/src/inventory/api/purchase_demand_routes.py @@ -12,6 +12,8 @@ from shared.models.database import User from ..schemas import ( PurchaseDemandCalculateRequest, PurchaseDemandResponse, + PurchaseDemandConvertRequest, + PurchaseDemandConvertResponse, ) from ..services.purchase_demand_service import purchase_demand_service @@ -26,3 +28,13 @@ async def calculate_purchase_demands( ): """根据销售订单 ID 列表,自动推导采购需求(BOM 展开 → 库存对比 → 供应商推荐)""" return await purchase_demand_service.calculate_demands(db_session, payload.sales_order_ids) + + +@router.post("/convert", response_model=PurchaseDemandConvertResponse) +async def convert_purchase_demands( + payload: PurchaseDemandConvertRequest, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user), +): + """将采购需求一键转换为采购单(按供应商分组,每组生成一张草稿采购单)""" + return await purchase_demand_service.convert_to_purchase_orders(db_session, payload, current_user) diff --git a/src/inventory/schemas/__init__.py b/src/inventory/schemas/__init__.py index 72390d6..ca08400 100644 --- a/src/inventory/schemas/__init__.py +++ b/src/inventory/schemas/__init__.py @@ -59,7 +59,11 @@ from .material_schemas import ( from .purchase_demand_schemas import ( PurchaseDemandCalculateRequest, PurchaseDemandItemResponse, - PurchaseDemandResponse + PurchaseDemandResponse, + PurchaseDemandConvertItem, + PurchaseDemandConvertRequest, + PurchaseOrderCreatedResponse, + PurchaseDemandConvertResponse ) from .common_schemas import PaginatedResponse diff --git a/src/inventory/schemas/purchase_demand_schemas.py b/src/inventory/schemas/purchase_demand_schemas.py index 7561fcf..69526b0 100644 --- a/src/inventory/schemas/purchase_demand_schemas.py +++ b/src/inventory/schemas/purchase_demand_schemas.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field from decimal import Decimal from typing import Optional, List +from datetime import date class PurchaseDemandCalculateRequest(BaseModel): @@ -34,3 +35,34 @@ class PurchaseDemandResponse(BaseModel): shortage_count: int = Field(default=0, description="缺货物料种类数") source_order_ids: List[int] = Field(default_factory=list, description="来源销售订单ID") source_order_nos: List[str] = Field(default_factory=list, description="来源销售订单编号") + + +class PurchaseDemandConvertItem(BaseModel): + """待转采购单的单条需求""" + material_id: int + quantity: Decimal = Field(..., gt=0, description="采购数量(通常取缺口量)") + unit_cost: Decimal = Field(..., ge=0, description="单价") + supplier_id: int + + +class PurchaseDemandConvertRequest(BaseModel): + """一键生成采购单请求(按 supplier_id 分组,每组生成一张草稿采购单)""" + items: List[PurchaseDemandConvertItem] = Field(..., min_length=1) + expected_date: Optional[date] = None + remark: Optional[str] = None + + +class PurchaseOrderCreatedResponse(BaseModel): + """已创建的采购单摘要""" + purchase_order_id: int + order_no: str + supplier_id: int + supplier_name: str + item_count: int + total_amount: Decimal + + +class PurchaseDemandConvertResponse(BaseModel): + """一键生成采购单结果""" + created_orders: List[PurchaseOrderCreatedResponse] = Field(default_factory=list) + skipped: int = Field(default=0, description="因数量<=0被跳过的条目数") diff --git a/src/inventory/services/purchase_demand_service.py b/src/inventory/services/purchase_demand_service.py index 3d563bd..2911c5b 100644 --- a/src/inventory/services/purchase_demand_service.py +++ b/src/inventory/services/purchase_demand_service.py @@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from shared.models.database import ( + User, Product, ProductMaterial, SalesOrder, @@ -18,10 +19,16 @@ from shared.models.database import ( Inventory, MaterialSupplier, Supplier, + PurchaseOrder, + PurchaseOrderItem, ) +from ..utils import generate_order_no from ..schemas.purchase_demand_schemas import ( PurchaseDemandItemResponse, PurchaseDemandResponse, + PurchaseDemandConvertRequest, + PurchaseDemandConvertResponse, + PurchaseOrderCreatedResponse, ) @@ -179,5 +186,90 @@ class PurchaseDemandService: source_order_nos=order_nos, ) + @staticmethod + async def convert_to_purchase_orders( + db_session: AsyncSession, + payload: PurchaseDemandConvertRequest, + current_user: User, + ) -> PurchaseDemandConvertResponse: + """将采购需求按供应商分组,每组生成一张草稿采购单(status=pending)。 + + 只转换 quantity>0 的条目;物料/供应商需存在且启用。 + 事务由 get_db_session 统一提交(路由返回成功即 commit,异常即 rollback)。 + """ + valid_items = [it for it in payload.items if it.quantity > 0] + skipped = len(payload.items) - len(valid_items) + if not valid_items: + raise HTTPException(status_code=400, detail="没有可转换的有效需求(数量需 > 0)") + + material_ids = {it.material_id for it in valid_items} + supplier_ids = {it.supplier_id for it in valid_items} + + # 校验物料 + mat_result = await db_session.execute( + select(Product) + .where(Product.id.in_(material_ids)) + .where(Product.is_active == True) + .where(Product.item_type == "material") + ) + materials = {m.id: m for m in mat_result.scalars().all()} + if len(materials) != len(material_ids): + raise HTTPException(status_code=400, detail="部分物料不存在或非物料类型") + + # 校验供应商 + sup_result = await db_session.execute( + select(Supplier) + .where(Supplier.id.in_(supplier_ids)) + .where(Supplier.is_active == True) + ) + suppliers = {s.id: s for s in sup_result.scalars().all()} + if len(suppliers) != len(supplier_ids): + raise HTTPException(status_code=400, detail="部分供应商不存在或已停用") + + # 按供应商分组生成采购单 + groups: dict = {} + for it in valid_items: + groups.setdefault(it.supplier_id, []).append(it) + + created: List[PurchaseOrderCreatedResponse] = [] + for supplier_id, items in groups.items(): + po = PurchaseOrder( + order_no=generate_order_no("PO"), + supplier_id=supplier_id, + expected_date=payload.expected_date, + remark=payload.remark or "由采购需求一键生成", + operator_id=current_user.id, + status="pending", + ) + db_session.add(po) + await db_session.flush() + + total = Decimal("0") + for it in items: + amount = Decimal(str(it.quantity)) * Decimal(str(it.unit_cost)) + total += amount + db_session.add( + PurchaseOrderItem( + order_id=po.id, + product_id=it.material_id, + quantity=int(it.quantity), + unit_price=Decimal(str(it.unit_cost)), + amount=amount, + ) + ) + po.total_amount = total + created.append( + PurchaseOrderCreatedResponse( + purchase_order_id=po.id, + order_no=po.order_no, + supplier_id=supplier_id, + supplier_name=suppliers[supplier_id].name, + item_count=len(items), + total_amount=total, + ) + ) + await db_session.flush() + return PurchaseDemandConvertResponse(created_orders=created, skipped=skipped) + purchase_demand_service = PurchaseDemandService()