This commit is contained in:
2026-05-11 09:34:50 +08:00
parent 79ca3613ae
commit 5e1d475a22
14 changed files with 336 additions and 264 deletions
+66 -51
View File
@@ -38,12 +38,14 @@ from .schemas import (
ProductionMaterialPlanItemResponse,
SalesOrderIssueRequest,
SalesOrderIssueResponse,
SalesOrderStatusUpdate
SalesOrderStatusUpdate,
PaginatedResponse,
)
from .utils import generate_order_no
router = APIRouter(prefix="/sales-orders", tags=["销售订单"])
VALID_ORDER_STATUSES = {"manufacturing", "delivered", "paid"}
VALID_ORDER_STATUSES = {"draft", "manufacturing", "delivered", "paid"}
PRODUCTION_STATUSES = {"not_started", "bom_missing", "material_issued", "completed"}
def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesOrderResponse:
@@ -225,7 +227,7 @@ async def _issue_materials_for_order_creation(
raise HTTPException(status_code=400, detail=f"物料库存不足:{shortage_text}")
production_no = order.production_no or generate_order_no("WO")
actual_material_cost = 0.0
actual_material_cost = Decimal("0")
movement_count = 0
for item in plan_items:
@@ -289,25 +291,28 @@ async def _rollback_issued_materials(
return
for movement in movements:
inv_result = await db_session.execute(
select(Inventory)
upd_result = await db_session.execute(
update(Inventory)
.where(Inventory.product_id == movement.product_id)
.where(Inventory.warehouse_id == movement.warehouse_id)
.values(quantity=Inventory.quantity + movement.quantity)
.returning(Inventory.quantity)
)
inventory = inv_result.scalar_one_or_none()
if not inventory:
after_qty = upd_result.scalar_one_or_none()
if after_qty is None:
inventory = Inventory(
product_id=movement.product_id,
warehouse_id=movement.warehouse_id,
quantity=0,
quantity=movement.quantity,
locked_quantity=0
)
db_session.add(inventory)
await db_session.flush()
before_qty = inventory.quantity
inventory.quantity += movement.quantity
after_qty = inventory.quantity
before_qty = 0
after_qty = movement.quantity
else:
after_qty = int(after_qty)
before_qty = after_qty - movement.quantity
revert_movement = StockMovement(
product_id=movement.product_id,
@@ -331,8 +336,8 @@ async def _apply_order_items(
db_session: AsyncSession,
order: SalesOrder,
order_data: SalesOrderCreate
) -> float:
total_amount = 0.0
) -> Decimal:
total_amount = Decimal("0")
for item_data in order_data.items:
product = None
if item_data.product_id is not None:
@@ -378,7 +383,7 @@ async def _apply_order_items(
return total_amount
@router.get("", response_model=List[SalesOrderResponse])
@router.get("", response_model=PaginatedResponse[SalesOrderResponse])
async def list_sales_orders(
status: Optional[str] = None,
skip: int = Query(0, ge=0),
@@ -386,23 +391,26 @@ async def list_sales_orders(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
query = (
base_query = (
select(SalesOrder, Customer)
.join(Customer, SalesOrder.customer_id == Customer.id)
.order_by(SalesOrder.created_at.desc())
)
if status:
query = query.where(SalesOrder.status == status)
base_query = base_query.where(SalesOrder.status == status)
query = query.offset(skip).limit(limit)
count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query)
orders = []
for order, customer in result.all():
orders.append(_build_sales_order_response(order, customer.name))
return orders
return PaginatedResponse(items=orders, total=total, skip=skip, limit=limit)
@router.post("", response_model=SalesOrderResponse, status_code=201)
@@ -418,7 +426,7 @@ async def create_sales_order(
customer_id=order_data.customer_id,
order_date=now,
delivery_date=order_data.delivery_date,
manufacturing_date=now.date(),
manufacturing_date=now,
created_at=now,
remark=order_data.remark,
operator_id=current_user.id,
@@ -427,9 +435,13 @@ async def create_sales_order(
db_session.add(order)
await db_session.flush()
order.total_amount = await _apply_order_items(db_session, order, order_data)
await _issue_materials_for_order_creation(db_session, order, current_user)
await db_session.commit()
try:
order.total_amount = await _apply_order_items(db_session, order, order_data)
await _issue_materials_for_order_creation(db_session, order, current_user)
await db_session.commit()
except (HTTPException, Exception):
await db_session.rollback()
raise
await db_session.refresh(order)
customer = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
@@ -458,21 +470,25 @@ async def update_sales_order(
order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "paid":
raise HTTPException(status_code=400, detail="已收款的销售订单禁止修改")
await _rollback_issued_materials(db_session, order, current_user)
await db_session.execute(delete(SalesOrderItem).where(SalesOrderItem.order_id == order.id))
try:
await _rollback_issued_materials(db_session, order, current_user)
await db_session.execute(delete(SalesOrderItem).where(SalesOrderItem.order_id == order.id))
order.customer_id = order_data.customer_id
order.delivery_date = order_data.delivery_date
order.remark = order_data.remark
order.production_status = "not_started"
order.production_no = None
order.planned_material_cost = 0
order.actual_material_cost = 0
order.status = "manufacturing"
order.customer_id = order_data.customer_id
order.delivery_date = order_data.delivery_date
order.remark = order_data.remark
order.production_status = "not_started"
order.production_no = None
order.planned_material_cost = 0
order.actual_material_cost = 0
order.status = "manufacturing"
order.total_amount = await _apply_order_items(db_session, order, order_data)
await _issue_materials_for_order_creation(db_session, order, current_user)
await db_session.commit()
order.total_amount = await _apply_order_items(db_session, order, order_data)
await _issue_materials_for_order_creation(db_session, order, current_user)
await db_session.commit()
except (HTTPException, Exception):
await db_session.rollback()
raise
await db_session.refresh(order)
customer_result = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
@@ -562,27 +578,26 @@ async def consume_materials(
cost = Decimal(str(material.cost_price or 0)) * item.quantity
total_cost += cost
# 更新物料库存
inventory_result = await db_session.execute(
select(Inventory)
# 更新物料库存(原子操作防并发)
upd_result = await db_session.execute(
update(Inventory)
.where(Inventory.product_id == material.id)
.where(Inventory.warehouse_id == default_warehouse.id)
.where(Inventory.quantity >= item.quantity)
.values(quantity=Inventory.quantity - item.quantity)
.returning(Inventory.quantity)
)
inventory = inventory_result.scalar()
if inventory:
before_qty = inventory.quantity
inventory.quantity -= item.quantity
after_qty = inventory.quantity
if after_qty < 0:
raise HTTPException(status_code=400, detail=f"物料 {material.name} 库存不足")
else:
raise HTTPException(status_code=400, detail=f"物料 {material.name} 没有库存记录")
after_qty = upd_result.scalar_one_or_none()
if after_qty is None:
raise HTTPException(status_code=400, detail=f"物料 {material.name} 库存不足")
after_qty = int(after_qty)
before_qty = after_qty + int(item.quantity)
# 记录物料消耗
movement = StockMovement(
product_id=material.id,
warehouse_id=default_warehouse.id,
quantity=-item.quantity,
quantity=item.quantity,
before_quantity=before_qty,
after_quantity=after_qty,
movement_type="consumption",
@@ -661,7 +676,7 @@ async def issue_sales_order_materials(
production_no = payload.production_no or order.production_no or generate_order_no("WO")
actual_material_cost = 0.0
actual_material_cost = Decimal("0")
movement_count = 0
for item in plan_items:
upd_result = await db_session.execute(