xx
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user