进销存系统重构
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
路由前缀: /api/inventory
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional, List
|
||||
@@ -15,7 +15,7 @@ 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, Product, Warehouse, Inventory
|
||||
from .schemas import InventoryResponse
|
||||
from .schemas import InventoryResponse, InventoryCreate, InventoryUpdate
|
||||
|
||||
router = APIRouter(prefix="/inventory", tags=["库存管理"])
|
||||
|
||||
@@ -64,3 +64,129 @@ async def list_inventory(
|
||||
))
|
||||
|
||||
return inventory_list
|
||||
|
||||
|
||||
@router.post("", response_model=InventoryResponse, status_code=201)
|
||||
async def create_inventory(
|
||||
payload: InventoryCreate,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
if payload.quantity < 0 or payload.locked_quantity < 0:
|
||||
raise HTTPException(status_code=400, detail="库存数量不能为负数")
|
||||
if payload.locked_quantity > payload.quantity:
|
||||
raise HTTPException(status_code=400, detail="锁定数量不能大于库存数量")
|
||||
|
||||
product_result = await db_session.execute(
|
||||
select(Product).where(Product.id == payload.product_id, Product.is_active == True)
|
||||
)
|
||||
product = product_result.scalar_one_or_none()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="物料不存在")
|
||||
if product.item_type != "material":
|
||||
raise HTTPException(status_code=400, detail="库存仅支持物料")
|
||||
|
||||
warehouse_result = await db_session.execute(
|
||||
select(Warehouse).where(Warehouse.id == payload.warehouse_id, Warehouse.is_active == True)
|
||||
)
|
||||
warehouse = warehouse_result.scalar_one_or_none()
|
||||
if not warehouse:
|
||||
raise HTTPException(status_code=404, detail="仓库不存在")
|
||||
|
||||
exists_result = await db_session.execute(
|
||||
select(Inventory).where(
|
||||
Inventory.product_id == payload.product_id,
|
||||
Inventory.warehouse_id == payload.warehouse_id
|
||||
)
|
||||
)
|
||||
if exists_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="该仓库已存在该物料库存记录")
|
||||
|
||||
inventory = Inventory(
|
||||
product_id=payload.product_id,
|
||||
warehouse_id=payload.warehouse_id,
|
||||
quantity=payload.quantity,
|
||||
locked_quantity=payload.locked_quantity,
|
||||
batch_number=payload.batch_number,
|
||||
location=payload.location
|
||||
)
|
||||
db_session.add(inventory)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(inventory)
|
||||
|
||||
return InventoryResponse(
|
||||
id=inventory.id,
|
||||
product_id=product.id,
|
||||
product_name=product.name,
|
||||
product_sku=product.sku,
|
||||
warehouse_id=warehouse.id,
|
||||
warehouse_name=warehouse.name,
|
||||
quantity=inventory.quantity,
|
||||
locked_quantity=inventory.locked_quantity,
|
||||
available_quantity=inventory.available_quantity
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{inventory_id}", response_model=InventoryResponse)
|
||||
async def update_inventory(
|
||||
inventory_id: int,
|
||||
payload: InventoryUpdate,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db_session.execute(
|
||||
select(Inventory, Product, Warehouse)
|
||||
.join(Product, Inventory.product_id == Product.id)
|
||||
.join(Warehouse, Inventory.warehouse_id == Warehouse.id)
|
||||
.where(Inventory.id == inventory_id)
|
||||
.where(Product.item_type == "material")
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="库存记录不存在")
|
||||
inventory, product, warehouse = row
|
||||
|
||||
if payload.quantity is not None:
|
||||
if payload.quantity < 0:
|
||||
raise HTTPException(status_code=400, detail="库存数量不能为负数")
|
||||
inventory.quantity = payload.quantity
|
||||
if payload.locked_quantity is not None:
|
||||
if payload.locked_quantity < 0:
|
||||
raise HTTPException(status_code=400, detail="锁定数量不能为负数")
|
||||
inventory.locked_quantity = payload.locked_quantity
|
||||
if inventory.locked_quantity > inventory.quantity:
|
||||
raise HTTPException(status_code=400, detail="锁定数量不能大于库存数量")
|
||||
|
||||
if payload.batch_number is not None:
|
||||
inventory.batch_number = payload.batch_number
|
||||
if payload.location is not None:
|
||||
inventory.location = payload.location
|
||||
|
||||
await db_session.commit()
|
||||
await db_session.refresh(inventory)
|
||||
return InventoryResponse(
|
||||
id=inventory.id,
|
||||
product_id=product.id,
|
||||
product_name=product.name,
|
||||
product_sku=product.sku,
|
||||
warehouse_id=warehouse.id,
|
||||
warehouse_name=warehouse.name,
|
||||
quantity=inventory.quantity,
|
||||
locked_quantity=inventory.locked_quantity,
|
||||
available_quantity=inventory.available_quantity
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{inventory_id}")
|
||||
async def delete_inventory(
|
||||
inventory_id: int,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db_session.execute(select(Inventory).where(Inventory.id == inventory_id))
|
||||
inventory = result.scalar_one_or_none()
|
||||
if not inventory:
|
||||
raise HTTPException(status_code=404, detail="库存记录不存在")
|
||||
await db_session.delete(inventory)
|
||||
await db_session.commit()
|
||||
return {"message": "库存记录已删除"}
|
||||
|
||||
@@ -15,13 +15,148 @@ 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, PurchaseOrder, PurchaseOrderItem
|
||||
from .schemas import PurchaseOrderCreate, PurchaseOrderResponse
|
||||
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}")
|
||||
item = PurchaseOrderItem(
|
||||
order_id=order.id,
|
||||
product_id=item_data.product_id,
|
||||
quantity=item_data.quantity,
|
||||
unit_price=item_data.unit_price,
|
||||
amount=item_data.quantity * item_data.unit_price,
|
||||
remark=item_data.remark
|
||||
)
|
||||
db_session.add(item)
|
||||
total_amount += item.amount
|
||||
return total_amount
|
||||
|
||||
|
||||
@router.get("", response_model=List[PurchaseOrderResponse])
|
||||
async def list_purchase_orders(
|
||||
status: Optional[str] = None,
|
||||
@@ -44,18 +179,7 @@ async def list_purchase_orders(
|
||||
|
||||
orders = []
|
||||
for order, supplier in result.all():
|
||||
orders.append(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
|
||||
))
|
||||
orders.append(_build_purchase_order_response(order, supplier.name))
|
||||
|
||||
return orders
|
||||
|
||||
@@ -77,43 +201,163 @@ async def create_purchase_order(
|
||||
db_session.add(order)
|
||||
await db_session.flush()
|
||||
|
||||
total_amount = 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}")
|
||||
item = PurchaseOrderItem(
|
||||
order_id=order.id,
|
||||
product_id=item_data.product_id,
|
||||
quantity=item_data.quantity,
|
||||
unit_price=item_data.unit_price,
|
||||
amount=item_data.quantity * item_data.unit_price,
|
||||
remark=item_data.remark
|
||||
)
|
||||
db_session.add(item)
|
||||
total_amount += item.amount
|
||||
|
||||
order.total_amount = total_amount
|
||||
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 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
|
||||
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 = "draft"
|
||||
|
||||
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.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)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, delete
|
||||
from typing import Optional, List
|
||||
from math import ceil
|
||||
|
||||
@@ -30,6 +30,8 @@ from models.database import (
|
||||
from .schemas import (
|
||||
SalesOrderCreate,
|
||||
SalesOrderResponse,
|
||||
SalesOrderDetailResponse,
|
||||
SalesOrderItemResponse,
|
||||
SalesOrderProductionPlanResponse,
|
||||
ProductionMaterialPlanItemResponse,
|
||||
SalesOrderIssueRequest,
|
||||
@@ -59,6 +61,45 @@ def _build_sales_order_response(order: SalesOrder, customer_name: str) -> SalesO
|
||||
)
|
||||
|
||||
|
||||
async def _build_sales_order_detail_response(
|
||||
db_session: AsyncSession,
|
||||
order: SalesOrder,
|
||||
customer_name: str
|
||||
) -> SalesOrderDetailResponse:
|
||||
items_result = await db_session.execute(
|
||||
select(SalesOrderItem).where(SalesOrderItem.order_id == order.id).order_by(SalesOrderItem.id.asc())
|
||||
)
|
||||
items = items_result.scalars().all()
|
||||
return SalesOrderDetailResponse(
|
||||
id=order.id,
|
||||
order_no=order.order_no,
|
||||
customer_id=order.customer_id,
|
||||
customer_name=customer_name,
|
||||
order_date=order.order_date,
|
||||
delivery_date=order.delivery_date,
|
||||
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,
|
||||
remark=order.remark,
|
||||
created_at=order.created_at,
|
||||
items=[
|
||||
SalesOrderItemResponse(
|
||||
id=item.id,
|
||||
product_id=item.product_id,
|
||||
quantity=item.quantity,
|
||||
delivered_quantity=item.delivered_quantity,
|
||||
unit_price=item.unit_price,
|
||||
amount=item.amount,
|
||||
remark=item.remark
|
||||
) for item in items
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def _get_sales_order_with_customer(
|
||||
db_session: AsyncSession,
|
||||
order_id: int,
|
||||
@@ -143,6 +184,160 @@ async def _build_material_plan(db_session: AsyncSession, order: SalesOrder) -> t
|
||||
return plan_items, planned_material_cost
|
||||
|
||||
|
||||
async def _get_default_warehouse(db_session: AsyncSession) -> Warehouse:
|
||||
warehouse_result = await db_session.execute(
|
||||
select(Warehouse).where(Warehouse.is_active == True).order_by(Warehouse.is_default.desc(), Warehouse.id.asc())
|
||||
)
|
||||
warehouse = warehouse_result.scalars().first()
|
||||
if not warehouse:
|
||||
raise HTTPException(status_code=400, detail="未配置可用仓库,无法自动扣减物料")
|
||||
return warehouse
|
||||
|
||||
|
||||
async def _issue_materials_for_order_creation(
|
||||
db_session: AsyncSession,
|
||||
order: SalesOrder,
|
||||
current_user: User
|
||||
) -> tuple[int, float, float]:
|
||||
warehouse = await _get_default_warehouse(db_session)
|
||||
plan_items, planned_material_cost = await _build_material_plan(db_session, order)
|
||||
if not plan_items:
|
||||
raise HTTPException(status_code=400, detail="销售单产品未配置物料BOM,无法自动扣减物料")
|
||||
shortage_items = [item for item in plan_items if item.shortage_quantity > 0]
|
||||
if shortage_items:
|
||||
shortage_text = ",".join([f"{item.material_name} 缺 {item.shortage_quantity}" for item in shortage_items])
|
||||
raise HTTPException(status_code=400, detail=f"物料库存不足:{shortage_text}")
|
||||
|
||||
production_no = order.production_no or generate_order_no("WO")
|
||||
actual_material_cost = 0.0
|
||||
movement_count = 0
|
||||
|
||||
for item in plan_items:
|
||||
inv_result = await db_session.execute(
|
||||
select(Inventory)
|
||||
.where(Inventory.product_id == item.material_id)
|
||||
.where(Inventory.warehouse_id == warehouse.id)
|
||||
)
|
||||
inventory = inv_result.scalar_one_or_none()
|
||||
if not inventory or inventory.quantity < item.required_quantity:
|
||||
raise HTTPException(status_code=400, detail=f"{item.material_name} 在默认仓库库存不足")
|
||||
|
||||
before_qty = inventory.quantity
|
||||
inventory.quantity -= item.required_quantity
|
||||
after_qty = inventory.quantity
|
||||
total_amount = item.required_quantity * item.unit_cost
|
||||
actual_material_cost += total_amount
|
||||
|
||||
movement = StockMovement(
|
||||
product_id=item.material_id,
|
||||
warehouse_id=warehouse.id,
|
||||
movement_type="issue_to_production",
|
||||
quantity=item.required_quantity,
|
||||
before_quantity=before_qty,
|
||||
after_quantity=after_qty,
|
||||
reference_type="sales_order",
|
||||
reference_id=order.id,
|
||||
reference_no=production_no,
|
||||
unit_price=item.unit_cost,
|
||||
total_amount=round(float(total_amount), 4),
|
||||
remark=f"销售单{order.order_no}创建时自动扣减物料",
|
||||
operator_id=current_user.id
|
||||
)
|
||||
db_session.add(movement)
|
||||
movement_count += 1
|
||||
|
||||
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.status = "pending"
|
||||
|
||||
return movement_count, planned_material_cost, actual_material_cost
|
||||
|
||||
|
||||
async def _rollback_issued_materials(
|
||||
db_session: AsyncSession,
|
||||
order: SalesOrder,
|
||||
current_user: User
|
||||
):
|
||||
movement_result = await db_session.execute(
|
||||
select(StockMovement)
|
||||
.where(StockMovement.reference_type == "sales_order")
|
||||
.where(StockMovement.reference_id == order.id)
|
||||
.where(StockMovement.movement_type == "issue_to_production")
|
||||
.order_by(StockMovement.id.asc())
|
||||
)
|
||||
movements = movement_result.scalars().all()
|
||||
if not movements:
|
||||
return
|
||||
|
||||
for movement in movements:
|
||||
inv_result = await db_session.execute(
|
||||
select(Inventory)
|
||||
.where(Inventory.product_id == movement.product_id)
|
||||
.where(Inventory.warehouse_id == movement.warehouse_id)
|
||||
)
|
||||
inventory = inv_result.scalar_one_or_none()
|
||||
if not inventory:
|
||||
inventory = Inventory(
|
||||
product_id=movement.product_id,
|
||||
warehouse_id=movement.warehouse_id,
|
||||
quantity=0,
|
||||
locked_quantity=0
|
||||
)
|
||||
db_session.add(inventory)
|
||||
await db_session.flush()
|
||||
|
||||
before_qty = inventory.quantity
|
||||
inventory.quantity += movement.quantity
|
||||
after_qty = inventory.quantity
|
||||
|
||||
revert_movement = StockMovement(
|
||||
product_id=movement.product_id,
|
||||
warehouse_id=movement.warehouse_id,
|
||||
movement_type="return_from_production",
|
||||
quantity=movement.quantity,
|
||||
before_quantity=before_qty,
|
||||
after_quantity=after_qty,
|
||||
reference_type="sales_order",
|
||||
reference_id=order.id,
|
||||
reference_no=order.production_no or order.order_no,
|
||||
unit_price=movement.unit_price,
|
||||
total_amount=movement.total_amount,
|
||||
remark=f"销售单{order.order_no}变更/删除,自动回补物料",
|
||||
operator_id=current_user.id
|
||||
)
|
||||
db_session.add(revert_movement)
|
||||
|
||||
|
||||
async def _apply_order_items(
|
||||
db_session: AsyncSession,
|
||||
order: SalesOrder,
|
||||
order_data: SalesOrderCreate
|
||||
) -> 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 != "finished":
|
||||
raise HTTPException(status_code=400, detail=f"销售单仅允许成品: {product.name}")
|
||||
item = SalesOrderItem(
|
||||
order_id=order.id,
|
||||
product_id=item_data.product_id,
|
||||
quantity=item_data.quantity,
|
||||
unit_price=item_data.unit_price,
|
||||
amount=item_data.quantity * item_data.unit_price,
|
||||
remark=item_data.remark
|
||||
)
|
||||
db_session.add(item)
|
||||
total_amount += item.amount
|
||||
return total_amount
|
||||
|
||||
|
||||
@router.get("", response_model=List[SalesOrderResponse])
|
||||
async def list_sales_orders(
|
||||
status: Optional[str] = None,
|
||||
@@ -187,28 +382,8 @@ async def create_sales_order(
|
||||
db_session.add(order)
|
||||
await db_session.flush()
|
||||
|
||||
total_amount = 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 != "finished":
|
||||
raise HTTPException(status_code=400, detail=f"销售单仅允许成品: {product.name}")
|
||||
item = SalesOrderItem(
|
||||
order_id=order.id,
|
||||
product_id=item_data.product_id,
|
||||
quantity=item_data.quantity,
|
||||
unit_price=item_data.unit_price,
|
||||
amount=item_data.quantity * item_data.unit_price,
|
||||
remark=item_data.remark
|
||||
)
|
||||
db_session.add(item)
|
||||
total_amount += item.amount
|
||||
|
||||
order.total_amount = total_amount
|
||||
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()
|
||||
await db_session.refresh(order)
|
||||
|
||||
@@ -218,6 +393,59 @@ async def create_sales_order(
|
||||
return _build_sales_order_response(order, customer.name)
|
||||
|
||||
|
||||
@router.get("/{order_id}", response_model=SalesOrderDetailResponse)
|
||||
async def get_sales_order_detail(
|
||||
order_id: int,
|
||||
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)
|
||||
return await _build_sales_order_detail_response(db_session, order, customer.name)
|
||||
|
||||
|
||||
@router.put("/{order_id}", response_model=SalesOrderResponse)
|
||||
async def update_sales_order(
|
||||
order_id: int,
|
||||
order_data: SalesOrderCreate,
|
||||
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)
|
||||
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.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()
|
||||
await db_session.refresh(order)
|
||||
|
||||
customer_result = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
|
||||
updated_customer = customer_result.scalar_one_or_none()
|
||||
customer_name = updated_customer.name if updated_customer else "未知客户"
|
||||
return _build_sales_order_response(order, customer_name)
|
||||
|
||||
|
||||
@router.delete("/{order_id}")
|
||||
async def delete_sales_order(
|
||||
order_id: int,
|
||||
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)
|
||||
await _rollback_issued_materials(db_session, order, current_user)
|
||||
await db_session.delete(order)
|
||||
await db_session.commit()
|
||||
return {"message": "销售订单已删除"}
|
||||
|
||||
|
||||
@router.get("/{order_id}/production-plan", response_model=SalesOrderProductionPlanResponse)
|
||||
async def get_sales_order_production_plan(
|
||||
order_id: int,
|
||||
@@ -248,6 +476,8 @@ async def issue_sales_order_materials(
|
||||
order, _ = await _get_sales_order_with_customer(db_session, order_id)
|
||||
if order.production_status == "completed":
|
||||
raise HTTPException(status_code=400, detail="该销售单已完成生产")
|
||||
if order.production_status == "material_issued":
|
||||
raise HTTPException(status_code=400, detail="该销售单已自动扣减过物料")
|
||||
|
||||
warehouse_result = await db_session.execute(
|
||||
select(Warehouse).where(Warehouse.id == payload.warehouse_id, Warehouse.is_active == True)
|
||||
|
||||
@@ -9,17 +9,23 @@ from .product_schemas import (
|
||||
from .supplier_schemas import SupplierCreate, SupplierResponse
|
||||
from .customer_schemas import CustomerCreate, CustomerResponse
|
||||
from .warehouse_schemas import WarehouseCreate, WarehouseResponse
|
||||
from .inventory_schemas import InventoryResponse
|
||||
from .inventory_schemas import InventoryResponse, InventoryCreate, InventoryUpdate
|
||||
from .stock_movement_schemas import StockMovementCreate, StockMovementResponse
|
||||
from .purchase_order_schemas import (
|
||||
PurchaseOrderCreate,
|
||||
PurchaseOrderResponse,
|
||||
PurchaseOrderItemCreate
|
||||
PurchaseOrderItemCreate,
|
||||
PurchaseOrderItemResponse,
|
||||
PurchaseOrderDetailResponse,
|
||||
PurchaseOrderReceiveItem,
|
||||
PurchaseOrderReceiveRequest
|
||||
)
|
||||
from .sales_order_schemas import (
|
||||
SalesOrderCreate,
|
||||
SalesOrderResponse,
|
||||
SalesOrderItemCreate,
|
||||
SalesOrderItemResponse,
|
||||
SalesOrderDetailResponse,
|
||||
ProductionMaterialPlanItemResponse,
|
||||
SalesOrderProductionPlanResponse,
|
||||
SalesOrderIssueRequest,
|
||||
@@ -47,10 +53,11 @@ __all__ = [
|
||||
"SupplierCreate", "SupplierResponse",
|
||||
"CustomerCreate", "CustomerResponse",
|
||||
"WarehouseCreate", "WarehouseResponse",
|
||||
"InventoryResponse",
|
||||
"InventoryResponse", "InventoryCreate", "InventoryUpdate",
|
||||
"StockMovementCreate", "StockMovementResponse",
|
||||
"PurchaseOrderCreate", "PurchaseOrderResponse", "PurchaseOrderItemCreate",
|
||||
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate",
|
||||
"PurchaseOrderItemResponse", "PurchaseOrderDetailResponse", "PurchaseOrderReceiveItem", "PurchaseOrderReceiveRequest",
|
||||
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate", "SalesOrderItemResponse", "SalesOrderDetailResponse",
|
||||
"ProductionMaterialPlanItemResponse", "SalesOrderProductionPlanResponse",
|
||||
"SalesOrderIssueRequest", "SalesOrderIssueResponse",
|
||||
"FinanceAllocationCreate", "FinanceTransactionCreate",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class InventoryResponse(BaseModel):
|
||||
@@ -14,3 +15,19 @@ class InventoryResponse(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InventoryCreate(BaseModel):
|
||||
product_id: int
|
||||
warehouse_id: int
|
||||
quantity: int = 0
|
||||
locked_quantity: int = 0
|
||||
batch_number: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
class InventoryUpdate(BaseModel):
|
||||
quantity: Optional[int] = None
|
||||
locked_quantity: Optional[int] = None
|
||||
batch_number: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
|
||||
@@ -31,3 +31,31 @@ class PurchaseOrderResponse(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PurchaseOrderItemResponse(BaseModel):
|
||||
id: int
|
||||
product_id: int
|
||||
product_sku: str
|
||||
product_name: str
|
||||
quantity: int
|
||||
received_quantity: int
|
||||
unit_price: float
|
||||
amount: float
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class PurchaseOrderDetailResponse(PurchaseOrderResponse):
|
||||
supplier_id: int
|
||||
items: List[PurchaseOrderItemResponse]
|
||||
|
||||
|
||||
class PurchaseOrderReceiveItem(BaseModel):
|
||||
item_id: int
|
||||
receive_quantity: int
|
||||
|
||||
|
||||
class PurchaseOrderReceiveRequest(BaseModel):
|
||||
warehouse_id: Optional[int] = None
|
||||
items: List[PurchaseOrderReceiveItem]
|
||||
remark: Optional[str] = None
|
||||
|
||||
@@ -37,6 +37,21 @@ class SalesOrderResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SalesOrderItemResponse(BaseModel):
|
||||
id: int
|
||||
product_id: int
|
||||
quantity: int
|
||||
delivered_quantity: int
|
||||
unit_price: float
|
||||
amount: float
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class SalesOrderDetailResponse(SalesOrderResponse):
|
||||
customer_id: int
|
||||
items: List[SalesOrderItemResponse]
|
||||
|
||||
|
||||
class ProductionMaterialPlanItemResponse(BaseModel):
|
||||
material_id: int
|
||||
material_sku: str
|
||||
|
||||
+512
-99
@@ -1487,6 +1487,9 @@ const InventoryView = {
|
||||
supplierProductStatement: [],
|
||||
products: [],
|
||||
materials: [],
|
||||
purchaseOrders: [],
|
||||
purchaseWarehouseId: null,
|
||||
purchaseReceiveItems: [],
|
||||
productionOrders: [],
|
||||
productionPlan: null,
|
||||
productionWarehouseId: null,
|
||||
@@ -1503,22 +1506,6 @@ const InventoryView = {
|
||||
form: {}
|
||||
});
|
||||
|
||||
const inboundMovementOptions = [
|
||||
{ value: 'purchase_in', label: '采购入库' },
|
||||
{ value: 'return_from_production', label: '生产退料入库' },
|
||||
{ value: 'outsource_return', label: '外协回库' },
|
||||
{ value: 'finish_in', label: '完工入库' },
|
||||
{ value: 'in', label: '其他入库' }
|
||||
];
|
||||
|
||||
const outboundMovementOptions = [
|
||||
{ value: 'issue_to_production', label: '生产领料出库' },
|
||||
{ value: 'outsource_send', label: '外协发料出库' },
|
||||
{ value: 'shipment_out', label: '销售出库' },
|
||||
{ value: 'scrap_out', label: '报废出库' },
|
||||
{ value: 'out', label: '其他出库' }
|
||||
];
|
||||
|
||||
const getMovementTypeLabel = (movementType) => {
|
||||
const movementLabelMap = {
|
||||
in: '其他入库',
|
||||
@@ -1643,6 +1630,25 @@ const InventoryView = {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPurchaseOrders = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
const [orders, warehouses] = await Promise.all([
|
||||
apiRequest('/api/purchase-orders?limit=100'),
|
||||
apiRequest('/api/warehouses')
|
||||
]);
|
||||
state.purchaseOrders = orders || [];
|
||||
state.warehouses = warehouses || [];
|
||||
if (!state.purchaseWarehouseId) {
|
||||
state.purchaseWarehouseId = state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null;
|
||||
}
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载采购订单');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadCustomers = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
@@ -1734,6 +1740,7 @@ const InventoryView = {
|
||||
case 'customers': loadCustomers(); break;
|
||||
case 'inventory': loadInventory(); break;
|
||||
case 'movements': loadMovements(); break;
|
||||
case 'purchases': loadPurchaseOrders(); break;
|
||||
case 'production': loadProductionOrders(); break;
|
||||
case 'finance': loadFinance(); break;
|
||||
}
|
||||
@@ -1773,16 +1780,65 @@ const InventoryView = {
|
||||
state.modalType = type;
|
||||
state.editingItem = item;
|
||||
if (item) {
|
||||
if (type === 'salesOrder') {
|
||||
await loadCustomers();
|
||||
await loadProducts();
|
||||
const detail = await apiRequest(`/api/sales-orders/${item.id}`);
|
||||
state.form = {
|
||||
customer_id: detail.customer_id,
|
||||
delivery_date: detail.delivery_date ? new Date(detail.delivery_date).toISOString().slice(0, 16) : '',
|
||||
remark: detail.remark || '',
|
||||
items: (detail.items || []).map(line => ({
|
||||
product_id: line.product_id,
|
||||
quantity: line.quantity,
|
||||
unit_price: line.unit_price,
|
||||
remark: line.remark || ''
|
||||
}))
|
||||
};
|
||||
} else if (type === 'purchaseOrder') {
|
||||
await loadSuppliers();
|
||||
await loadMaterials();
|
||||
const detail = await apiRequest(`/api/purchase-orders/${item.id}`);
|
||||
state.form = {
|
||||
supplier_id: detail.supplier_id,
|
||||
expected_date: detail.expected_date ? new Date(detail.expected_date).toISOString().slice(0, 16) : '',
|
||||
remark: detail.remark || '',
|
||||
items: (detail.items || []).map(line => ({
|
||||
product_id: line.product_id,
|
||||
quantity: line.quantity,
|
||||
unit_price: line.unit_price,
|
||||
remark: line.remark || ''
|
||||
}))
|
||||
};
|
||||
} else if (type === 'purchaseReceive') {
|
||||
await loadWarehouses();
|
||||
const detail = await apiRequest(`/api/purchase-orders/${item.id}`);
|
||||
state.purchaseReceiveItems = (detail.items || [])
|
||||
.map(line => ({
|
||||
item_id: line.id,
|
||||
material_label: `${line.product_sku || line.product_id} - ${line.product_name || ''}`.trim(),
|
||||
remaining_quantity: Math.max((line.quantity || 0) - (line.received_quantity || 0), 0),
|
||||
receive_quantity: Math.max((line.quantity || 0) - (line.received_quantity || 0), 0)
|
||||
}))
|
||||
.filter(line => line.remaining_quantity > 0);
|
||||
state.form = {
|
||||
warehouse_id: state.purchaseWarehouseId || state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null,
|
||||
remark: ''
|
||||
};
|
||||
} else {
|
||||
state.form = { ...item };
|
||||
}
|
||||
} else {
|
||||
state.form = {};
|
||||
if (type === 'stockIn' || type === 'stockOut') {
|
||||
if (type === 'inventoryItem') {
|
||||
await ensureStockBaseData();
|
||||
state.form = {
|
||||
product_id: state.materials[0]?.id || null,
|
||||
warehouse_id: state.warehouses.find(w => w.is_default)?.id || state.warehouses[0]?.id || null,
|
||||
quantity: 1,
|
||||
movement_type: type === 'stockIn' ? 'purchase_in' : 'issue_to_production'
|
||||
quantity: 0,
|
||||
locked_quantity: 0,
|
||||
batch_number: '',
|
||||
location: ''
|
||||
};
|
||||
}
|
||||
if (type === 'product') {
|
||||
@@ -1795,6 +1851,36 @@ const InventoryView = {
|
||||
sale_price: 0
|
||||
};
|
||||
}
|
||||
if (type === 'salesOrder') {
|
||||
await loadCustomers();
|
||||
await loadProducts();
|
||||
state.form = {
|
||||
customer_id: state.customers[0]?.id || null,
|
||||
delivery_date: '',
|
||||
remark: '',
|
||||
items: [{
|
||||
product_id: state.products.find(p => p.item_type === 'finished')?.id || null,
|
||||
quantity: 1,
|
||||
unit_price: 0,
|
||||
remark: ''
|
||||
}]
|
||||
};
|
||||
}
|
||||
if (type === 'purchaseOrder') {
|
||||
await loadSuppliers();
|
||||
await loadMaterials();
|
||||
state.form = {
|
||||
supplier_id: state.suppliers[0]?.id || null,
|
||||
expected_date: '',
|
||||
remark: '',
|
||||
items: [{
|
||||
product_id: state.materials[0]?.id || null,
|
||||
quantity: 1,
|
||||
unit_price: 0,
|
||||
remark: ''
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
state.showModal = true;
|
||||
};
|
||||
@@ -1804,6 +1890,7 @@ const InventoryView = {
|
||||
state.modalType = '';
|
||||
state.editingItem = null;
|
||||
state.productBomItems = [];
|
||||
state.purchaseReceiveItems = [];
|
||||
state.form = {};
|
||||
};
|
||||
|
||||
@@ -1951,33 +2038,193 @@ const InventoryView = {
|
||||
}
|
||||
};
|
||||
|
||||
const stockIn = async () => {
|
||||
const saveInventoryItem = async () => {
|
||||
try {
|
||||
await apiRequest('/api/stock-movements', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...state.form, movement_type: state.form.movement_type || 'purchase_in' })
|
||||
if (state.editingItem) {
|
||||
await apiRequest(`/api/inventory/${state.editingItem.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
quantity: state.form.quantity,
|
||||
locked_quantity: state.form.locked_quantity,
|
||||
batch_number: state.form.batch_number,
|
||||
location: state.form.location
|
||||
})
|
||||
});
|
||||
addNotification('入库成功', 'success');
|
||||
addNotification('物料库存更新成功', 'success');
|
||||
} else {
|
||||
await apiRequest('/api/inventory', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(state.form)
|
||||
});
|
||||
addNotification('物料库存创建成功', 'success');
|
||||
}
|
||||
closeModal();
|
||||
loadInventory();
|
||||
loadMovements();
|
||||
} catch (e) {
|
||||
handleApiError(e, '入库操作');
|
||||
handleApiError(e, '保存物料库存');
|
||||
}
|
||||
};
|
||||
|
||||
const stockOut = async () => {
|
||||
const deleteInventoryItem = async (id) => {
|
||||
if (!confirm('确定要删除这个物料库存记录吗?')) return;
|
||||
try {
|
||||
await apiRequest('/api/stock-movements', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...state.form, movement_type: state.form.movement_type || 'issue_to_production' })
|
||||
await apiRequest(`/api/inventory/${id}`, { method: 'DELETE' });
|
||||
addNotification('物料库存已删除', 'success');
|
||||
loadInventory();
|
||||
} catch (e) {
|
||||
handleApiError(e, '删除物料库存');
|
||||
}
|
||||
};
|
||||
|
||||
const addSalesOrderItem = () => {
|
||||
state.form.items = state.form.items || [];
|
||||
state.form.items.push({
|
||||
product_id: state.products.find(p => p.item_type === 'finished')?.id || null,
|
||||
quantity: 1,
|
||||
unit_price: 0,
|
||||
remark: ''
|
||||
});
|
||||
addNotification('出库成功', 'success');
|
||||
};
|
||||
|
||||
const removeSalesOrderItem = (index) => {
|
||||
state.form.items.splice(index, 1);
|
||||
};
|
||||
|
||||
const saveSalesOrder = async () => {
|
||||
try {
|
||||
if (!state.form.items || !state.form.items.length) {
|
||||
addNotification('请至少添加一个成品明细', 'warning');
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
customer_id: state.form.customer_id,
|
||||
delivery_date: state.form.delivery_date ? new Date(state.form.delivery_date).toISOString() : null,
|
||||
remark: state.form.remark,
|
||||
items: state.form.items
|
||||
};
|
||||
if (state.editingItem) {
|
||||
await apiRequest(`/api/sales-orders/${state.editingItem.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
addNotification('销售订单更新成功', 'success');
|
||||
} else {
|
||||
await apiRequest('/api/sales-orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
addNotification('销售订单创建成功并已自动扣减物料', 'success');
|
||||
}
|
||||
closeModal();
|
||||
loadProductionOrders();
|
||||
loadInventory();
|
||||
loadMovements();
|
||||
} catch (e) {
|
||||
handleApiError(e, '出库操作');
|
||||
handleApiError(e, '保存销售订单');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSalesOrder = async (orderId) => {
|
||||
if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return;
|
||||
try {
|
||||
await apiRequest(`/api/sales-orders/${orderId}`, { method: 'DELETE' });
|
||||
addNotification('销售订单已删除并回补物料', 'success');
|
||||
if (state.productionPlan?.sales_order_id === orderId) {
|
||||
state.productionPlan = null;
|
||||
}
|
||||
loadProductionOrders();
|
||||
loadInventory();
|
||||
loadMovements();
|
||||
} catch (e) {
|
||||
handleApiError(e, '删除销售订单');
|
||||
}
|
||||
};
|
||||
|
||||
const addPurchaseOrderItem = () => {
|
||||
state.form.items = state.form.items || [];
|
||||
state.form.items.push({
|
||||
product_id: state.materials[0]?.id || null,
|
||||
quantity: 1,
|
||||
unit_price: 0,
|
||||
remark: ''
|
||||
});
|
||||
};
|
||||
|
||||
const removePurchaseOrderItem = (index) => {
|
||||
state.form.items.splice(index, 1);
|
||||
};
|
||||
|
||||
const savePurchaseOrder = async () => {
|
||||
try {
|
||||
if (!state.form.items || !state.form.items.length) {
|
||||
addNotification('请至少添加一个物料明细', 'warning');
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
supplier_id: state.form.supplier_id,
|
||||
expected_date: state.form.expected_date ? new Date(state.form.expected_date).toISOString() : null,
|
||||
remark: state.form.remark,
|
||||
items: state.form.items
|
||||
};
|
||||
if (state.editingItem) {
|
||||
await apiRequest(`/api/purchase-orders/${state.editingItem.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
addNotification('采购订单更新成功', 'success');
|
||||
} else {
|
||||
await apiRequest('/api/purchase-orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
addNotification('采购订单创建成功', 'success');
|
||||
}
|
||||
closeModal();
|
||||
loadPurchaseOrders();
|
||||
} catch (e) {
|
||||
handleApiError(e, '保存采购订单');
|
||||
}
|
||||
};
|
||||
|
||||
const deletePurchaseOrder = async (orderId) => {
|
||||
if (!confirm('确定删除这个采购订单吗?')) return;
|
||||
try {
|
||||
await apiRequest(`/api/purchase-orders/${orderId}`, { method: 'DELETE' });
|
||||
addNotification('采购订单已删除', 'success');
|
||||
loadPurchaseOrders();
|
||||
} catch (e) {
|
||||
handleApiError(e, '删除采购订单');
|
||||
}
|
||||
};
|
||||
|
||||
const receivePurchaseOrder = async () => {
|
||||
try {
|
||||
if (!state.editingItem?.id) return;
|
||||
const items = (state.purchaseReceiveItems || [])
|
||||
.filter(line => Number(line.receive_quantity) > 0)
|
||||
.map(line => ({
|
||||
item_id: line.item_id,
|
||||
receive_quantity: Number(line.receive_quantity)
|
||||
}));
|
||||
if (!items.length) {
|
||||
addNotification('请填写本次入库数量', 'warning');
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/api/purchase-orders/${state.editingItem.id}/receive`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
warehouse_id: state.form.warehouse_id || state.purchaseWarehouseId,
|
||||
items,
|
||||
remark: state.form.remark || ''
|
||||
})
|
||||
});
|
||||
addNotification('采购到货入库成功', 'success');
|
||||
closeModal();
|
||||
loadPurchaseOrders();
|
||||
loadInventory();
|
||||
loadMovements();
|
||||
} catch (e) {
|
||||
handleApiError(e, '采购到货入库');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2007,14 +2254,22 @@ const InventoryView = {
|
||||
deleteSupplier,
|
||||
saveCustomer,
|
||||
deleteCustomer,
|
||||
stockIn,
|
||||
stockOut,
|
||||
saveInventoryItem,
|
||||
deleteInventoryItem,
|
||||
addSalesOrderItem,
|
||||
removeSalesOrderItem,
|
||||
saveSalesOrder,
|
||||
deleteSalesOrder,
|
||||
addPurchaseOrderItem,
|
||||
removePurchaseOrderItem,
|
||||
savePurchaseOrder,
|
||||
deletePurchaseOrder,
|
||||
receivePurchaseOrder,
|
||||
loadPurchaseOrders,
|
||||
loadProductionOrders,
|
||||
loadOrderProductionPlan,
|
||||
issueOrderMaterials,
|
||||
refreshFinanceByPeriod,
|
||||
inboundMovementOptions,
|
||||
outboundMovementOptions,
|
||||
getMovementTypeLabel,
|
||||
getMovementBadgeClass
|
||||
};
|
||||
@@ -2030,6 +2285,7 @@ const InventoryView = {
|
||||
<button :class="['tab', { active: state.activeTab === 'dashboard' }]" @click="switchTab('dashboard')">仪表盘</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'products' }]" @click="switchTab('products')">产品</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'inventory' }]" @click="switchTab('inventory')">库存</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'purchases' }]" @click="switchTab('purchases')">采购</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'suppliers' }]" @click="switchTab('suppliers')">供应商</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'customers' }]" @click="switchTab('customers')">客户</button>
|
||||
<button :class="['tab', { active: state.activeTab === 'movements' }]" @click="switchTab('movements')">变动记录</button>
|
||||
@@ -2132,18 +2388,18 @@ const InventoryView = {
|
||||
|
||||
<div v-else-if="state.activeTab === 'inventory'">
|
||||
<div class="table-header">
|
||||
<button class="btn btn-primary" @click="openModal('stockIn')">📥 采购/完工入库</button>
|
||||
<button class="btn btn-secondary" @click="openModal('stockOut')" style="margin-left: 8px;">📤 领料/销售出库</button>
|
||||
<button class="btn btn-primary" @click="openModal('inventoryItem')">+ 新增物料库存</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>产品</th>
|
||||
<th>物料</th>
|
||||
<th>仓库</th>
|
||||
<th>数量</th>
|
||||
<th>可用</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -2153,6 +2409,59 @@ const InventoryView = {
|
||||
<td>{{ item.warehouse_name }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.available_quantity }}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button class="btn btn-sm btn-secondary" @click="openModal('inventoryItem', item)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" @click="deleteInventoryItem(item.id)">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="state.activeTab === 'purchases'">
|
||||
<div class="table-header" style="margin-bottom: 12px;">
|
||||
<button class="btn btn-primary" @click="openModal('purchaseOrder')">+ 新增采购订单</button>
|
||||
</div>
|
||||
<div class="table-container" style="margin-bottom: 16px;">
|
||||
<div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||
<label>到货仓库</label>
|
||||
<select v-model.number="state.purchaseWarehouseId" class="form-input" style="width:260px;">
|
||||
<option v-for="warehouse in state.warehouses" :key="'purchase-warehouse-' + warehouse.id" :value="warehouse.id">
|
||||
{{ warehouse.name }}{{ warehouse.is_default ? ' [默认]' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" @click="loadPurchaseOrders">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>采购单</th>
|
||||
<th>供应商</th>
|
||||
<th>状态</th>
|
||||
<th>总金额</th>
|
||||
<th>已付款</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="order in state.purchaseOrders" :key="'purchase-order-' + order.id">
|
||||
<td>{{ order.order_no }}</td>
|
||||
<td>{{ order.supplier_name }}</td>
|
||||
<td>{{ order.status }}</td>
|
||||
<td>{{ formatCurrency(order.total_amount || 0) }}</td>
|
||||
<td>{{ formatCurrency(order.paid_amount || 0) }}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button class="btn btn-sm btn-secondary" @click="openModal('purchaseOrder', order)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" @click="deletePurchaseOrder(order.id)">删除</button>
|
||||
<button class="btn btn-sm btn-primary" @click="openModal('purchaseReceive', order)">到货入库</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -2230,6 +2539,9 @@ const InventoryView = {
|
||||
</div>
|
||||
|
||||
<div v-else-if="state.activeTab === 'production'">
|
||||
<div class="table-header" style="margin-bottom: 12px;">
|
||||
<button class="btn btn-primary" @click="openModal('salesOrder')">+ 新增销售订单</button>
|
||||
</div>
|
||||
<div class="table-container" style="margin-bottom: 16px;">
|
||||
<div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||
<label>领料仓库</label>
|
||||
@@ -2264,8 +2576,9 @@ const InventoryView = {
|
||||
<td>{{ formatCurrency(order.actual_material_cost || 0) }}</td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button class="btn btn-sm btn-secondary" @click="openModal('salesOrder', order)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" @click="deleteSalesOrder(order.id)">删除</button>
|
||||
<button class="btn btn-sm btn-secondary" @click="loadOrderProductionPlan(order.id)">领料建议</button>
|
||||
<button class="btn btn-sm btn-primary" @click="issueOrderMaterials(order)">执行领料</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2532,7 +2845,7 @@ const InventoryView = {
|
||||
<div v-if="state.showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>{{ state.editingItem ? '编辑' : '新增' }}{{ state.modalType === 'product' ? '产品/物料' : state.modalType === 'productBom' ? '产品BOM' : state.modalType === 'supplier' ? '供应商' : state.modalType === 'customer' ? '客户' : state.modalType === 'stockIn' ? '入库' : '出库' }}</h3>
|
||||
<h3>{{ state.editingItem ? '编辑' : '新增' }}{{ state.modalType === 'product' ? '产品/物料' : state.modalType === 'productBom' ? '产品BOM' : state.modalType === 'inventoryItem' ? '物料库存' : state.modalType === 'salesOrder' ? '销售订单' : state.modalType === 'purchaseOrder' ? '采购订单' : state.modalType === 'purchaseReceive' ? '采购到货入库' : state.modalType === 'supplier' ? '供应商' : '客户' }}</h3>
|
||||
<button class="modal-close" @click="closeModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -2583,6 +2896,153 @@ const InventoryView = {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else-if="state.modalType === 'salesOrder'" @submit.prevent="saveSalesOrder">
|
||||
<div class="form-group">
|
||||
<label class="form-label">客户 *</label>
|
||||
<select v-model.number="state.form.customer_id" class="form-input" required>
|
||||
<option v-for="customer in state.customers" :key="'order-customer-' + customer.id" :value="customer.id">
|
||||
{{ customer.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">交付日期</label>
|
||||
<input v-model="state.form.delivery_date" type="datetime-local" class="form-input" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">备注</label>
|
||||
<input v-model="state.form.remark" class="form-input" placeholder="订单备注" />
|
||||
</div>
|
||||
<div class="table-header" style="margin-bottom: 8px;">
|
||||
<button type="button" class="btn btn-secondary" @click="addSalesOrderItem">+ 添加成品</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>成品</th>
|
||||
<th>数量</th>
|
||||
<th>单价</th>
|
||||
<th>备注</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(line, index) in state.form.items" :key="'sales-order-line-' + index">
|
||||
<td>
|
||||
<select v-model.number="line.product_id" class="form-input" required>
|
||||
<option v-for="product in state.products.filter(p => p.item_type === 'finished')" :key="'sales-order-product-' + product.id" :value="product.id">
|
||||
{{ product.sku }} - {{ product.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input v-model.number="line.quantity" type="number" min="1" class="form-input" required /></td>
|
||||
<td><input v-model.number="line.unit_price" type="number" min="0" step="0.01" class="form-input" required /></td>
|
||||
<td><input v-model="line.remark" class="form-input" placeholder="明细备注" /></td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger" @click="removeSalesOrderItem(index)">删除</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">保存订单</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else-if="state.modalType === 'purchaseOrder'" @submit.prevent="savePurchaseOrder">
|
||||
<div class="form-group">
|
||||
<label class="form-label">供应商 *</label>
|
||||
<select v-model.number="state.form.supplier_id" class="form-input" required>
|
||||
<option v-for="supplier in state.suppliers" :key="'purchase-supplier-' + supplier.id" :value="supplier.id">
|
||||
{{ supplier.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">预计到货</label>
|
||||
<input v-model="state.form.expected_date" type="datetime-local" class="form-input" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">备注</label>
|
||||
<input v-model="state.form.remark" class="form-input" placeholder="采购单备注" />
|
||||
</div>
|
||||
<div class="table-header" style="margin-bottom: 8px;">
|
||||
<button type="button" class="btn btn-secondary" @click="addPurchaseOrderItem">+ 添加物料</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>物料</th>
|
||||
<th>数量</th>
|
||||
<th>单价</th>
|
||||
<th>备注</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(line, index) in state.form.items" :key="'purchase-order-line-' + index">
|
||||
<td>
|
||||
<select v-model.number="line.product_id" class="form-input" required>
|
||||
<option v-for="material in state.materials" :key="'purchase-order-material-' + material.id" :value="material.id">
|
||||
{{ material.sku }} - {{ material.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input v-model.number="line.quantity" type="number" min="1" class="form-input" required /></td>
|
||||
<td><input v-model.number="line.unit_price" type="number" min="0" step="0.01" class="form-input" required /></td>
|
||||
<td><input v-model="line.remark" class="form-input" placeholder="明细备注" /></td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger" @click="removePurchaseOrderItem(index)">删除</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">保存采购单</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else-if="state.modalType === 'purchaseReceive'" @submit.prevent="receivePurchaseOrder">
|
||||
<div class="form-group">
|
||||
<label class="form-label">入库仓库 *</label>
|
||||
<select v-model.number="state.form.warehouse_id" class="form-input" required>
|
||||
<option v-for="warehouse in state.warehouses" :key="'purchase-receive-warehouse-' + warehouse.id" :value="warehouse.id">
|
||||
{{ warehouse.name }}{{ warehouse.is_default ? ' [默认]' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">备注</label>
|
||||
<input v-model="state.form.remark" class="form-input" placeholder="到货说明" />
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>物料</th>
|
||||
<th>明细ID</th>
|
||||
<th>剩余待入库</th>
|
||||
<th>本次入库</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="line in state.purchaseReceiveItems" :key="'purchase-receive-line-' + line.item_id">
|
||||
<td>{{ line.material_label }}</td>
|
||||
<td>{{ line.item_id }}</td>
|
||||
<td>{{ line.remaining_quantity }}</td>
|
||||
<td><input v-model.number="line.receive_quantity" type="number" min="0" :max="line.remaining_quantity" class="form-input" required /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">确认入库</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else-if="state.modalType === 'productBom'" @submit.prevent="saveProductBom">
|
||||
<div class="table-header" style="margin-bottom: 12px;">
|
||||
<button type="button" class="btn btn-secondary" @click="addBomItem">+ 添加物料</button>
|
||||
@@ -2675,8 +3135,7 @@ const InventoryView = {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 入库表单 -->
|
||||
<form v-else-if="state.modalType === 'stockIn'" @submit.prevent="stockIn">
|
||||
<form v-else-if="state.modalType === 'inventoryItem'" @submit.prevent="saveInventoryItem">
|
||||
<div class="form-group">
|
||||
<label class="form-label">物料 *</label>
|
||||
<select v-model.number="state.form.product_id" class="form-input" required>
|
||||
@@ -2695,71 +3154,25 @@ const InventoryView = {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">业务类型 *</label>
|
||||
<select v-model="state.form.movement_type" class="form-input" required>
|
||||
<option v-for="item in inboundMovementOptions" :key="'stockin-type-' + item.value" :value="item.value">{{ item.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">数量 *</label>
|
||||
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="入库数量" />
|
||||
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="库存数量" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">单价</label>
|
||||
<input v-model.number="state.form.unit_price" type="number" step="0.01" class="form-input" placeholder="采购单价" />
|
||||
<label class="form-label">锁定数量</label>
|
||||
<input v-model.number="state.form.locked_quantity" type="number" class="form-input" placeholder="锁定库存" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">备注</label>
|
||||
<input v-model="state.form.remark" class="form-input" placeholder="备注信息" />
|
||||
<label class="form-label">批次号</label>
|
||||
<input v-model="state.form.batch_number" class="form-input" placeholder="批次号(可选)" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">库位</label>
|
||||
<input v-model="state.form.location" class="form-input" placeholder="库位(可选)" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="!state.form.product_id || !state.form.warehouse_id">确认入库</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 出库表单 -->
|
||||
<form v-else-if="state.modalType === 'stockOut'" @submit.prevent="stockOut">
|
||||
<div class="form-group">
|
||||
<label class="form-label">物料 *</label>
|
||||
<select v-model.number="state.form.product_id" class="form-input" required>
|
||||
<option v-if="!state.materials.length" :value="null" disabled>暂无物料,请先新增物料</option>
|
||||
<option v-for="product in state.materials" :key="'stockout-product-' + product.id" :value="product.id">
|
||||
{{ product.sku }} - {{ product.name }}(ID: {{ product.id }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">仓库 *</label>
|
||||
<select v-model.number="state.form.warehouse_id" class="form-input" required>
|
||||
<option v-if="!state.warehouses.length" :value="null" disabled>暂无仓库,系统将自动创建默认仓库</option>
|
||||
<option v-for="warehouse in state.warehouses" :key="'stockout-warehouse-' + warehouse.id" :value="warehouse.id">
|
||||
{{ warehouse.name }}{{ warehouse.code ? ' (' + warehouse.code + ')' : '' }}{{ warehouse.is_default ? ' [默认]' : '' }}(ID: {{ warehouse.id }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">业务类型 *</label>
|
||||
<select v-model="state.form.movement_type" class="form-input" required>
|
||||
<option v-for="item in outboundMovementOptions" :key="'stockout-type-' + item.value" :value="item.value">{{ item.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">数量 *</label>
|
||||
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="出库数量" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">单价</label>
|
||||
<input v-model.number="state.form.unit_price" type="number" step="0.01" class="form-input" placeholder="销售单价" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">备注</label>
|
||||
<input v-model="state.form.remark" class="form-input" placeholder="备注信息" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="!state.form.product_id || !state.form.warehouse_id">确认出库</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="!state.form.product_id || !state.form.warehouse_id">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user