315 lines
10 KiB
Vue
315 lines
10 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,
|
|||
|
|
loadMaterials,
|
|||
|
|
loadSuppliers,
|
|||
|
|
loadPurchaseOrders
|
|||
|
|
} = useInventory()
|
|||
|
|
|
|||
|
|
const showModal = ref(false)
|
|||
|
|
const editingItem = ref<any>(null)
|
|||
|
|
const form = ref<any>({})
|
|||
|
|
const showRestockModal = ref(false)
|
|||
|
|
const restockItems = ref<any[]>([])
|
|||
|
|
const restockSupplierId = ref<number | null>(null)
|
|||
|
|
|
|||
|
|
function openCreateMaterial() {
|
|||
|
|
editingItem.value = null
|
|||
|
|
form.value = {
|
|||
|
|
item_type: 'material',
|
|||
|
|
unit: 'kg',
|
|||
|
|
min_stock: 0,
|
|||
|
|
max_stock: 1000,
|
|||
|
|
cost_price: 0,
|
|||
|
|
sale_price: 0,
|
|||
|
|
sku: '',
|
|||
|
|
name: '',
|
|||
|
|
category: ''
|
|||
|
|
}
|
|||
|
|
showModal.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function editMaterial(product: any) {
|
|||
|
|
editingItem.value = product
|
|||
|
|
form.value = { ...product }
|
|||
|
|
showModal.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveProduct() {
|
|||
|
|
if (!form.value.sku) {
|
|||
|
|
addNotification('请输入SKU', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!form.value.name) {
|
|||
|
|
addNotification('请输入名称', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
if (editingItem.value) {
|
|||
|
|
await apiRequest(`/api/products/${editingItem.value.id}`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
body: JSON.stringify(form.value)
|
|||
|
|
})
|
|||
|
|
addNotification('物料更新成功', 'success')
|
|||
|
|
} else {
|
|||
|
|
await apiRequest('/api/products', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(form.value)
|
|||
|
|
})
|
|||
|
|
addNotification('物料创建成功', 'success')
|
|||
|
|
}
|
|||
|
|
showModal.value = false
|
|||
|
|
editingItem.value = null
|
|||
|
|
form.value = {}
|
|||
|
|
loadMaterials()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存物料')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function deleteMaterial(id: number) {
|
|||
|
|
if (!confirm('确定要删除这个物料吗?')) return
|
|||
|
|
try {
|
|||
|
|
await apiRequest(`/api/products/${id}`, { method: 'DELETE' })
|
|||
|
|
addNotification('物料已删除', 'success')
|
|||
|
|
loadMaterials()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '删除物料')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function openRestockModal() {
|
|||
|
|
await loadMaterials()
|
|||
|
|
await loadSuppliers()
|
|||
|
|
restockItems.value = []
|
|||
|
|
showRestockModal.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addRestockItem() {
|
|||
|
|
restockItems.value.push({
|
|||
|
|
material_id: state.materials[0]?.id || null,
|
|||
|
|
quantity: 1,
|
|||
|
|
unit_price: 0,
|
|||
|
|
remark: ''
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeRestockItem(index: number) {
|
|||
|
|
restockItems.value.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const restockTotalAmount = computed(() => {
|
|||
|
|
return restockItems.value.reduce((sum: number, item: any) => {
|
|||
|
|
return sum + (item.unit_price || 0) * (item.quantity || 0)
|
|||
|
|
}, 0)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function saveRestock() {
|
|||
|
|
if (!restockItems.value || restockItems.value.length === 0) {
|
|||
|
|
addNotification('请至少添加一个补货物料', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
for (const [i, item] of restockItems.value.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: restockSupplierId.value || state.suppliers[0]?.id || null,
|
|||
|
|
expected_date: new Date().toISOString().split('T')[0],
|
|||
|
|
remark: '物料补货',
|
|||
|
|
items: restockItems.value.map((item: any) => ({
|
|||
|
|
product_id: item.material_id,
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
unit_price: item.unit_price,
|
|||
|
|
remark: item.remark
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
if (!payload.supplier_id) {
|
|||
|
|
addNotification('请先添加供应商', 'warning')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await apiRequest('/api/purchase-orders', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
addNotification('采购订单创建成功', 'success')
|
|||
|
|
showRestockModal.value = false
|
|||
|
|
restockItems.value = []
|
|||
|
|
loadPurchaseOrders()
|
|||
|
|
} catch (e) {
|
|||
|
|
handleApiError(e, '保存补货订单')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
showModal.value = false
|
|||
|
|
editingItem.value = null
|
|||
|
|
form.value = {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeRestockModal() {
|
|||
|
|
showRestockModal.value = false
|
|||
|
|
restockItems.value = []
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div>
|
|||
|
|
<div style="margin-bottom: 12px; display: flex; gap: 8px;">
|
|||
|
|
<el-button type="primary" @click="openCreateMaterial">新增物料</el-button>
|
|||
|
|
<el-button type="success" @click="openRestockModal">物料补货</el-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-table :data="state.materials" v-loading="state.loading" stripe>
|
|||
|
|
<el-table-column prop="sku" label="SKU" />
|
|||
|
|
<el-table-column prop="name" label="名称" />
|
|||
|
|
<el-table-column prop="category" label="分类">
|
|||
|
|
<template #default="{ row }">{{ row.category || '-' }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="unit" label="单位" />
|
|||
|
|
<el-table-column label="成本价">
|
|||
|
|
<template #default="{ row }">{{ formatCurrency(row.cost_price) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="min_stock" label="最低库存">
|
|||
|
|
<template #default="{ row }">{{ row.min_stock ?? '-' }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="操作" width="180">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-button type="primary" size="small" @click="editMaterial(row)">编辑</el-button>
|
|||
|
|
<el-button type="danger" size="small" @click="deleteMaterial(row.id)">删除</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-empty v-if="!state.loading && state.materials.length === 0" description="暂无物料数据" />
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="showModal"
|
|||
|
|
:title="(editingItem ? '编辑' : '新增') + '物料'"
|
|||
|
|
width="560px"
|
|||
|
|
@closed="closeModal"
|
|||
|
|
>
|
|||
|
|
<el-form label-width="100px">
|
|||
|
|
<el-form-item label="类型">
|
|||
|
|
<el-input model-value="物料(纳入库存)" disabled />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="SKU" required>
|
|||
|
|
<el-input v-model="form.sku" placeholder="产品编码" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="名称" required>
|
|||
|
|
<el-input v-model="form.name" placeholder="产品名称" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="分类">
|
|||
|
|
<el-input v-model="form.category" placeholder="产品分类" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="单位">
|
|||
|
|
<el-input v-model="form.unit" placeholder="kg" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="成本价">
|
|||
|
|
<el-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="最低库存">
|
|||
|
|
<el-input-number v-model="form.min_stock" :min="0" style="width:100%" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="最高库存">
|
|||
|
|
<el-input-number v-model="form.max_stock" :min="0" style="width:100%" />
|
|||
|
|
</el-form-item>
|
|||
|
|
</el-form>
|
|||
|
|
<template #footer>
|
|||
|
|
<el-button @click="showModal = false">取消</el-button>
|
|||
|
|
<el-button type="primary" @click="saveProduct">保存</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="showRestockModal"
|
|||
|
|
title="物料补货"
|
|||
|
|
width="900px"
|
|||
|
|
@closed="closeRestockModal"
|
|||
|
|
>
|
|||
|
|
<el-form label-width="100px" style="margin-bottom: 16px;">
|
|||
|
|
<el-form-item label="供应商">
|
|||
|
|
<el-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%">
|
|||
|
|
<el-option
|
|||
|
|
v-for="supplier in state.suppliers"
|
|||
|
|
:key="supplier.id"
|
|||
|
|
:label="supplier.name"
|
|||
|
|
:value="supplier.id"
|
|||
|
|
/>
|
|||
|
|
</el-select>
|
|||
|
|
</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>
|
|||
|
|
<el-button type="primary" size="small" @click="addRestockItem">+ 添加物料</el-button>
|
|||
|
|
</div>
|
|||
|
|
<el-table :data="restockItems" 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="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 }">
|
|||
|
|
{{ formatCurrency((line.unit_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="removeRestockItem($index)">删除</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
|
|||
|
|
补货总金额:{{ formatCurrency(restockTotalAmount) }}
|
|||
|
|
</div>
|
|||
|
|
<template #footer>
|
|||
|
|
<el-button @click="showRestockModal = false">取消</el-button>
|
|||
|
|
<el-button type="primary" @click="saveRestock">创建采购订单</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
</style>
|