This commit is contained in:
2026-07-30 11:01:00 +08:00
parent 853c478657
commit a330757946
4 changed files with 141 additions and 1 deletions
@@ -12,6 +12,8 @@ from shared.models.database import User
from ..schemas import ( from ..schemas import (
PurchaseDemandCalculateRequest, PurchaseDemandCalculateRequest,
PurchaseDemandResponse, PurchaseDemandResponse,
PurchaseDemandConvertRequest,
PurchaseDemandConvertResponse,
) )
from ..services.purchase_demand_service import purchase_demand_service from ..services.purchase_demand_service import purchase_demand_service
@@ -26,3 +28,13 @@ async def calculate_purchase_demands(
): ):
"""根据销售订单 ID 列表,自动推导采购需求(BOM 展开 → 库存对比 → 供应商推荐)""" """根据销售订单 ID 列表,自动推导采购需求(BOM 展开 → 库存对比 → 供应商推荐)"""
return await purchase_demand_service.calculate_demands(db_session, payload.sales_order_ids) 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)
+5 -1
View File
@@ -59,7 +59,11 @@ from .material_schemas import (
from .purchase_demand_schemas import ( from .purchase_demand_schemas import (
PurchaseDemandCalculateRequest, PurchaseDemandCalculateRequest,
PurchaseDemandItemResponse, PurchaseDemandItemResponse,
PurchaseDemandResponse PurchaseDemandResponse,
PurchaseDemandConvertItem,
PurchaseDemandConvertRequest,
PurchaseOrderCreatedResponse,
PurchaseDemandConvertResponse
) )
from .common_schemas import PaginatedResponse from .common_schemas import PaginatedResponse
@@ -5,6 +5,7 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from decimal import Decimal from decimal import Decimal
from typing import Optional, List from typing import Optional, List
from datetime import date
class PurchaseDemandCalculateRequest(BaseModel): class PurchaseDemandCalculateRequest(BaseModel):
@@ -34,3 +35,34 @@ class PurchaseDemandResponse(BaseModel):
shortage_count: int = Field(default=0, description="缺货物料种类数") shortage_count: int = Field(default=0, description="缺货物料种类数")
source_order_ids: List[int] = Field(default_factory=list, description="来源销售订单ID") source_order_ids: List[int] = Field(default_factory=list, description="来源销售订单ID")
source_order_nos: List[str] = Field(default_factory=list, description="来源销售订单编号") 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被跳过的条目数")
@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func from sqlalchemy import select, func
from shared.models.database import ( from shared.models.database import (
User,
Product, Product,
ProductMaterial, ProductMaterial,
SalesOrder, SalesOrder,
@@ -18,10 +19,16 @@ from shared.models.database import (
Inventory, Inventory,
MaterialSupplier, MaterialSupplier,
Supplier, Supplier,
PurchaseOrder,
PurchaseOrderItem,
) )
from ..utils import generate_order_no
from ..schemas.purchase_demand_schemas import ( from ..schemas.purchase_demand_schemas import (
PurchaseDemandItemResponse, PurchaseDemandItemResponse,
PurchaseDemandResponse, PurchaseDemandResponse,
PurchaseDemandConvertRequest,
PurchaseDemandConvertResponse,
PurchaseOrderCreatedResponse,
) )
@@ -179,5 +186,90 @@ class PurchaseDemandService:
source_order_nos=order_nos, 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() purchase_demand_service = PurchaseDemandService()