539 lines
18 KiB
Vue
539 lines
18 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { useInventory } from '../composables/useInventory'
|
||
import { apiRequest } from '@/shared/api'
|
||
import { addNotification, handleApiError } from '@/shared/notification'
|
||
|
||
const {
|
||
state,
|
||
formatCurrency,
|
||
formatDateTime,
|
||
formatDate,
|
||
getSalesOrderStatusLabel,
|
||
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) {
|
||
if (!confirm('确定要删除这个销售订单吗?')) return
|
||
try {
|
||
await apiRequest(`/api/sales-orders/${id}`, { method: 'DELETE' })
|
||
addNotification('销售订单已删除', 'success')
|
||
loadProductionOrders()
|
||
loadInventory()
|
||
loadMovements()
|
||
} catch (e) {
|
||
handleApiError(e, '删除销售订单')
|
||
}
|
||
}
|
||
|
||
async function updateStatus(orderId: number, status: string) {
|
||
const label = status === 'delivered' ? '已交付' : '已收款'
|
||
if (!confirm(`确认标记为${label}?`)) return
|
||
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 loadProductionOrders()
|
||
await loadCustomers()
|
||
await loadMaterials()
|
||
await loadFinishedProducts()
|
||
}
|
||
|
||
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="订单状态">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'paid' ? 'success' : row.status === 'delivered' ? 'warning' : 'info'">
|
||
{{ getSalesOrderStatusLabel(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="240">
|
||
<template #default="{ row }">
|
||
<el-dropdown style="margin-right: 8px;">
|
||
<el-button size="small">
|
||
状态 ▾
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item @click="updateStatus(row.id, 'delivered')">已交付</el-dropdown-item>
|
||
<el-dropdown-item @click="updateStatus(row.id, 'paid')">已收款</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
:disabled="row.status === 'paid'"
|
||
@click="editOrder(row)"
|
||
>
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
size="small"
|
||
@click="deleteOrder(row.id)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<el-empty v-if="!state.loading && state.productionOrders.length === 0" description="暂无销售订单" />
|
||
</el-table>
|
||
|
||
<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>
|