<template>
|
<div>
|
<el-dialog v-model="dialogVisible"
|
title="工序领料"
|
width="1200px"
|
@close="handleCloseMaterialDialog">
|
<div class="material-toolbar">
|
<span>
|
按当前工序的 BOM 物料领用,也可添加 BOM 外的物料;填写本次领料数量并选择批号后领用,库存立即扣减。
|
</span>
|
<el-button type="primary"
|
link
|
@click="openProductDialog">添加物料</el-button>
|
</div>
|
<el-table v-loading="materialTableLoading"
|
:data="pickRows"
|
border
|
row-key="rowKey">
|
<el-table-column label="工序名称"
|
prop="operationName"
|
min-width="120" />
|
<el-table-column label="物料名称"
|
prop="productName"
|
min-width="140" />
|
<el-table-column label="物料规格"
|
prop="model"
|
min-width="140" />
|
<el-table-column label="计量单位"
|
prop="unit"
|
width="90" />
|
<el-table-column label="来源"
|
width="90">
|
<template #default="{ row }">
|
<el-tag :type="row.bom ? 'success' : 'info'"
|
size="small">{{ row.bom ? "BOM" : "手工" }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="可用库存"
|
prop="stockQuantity"
|
min-width="100" />
|
<el-table-column label="批号"
|
min-width="200">
|
<template #default="{ row }">
|
<el-select v-model="row.batchNo"
|
placeholder="请选择批号"
|
filterable
|
clearable
|
style="width: 100%;">
|
<el-option v-for="batchNo in row.batchNoList || []"
|
:key="batchNo"
|
:label="batchNo"
|
:value="batchNo" />
|
</el-select>
|
</template>
|
</el-table-column>
|
<el-table-column label="本次领料数量"
|
min-width="150">
|
<template #default="{ row }">
|
<el-input-number v-model="row.pickQuantity"
|
:min="0"
|
:precision="3"
|
:step="1"
|
controls-position="right"
|
style="width: 100%;" />
|
</template>
|
</el-table-column>
|
<el-table-column label="操作"
|
align="center"
|
fixed="right"
|
width="90">
|
<template #default="{ row, $index }">
|
<el-button v-if="!row.bom"
|
type="danger"
|
link
|
@click="removePickRow($index)">移除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="material-section-title">本工序已领料记录</div>
|
<el-table v-loading="materialTableLoading"
|
:data="pickedRows"
|
border
|
row-key="id">
|
<el-table-column label="物料名称"
|
prop="productName"
|
min-width="140" />
|
<el-table-column label="物料规格"
|
prop="model"
|
min-width="140" />
|
<el-table-column label="批号"
|
prop="batchNo"
|
min-width="150" />
|
<el-table-column label="计量单位"
|
prop="unit"
|
width="90" />
|
<el-table-column label="领料数量"
|
prop="pickQuantity"
|
min-width="110" />
|
<el-table-column label="退料数量"
|
min-width="110">
|
<template #default="{ row }">{{ row.returnQty || 0 }}</template>
|
</el-table-column>
|
<el-table-column label="净领料数量"
|
prop="effectiveQuantity"
|
min-width="120" />
|
<el-table-column label="领料时间"
|
min-width="120">
|
<template #default="{ row }">
|
{{ formatDate(row.createTime) }}
|
</template>
|
</el-table-column>
|
<el-table-column label="本次退料数量"
|
min-width="150">
|
<template #default="{ row }">
|
<el-input-number v-model="row.currentReturnQty"
|
:min="0"
|
:max="Number(row.effectiveQuantity || 0)"
|
:precision="3"
|
:step="1"
|
controls-position="right"
|
style="width: 100%;" />
|
</template>
|
</el-table-column>
|
<el-table-column label="操作"
|
align="center"
|
fixed="right"
|
width="90">
|
<template #default="{ row }">
|
<el-button type="primary"
|
link
|
:loading="returningId === row.id"
|
:disabled="Number(row.effectiveQuantity || 0) <= 0"
|
@click="handleReturnMaterial(row)">退料</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button type="primary"
|
:loading="pickSubmitting"
|
@click="handleSubmitPick">领用</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
<ProductSelectDialog v-if="productDialogVisible"
|
v-model:model-value="productDialogVisible"
|
:single="true"
|
@confirm="handleProductConfirm" />
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, defineAsyncComponent, ref, watch } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import {
|
listWorkOrderMaterialLedger,
|
pickWorkOrderMaterial,
|
returnWorkOrderMaterial,
|
} from "@/api/productionManagement/workOrder.js";
|
|
const ProductSelectDialog = defineAsyncComponent(() =>
|
import("@/views/basicData/product/ProductSelectDialog.vue")
|
);
|
|
const props = defineProps({
|
modelValue: {
|
type: Boolean,
|
default: false,
|
},
|
rowData: {
|
type: Object,
|
default: () => null,
|
},
|
});
|
|
const emit = defineEmits(["update:modelValue", "refresh"]);
|
|
const dialogVisible = computed({
|
get: () => props.modelValue,
|
set: val => emit("update:modelValue", val),
|
});
|
|
const materialTableLoading = ref(false);
|
// 本次可领用的物料行:BOM 建议 + 手工添加
|
const pickRows = ref([]);
|
// 当前工序已产生的领料记录
|
const pickedRows = ref([]);
|
const productDialogVisible = ref(false);
|
const currentWorkOrderRow = ref(null);
|
const pickSubmitting = ref(false);
|
const returningId = ref(null);
|
|
const formatDate = value => (value ? String(value).slice(0, 10) : "-");
|
|
const createRowKey = () => `${Date.now()}_${Math.random()}`;
|
|
const loadMaterialTable = async row => {
|
if (!row?.id) return;
|
currentWorkOrderRow.value = row;
|
materialTableLoading.value = true;
|
pickRows.value = [];
|
pickedRows.value = [];
|
try {
|
const res = await listWorkOrderMaterialLedger(row.id);
|
const records = Array.isArray(res.data) ? res.data : res.data?.records || [];
|
pickRows.value = records
|
.filter(item => item.picked === false || !item.id)
|
.map(item => ({
|
rowKey: createRowKey(),
|
bom: item.bom !== false,
|
productModelId: item.productModelId,
|
productName: item.productName || "",
|
model: item.model || "",
|
unit: item.unit || "",
|
operationName: item.operationName || "",
|
stockQuantity: item.stockQuantity ?? 0,
|
batchNoList: item.batchNoList || [],
|
batchNo: "",
|
pickQuantity: 0,
|
}));
|
pickedRows.value = records
|
.filter(item => item.picked === true || item.id)
|
.map(item => ({
|
...item,
|
pickQuantity: item.pickQuantity ?? item.quantity ?? 0,
|
effectiveQuantity: item.effectiveQuantity ?? 0,
|
currentReturnQty: 0,
|
}));
|
} catch (e) {
|
console.error("获取工序物料失败", e);
|
ElMessage.error("获取工序物料失败");
|
} finally {
|
materialTableLoading.value = false;
|
}
|
};
|
|
watch(
|
() => props.modelValue,
|
visible => {
|
if (visible && props.rowData) {
|
loadMaterialTable(props.rowData);
|
}
|
}
|
);
|
|
const handleCloseMaterialDialog = () => {
|
pickRows.value = [];
|
pickedRows.value = [];
|
currentWorkOrderRow.value = null;
|
};
|
|
const openProductDialog = () => {
|
productDialogVisible.value = true;
|
};
|
|
const handleProductConfirm = products => {
|
const product = Array.isArray(products) ? products[0] : null;
|
if (!product) return;
|
|
const productModelId = product.id || product.productModelId || product.modelId;
|
const exists = pickRows.value.some(
|
item => String(item.productModelId || "") === String(productModelId || "")
|
);
|
if (exists) {
|
ElMessage.warning("该物料已在领料列表中");
|
return;
|
}
|
|
pickRows.value.push({
|
rowKey: createRowKey(),
|
bom: false,
|
productModelId,
|
productName: product.productName || product.materialName || "",
|
model: product.model || product.materialModel || "",
|
unit: product.unit || "",
|
operationName:
|
currentWorkOrderRow.value?.processName ||
|
currentWorkOrderRow.value?.operationName ||
|
"",
|
stockQuantity: product.stockQuantity ?? 0,
|
batchNoList: product.batchNoList || [],
|
batchNo: "",
|
pickQuantity: 0,
|
});
|
};
|
|
const removePickRow = index => {
|
pickRows.value.splice(index, 1);
|
};
|
|
const handleReturnMaterial = async row => {
|
const returnQty = Number(row.currentReturnQty || 0);
|
if (returnQty <= 0) {
|
ElMessage.warning("请输入本次退料数量");
|
return;
|
}
|
if (returnQty > Number(row.effectiveQuantity || 0)) {
|
ElMessage.warning("退料数量不能超过净领料数量");
|
return;
|
}
|
|
await ElMessageBox.confirm(
|
`确认退回 ${row.productName || "该物料"} ${returnQty}${row.unit || ""}?`,
|
"退料确认",
|
{ type: "warning" }
|
);
|
|
returningId.value = row.id;
|
try {
|
await returnWorkOrderMaterial({
|
productionOrderId: row.productionOrderId,
|
pickList: [
|
{
|
id: row.id,
|
productionOrderId: row.productionOrderId,
|
returned: true,
|
returnQty,
|
},
|
],
|
});
|
ElMessage.success("退料成功");
|
await loadMaterialTable(currentWorkOrderRow.value);
|
emit("refresh");
|
} catch (e) {
|
if (e !== "cancel" && e !== "close") {
|
console.error("退料失败", e);
|
}
|
} finally {
|
returningId.value = null;
|
}
|
};
|
|
const handleSubmitPick = async () => {
|
if (!currentWorkOrderRow.value?.id) return;
|
const submitRows = pickRows.value.filter(
|
item => Number(item.pickQuantity || 0) > 0
|
);
|
if (submitRows.length === 0) {
|
ElMessage.warning("请至少填写一个物料的本次领料数量");
|
return;
|
}
|
const missingBatchRow = submitRows.find(
|
item => (item.batchNoList || []).length > 0 && !item.batchNo
|
);
|
if (missingBatchRow) {
|
ElMessage.warning("请选择领料批号");
|
return;
|
}
|
|
pickSubmitting.value = true;
|
try {
|
await pickWorkOrderMaterial({
|
productionOperationTaskId: currentWorkOrderRow.value.id,
|
pickList: submitRows.map(item => ({
|
productModelId: item.productModelId,
|
batchNo: item.batchNo || undefined,
|
pickQuantity: Number(item.pickQuantity || 0),
|
})),
|
});
|
ElMessage.success("领用成功");
|
await loadMaterialTable(currentWorkOrderRow.value);
|
emit("refresh");
|
} catch (e) {
|
console.error("领用失败", e);
|
} finally {
|
pickSubmitting.value = false;
|
}
|
};
|
</script>
|
|
<style scoped>
|
.material-toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 12px;
|
color: #606266;
|
font-size: 13px;
|
}
|
|
.material-section-title {
|
margin: 16px 0 8px;
|
color: #303133;
|
font-size: 14px;
|
font-weight: 600;
|
}
|
</style>
|