This commit is contained in:
2026-03-29 16:24:43 +08:00
parent 3228b7e28d
commit c72c47b102
6 changed files with 609 additions and 22 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ DB_USER=moldinsight
DB_PASSWORD=Qqs1996* DB_PASSWORD=Qqs1996*
# RustFS 对象存储配置 (S3v4 API) # RustFS 对象存储配置 (S3v4 API)
RUSTFS_ENDPOINT=http://szcjw:9000 RUSTFS_ENDPOINT=http://szcjw:8010
RUSTFS_ACCESS_KEY=1RlKXw7v3DAsFr4fLckt RUSTFS_ACCESS_KEY=1RlKXw7v3DAsFr4fLckt
RUSTFS_SECRET_KEY=KjWCHXZOh7GAtkLq0eQgNpMSmE6zw8Ddyiou21bB RUSTFS_SECRET_KEY=KjWCHXZOh7GAtkLq0eQgNpMSmE6zw8Ddyiou21bB
RUSTFS_TIMEOUT=30 RUSTFS_TIMEOUT=30
+35 -2
View File
@@ -205,7 +205,7 @@ async def create_purchase_order(
expected_date=order_data.expected_date, expected_date=order_data.expected_date,
remark=order_data.remark, remark=order_data.remark,
operator_id=current_user.id, operator_id=current_user.id,
status="draft" status="pending"
) )
db_session.add(order) db_session.add(order)
await db_session.flush() await db_session.flush()
@@ -255,7 +255,7 @@ async def update_purchase_order(
order.expected_date = order_data.expected_date order.expected_date = order_data.expected_date
order.remark = order_data.remark order.remark = order_data.remark
order.total_amount = await _apply_order_items(db_session, order, order_data) 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.commit()
await db_session.refresh(order) await db_session.refresh(order)
@@ -283,6 +283,39 @@ async def delete_purchase_order(
return {"message": "采购订单已删除"} 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
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) @router.post("/{order_id}/receive", response_model=PurchaseOrderDetailResponse)
async def receive_purchase_order( async def receive_purchase_order(
order_id: int, order_id: int,
+84 -1
View File
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, delete, update from sqlalchemy import select, func, delete, update
from typing import Optional, List from typing import Optional, List
from math import ceil from math import ceil
from pydantic import BaseModel, Field
from database.database import get_db_session from database.database import get_db_session
from services.auth_service import get_current_active_user 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), db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user) 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": if order.status == "delivered":
raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除") raise HTTPException(status_code=400, detail="已交付的销售订单禁止删除")
await _rollback_issued_materials(db_session, order, current_user) await _rollback_issued_materials(db_session, order, current_user)
@@ -522,6 +523,88 @@ async def delete_sales_order(
return {"message": "销售订单已删除"} 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) @router.get("/{order_id}/production-plan", response_model=SalesOrderProductionPlanResponse)
async def get_sales_order_production_plan( async def get_sales_order_production_plan(
order_id: int, order_id: int,
+5 -5
View File
@@ -653,8 +653,8 @@ class Inventory(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True) warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True)
quantity = Column(Integer, default=0) quantity = Column(Float, default=0)
locked_quantity = Column(Integer, default=0) locked_quantity = Column(Float, default=0)
batch_number = Column(String(50), nullable=True) batch_number = Column(String(50), nullable=True)
location = Column(String(100), nullable=True) location = Column(String(100), nullable=True)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
@@ -678,9 +678,9 @@ class StockMovement(Base):
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True) product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False) warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False)
movement_type = Column(String(20), nullable=False) movement_type = Column(String(20), nullable=False)
quantity = Column(Integer, nullable=False) quantity = Column(Float, nullable=False)
before_quantity = Column(Integer, default=0) before_quantity = Column(Float, default=0)
after_quantity = Column(Integer, default=0) after_quantity = Column(Float, default=0)
reference_type = Column(String(50), nullable=True) reference_type = Column(String(50), nullable=True)
reference_id = Column(Integer, nullable=True) reference_id = Column(Integer, nullable=True)
reference_no = Column(String(50), nullable=True) reference_no = Column(String(50), nullable=True)
+41
View File
@@ -2764,6 +2764,47 @@ select.form-input {
padding-top: var(--space-3); padding-top: var(--space-3);
} }
/* 物料消耗记录样式 */
.material-consumption-list {
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.material-consumption-header {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
background: var(--bg-secondary);
padding: 12px 16px;
font-weight: 600;
border-bottom: 1px solid var(--border);
}
.material-consumption-row {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.material-consumption-row:last-child {
border-bottom: none;
}
.material-consumption-total {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 12px 16px;
background: var(--bg-secondary);
font-weight: 600;
margin-top: 8px;
border-radius: 4px;
}
.material-consumption-item {
text-align: left;
}
.viewer-frame { .viewer-frame {
width: 100%; width: 100%;
height: 500px; height: 500px;
+443 -13
View File
@@ -1588,6 +1588,11 @@ const InventoryView = {
modalType: '', modalType: '',
editingItem: null, editingItem: null,
productBomItems: [], productBomItems: [],
materialConsumptionItems: [],
showMaterialConsumptionModal: false,
consumedMaterials: [],
restockItems: [],
showRestockModal: false,
form: {} form: {}
}); });
@@ -1620,17 +1625,25 @@ const InventoryView = {
const toApiDateTime = (value) => { const toApiDateTime = (value) => {
if (!value) return null; if (!value) return null;
const text = String(value).trim(); const text = String(value).trim();
// 提取日期部分,忽略时间部分
if (text.includes('T')) {
return text.split('T')[0];
}
if (text.includes(' ')) {
return text.split(' ')[0];
}
// 如果是只有日期部分的格式 (yyyy-MM-dd) // 如果是只有日期部分的格式 (yyyy-MM-dd)
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) { if (text.length === 10) {
return text; // 直接返回日期格式,后端 Pydantic 会自动处理 return text; // 直接返回日期格式,后端 Pydantic 会自动处理
} }
// 如果是带时间的格式,转换为 ISO 8601 格式 // 如果是日期对象
if (text.includes('T')) { if (value instanceof Date) {
return text.length === 16 ? `${text}:00` : text; const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, '0');
const day = String(value.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
} }
// 将空格格式转换为 ISO 格式 return text;
const isoText = text.replace(' ', 'T');
return isoText.length === 16 ? `${isoText}:00` : isoText;
}; };
const toNativeValue = (value) => { const toNativeValue = (value) => {
@@ -1752,6 +1765,20 @@ const InventoryView = {
return 'badge-warning'; return 'badge-warning';
}; };
const getPurchaseOrderStatusLabel = (status) => {
const statusMap = {
draft: '已下单',
pending: '已下单',
received: '已收货',
paid: '已付款'
};
return statusMap[status] || status;
};
const isPurchaseOrderLocked = (status) => {
return ['received', 'paid'].includes(status);
};
const loadDashboard = async () => { const loadDashboard = async () => {
state.loading = true; state.loading = true;
try { try {
@@ -2108,6 +2135,14 @@ const InventoryView = {
await loadCustomers(); await loadCustomers();
await loadFinishedProducts(); await loadFinishedProducts();
const detail = await apiRequest(`/api/sales-orders/${item.id}`); const detail = await apiRequest(`/api/sales-orders/${item.id}`);
// 加载已消耗的物料
const movements = await apiRequest(`/api/stock-movements?reference_type=sales_order&reference_id=${item.id}&movement_type=consumption`);
state.consumedMaterials = (movements || []).map(movement => ({
material_name: movement.product_name || movement.product_sku || '未知物料',
quantity: Math.abs(movement.quantity),
unit_price: movement.unit_price || 0,
amount: movement.total_amount || 0
}));
state.form = { state.form = {
customer_id: detail.customer_id, customer_id: detail.customer_id,
delivery_date: toPickerValue(detail.delivery_date), delivery_date: toPickerValue(detail.delivery_date),
@@ -2236,6 +2271,171 @@ const InventoryView = {
}); });
}; };
const openMaterialConsumptionModal = async () => {
// 确保是在销售订单编辑或新增页面
if (state.modalType !== 'salesOrder') {
addNotification('请先打开销售订单编辑页面', 'warning');
return;
}
// 加载物料和仓库数据
await loadMaterials();
await loadWarehouses();
// 初始化物料消耗列表
state.materialConsumptionItems = [];
// 打开物料消耗模态框
state.showMaterialConsumptionModal = true;
};
const addMaterialConsumptionItem = () => {
state.materialConsumptionItems.push({
material_id: state.materials[0]?.id || null,
quantity: 1,
remark: ''
});
};
const removeMaterialConsumptionItem = (index) => {
state.materialConsumptionItems.splice(index, 1);
};
const saveMaterialConsumption = async () => {
// 确保是在销售订单页面
if (state.modalType !== 'salesOrder') {
addNotification('请先打开销售订单编辑页面', 'warning');
return;
}
// 如果是新增订单,先保存订单再添加物料消耗
if (!state.editingItem) {
// 关闭物料消耗模态框
state.showMaterialConsumptionModal = false;
await saveSalesOrder();
return;
}
// 验证物料消耗项
for (const [i, item] of state.materialConsumptionItems.entries()) {
const idx = i + 1;
if (!item.material_id) {
addNotification(`第 ${idx} 行:请选择物料`, 'warning');
return;
}
if (!Number.isFinite(item.quantity) || item.quantity <= 0) {
addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning');
return;
}
}
try {
// 调用 API 保存物料消耗
const result = await apiRequest(`/api/sales-orders/${state.editingItem.id}/consume-materials`, {
method: 'POST',
body: JSON.stringify({
items: state.materialConsumptionItems.map(item => ({
material_id: item.material_id,
quantity: item.quantity,
remark: item.remark
}))
})
});
addNotification('物料消耗记录保存成功', 'success');
// 关闭模态框
state.showMaterialConsumptionModal = false;
// 刷新订单详情
const detail = await apiRequest(`/api/sales-orders/${state.editingItem.id}`);
state.form = {
...state.form,
actual_material_cost: detail.actual_material_cost
};
// 刷新已消耗的物料
const movements = await apiRequest(`/api/stock-movements?reference_type=sales_order&reference_id=${state.editingItem.id}&movement_type=consumption`);
state.consumedMaterials = (movements || []).map(movement => ({
material_name: movement.product_name || movement.product_sku || '未知物料',
quantity: Math.abs(movement.quantity),
unit_price: movement.unit_price || 0,
amount: movement.total_amount || 0
}));
} catch (e) {
handleApiError(e, '保存物料消耗');
}
};
const openRestockModal = async () => {
// 加载物料和供应商数据
await loadMaterials();
await loadSuppliers();
// 初始化补货列表
state.restockItems = [];
// 打开补货模态框
state.showRestockModal = true;
};
const addRestockItem = () => {
state.restockItems.push({
material_id: state.materials[0]?.id || null,
quantity: 1,
unit_price: 0,
remark: ''
});
};
const removeRestockItem = (index) => {
state.restockItems.splice(index, 1);
};
const saveRestock = async () => {
// 验证补货项
if (!state.restockItems || state.restockItems.length === 0) {
addNotification('请至少添加一个补货物料', 'warning');
return;
}
for (const [i, item] of state.restockItems.entries()) {
const idx = i + 1;
if (!item.material_id) {
addNotification(`第 ${idx} 行:请选择物料`, 'warning');
return;
}
if (!Number.isFinite(item.quantity) || item.quantity <= 0) {
addNotification(`第 ${idx} 行:数量必须大于 0`, 'warning');
return;
}
if (!Number.isFinite(item.unit_price) || item.unit_price < 0) {
addNotification(`第 ${idx} 行:单价必须大于等于 0`, 'warning');
return;
}
}
try {
// 创建采购订单
const payload = {
supplier_id: state.suppliers[0]?.id || null,
expected_date: new Date().toISOString().split('T')[0],
remark: '物料补货',
items: state.restockItems.map(item => ({
product_id: item.material_id,
quantity: item.quantity,
remark: item.remark
}))
};
const result = await apiRequest('/api/purchase-orders', {
method: 'POST',
body: JSON.stringify(payload)
});
addNotification('采购订单创建成功', 'success');
// 关闭补货模态框
state.showRestockModal = false;
// 清空补货列表
state.restockItems = [];
// 刷新采购订单列表
loadPurchaseOrders();
} catch (e) {
handleApiError(e, '保存补货订单');
}
};
const closeModal = () => { const closeModal = () => {
state.showModal = false; state.showModal = false;
state.modalType = ''; state.modalType = '';
@@ -2513,19 +2713,45 @@ const InventoryView = {
remark: item.remark || '' remark: item.remark || ''
})) }))
}; };
let orderId;
if (state.editingItem) { if (state.editingItem) {
const result = await apiRequest(`/api/sales-orders/${state.editingItem.id}`, { const result = await apiRequest(`/api/sales-orders/${state.editingItem.id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
orderId = state.editingItem.id;
addNotification(result.production_status === 'bom_missing' ? '订单已保存,但成品未配置BOM,未扣减物料' : '销售订单更新成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success'); addNotification(result.production_status === 'bom_missing' ? '订单已保存,但成品未配置BOM,未扣减物料' : '销售订单更新成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success');
} else { } else {
const result = await apiRequest('/api/sales-orders', { const result = await apiRequest('/api/sales-orders', {
method: 'POST', method: 'POST',
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
orderId = result.id;
addNotification(result.production_status === 'bom_missing' ? '订单已创建,但成品未配置BOM,未扣减物料' : '销售订单创建成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success'); addNotification(result.production_status === 'bom_missing' ? '订单已创建,但成品未配置BOM,未扣减物料' : '销售订单创建成功并已自动扣减物料', result.production_status === 'bom_missing' ? 'warning' : 'success');
} }
// 如果有物料消耗记录,保存物料消耗
if (state.materialConsumptionItems && state.materialConsumptionItems.length > 0) {
try {
// 调用 API 保存物料消耗
const result = await apiRequest(`/api/sales-orders/${orderId}/consume-materials`, {
method: 'POST',
body: JSON.stringify({
items: state.materialConsumptionItems.map(item => ({
material_id: item.material_id,
quantity: item.quantity,
remark: item.remark
}))
})
});
addNotification('物料消耗记录保存成功', 'success');
// 清空物料消耗记录
state.materialConsumptionItems = [];
} catch (e) {
handleApiError(e, '保存物料消耗');
}
}
closeModal(); closeModal();
loadProductionOrders(); loadProductionOrders();
loadFinishedProducts(); loadFinishedProducts();
@@ -2549,6 +2775,19 @@ const InventoryView = {
} }
}; };
const updatePurchaseOrderStatus = async (order, targetStatus) => {
try {
await apiRequest(`/api/purchase-orders/${order.id}/status`, {
method: 'PATCH',
body: JSON.stringify({ status: targetStatus })
});
addNotification('采购订单状态已更新', 'success');
loadPurchaseOrders();
} catch (e) {
handleApiError(e, '更新采购订单状态');
}
};
const deleteSalesOrder = async (orderId) => { const deleteSalesOrder = async (orderId) => {
if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return; if (!confirm('确定删除这个销售订单吗?系统会自动回补已扣减物料。')) return;
try { try {
@@ -2721,8 +2960,9 @@ const InventoryView = {
removeSalesOrderItem, removeSalesOrderItem,
setSalesOrderLineMode, setSalesOrderLineMode,
saveSalesOrder, saveSalesOrder,
deleteSalesOrder,
updateSalesOrderStatus, updateSalesOrderStatus,
updatePurchaseOrderStatus,
deleteSalesOrder,
addPurchaseOrderItem, addPurchaseOrderItem,
removePurchaseOrderItem, removePurchaseOrderItem,
savePurchaseOrder, savePurchaseOrder,
@@ -2733,7 +2973,17 @@ const InventoryView = {
refreshFinanceByPeriod, refreshFinanceByPeriod,
getMovementTypeLabel, getMovementTypeLabel,
getMovementBadgeClass getMovementBadgeClass,
openMaterialConsumptionModal,
addMaterialConsumptionItem,
removeMaterialConsumptionItem,
saveMaterialConsumption,
openRestockModal,
addRestockItem,
removeRestockItem,
saveRestock,
getPurchaseOrderStatusLabel,
isPurchaseOrderLocked
}; };
}, },
template: ` template: `
@@ -2874,6 +3124,7 @@ const InventoryView = {
<div v-else-if="state.activeTab === 'materials'"> <div v-else-if="state.activeTab === 'materials'">
<div class="table-header"> <div class="table-header">
<button class="btn btn-primary" @click="openCreateProduct('material')">+ 新增物料</button> <button class="btn btn-primary" @click="openCreateProduct('material')">+ 新增物料</button>
<button class="btn btn-secondary" @click="openRestockModal">+ 物料补货</button>
</div> </div>
<div class="table-container"> <div class="table-container">
<table class="data-table"> <table class="data-table">
@@ -2975,15 +3226,16 @@ const InventoryView = {
<tr v-for="order in state.purchaseOrders" :key="'purchase-order-' + order.id"> <tr v-for="order in state.purchaseOrders" :key="'purchase-order-' + order.id">
<td>{{ order.order_no }}</td> <td>{{ order.order_no }}</td>
<td>{{ order.supplier_name }}</td> <td>{{ order.supplier_name }}</td>
<td>{{ order.status }}</td> <td>{{ getPurchaseOrderStatusLabel(order.status) }}</td>
<td>{{ order.expected_date ? formatDate(order.expected_date) : '-' }}</td> <td>{{ order.expected_date ? formatDate(order.expected_date) : '-' }}</td>
<td>{{ formatCurrency(order.total_amount || 0) }}</td> <td>{{ formatCurrency(order.total_amount || 0) }}</td>
<td>{{ formatCurrency(order.paid_amount || 0) }}</td> <td>{{ formatCurrency(order.paid_amount || 0) }}</td>
<td> <td>
<div class="action-btns"> <div class="action-btns">
<button class="btn btn-sm btn-secondary" @click="openModal('purchaseOrder', order)">编辑</button> <button class="btn btn-sm btn-secondary" @click="openModal('purchaseOrder', order)" :disabled="isPurchaseOrderLocked(order.status)">编辑</button>
<button class="btn btn-sm btn-danger" @click="deletePurchaseOrder(order.id)">删除</button> <button class="btn btn-sm btn-danger" @click="deletePurchaseOrder(order.id)" :disabled="isPurchaseOrderLocked(order.status)">删除</button>
<button class="btn btn-sm btn-primary" @click="openModal('purchaseReceive', order)">到货入库</button> <button v-if="order.status === 'pending'" class="btn btn-sm btn-primary" @click="openModal('purchaseReceive', order)">到货入库</button>
<button v-else-if="order.status === 'received'" class="btn btn-sm btn-success" @click="updatePurchaseOrderStatus(order, 'paid')">标记为已付款</button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -3424,6 +3676,7 @@ const InventoryView = {
<label class="form-label">备注</label> <label class="form-label">备注</label>
<input v-model="state.form.remark" class="form-input" placeholder="订单备注" /> <input v-model="state.form.remark" class="form-input" placeholder="订单备注" />
</div> </div>
<!-- 模具信息 -->
<div class="form-group" style="margin-bottom: var(--space-4);"> <div class="form-group" style="margin-bottom: var(--space-4);">
<button type="button" class="btn btn-secondary" @click="addSalesOrderItem">+ 添加模具</button> <button type="button" class="btn btn-secondary" @click="addSalesOrderItem">+ 添加模具</button>
</div> </div>
@@ -3486,6 +3739,40 @@ const InventoryView = {
</div> </div>
</div> </div>
</div> </div>
<!-- 物料信息 -->
<div class="form-group" style="margin-bottom: var(--space-4);">
<button type="button" class="btn btn-secondary" @click="openMaterialConsumptionModal">+ 消耗物料</button>
</div>
<div class="form-group" style="margin-bottom: var(--space-4);">
<label class="form-label">已消耗物料</label>
<div v-if="state.editingItem && state.editingItem.actual_material_cost > 0" class="material-consumption-list">
<div class="material-consumption-header">
<div class="material-consumption-item">物料名称</div>
<div class="material-consumption-item">数量</div>
<div class="material-consumption-item">单价</div>
<div class="material-consumption-item">金额</div>
</div>
<div v-for="(item, index) in state.consumedMaterials" :key="'consumed-material-' + index" class="material-consumption-row">
<div class="material-consumption-item">{{ item.material_name }}</div>
<div class="material-consumption-item">{{ item.quantity }}</div>
<div class="material-consumption-item">{{ formatCurrency(item.unit_price) }}</div>
<div class="material-consumption-item">{{ formatCurrency(item.amount) }}</div>
</div>
<div class="material-consumption-total">
<div class="material-consumption-item">合计</div>
<div class="material-consumption-item"></div>
<div class="material-consumption-item"></div>
<div class="material-consumption-item">{{ formatCurrency(state.editingItem.actual_material_cost) }}</div>
</div>
</div>
<div v-else class="empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">暂无物料消耗记录</div>
<div class="empty-desc">点击"消耗物料"按钮添加物料消耗明细</div>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button> <button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
<button type="submit" class="btn btn-primary">保存订单</button> <button type="submit" class="btn btn-primary">保存订单</button>
@@ -3749,6 +4036,149 @@ const InventoryView = {
</div> </div>
</div> </div>
</div> </div>
<!-- 物料消耗模态框 -->
<div v-if="state.showMaterialConsumptionModal" class="modal-overlay" @click.self="state.showMaterialConsumptionModal = false">
<div class="modal-content">
<div class="modal-header">
<h3>添加物料消耗</h3>
<button class="modal-close" @click="state.showMaterialConsumptionModal = false">&times;</button>
</div>
<div class="modal-body">
<div class="form-group" style="margin-bottom: var(--space-4);">
<button type="button" class="btn btn-secondary" @click="addMaterialConsumptionItem">+ 添加物料</button>
</div>
<div v-if="state.materialConsumptionItems.length === 0" class="empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">暂无物料</div>
<div class="empty-desc">请点击"添加物料"按钮添加物料消耗明细</div>
</div>
<div v-else class="purchase-order-items">
<div v-for="(line, index) in state.materialConsumptionItems" :key="'material-consumption-line-' + index" class="purchase-order-item">
<div class="purchase-order-item-content">
<div class="form-group">
<label class="form-label">物料 *</label>
<select v-model.number="line.material_id" class="form-input" required>
<option value="">请选择物料</option>
<option v-for="material in state.materials" :key="'consumption-material-' + material.id" :value="material.id">
{{ material.sku }} - {{ material.name }}
</option>
</select>
</div>
<div class="purchase-order-item-row">
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">数量 *</label>
<input v-model.number="line.quantity" type="number" min="1" class="form-input" placeholder="请输入数量" required />
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">单价</label>
<div class="form-input" style="padding: 8px 12px; background: var(--bg-secondary);">
¥{{ (state.materials.find(m => m.id === line.material_id)?.cost_price || 0).toFixed(2) }}
</div>
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">总价</label>
<div class="form-input" style="padding: 8px 12px; background: var(--bg-secondary);">
¥{{ ((state.materials.find(m => m.id === line.material_id)?.cost_price || 0) * (line.quantity || 0)).toFixed(2) }}
</div>
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">备注</label>
<input v-model="line.remark" class="form-input" placeholder="请输入备注" />
</div>
</div>
<div class="purchase-order-item-actions">
<button type="button" class="btn btn-sm btn-danger" @click="removeMaterialConsumptionItem(index)">删除</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="state.showMaterialConsumptionModal = false">取消</button>
<button type="button" class="btn btn-primary" @click="saveMaterialConsumption">保存物料消耗</button>
</div>
</div>
</div>
</div>
<!-- 物料补货模态框 -->
<div v-if="state.showRestockModal" class="modal-overlay" @click.self="state.showRestockModal = false">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h3>物料补货</h3>
<button class="modal-close" @click="state.showRestockModal = false">&times;</button>
</div>
<div class="modal-body">
<div class="form-section">
<div class="section-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4>补货物料明细</h4>
<button type="button" class="btn btn-secondary" @click="addRestockItem">+ 添加物料</button>
</div>
<div v-if="state.restockItems.length === 0" class="empty-state">
<div class="empty-icon">📦</div>
<div class="empty-desc">请点击"添加物料"按钮添加补货物料明细</div>
</div>
<div v-else class="purchase-order-items">
<div v-for="(line, index) in state.restockItems" :key="'restock-line-' + index" class="purchase-order-item">
<div class="purchase-order-item-row">
<div class="purchase-order-item-column" style="flex: 2;">
<div class="form-group">
<label class="form-label">物料 <span class="required">*</span></label>
<select v-model.number="line.material_id" class="form-input" required>
<option v-for="material in state.materials" :key="'restock-material-' + material.id" :value="material.id">
{{ material.sku }} - {{ material.name }}
</option>
</select>
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">数量 <span class="required">*</span></label>
<input v-model.number="line.quantity" type="number" class="form-input" min="1" step="1" required />
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">单价</label>
<input v-model.number="line.unit_price" type="number" class="form-input" min="0" step="0.01" />
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">总价</label>
<div class="form-input" style="padding: 8px 12px; background: var(--bg-secondary);">
¥{{ ((line.unit_price || 0) * (line.quantity || 0)).toFixed(2) }}
</div>
</div>
</div>
<div class="purchase-order-item-column">
<div class="form-group">
<label class="form-label">备注</label>
<input v-model="line.remark" class="form-input" placeholder="请输入备注" />
</div>
</div>
<div class="purchase-order-item-actions">
<button type="button" class="btn btn-sm btn-danger" @click="removeRestockItem(index)">删除</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="state.showRestockModal = false">取消</button>
<button type="button" class="btn btn-primary" @click="saveRestock">创建采购订单</button>
</div>
</div>
</div>
</div> </div>
` `
}; };