Files
geMoldInsight/frontend/src/modules/inventory/components/SalesOrdersTab.vue
T
2026-06-09 15:44:17 +08:00

585 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import { useInventory } from '../composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
const {
state,
formatCurrency,
formatDateTime,
formatDate,
getSalesOrderStatusLabel,
getDeliveryStatusLabel,
getPaymentStatusLabel,
loadProductionOrders,
loadFinishedProducts,
loadCustomers,
loadMaterials,
loadInventory,
loadMovements
} = useInventory()
const showModal = ref(false)
const editingItem = ref<any>(null)
const form = ref<any>({})
const moldItems = ref<any[]>([])
const productionWarehouseId = ref<number | null>(null)
const showConsumptionModal = ref(false)
const consumptionItems = ref<any[]>([])
const consumedMaterials = ref<any[]>([])
function openCreateOrder() {
editingItem.value = null
form.value = {
customer_id: null,
delivery_date: '',
remark: ''
}
moldItems.value = []
consumedMaterials.value = []
showModal.value = true
}
function addMoldItem() {
moldItems.value.push({
mold_mode: 'new',
mold_sku: '',
mold_name: '',
mold_id: null,
quantity: 1,
unit_price: 0,
remark: ''
})
}
function removeMoldItem(index: number) {
moldItems.value.splice(index, 1)
}
const orderTotalAmount = computed(() => {
return moldItems.value.reduce((sum: number, item: any) => {
return sum + (item.unit_price || 0) * (item.quantity || 0)
}, 0)
})
async function editOrder(order: any) {
await loadCustomers()
await loadFinishedProducts()
await loadMaterials()
try {
const detail = await apiRequest(`/api/sales-orders/${order.id}`)
editingItem.value = order
form.value = {
customer_id: detail.customer_id || order.customer_id || null,
delivery_date: detail.delivery_date || order.delivery_date || '',
remark: detail.remark || order.remark || ''
}
moldItems.value = (detail.items || []).map((item: any) => ({
mold_mode: item.mold_id ? 'existing' : 'new',
mold_sku: item.sku || '',
mold_name: item.name || '',
mold_id: item.mold_id || null,
quantity: item.quantity || 1,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
try {
const consumptionData = await apiRequest(`/api/sales-orders/${order.id}/consume-materials`)
consumedMaterials.value = (consumptionData.items || []).map((item: any) => ({
material_id: item.material_id,
material_name: item.material_name || item.material_sku || '',
quantity: item.quantity,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
} catch {
consumedMaterials.value = []
}
} catch (e) {
handleApiError(e, '加载销售订单详情')
}
showModal.value = true
}
async function saveSalesOrder() {
if (!form.value.customer_id) {
addNotification('请选择客户', 'warning')
return
}
if (!moldItems.value || moldItems.value.length === 0) {
addNotification('请至少添加一个模具', 'warning')
return
}
try {
const payload = {
customer_id: form.value.customer_id,
delivery_date: form.value.delivery_date || null,
remark: form.value.remark || '',
items: moldItems.value.map((item: any) => ({
mold_mode: item.mold_mode,
mold_sku: item.mold_sku || '',
mold_name: item.mold_name || '',
mold_id: item.mold_id || null,
quantity: item.quantity,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
}
if (editingItem.value) {
await apiRequest(`/api/sales-orders/${editingItem.value.id}`, {
method: 'PUT',
body: JSON.stringify(payload)
})
addNotification('销售订单更新成功', 'success')
} else {
await apiRequest('/api/sales-orders', {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('销售订单创建成功', 'success')
}
showModal.value = false
editingItem.value = null
form.value = {}
moldItems.value = []
consumedMaterials.value = []
loadProductionOrders()
loadFinishedProducts()
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '保存销售订单')
}
}
async function deleteOrder(id: number) {
try {
await ElMessageBox.confirm('确定要删除这个销售订单吗?', '删除确认', {
confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning'
})
} catch { return }
try {
await apiRequest(`/api/sales-orders/${id}`, { method: 'DELETE' })
addNotification('销售订单已删除', 'success')
loadProductionOrders()
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '删除销售订单')
}
}
async function markDelivered(orderId: number) {
try {
await ElMessageBox.confirm('确认该订单已交付?', '交付确认', {
confirmButtonText: '确认交付', cancelButtonText: '取消', type: 'success'
})
} catch { return }
await doUpdateStatus(orderId, 'delivered', '已交付')
}
async function markPaid(orderId: number, deliveryStatus: string) {
if (deliveryStatus !== 'delivered') {
try {
await ElMessageBox.confirm(
'当前订单尚未交付,确定要提前收款吗?建议先确认交付后再收款。',
'非标准流程提醒',
{ confirmButtonText: '仍要收款', cancelButtonText: '取消', type: 'warning' }
)
} catch { return }
} else {
try {
await ElMessageBox.confirm('确认已收到该订单款项?', '收款确认', {
confirmButtonText: '确认收款', cancelButtonText: '取消', type: 'success'
})
} catch { return }
}
await doUpdateStatus(orderId, 'paid', '已收款')
}
async function cancelOrder(orderId: number) {
try {
await ElMessageBox.confirm('确定要作废该订单吗?此操作不可撤销。', '作废确认', {
confirmButtonText: '确认作废', cancelButtonText: '取消', type: 'error'
})
} catch { return }
await doUpdateStatus(orderId, 'cancelled', '已作废')
}
async function doUpdateStatus(orderId: number, status: string, label: string) {
try {
await apiRequest(`/api/sales-orders/${orderId}/status`, {
method: 'PATCH',
body: JSON.stringify({ status })
})
addNotification(`订单已${label}`, 'success')
loadProductionOrders()
loadMovements()
} catch (e) {
handleApiError(e, '更新状态')
}
}
function openConsumptionModal() {
consumptionItems.value = []
showConsumptionModal.value = true
}
function addConsumptionItem() {
consumptionItems.value.push({
material_id: state.materials[0]?.id || null,
quantity: 1,
remark: ''
})
}
function removeConsumptionItem(index: number) {
consumptionItems.value.splice(index, 1)
}
const consumptionTotalAmount = computed(() => {
return consumptionItems.value.reduce((sum: number, item: any) => {
const material = state.materials.find((m: any) => m.id === item.material_id)
const unitPrice = material?.cost_price || 0
return sum + unitPrice * (item.quantity || 0)
}, 0)
})
async function saveConsumption() {
if (!editingItem.value) return
if (!consumptionItems.value || consumptionItems.value.length === 0) {
addNotification('请至少添加一个消耗物料', 'warning')
return
}
try {
const payload = {
items: consumptionItems.value.map((item: any) => ({
material_id: item.material_id,
quantity: item.quantity,
unit_price: state.materials.find((m: any) => m.id === item.material_id)?.cost_price || 0,
remark: item.remark || ''
}))
}
await apiRequest(`/api/sales-orders/${editingItem.value.id}/consume-materials`, {
method: 'POST',
body: JSON.stringify(payload)
})
addNotification('物料消耗记录保存成功', 'success')
showConsumptionModal.value = false
try {
const consumptionData = await apiRequest(`/api/sales-orders/${editingItem.value.id}/consume-materials`)
consumedMaterials.value = (consumptionData.items || []).map((item: any) => ({
material_id: item.material_id,
material_name: item.material_name || item.material_sku || '',
quantity: item.quantity,
unit_price: item.unit_price || 0,
remark: item.remark || ''
}))
} catch {
consumedMaterials.value = []
}
loadInventory()
loadMovements()
} catch (e) {
handleApiError(e, '保存物料消耗')
}
}
function closeModal() {
showModal.value = false
editingItem.value = null
form.value = {}
moldItems.value = []
consumedMaterials.value = []
}
function closeConsumptionModal() {
showConsumptionModal.value = false
consumptionItems.value = []
}
async function initData() {
await loadCustomers()
await loadMaterials()
await loadFinishedProducts()
await loadProductionOrders()
}
onMounted(() => {
initData()
})
</script>
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<el-select
v-model="productionWarehouseId"
placeholder="选择仓库"
style="width: 200px;"
clearable
>
<el-option
v-for="warehouse in state.warehouses"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
<el-button @click="loadProductionOrders">刷新</el-button>
<el-button type="primary" @click="openCreateOrder">新增销售订单</el-button>
</div>
<el-table :data="state.productionOrders" v-loading="state.loading" stripe>
<el-table-column prop="order_no" label="销售单" />
<el-table-column prop="customer_name" label="客户" />
<el-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.total_amount) }}</template>
</el-table-column>
<el-table-column label="交付日期">
<template #default="{ row }">{{ formatDate(row.delivery_date) }}</template>
</el-table-column>
<el-table-column label="订单创建">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="实际交付">
<template #default="{ row }">{{ row.delivered_at ? formatDateTime(row.delivered_at) : '-' }}</template>
</el-table-column>
<el-table-column label="实际收款">
<template #default="{ row }">{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}</template>
</el-table-column>
<el-table-column label="订单状态" width="160">
<template #default="{ row }">
<div style="display: flex; gap: 4px;">
<el-tag
:type="row.delivery_status === 'delivered' ? 'warning' : row.delivery_status === 'cancelled' ? 'danger' : 'info'"
size="small"
>{{ getDeliveryStatusLabel(row.delivery_status) }}</el-tag>
<el-tag
:type="row.payment_status === 'paid' ? 'success' : 'info'"
size="small"
>{{ getPaymentStatusLabel(row.payment_status) }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button
v-if="row.delivery_status === 'manufacturing'"
type="success" size="small"
@click="markDelivered(row.id)"
>已交付</el-button>
<el-button
v-if="row.payment_status !== 'paid' && row.delivery_status !== 'cancelled'"
type="warning" size="small"
@click="markPaid(row.id, row.delivery_status)"
>已收款</el-button>
<el-button
type="primary" size="small"
:disabled="row.payment_status === 'paid' || row.delivery_status === 'cancelled'"
@click="editOrder(row)"
>编辑</el-button>
<el-button type="danger" size="small" @click="deleteOrder(row.id)">删除</el-button>
<el-button
v-if="row.payment_status !== 'paid' && row.delivery_status !== 'cancelled'"
type="info" size="small"
@click="cancelOrder(row.id)"
>作废</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.productionOrders.length === 0" description="暂无销售订单" />
<el-dialog
v-model="showModal"
:title="(editingItem ? '编辑' : '新增') + '销售订单'"
width="900px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="客户" required>
<el-select v-model="form.customer_id" placeholder="请选择客户" style="width:100%">
<el-option
v-for="customer in state.customers"
:key="customer.id"
:label="customer.name"
:value="customer.id"
/>
</el-select>
</el-form-item>
<el-form-item label="交付日期">
<el-date-picker
v-model="form.delivery_date"
type="date"
placeholder="选择日期"
style="width:100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="备注信息" />
</el-form-item>
</el-form>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">模具明细</span>
<div style="display: flex; gap: 8px;">
<el-button v-if="editingItem" type="warning" size="small" @click="openConsumptionModal">+ 消耗物料</el-button>
<el-button type="primary" size="small" @click="addMoldItem">+ 添加模具</el-button>
</div>
</div>
<el-table :data="moldItems" border size="small">
<el-table-column label="模式" width="80">
<template #default="{ row: line }">
<el-select v-model="line.mold_mode" size="small" style="width:100%">
<el-option label="新模" value="new" />
<el-option label="改模" value="existing" />
</el-select>
</template>
</el-table-column>
<el-table-column label="模具SKU" min-width="120">
<template #default="{ row: line }">
<el-input v-if="line.mold_mode === 'new'" v-model="line.mold_sku" size="small" placeholder="模具SKU" />
<el-select v-else v-model="line.mold_id" placeholder="选择模具" size="small" style="width:100%">
<el-option
v-for="product in state.finishedProducts"
:key="product.id"
:label="`${product.sku} - ${product.name}`"
:value="product.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="模具名称" min-width="120">
<template #default="{ row: line }">
<el-input
v-if="line.mold_mode === 'new'"
v-model="line.mold_name"
size="small"
placeholder="模具名称"
/>
<span v-else style="padding-left: 8px;">
{{ state.finishedProducts.find((p: any) => p.id === line.mold_id)?.name || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="数量" width="100">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="备注" width="120">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeMoldItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
订单总金额:{{ formatCurrency(orderTotalAmount) }}
</div>
<template v-if="editingItem && consumedMaterials.length > 0">
<el-divider content-position="left">已消耗物料</el-divider>
<el-table :data="consumedMaterials" border size="small">
<el-table-column label="物料">
<template #default="{ row: item }">{{ item.material_name }}</template>
</el-table-column>
<el-table-column label="数量">
<template #default="{ row: item }">{{ item.quantity }}</template>
</el-table-column>
<el-table-column label="单价">
<template #default="{ row: item }">{{ formatCurrency(item.unit_price) }}</template>
</el-table-column>
<el-table-column label="备注">
<template #default="{ row: item }">{{ item.remark || '-' }}</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveSalesOrder">保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="showConsumptionModal"
title="物料消耗"
width="800px"
@closed="closeConsumptionModal"
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">消耗物料明细</span>
<el-button type="primary" size="small" @click="addConsumptionItem">+ 添加物料</el-button>
</div>
<el-table :data="consumptionItems" border size="small">
<el-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<el-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<el-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number
:model-value="state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0"
disabled
size="small"
style="width:100%"
/>
</template>
</el-table-column>
<el-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0) * (line.quantity || 0)) }}
</template>
</el-table-column>
<el-table-column label="备注" width="150">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeConsumptionItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
消耗总金额:{{ formatCurrency(consumptionTotalAmount) }}
</div>
<template #footer>
<el-button @click="showConsumptionModal = false">取消</el-button>
<el-button type="primary" @click="saveConsumption">保存消耗</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
</style>