Files
geMoldInsight/frontend/src/modules/inventory/components/SalesOrdersTab.vue
T

539 lines
18 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 } 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>