This commit is contained in:
2026-04-19 23:41:35 +08:00
parent 7660cda647
commit ef257e7120
31 changed files with 6317 additions and 1332 deletions
+41 -38
View File
@@ -14,6 +14,7 @@ from sqlalchemy import select, func, delete, update
from typing import Optional, List
from math import ceil
from pydantic import BaseModel, Field
from decimal import Decimal
from database.database import get_db_session
from services.auth_service import get_current_active_user
@@ -58,10 +59,10 @@ def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesO
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
planned_material_cost=round(float(order.planned_material_cost or 0), 4),
actual_material_cost=round(float(order.actual_material_cost or 0), 4),
total_amount=order.total_amount,
received_amount=order.received_amount,
planned_material_cost=Decimal(str(order.planned_material_cost or 0)),
actual_material_cost=Decimal(str(order.actual_material_cost or 0)),
total_amount=Decimal(str(order.total_amount or 0)),
received_amount=Decimal(str(order.received_amount or 0)),
remark=order.remark,
created_at=order.created_at
)
@@ -89,10 +90,10 @@ async def _build_sales_order_detail_response(
status=order.status,
production_status=order.production_status or "not_started",
production_no=order.production_no,
planned_material_cost=round(float(order.planned_material_cost or 0), 4),
actual_material_cost=round(float(order.actual_material_cost or 0), 4),
total_amount=order.total_amount,
received_amount=order.received_amount,
planned_material_cost=Decimal(str(order.planned_material_cost or 0)),
actual_material_cost=Decimal(str(order.actual_material_cost or 0)),
total_amount=Decimal(str(order.total_amount or 0)),
received_amount=Decimal(str(order.received_amount or 0)),
remark=order.remark,
created_at=order.created_at,
items=[
@@ -101,8 +102,8 @@ async def _build_sales_order_detail_response(
product_id=item.product_id,
quantity=item.quantity,
delivered_quantity=item.delivered_quantity,
unit_price=item.unit_price,
amount=item.amount,
unit_price=Decimal(str(item.unit_price or 0)),
amount=Decimal(str(item.amount or 0)),
remark=item.remark
) for item in items
]
@@ -152,12 +153,12 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
for order_item in order_items:
bom_items = bom_by_finished_id.get(int(order_item.product_id)) or []
for bom, material in bom_items:
qty = float(order_item.quantity) * float(bom.quantity or 0) * (1 + float(bom.loss_rate or 0))
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": 0.0})
qty = Decimal(str(order_item.quantity)) * Decimal(str(bom.quantity or 0)) * (1 + Decimal(str(bom.loss_rate or 0)))
entry = required_qty_map.setdefault(material.id, {"material": material, "required_qty": Decimal("0")})
entry["required_qty"] += qty
if not required_qty_map:
return [], 0
return [], Decimal("0")
material_ids = list(required_qty_map.keys())
stock_result = await db_session.execute(
@@ -165,28 +166,28 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
.where(Inventory.product_id.in_(material_ids))
.group_by(Inventory.product_id)
)
stock_map = {row[0]: int(row[1] or 0) for row in stock_result.all()}
stock_map = {row[0]: Decimal(str(row[1] or 0)) for row in stock_result.all()}
plan_items = []
planned_material_cost = 0.0
planned_material_cost = Decimal("0")
for material_id, entry in required_qty_map.items():
material = entry["material"]
required_qty = int(ceil(entry["required_qty"]))
available_qty = stock_map.get(material_id, 0)
shortage_qty = max(required_qty - available_qty, 0)
unit_cost = float(material.cost_price or 0)
required_cost = required_qty * unit_cost
available_qty = stock_map.get(material_id, Decimal("0"))
shortage_qty = max(required_qty - int(available_qty), 0)
unit_cost = Decimal(str(material.cost_price or 0))
required_cost = Decimal(str(required_qty)) * unit_cost
planned_material_cost += required_cost
plan_items.append(
ProductionMaterialPlanItemResponse(
material_id=material.id,
material_sku=material.sku,
material_name=material.name,
required_quantity=required_qty,
required_quantity=Decimal(str(required_qty)),
available_quantity=available_qty,
shortage_quantity=shortage_qty,
unit_cost=round(unit_cost, 4),
required_cost=round(required_cost, 4),
shortage_quantity=Decimal(str(shortage_qty)),
unit_cost=unit_cost,
required_cost=required_cost,
)
)
@@ -255,7 +256,7 @@ async def _issue_materials_for_order_creation(
reference_id=order.id,
reference_no=production_no,
unit_price=item.unit_cost,
total_amount=round(float(total_amount), 4),
total_amount=Decimal(str(total_amount)),
remark=f"销售单{order.order_no}创建时自动扣减物料",
operator_id=current_user.id
)
@@ -264,8 +265,8 @@ async def _issue_materials_for_order_creation(
order.production_no = production_no
order.production_status = "material_issued"
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
order.planned_material_cost = planned_material_cost
order.actual_material_cost = actual_material_cost
order.status = "manufacturing"
return movement_count, planned_material_cost, actual_material_cost
@@ -543,8 +544,10 @@ async def consume_materials(
"""记录销售订单的物料消耗"""
order, customer = await _get_sales_order_with_customer(db_session, order_id)
default_warehouse = await _get_default_warehouse(db_session)
# 计算总物料成本
total_cost = 0
total_cost = Decimal("0")
# 处理每个物料消耗项
for item in request.items:
@@ -556,14 +559,14 @@ async def consume_materials(
raise HTTPException(status_code=400, detail=f"只能消耗物料类型的产品: {material.name}")
# 计算成本
cost = material.cost_price * item.quantity
cost = Decimal(str(material.cost_price or 0)) * item.quantity
total_cost += cost
# 更新物料库存
inventory_result = await db_session.execute(
select(Inventory)
.where(Inventory.product_id == material.id)
.where(Inventory.warehouse_id == 1)
.where(Inventory.warehouse_id == default_warehouse.id)
)
inventory = inventory_result.scalar()
if inventory:
@@ -578,7 +581,7 @@ async def consume_materials(
# 记录物料消耗
movement = StockMovement(
product_id=material.id,
warehouse_id=1, # 默认仓库
warehouse_id=default_warehouse.id,
quantity=-item.quantity,
before_quantity=before_qty,
after_quantity=after_qty,
@@ -620,7 +623,7 @@ async def get_sales_order_production_plan(
order_no=order.order_no,
customer_name=customer.name,
production_no=production_no,
planned_material_cost=round(float(planned_material_cost), 4),
planned_material_cost=planned_material_cost,
items=plan_items,
)
@@ -688,7 +691,7 @@ async def issue_sales_order_materials(
reference_id=order.id,
reference_no=production_no,
unit_price=item.unit_cost,
total_amount=round(float(total_amount), 4),
total_amount=Decimal(str(total_amount)),
remark=payload.remark or f"销售单{order.order_no}按单生产领料",
operator_id=current_user.id
)
@@ -697,22 +700,22 @@ async def issue_sales_order_materials(
order.production_no = production_no
order.production_status = "material_issued"
order.planned_material_cost = round(float(planned_material_cost), 4)
order.actual_material_cost = round(float(actual_material_cost), 4)
order.planned_material_cost = planned_material_cost
order.actual_material_cost = actual_material_cost
if order.status == "draft":
order.status = "manufacturing"
await db_session.commit()
cost_deviation = round(float(actual_material_cost - planned_material_cost), 4)
cost_deviation_rate = round((cost_deviation / planned_material_cost), 6) if planned_material_cost > 1e-9 else 0.0
cost_deviation = actual_material_cost - planned_material_cost
cost_deviation_rate = (cost_deviation / planned_material_cost) if planned_material_cost > Decimal("1e-9") else Decimal("0")
return SalesOrderIssueResponse(
sales_order_id=order.id,
order_no=order.order_no,
production_no=production_no,
movement_count=movement_count,
planned_material_cost=round(float(planned_material_cost), 4),
actual_material_cost=round(float(actual_material_cost), 4),
planned_material_cost=planned_material_cost,
actual_material_cost=actual_material_cost,
cost_deviation=cost_deviation,
cost_deviation_rate=cost_deviation_rate,
production_status=order.production_status,