Files
geMoldInsight/src/api/inventory/purchase_order_routes.py
T
2026-03-29 16:24:43 +08:00

406 lines
15 KiB
Python

"""
采购订单路由模块
提供采购订单的管理功能,包括:
- 采购订单列表查询(支持分页、状态筛选)
- 创建采购订单(自动生成订单号、计算总金额)
- 采购订单明细管理
路由前缀: /api/purchase-orders
"""
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional, List
from database.database import get_db_session
from services.auth_service import get_current_active_user
from models.database import (
User,
Supplier,
Product,
Warehouse,
Inventory,
StockMovement,
PurchaseOrder,
PurchaseOrderItem
)
from .schemas import (
PurchaseOrderCreate,
PurchaseOrderResponse,
PurchaseOrderDetailResponse,
PurchaseOrderItemResponse,
PurchaseOrderReceiveRequest,
)
from .utils import generate_order_no
router = APIRouter(prefix="/purchase-orders", tags=["采购订单"])
def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) -> PurchaseOrderResponse:
return PurchaseOrderResponse(
id=order.id,
order_no=order.order_no,
supplier_name=supplier_name,
order_date=order.order_date,
expected_date=order.expected_date,
status=order.status,
total_amount=order.total_amount,
paid_amount=order.paid_amount,
remark=order.remark,
created_at=order.created_at
)
async def _get_order_with_supplier(
db_session: AsyncSession,
order_id: int
) -> tuple[PurchaseOrder, Supplier]:
result = await db_session.execute(
select(PurchaseOrder, Supplier)
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
.where(PurchaseOrder.id == order_id)
)
row = result.first()
if not row:
raise HTTPException(status_code=404, detail="采购订单不存在")
return row[0], row[1]
async def _build_purchase_order_detail(
db_session: AsyncSession,
order: PurchaseOrder,
supplier_name: str
) -> PurchaseOrderDetailResponse:
item_result = await db_session.execute(
select(PurchaseOrderItem, Product)
.join(Product, PurchaseOrderItem.product_id == Product.id)
.where(PurchaseOrderItem.order_id == order.id)
.order_by(PurchaseOrderItem.id.asc())
)
item_rows = item_result.all()
return PurchaseOrderDetailResponse(
id=order.id,
order_no=order.order_no,
supplier_id=order.supplier_id,
supplier_name=supplier_name,
order_date=order.order_date,
expected_date=order.expected_date,
status=order.status,
total_amount=order.total_amount,
paid_amount=order.paid_amount,
remark=order.remark,
created_at=order.created_at,
items=[
PurchaseOrderItemResponse(
id=item.id,
product_id=item.product_id,
product_sku=product.sku,
product_name=product.name,
quantity=item.quantity,
received_quantity=item.received_quantity,
unit_price=item.unit_price,
amount=item.amount,
remark=item.remark
) for item, product in item_rows
]
)
async def _resolve_receive_warehouse(
db_session: AsyncSession,
warehouse_id: Optional[int]
) -> Warehouse:
if warehouse_id:
result = await db_session.execute(
select(Warehouse).where(Warehouse.id == warehouse_id, Warehouse.is_active == True)
)
warehouse = result.scalar_one_or_none()
if not warehouse:
raise HTTPException(status_code=404, detail="仓库不存在")
return warehouse
result = await db_session.execute(
select(Warehouse).where(Warehouse.is_active == True).order_by(Warehouse.is_default.desc(), Warehouse.id.asc())
)
warehouse = result.scalars().first()
if not warehouse:
raise HTTPException(status_code=400, detail="未配置可用仓库")
return warehouse
async def _apply_order_items(
db_session: AsyncSession,
order: PurchaseOrder,
order_data: PurchaseOrderCreate
) -> float:
total_amount = 0.0
for item_data in order_data.items:
product_result = await db_session.execute(
select(Product).where(Product.id == item_data.product_id, Product.is_active == True)
)
product = product_result.scalar_one_or_none()
if not product:
raise HTTPException(status_code=400, detail=f"物料不存在: {item_data.product_id}")
if product.item_type != "material":
raise HTTPException(status_code=400, detail=f"采购单仅允许物料: {product.name}")
# 使用物料的成本价格作为单价,忽略前端提交的单价
unit_price = product.cost_price or 0
if unit_price <= 0:
raise HTTPException(status_code=400, detail=f"物料 {product.name} 未设置成本价格,请在物料管理界面设置")
try:
item = PurchaseOrderItem(
order_id=order.id,
product_id=item_data.product_id,
quantity=int(item_data.quantity),
unit_price=float(unit_price),
amount=float(item_data.quantity) * float(unit_price),
remark=item_data.remark or None
)
db_session.add(item)
total_amount += item.amount
except Exception as e:
raise HTTPException(status_code=400, detail=f"创建订单明细失败: {str(e)}")
return total_amount
@router.get("", response_model=List[PurchaseOrderResponse])
async def list_purchase_orders(
status: Optional[str] = None,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
query = (
select(PurchaseOrder, Supplier)
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
.order_by(PurchaseOrder.created_at.desc())
)
if status:
query = query.where(PurchaseOrder.status == status)
query = query.offset(skip).limit(limit)
result = await db_session.execute(query)
orders = []
for order, supplier in result.all():
orders.append(_build_purchase_order_response(order, supplier.name))
return orders
@router.post("", response_model=PurchaseOrderResponse, status_code=201)
async def create_purchase_order(
order_data: PurchaseOrderCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
order = PurchaseOrder(
order_no=generate_order_no("PO"),
supplier_id=order_data.supplier_id,
expected_date=order_data.expected_date,
remark=order_data.remark,
operator_id=current_user.id,
status="pending"
)
db_session.add(order)
await db_session.flush()
order.total_amount = await _apply_order_items(db_session, order, order_data)
await db_session.commit()
await db_session.refresh(order)
supplier = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
supplier = supplier.scalar_one()
return _build_purchase_order_response(order, supplier.name)
@router.get("/{order_id}", response_model=PurchaseOrderDetailResponse)
async def get_purchase_order_detail(
order_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
order, supplier = await _get_order_with_supplier(db_session, order_id)
return await _build_purchase_order_detail(db_session, order, supplier.name)
@router.put("/{order_id}", response_model=PurchaseOrderResponse)
async def update_purchase_order(
order_id: int,
order_data: PurchaseOrderCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
order, _ = await _get_order_with_supplier(db_session, order_id)
if order.paid_amount and order.paid_amount > 0:
raise HTTPException(status_code=400, detail="已付款采购单不允许修改")
item_result = await db_session.execute(
select(PurchaseOrderItem).where(PurchaseOrderItem.order_id == order.id)
)
existing_items = item_result.scalars().all()
if any((item.received_quantity or 0) > 0 for item in existing_items):
raise HTTPException(status_code=400, detail="已发生入库的采购单不允许直接修改")
for item in existing_items:
await db_session.delete(item)
order.supplier_id = order_data.supplier_id
order.expected_date = order_data.expected_date
order.remark = order_data.remark
order.total_amount = await _apply_order_items(db_session, order, order_data)
order.status = "pending"
await db_session.commit()
await db_session.refresh(order)
supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
supplier = supplier_result.scalar_one_or_none()
return _build_purchase_order_response(order, supplier.name if supplier else "未知供应商")
@router.delete("/{order_id}")
async def delete_purchase_order(
order_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
order, _ = await _get_order_with_supplier(db_session, order_id)
if order.paid_amount and order.paid_amount > 0:
raise HTTPException(status_code=400, detail="已付款采购单不允许删除")
item_result = await db_session.execute(
select(PurchaseOrderItem).where(PurchaseOrderItem.order_id == order.id)
)
if any((item.received_quantity or 0) > 0 for item in item_result.scalars().all()):
raise HTTPException(status_code=400, detail="已发生入库的采购单不允许删除")
await db_session.delete(order)
await db_session.commit()
return {"message": "采购订单已删除"}
@router.patch("/{order_id}/status")
async def update_purchase_order_status(
order_id: int,
status: dict,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
order, _ = await _get_order_with_supplier(db_session, order_id)
new_status = status.get("status")
if not new_status:
raise HTTPException(status_code=400, detail="状态不能为空")
valid_statuses = ["pending", "received", "paid"]
if new_status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"无效的状态值,有效值为: {valid_statuses}")
# 状态转换逻辑
if order.status == "paid":
raise HTTPException(status_code=400, detail="已付款的采购订单禁止修改状态")
if order.status == "received" and new_status != "paid":
raise HTTPException(status_code=400, detail="已收货的采购订单只能标记为已付款")
order.status = new_status
await db_session.commit()
await db_session.refresh(order)
supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
supplier = supplier_result.scalar_one_or_none()
return _build_purchase_order_response(order, supplier.name if supplier else "未知供应商")
@router.post("/{order_id}/receive", response_model=PurchaseOrderDetailResponse)
async def receive_purchase_order(
order_id: int,
payload: PurchaseOrderReceiveRequest,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
order, supplier = await _get_order_with_supplier(db_session, order_id)
warehouse = await _resolve_receive_warehouse(db_session, payload.warehouse_id)
item_result = await db_session.execute(
select(PurchaseOrderItem).where(PurchaseOrderItem.order_id == order.id)
)
item_map = {item.id: item for item in item_result.scalars().all()}
if not item_map:
raise HTTPException(status_code=400, detail="采购单无明细,无法入库")
if not payload.items:
raise HTTPException(status_code=400, detail="请提供本次入库明细")
for receive_item in payload.items:
item = item_map.get(receive_item.item_id)
if not item:
raise HTTPException(status_code=400, detail=f"采购明细不存在: {receive_item.item_id}")
if receive_item.receive_quantity <= 0:
raise HTTPException(status_code=400, detail="入库数量必须大于0")
remaining_qty = (item.quantity or 0) - (item.received_quantity or 0)
if receive_item.receive_quantity > remaining_qty:
raise HTTPException(status_code=400, detail=f"明细{item.id}入库超量,剩余可入库{remaining_qty}")
for receive_item in payload.items:
item = item_map[receive_item.item_id]
product_result = await db_session.execute(
select(Product).where(Product.id == item.product_id)
)
product = product_result.scalar_one_or_none()
if not product:
raise HTTPException(status_code=400, detail=f"物料不存在: {item.product_id}")
inv_result = await db_session.execute(
select(Inventory)
.where(Inventory.product_id == item.product_id)
.where(Inventory.warehouse_id == warehouse.id)
)
inventory = inv_result.scalar_one_or_none()
if not inventory:
inventory = Inventory(
product_id=item.product_id,
warehouse_id=warehouse.id,
quantity=0,
locked_quantity=0
)
db_session.add(inventory)
await db_session.flush()
before_qty = inventory.quantity
inventory.quantity += receive_item.receive_quantity
after_qty = inventory.quantity
item.received_quantity = (item.received_quantity or 0) + receive_item.receive_quantity
movement = StockMovement(
product_id=item.product_id,
warehouse_id=warehouse.id,
movement_type="purchase_in",
quantity=receive_item.receive_quantity,
before_quantity=before_qty,
after_quantity=after_qty,
reference_type="purchase_order",
reference_id=order.id,
reference_no=order.order_no,
unit_price=item.unit_price,
total_amount=round(float(item.unit_price * receive_item.receive_quantity), 4),
remark=payload.remark or f"采购单{order.order_no}到货入库",
operator_id=current_user.id
)
db_session.add(movement)
all_received = all((item.received_quantity or 0) >= (item.quantity or 0) for item in item_map.values())
any_received = any((item.received_quantity or 0) > 0 for item in item_map.values())
if all_received:
order.status = "received"
elif any_received:
order.status = "partial_received"
await db_session.commit()
await db_session.refresh(order)
return await _build_purchase_order_detail(db_session, order, supplier.name)