Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -36,7 +36,7 @@ async def _calculate_material_cost_map(db_session: AsyncSession, product_ids: Li
|
||||
ProductMaterial.finished_product_id,
|
||||
func.coalesce(
|
||||
func.sum(
|
||||
Product.cost_price * ProductMaterial.quantity * (1 + ProductMaterial.loss_rate)
|
||||
Product.cost_price * ProductMaterial.quantity
|
||||
),
|
||||
0
|
||||
)
|
||||
@@ -186,7 +186,7 @@ async def get_product_bom(
|
||||
items: List[ProductMaterialItemResponse] = []
|
||||
total_material_cost = 0.0
|
||||
for bom, material in bom_result.all():
|
||||
line_cost = float(material.cost_price or 0) * float(bom.quantity) * (1 + float(bom.loss_rate or 0))
|
||||
line_cost = float(material.cost_price or 0) * float(bom.quantity)
|
||||
total_material_cost += line_cost
|
||||
items.append(
|
||||
ProductMaterialItemResponse(
|
||||
@@ -194,7 +194,6 @@ async def get_product_bom(
|
||||
material_sku=material.sku,
|
||||
material_name=material.name,
|
||||
quantity=round(float(bom.quantity), 4),
|
||||
loss_rate=round(float(bom.loss_rate or 0), 4),
|
||||
unit_cost=round(float(material.cost_price or 0), 4),
|
||||
line_cost=round(float(line_cost), 4),
|
||||
)
|
||||
@@ -246,15 +245,12 @@ async def replace_product_bom(
|
||||
|
||||
for item in payload.items:
|
||||
if item.quantity <= 0:
|
||||
raise HTTPException(status_code=400, detail="物料数量必须大于0")
|
||||
if item.loss_rate < 0:
|
||||
raise HTTPException(status_code=400, detail="损耗率不能为负数")
|
||||
raise HTTPException(status_code=400, detail="物料数量必须大于 0")
|
||||
db_session.add(
|
||||
ProductMaterial(
|
||||
finished_product_id=product_id,
|
||||
material_product_id=item.material_id,
|
||||
quantity=item.quantity,
|
||||
loss_rate=item.loss_rate,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, func
|
||||
from typing import Optional, List
|
||||
|
||||
from database.database import get_db_session
|
||||
@@ -38,6 +38,10 @@ router = APIRouter(prefix="/purchase-orders", tags=["采购订单"])
|
||||
|
||||
|
||||
def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) -> PurchaseOrderResponse:
|
||||
# 检查字段是否存在,避免数据库中没有这些字段时的错误
|
||||
received_date = getattr(order, 'received_date', None)
|
||||
paid_date = getattr(order, 'paid_date', None)
|
||||
|
||||
return PurchaseOrderResponse(
|
||||
id=order.id,
|
||||
order_no=order.order_no,
|
||||
@@ -48,7 +52,9 @@ def _build_purchase_order_response(order: PurchaseOrder, supplier_name: str) ->
|
||||
total_amount=order.total_amount,
|
||||
paid_amount=order.paid_amount,
|
||||
remark=order.remark,
|
||||
created_at=order.created_at
|
||||
created_at=order.created_at,
|
||||
received_date=received_date,
|
||||
paid_date=paid_date
|
||||
)
|
||||
|
||||
|
||||
@@ -79,6 +85,10 @@ async def _build_purchase_order_detail(
|
||||
.order_by(PurchaseOrderItem.id.asc())
|
||||
)
|
||||
item_rows = item_result.all()
|
||||
# 检查字段是否存在,避免数据库中没有这些字段时的错误
|
||||
received_date = getattr(order, 'received_date', None)
|
||||
paid_date = getattr(order, 'paid_date', None)
|
||||
|
||||
return PurchaseOrderDetailResponse(
|
||||
id=order.id,
|
||||
order_no=order.order_no,
|
||||
@@ -91,6 +101,8 @@ async def _build_purchase_order_detail(
|
||||
paid_amount=order.paid_amount,
|
||||
remark=order.remark,
|
||||
created_at=order.created_at,
|
||||
received_date=received_date,
|
||||
paid_date=paid_date,
|
||||
items=[
|
||||
PurchaseOrderItemResponse(
|
||||
id=item.id,
|
||||
@@ -205,7 +217,7 @@ async def create_purchase_order(
|
||||
expected_date=order_data.expected_date,
|
||||
remark=order_data.remark,
|
||||
operator_id=current_user.id,
|
||||
status="draft"
|
||||
status="pending"
|
||||
)
|
||||
db_session.add(order)
|
||||
await db_session.flush()
|
||||
@@ -214,8 +226,10 @@ async def create_purchase_order(
|
||||
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()
|
||||
supplier_result = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
|
||||
supplier = supplier_result.scalar_one_or_none()
|
||||
if not supplier:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
return _build_purchase_order_response(order, supplier.name)
|
||||
|
||||
@@ -255,7 +269,7 @@ async def update_purchase_order(
|
||||
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 = "draft"
|
||||
order.status = "pending"
|
||||
|
||||
await db_session.commit()
|
||||
await db_session.refresh(order)
|
||||
@@ -283,6 +297,46 @@ async def delete_purchase_order(
|
||||
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
|
||||
# 检查字段是否存在,避免数据库中没有这些字段时的错误
|
||||
if hasattr(order, 'received_date') and new_status == "received":
|
||||
order.received_date = func.now()
|
||||
elif hasattr(order, 'paid_date') and new_status == "paid":
|
||||
order.paid_date = func.now()
|
||||
|
||||
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,
|
||||
@@ -364,6 +418,9 @@ async def receive_purchase_order(
|
||||
any_received = any((item.received_quantity or 0) > 0 for item in item_map.values())
|
||||
if all_received:
|
||||
order.status = "received"
|
||||
# 检查字段是否存在,避免数据库中没有这些字段时的错误
|
||||
if hasattr(order, 'received_date'):
|
||||
order.received_date = func.now()
|
||||
elif any_received:
|
||||
order.status = "partial_received"
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, delete, update
|
||||
from typing import Optional, List
|
||||
from math import ceil
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from database.database import get_db_session
|
||||
from services.auth_service import get_current_active_user
|
||||
@@ -513,7 +514,7 @@ async def delete_sales_order(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
order, _ = await _get_sales_order_with_customer(db_session, order_id)
|
||||
order, customer = await _get_sales_order_with_customer(db_session, order_id)
|
||||
if order.status == "delivered":
|
||||
raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除")
|
||||
await _rollback_issued_materials(db_session, order, current_user)
|
||||
@@ -522,6 +523,88 @@ async def delete_sales_order(
|
||||
return {"message": "销售订单已删除"}
|
||||
|
||||
|
||||
class MaterialConsumptionItem(BaseModel):
|
||||
material_id: int
|
||||
quantity: float = Field(gt=0)
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class MaterialConsumptionRequest(BaseModel):
|
||||
items: List[MaterialConsumptionItem] = Field(min_length=1)
|
||||
|
||||
|
||||
@router.post("/{order_id}/consume-materials")
|
||||
async def consume_materials(
|
||||
order_id: int,
|
||||
request: MaterialConsumptionRequest,
|
||||
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)
|
||||
|
||||
# 计算总物料成本
|
||||
total_cost = 0
|
||||
|
||||
# 处理每个物料消耗项
|
||||
for item in request.items:
|
||||
# 获取物料信息
|
||||
material = await db_session.get(Product, item.material_id)
|
||||
if not material:
|
||||
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 = material.cost_price * item.quantity
|
||||
total_cost += cost
|
||||
|
||||
# 更新物料库存
|
||||
inventory_result = await db_session.execute(
|
||||
select(Inventory)
|
||||
.where(Inventory.product_id == material.id)
|
||||
.where(Inventory.warehouse_id == 1)
|
||||
)
|
||||
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} 没有库存记录")
|
||||
|
||||
# 记录物料消耗
|
||||
movement = StockMovement(
|
||||
product_id=material.id,
|
||||
warehouse_id=1, # 默认仓库
|
||||
quantity=-item.quantity,
|
||||
before_quantity=before_qty,
|
||||
after_quantity=after_qty,
|
||||
movement_type="consumption",
|
||||
reference_type="sales_order",
|
||||
reference_id=order.id,
|
||||
unit_price=material.cost_price,
|
||||
total_amount=cost,
|
||||
operator_id=current_user.id,
|
||||
remark=item.remark
|
||||
)
|
||||
db_session.add(movement)
|
||||
|
||||
# 更新订单的实际物料成本
|
||||
order.actual_material_cost = total_cost
|
||||
|
||||
await db_session.commit()
|
||||
await db_session.refresh(order)
|
||||
|
||||
return {
|
||||
"message": "物料消耗记录保存成功",
|
||||
"total_cost": total_cost,
|
||||
"order": _build_sales_order_response(order, customer.name)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{order_id}/production-plan", response_model=SalesOrderProductionPlanResponse)
|
||||
async def get_sales_order_production_plan(
|
||||
order_id: int,
|
||||
|
||||
@@ -39,7 +39,6 @@ class ProductResponse(BaseModel):
|
||||
class ProductMaterialItemUpdate(BaseModel):
|
||||
material_id: int
|
||||
quantity: float
|
||||
loss_rate: float = 0
|
||||
|
||||
|
||||
class ProductBOMUpdate(BaseModel):
|
||||
@@ -51,7 +50,6 @@ class ProductMaterialItemResponse(BaseModel):
|
||||
material_sku: str
|
||||
material_name: str
|
||||
quantity: float
|
||||
loss_rate: float
|
||||
unit_cost: float
|
||||
line_cost: float
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ class PurchaseOrderResponse(BaseModel):
|
||||
paid_amount: float
|
||||
remark: Optional[str]
|
||||
created_at: datetime
|
||||
received_date: Optional[datetime]
|
||||
paid_date: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
Reference in New Issue
Block a user