This commit is contained in:
2026-06-23 09:38:31 +08:00
parent f6d8829071
commit 6b3188e693
11 changed files with 197 additions and 47 deletions
+7 -1
View File
@@ -10,6 +10,7 @@
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, func
from sqlalchemy.exc import IntegrityError
from typing import Optional, List
from shared.database.database import get_db_session
@@ -114,7 +115,12 @@ async def create_inventory(
location=payload.location
)
db_session.add(inventory)
await db_session.commit()
try:
await db_session.commit()
except IntegrityError:
# 并发创建命中 (product_id, warehouse_id) 唯一约束
await db_session.rollback()
raise HTTPException(status_code=400, detail="该仓库已存在该物料库存记录")
await db_session.refresh(inventory)
return InventoryResponse(
@@ -344,6 +344,10 @@ async def update_purchase_order_status(
valid_statuses = ["pending", "partial_received", "received", "paid", "cancelled"]
if new_status not in valid_statuses:
raise HTTPException(status_code=400, detail=f"无效的状态值,有效值为: {valid_statuses}")
# 收货状态(partial_received/received)只能由收货端点驱动,禁止手动设置,避免与实际入库脱钩
if new_status in ("partial_received", "received"):
raise HTTPException(status_code=400, detail="收货状态只能通过收货入库操作自动变更,不能手动设置")
# 状态转换逻辑
if order.status == "paid":
@@ -380,6 +384,8 @@ async def receive_purchase_order(
current_user: User = Depends(get_current_active_user)
):
order, supplier = await _get_order_with_supplier(db_session, order_id)
if order.status in ("cancelled", "paid"):
raise HTTPException(status_code=400, detail=f"当前采购单状态为 {order.status},不允许收货")
warehouse = await _resolve_receive_warehouse(db_session, payload.warehouse_id)
item_result = await db_session.execute(
+26 -15
View File
@@ -446,7 +446,7 @@ async def create_sales_order(
customer_id=order_data.customer_id,
order_date=now,
delivery_date=order_data.delivery_date,
manufacturing_date=now,
manufacturing_date=now.date(),
created_at=now,
remark=order_data.remark,
operator_id=current_user.id,
@@ -563,6 +563,10 @@ async def delete_sales_order(
order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status == "delivered":
raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除")
if order.status == "paid":
raise HTTPException(status_code=400, detail="已收款的销售订单禁止删除,请先作废相关收款单")
if (order.received_amount or 0) > 0:
raise HTTPException(status_code=400, detail="该销售订单已存在收款记录(received_amount>0),禁止删除,请先作废相关收款单")
await _rollback_issued_materials(db_session, order, current_user)
await db_session.delete(order)
await db_session.commit()
@@ -586,9 +590,14 @@ async def consume_materials(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
"""记录销售订单的物料消耗"""
"""记录销售订单的物料消耗(在已发料基础上记录额外消耗,如报废/超耗)"""
order, customer = await _get_sales_order_with_customer(db_session, order_id)
if order.status in ("delivered", "paid", "cancelled"):
raise HTTPException(status_code=400, detail="当前订单状态不允许记录物料消耗")
if order.production_status == "completed":
raise HTTPException(status_code=400, detail="该销售单已完成生产,不允许记录物料消耗")
default_warehouse = await _get_default_warehouse(db_session)
# 计算总物料成本
@@ -602,45 +611,47 @@ async def consume_materials(
raise HTTPException(status_code=404, detail=f"物料 ID {item.material_id} 不存在")
if material.item_type != "material":
raise HTTPException(status_code=400, detail=f"只能消耗物料类型的产品: {material.name}")
# 计算成本
cost = Decimal(str(material.cost_price or 0)) * item.quantity
# 统一转 Decimal,避免 Decimal * float 在 Postgres(Numeric) 上抛 TypeError
consume_qty = Decimal(str(item.quantity))
unit_cost = Decimal(str(material.cost_price or 0))
cost = unit_cost * consume_qty
total_cost += cost
# 更新物料库存(原子操作防并发)
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)
.where(Inventory.quantity >= consume_qty)
.values(quantity=Inventory.quantity - consume_qty)
.returning(Inventory.quantity)
)
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)
after_qty = Decimal(str(after_qty))
before_qty = after_qty + consume_qty
# 记录物料消耗
movement = StockMovement(
product_id=material.id,
warehouse_id=default_warehouse.id,
quantity=item.quantity,
quantity=consume_qty,
before_quantity=before_qty,
after_quantity=after_qty,
movement_type="consumption",
reference_type="sales_order",
reference_id=order.id,
unit_price=material.cost_price,
unit_price=unit_cost,
total_amount=cost,
operator_id=current_user.id,
remark=item.remark
)
db_session.add(movement)
# 更新订单的实际物料成本
order.actual_material_cost = total_cost
# 累加实际物料成本(创建时已记录计划发料成本,此处为额外消耗,不应覆盖)
order.actual_material_cost = Decimal(str(order.actual_material_cost or 0)) + total_cost
await db_session.commit()
await db_session.refresh(order)
+4 -1
View File
@@ -660,7 +660,10 @@ class Warehouse(Base):
class Inventory(Base):
"""库存表"""
__tablename__ = "inventory"
__table_args__ = (
UniqueConstraint("product_id", "warehouse_id", name="uq_inventory_product_warehouse"),
)
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True)