src/api/productionManagement/productionOrder.js
@@ -72,23 +72,47 @@ } // 生产订单-保存领料台账 // export function saveMaterialPickingLedger(data) { // return request({ // url: "/productOrderMaterial/save", // method: "post", // data, // }); // } export function saveMaterialPickingLedger(data) { return request({ url: "/productOrderMaterial/save", url: "/productionOrderPick/savePick", method: "post", data, }); } // 生产订单-领料详情列表 export function listMaterialPickingDetail(query) { export function updateMaterialPickingLedger(data) { return request({ url: "/productOrderMaterial/detailList", method: "get", params: query, url: "/productionOrderPick/updatePick", method: "post", data, }); } // 生产订单-领料详情列表 // export function listMaterialPickingDetail(query) { // return request({ // url: "/productOrderMaterial/detailList", // method: "get", // params: query, // }); // } export function listMaterialPickingBom(productionOrderId) { return request({ url: "/productionOrder/pick/" + productionOrderId, method: "get", }); } export function listMaterialPickingDetail(productionOrderId) { return request({ url: "/productionOrderPick/detail/" + productionOrderId, method: "get", }); } // 生产订单-补料记录列表 export function listMaterialSupplementRecord(query) { return request({ src/views/inventoryManagement/stockManagement/New.vue
@@ -1,96 +1,97 @@ <template> <div> <el-dialog v-model="isShow" title="新增库存" width="800" @close="closeModal" > <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> <el-form-item label="产品名称" prop="productModelId" :rules="[ <el-dialog v-model="isShow" title="新增库存" width="800" @close="closeModal"> <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> <el-form-item label="产品名称" prop="productModelId" :rules="[ { required: true, message: '请选择产品', trigger: 'change', } ]" > <el-button type="primary" @click="showProductSelectDialog = true"> ]"> <el-button type="primary" @click="showProductSelectDialog = true"> {{ formState.productName ? formState.productName : '选择产品' }} </el-button> </el-form-item> <el-form-item label="规格" prop="productModelName" > <el-input v-model="formState.productModelName" disabled /> <el-form-item label="规格" prop="productModelName"> <el-input v-model="formState.productModelName" disabled /> </el-form-item> <el-form-item label="单位" prop="unit" > <el-input v-model="formState.unit" disabled /> <el-form-item label="单位" prop="unit"> <el-input v-model="formState.unit" disabled /> </el-form-item> <el-form-item label="库存类型" prop="type" :rules="[ <el-form-item label="库存类型" prop="type" :rules="[ { required: true, message: '请选择库存类型', trigger: 'change', } ]" > <el-select v-model="formState.type" placeholder="请选择库存类型"> <el-option label="合格库存" value="qualified" /> <el-option label="不合格库存" value="unqualified" /> ]"> <el-select v-model="formState.type" placeholder="请选择库存类型"> <el-option label="合格库存" value="qualified" /> <el-option label="不合格库存" value="unqualified" /> </el-select> </el-form-item> <el-form-item label="库存数量" prop="qualitity" > <el-input-number v-model="formState.qualitity" :step="1" :min="1" style="width: 100%" /> <el-form-item label="库存数量" prop="qualitity"> <el-input-number v-model="formState.qualitity" :step="1" :min="1" style="width: 100%" /> </el-form-item> <el-form-item label="批号" prop="batchNo" > <el-input v-model="formState.batchNo" placeholder="请输入批号" /> <el-form-item label="批号" prop="batchNo" :rules="[ { required: true, message: '请输入批号', trigger: 'blur', } ]"> <el-input v-model="formState.batchNo" placeholder="请输入批号" /> </el-form-item> <el-form-item v-if="formState.type === 'qualified'" label="库存预警数量" prop="warnNum" > <el-input-number v-model="formState.warnNum" :step="1" :min="0" :max="formState.qualitity" style="width: 100%" /> <el-form-item v-if="formState.type === 'qualified'" label="库存预警数量" prop="warnNum"> <el-input-number v-model="formState.warnNum" :step="1" :min="0" :max="formState.qualitity" style="width: 100%" /> </el-form-item> <el-form-item label="备注" prop="remark"> <el-input v-model="formState.remark" type="textarea" /> <el-form-item label="备注" prop="remark"> <el-input v-model="formState.remark" type="textarea" /> </el-form-item> </el-form> <!-- 产品选择弹窗 --> <ProductSelectDialog v-model="showProductSelectDialog" @confirm="handleProductSelect" :top-product-parent-id="props.topProductParentId" single /> <ProductSelectDialog v-model="showProductSelectDialog" @confirm="handleProductSelect" :top-product-parent-id="props.topProductParentId" single /> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="handleSubmit">确认</el-button> <el-button type="primary" @click="handleSubmit">确认</el-button> <el-button @click="closeModal">取消</el-button> </div> </template> @@ -99,62 +100,27 @@ </template> <script setup> import {ref, computed, watch, getCurrentInstance} from "vue"; import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; import {addStockInRecordOnly} from "@/api/inventoryManagement/stockInventory.js"; import {createStockUnInventory} from "@/api/inventoryManagement/stockUninventory.js"; import { ref, computed, watch, getCurrentInstance } from "vue"; import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; import { addStockInRecordOnly } from "@/api/inventoryManagement/stockInventory.js"; import { createStockUnInventory } from "@/api/inventoryManagement/stockUninventory.js"; const props = defineProps({ visible: { type: Boolean, required: true, }, topProductParentId: { type: Number, default: undefined, required: false, } }); const props = defineProps({ visible: { type: Boolean, required: true, }, topProductParentId: { type: Number, default: undefined, required: false, }, }); const emit = defineEmits(['update:visible', 'completed']); const emit = defineEmits(["update:visible", "completed"]); // 响应式数据(替代选项式的 data) const formState = ref({ productId: undefined, productModelId: undefined, productName: "", productModelName: "", unit: "", type: undefined, qualitity: 0, batchNo: null, warnNum: 0, remark: '', }); const isShow = computed({ get() { return props.visible; }, set(val) { emit('update:visible', val); }, }); const showProductSelectDialog = ref(false); // 批号为空时转为 null watch(() => formState.value.batchNo, (val) => { if (val === '') { formState.value.batchNo = null; } }); let { proxy } = getCurrentInstance() const closeModal = () => { // 重置表单数据 formState.value = { // 响应式数据(替代选项式的 data) const formState = ref({ productId: undefined, productModelId: undefined, productName: "", @@ -164,65 +130,101 @@ qualitity: 0, batchNo: null, warnNum: 0, remark: '', }; isShow.value = false; }; remark: "", }); // 产品选择处理 const handleProductSelect = async (products) => { if (products && products.length > 0) { const product = products[0]; formState.value.productId = product.productId; formState.value.productName = product.productName; formState.value.productModelName = product.model; formState.value.productModelId = product.id; formState.value.unit = product.unit; showProductSelectDialog.value = false; // 触发表单验证更新 proxy.$refs["formRef"]?.validateField('productModelId'); } }; const isShow = computed({ get() { return props.visible; }, set(val) { emit("update:visible", val); }, }); const handleSubmit = () => { proxy.$refs["formRef"].validate(valid => { if (valid) { // 验证是否选择了产品和规格 if (!formState.value.productModelId) { proxy.$modal.msgError("请选择产品"); return; } if (!formState.value.productModelId) { proxy.$modal.msgError("请选择规格"); return; } if (formState.value.type === 'qualified') { addStockInRecordOnly(formState.value).then(res => { // 关闭模态框 isShow.value = false; // 告知父组件已完成 emit('completed'); proxy.$modal.msgSuccess("提交成功"); }) } else { formState.value.warnNum = 0; createStockUnInventory(formState.value).then(res => { // 关闭模态框 isShow.value = false; // 告知父组件已完成 emit('completed'); proxy.$modal.msgSuccess("提交成功"); }) } const showProductSelectDialog = ref(false); // 批号为空时转为 null watch( () => formState.value.batchNo, val => { if (val === "") { formState.value.batchNo = null; } } }) }; ); let { proxy } = getCurrentInstance(); defineExpose({ closeModal, handleSubmit, isShow, }); const closeModal = () => { // 重置表单数据 formState.value = { productId: undefined, productModelId: undefined, productName: "", productModelName: "", unit: "", type: undefined, qualitity: 0, batchNo: null, warnNum: 0, remark: "", }; isShow.value = false; }; // 产品选择处理 const handleProductSelect = async products => { if (products && products.length > 0) { const product = products[0]; formState.value.productId = product.productId; formState.value.productName = product.productName; formState.value.productModelName = product.model; formState.value.productModelId = product.id; formState.value.unit = product.unit; showProductSelectDialog.value = false; // 触发表单验证更新 proxy.$refs["formRef"]?.validateField("productModelId"); } }; const handleSubmit = () => { proxy.$refs["formRef"].validate(valid => { if (valid) { // 验证是否选择了产品和规格 if (!formState.value.productModelId) { proxy.$modal.msgError("请选择产品"); return; } if (!formState.value.productModelId) { proxy.$modal.msgError("请选择规格"); return; } if (formState.value.type === "qualified") { addStockInRecordOnly(formState.value).then(res => { // 关闭模态框 isShow.value = false; // 告知父组件已完成 emit("completed"); proxy.$modal.msgSuccess("提交成功"); }); } else { formState.value.warnNum = 0; createStockUnInventory(formState.value).then(res => { // 关闭模态框 isShow.value = false; // 告知父组件已完成 emit("completed"); proxy.$modal.msgSuccess("提交成功"); }); } } }); }; defineExpose({ closeModal, handleSubmit, isShow, }); </script> src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -1,44 +1,82 @@ <template> <div> <el-dialog v-model="dialogVisible" title="领料详情" width="1400px" @close="handleClose"> <el-table v-loading="materialDetailLoading" :data="materialDetailTableData" border row-key="id"> <el-table-column label="工序名称" prop="processName" min-width="180" /> <el-table-column label="原料名称" prop="materialName" min-width="160" /> <el-table-column label="原料型号" prop="materialModel" min-width="180" /> <el-table-column label="需求数量" prop="requiredQty" min-width="110" /> <el-table-column label="计量单位" prop="unit" width="100" /> <el-table-column label="领用数量" prop="pickQty" min-width="110" /> <el-table-column label="补料数量" min-width="120"> <el-dialog v-model="dialogVisible" title="领料详情" width="1400px" @close="handleClose"> <el-table v-loading="materialDetailLoading" :data="materialDetailTableData" border row-key="id"> <el-table-column label="工序名称" prop="operationName" min-width="180" /> <el-table-column label="原料名称" prop="productName" min-width="160" /> <el-table-column label="原料型号" prop="model" min-width="180" /> <el-table-column label="批号" prop="batchNo" min-width="150" /> <el-table-column label="需求数量" prop="demandedQuantity" min-width="110" /> <el-table-column label="计量单位" prop="unit" width="100" /> <el-table-column label="领用数量" prop="pickQuantity" min-width="110" /> <el-table-column label="补料数量" min-width="120"> <template #default="{ row }"> <el-button type="primary" link @click="handleViewSupplementRecord(row)"> <el-button type="primary" link @click="handleViewSupplementRecord(row)"> {{ row.supplementQty ?? 0 }} </el-button> </template> </el-table-column> <el-table-column label="退料数量" prop="returnQty" min-width="110" /> <el-table-column label="实际数量" prop="actualQty" min-width="110" /> <el-table-column label="退料数量" prop="returnQty" min-width="110" /> <el-table-column label="实际数量" prop="actualQty" min-width="110" /> </el-table> <template #footer> <span class="dialog-footer"> <el-button type="warning" :loading="materialReturnConfirming" :disabled="!canOpenReturnSummary" @click="openReturnSummaryDialog" > <el-button type="warning" :loading="materialReturnConfirming" :disabled="!canOpenReturnSummary" @click="openReturnSummaryDialog"> 退料确认 </el-button> <el-button @click="dialogVisible = false">取消</el-button> </span> </template> </el-dialog> <el-dialog v-model="supplementRecordDialogVisible" title="补料记录" width="800px"> <el-table v-loading="supplementRecordLoading" :data="supplementRecordTableData" border row-key="id"> <el-table-column label="补料数量" prop="supplementQty" min-width="120" /> <el-table-column label="补料人" prop="supplementUserName" min-width="120" /> <el-table-column label="补料日期" prop="supplementTime" min-width="160" /> <el-table-column label="补料原因" prop="supplementReason" min-width="200" /> <el-dialog v-model="supplementRecordDialogVisible" title="补料记录" width="800px"> <el-table v-loading="supplementRecordLoading" :data="supplementRecordTableData" border row-key="id"> <el-table-column label="补料数量" prop="supplementQty" min-width="120" /> <el-table-column label="补料人" prop="supplementUserName" min-width="120" /> <el-table-column label="补料日期" prop="supplementTime" min-width="160" /> <el-table-column label="补料原因" prop="supplementReason" min-width="200" /> </el-table> <template #footer> <span class="dialog-footer"> @@ -46,18 +84,30 @@ </span> </template> </el-dialog> <el-dialog v-model="returnSummaryDialogVisible" title="退料汇总确认" width="900px"> <el-table :data="returnSummaryList" border row-key="summaryKey"> <el-table-column label="原料名称" prop="materialName" min-width="180" /> <el-table-column label="原料型号" prop="materialModel" min-width="180" /> <el-table-column label="计量单位" prop="unit" min-width="100" /> <el-table-column label="退料汇总数量" prop="returnQtyTotal" min-width="140" /> <el-dialog v-model="returnSummaryDialogVisible" title="退料汇总确认" width="900px"> <el-table :data="returnSummaryList" border row-key="summaryKey"> <el-table-column label="原料名称" prop="materialName" min-width="180" /> <el-table-column label="原料型号" prop="materialModel" min-width="180" /> <el-table-column label="计量单位" prop="unit" min-width="100" /> <el-table-column label="退料汇总数量" prop="returnQtyTotal" min-width="140" /> </el-table> <template #footer> <span class="dialog-footer"> <el-button type="primary" :loading="materialReturnConfirming" @click="handleReturnConfirm">确认提交</el-button> <el-button type="primary" :loading="materialReturnConfirming" @click="handleReturnConfirm">确认提交</el-button> <el-button @click="returnSummaryDialogVisible = false">取消</el-button> </span> </template> @@ -66,116 +116,126 @@ </template> <script setup> import { computed, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import { listMaterialPickingDetail, listMaterialSupplementRecord, confirmMaterialReturn } from "@/api/productionManagement/productionOrder.js"; import { computed, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import { listMaterialPickingDetail, listMaterialSupplementRecord, confirmMaterialReturn, } from "@/api/productionManagement/productionOrder.js"; const props = defineProps({ modelValue: { type: Boolean, default: false }, orderRow: { type: Object, default: null }, }); const emit = defineEmits(["update:modelValue", "confirmed"]); const dialogVisible = computed({ get: () => props.modelValue, set: val => emit("update:modelValue", val), }); const materialDetailLoading = ref(false); const materialDetailTableData = ref([]); const materialReturnConfirming = ref(false); const supplementRecordDialogVisible = ref(false); const supplementRecordLoading = ref(false); const supplementRecordTableData = ref([]); const returnSummaryDialogVisible = ref(false); const returnSummaryList = ref([]); const calcReturnQty = item => Number(item.pickQty || 0) + Number(item.supplementQty || 0) - Number(item.actualQty || 0); const canOpenReturnSummary = computed(() => materialDetailTableData.value.some(item => calcReturnQty(item) > 0) ); const loadDetailList = async () => { if (!props.orderRow?.id) return; materialDetailLoading.value = true; materialDetailTableData.value = []; try { const res = await listMaterialPickingDetail({ orderId: props.orderRow.id }); materialDetailTableData.value = res.data || []; } finally { materialDetailLoading.value = false; } }; watch( () => dialogVisible.value, visible => { if (visible) { loadDetailList(); } } ); const handleClose = () => { materialDetailTableData.value = []; }; const handleViewSupplementRecord = async row => { if (!row?.id) return; supplementRecordDialogVisible.value = true; supplementRecordLoading.value = true; supplementRecordTableData.value = []; try { const res = await listMaterialSupplementRecord({ materialDetailId: row.id }); supplementRecordTableData.value = res.data || []; } finally { supplementRecordLoading.value = false; } }; const buildReturnSummary = () => { const map = new Map(); materialDetailTableData.value.forEach(item => { const returnQty = calcReturnQty(item); if (returnQty <= 0) return; const key = `${item.materialModelId || ""}_${item.materialName || ""}_${item.materialModel || ""}_${item.unit || ""}`; const old = map.get(key) || { summaryKey: key, materialName: item.materialName || "", materialModel: item.materialModel || "", unit: item.unit || "", returnQtyTotal: 0, }; old.returnQtyTotal += returnQty; map.set(key, old); const props = defineProps({ modelValue: { type: Boolean, default: false }, orderRow: { type: Object, default: null }, }); return Array.from(map.values()); }; const emit = defineEmits(["update:modelValue", "confirmed"]); const openReturnSummaryDialog = async () => { if (!canOpenReturnSummary.value) { ElMessage.warning("退料数量=领用数量+补料数量-实际数量,且需大于0"); return; } returnSummaryList.value = buildReturnSummary(); returnSummaryDialogVisible.value = true; }; const dialogVisible = computed({ get: () => props.modelValue, set: val => emit("update:modelValue", val), }); const handleReturnConfirm = async () => { if (!props.orderRow?.id) return; materialReturnConfirming.value = true; try { await confirmMaterialReturn({ orderId: props.orderRow.id, returnSummaryList: returnSummaryList.value, const materialDetailLoading = ref(false); const materialDetailTableData = ref([]); const materialReturnConfirming = ref(false); const supplementRecordDialogVisible = ref(false); const supplementRecordLoading = ref(false); const supplementRecordTableData = ref([]); const returnSummaryDialogVisible = ref(false); const returnSummaryList = ref([]); const calcReturnQty = item => Number(item.pickQuantity || 0) + Number(item.supplementQty || 0) - Number(item.actualQty || 0); const canOpenReturnSummary = computed(() => materialDetailTableData.value.some(item => calcReturnQty(item) > 0) ); const loadDetailList = async () => { if (!props.orderRow?.id) return; materialDetailLoading.value = true; materialDetailTableData.value = []; try { const res = await listMaterialPickingDetail(props.orderRow.id); materialDetailTableData.value = res.data || []; } finally { materialDetailLoading.value = false; } }; watch( () => dialogVisible.value, visible => { if (visible) { loadDetailList(); } } ); const handleClose = () => { materialDetailTableData.value = []; }; const handleViewSupplementRecord = async row => { if (!row?.id) return; supplementRecordDialogVisible.value = true; supplementRecordLoading.value = true; supplementRecordTableData.value = []; try { const res = await listMaterialSupplementRecord({ materialDetailId: row.id, }); supplementRecordTableData.value = res.data || []; } finally { supplementRecordLoading.value = false; } }; const buildReturnSummary = () => { const map = new Map(); materialDetailTableData.value.forEach(item => { const returnQty = calcReturnQty(item); if (returnQty <= 0) return; const key = `${item.productModelId || ""}_${item.productName || ""}_${ item.model || "" }_${item.unit || ""}`; const old = map.get(key) || { summaryKey: key, materialName: item.productName || "", materialModel: item.model || "", unit: item.unit || "", returnQtyTotal: 0, }; old.returnQtyTotal += returnQty; map.set(key, old); }); returnSummaryDialogVisible.value = false; dialogVisible.value = false; emit("confirmed"); } finally { materialReturnConfirming.value = false; } }; return Array.from(map.values()); }; const openReturnSummaryDialog = async () => { if (!canOpenReturnSummary.value) { ElMessage.warning("退料数量=领用数量+补料数量-实际数量,且需大于0"); return; } returnSummaryList.value = buildReturnSummary(); returnSummaryDialogVisible.value = true; }; const handleReturnConfirm = async () => { if (!props.orderRow?.id) return; materialReturnConfirming.value = true; try { await confirmMaterialReturn({ orderId: props.orderRow.id, returnSummaryList: returnSummaryList.value, }); returnSummaryDialogVisible.value = false; dialogVisible.value = false; emit("confirmed"); } finally { materialReturnConfirming.value = false; } }; </script> <style scoped lang="scss"></style> src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -13,25 +13,25 @@ border row-key="tempId"> <el-table-column label="工序名称" min-width="180"> min-width="140"> <template #default="{ row }"> <span v-if="row.bom === true">{{ row.processName || "-" }}</span> <span v-if="row.bom === true">{{ row.operationName || "-" }}</span> <el-select v-else v-model="row.processName" v-model="row.operationName" placeholder="请选择工序" clearable filterable style="width: 100%;" @change="val => handleProcessNameChange(row, val)"> <el-option v-for="item in processOptions" :key="item.id" :key="item.technologyOperationId" :label="item.name" :value="item.name" /> </el-select> </template> </el-table-column> <el-table-column label="原料名称" min-width="160"> min-width="140"> <template #default="{ row }"> <span v-if="row.bom === true">{{ row.materialName || "-" }}</span> <el-button v-else @@ -43,17 +43,37 @@ </template> </el-table-column> <el-table-column label="原料型号" min-width="180"> min-width="140"> <template #default="{ row }"> {{ row.materialModel || "-" }} </template> </el-table-column> <!-- 批号多选 --> <el-table-column min-width="200"> <template #header> <span style="color: #f56c6c; margin-right: 4px;">*</span> <span>批号</span> </template> <template #default="{ row }"> <el-select v-model="row.batchNo" multiple collapse-tags collapse-tags-indicator placeholder="请选择批号" style="width: 100%;"> <el-option v-for="item in row.batchNoList" :key="item" :label="item" :value="item" /> </el-select> </template> </el-table-column> <el-table-column label="需求数量" min-width="120"> <template #default="{ row }"> <span v-if="row.bom === true">{{ row.requiredQty ?? "-" }}</span> <span v-if="row.bom === true">{{ row.demandedQuantity ?? "-" }}</span> <el-input-number v-else v-model="row.requiredQty" v-model="row.demandedQuantity" :min="0" :precision="3" :step="1" @@ -63,7 +83,7 @@ </template> </el-table-column> <el-table-column label="计量单位" width="120"> width="100"> <template #default="{ row }"> {{ row.unit || "-" }} </template> @@ -110,12 +130,18 @@ import { computed, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js"; import { findProductProcessRouteItemList, listMain, } from "@/api/productionManagement/productProcessRoute.js"; import { listMaterialPickingDetail, listMaterialPickingBom, listMaterialPickingLedger, saveMaterialPickingLedger, updateMaterialPickingLedger, } from "@/api/productionManagement/productionOrder.js"; import { queryList2 } from "@/api/productionManagement/productStructure.js"; const props = defineProps({ modelValue: { type: Boolean, default: false }, @@ -139,16 +165,22 @@ const createMaterialRow = (row = {}) => ({ tempId: row.id || `temp_${++materialTempId}`, id: row.id, processId: row.processId, productProcessId: row.productProcessId || row.processId, processName: row.processName || "", processId: row.processId || row.technologyOperationId, technologyOperationId: row.technologyOperationId || row.processId, operationName: row.operationName || "", bom: row.bom === true, materialModelId: row.materialModelId, materialName: row.materialName || "", materialModel: row.materialModel || "", requiredQty: Number(row.requiredQty ?? 0), materialModelId: row.materialModelId || row.productModelId, materialName: row.materialName || row.productName || "", materialModel: row.materialModel || row.model || "", demandedQuantity: Number(row.requiredQty ?? row.demandedQuantity ?? 0), unit: row.unit || "", pickQty: Number(row.pickQty ?? row.requiredQty ?? 0), pickQty: Number(row.pickQty ?? row.pickQuantity ?? 0), batchNo: row.batchNo ? typeof row.batchNo === "string" ? row.batchNo.split(",") : row.batchNo : [], batchNoList: row.batchNoList || [], }); const getProcessOptions = async () => { @@ -161,19 +193,20 @@ : res?.data?.records || []; const processMap = new Map(); routeList.forEach(item => { const processId = item.processId; const processName = item.processName; if (!processId || !processName) return; const key = `${processId}_${processName}`; const processId = item.technologyOperationId; const operationName = item.operationName; if (!processId || !operationName) return; const key = `${processId}_${operationName}`; if (!processMap.has(key)) { processMap.set(key, { id: processId, name: processName, name: operationName, }); } }); processOptions.value = Array.from(processMap.values()); }; const isDetail = ref(true); const loadMaterialData = async () => { if (!props.orderRow?.id) return; @@ -181,23 +214,23 @@ materialTableData.value = []; await getProcessOptions(); try { const detailRes = await listMaterialPickingDetail({ orderId: props.orderRow.id, }); const detailRes = await listMaterialPickingDetail(props.orderRow.id); const detailList = Array.isArray(detailRes?.data) ? detailRes.data : detailRes?.data?.records || []; if (detailList.length > 0) { isDetail.value = true; materialTableData.value = detailList.map(item => createMaterialRow(item)); return; } else { isDetail.value = false; const bomRes = await listMaterialPickingBom(props.orderRow.id); const bomList = Array.isArray(bomRes?.data) ? bomRes.data : bomRes?.data?.records || []; materialTableData.value = bomList.map(item => createMaterialRow(item)); return; } const ledgerRes = await listMaterialPickingLedger({ orderId: props.orderRow.id, }); const ledgerList = Array.isArray(ledgerRes?.data) ? ledgerRes.data : ledgerRes?.data?.records || []; materialTableData.value = ledgerList.map(item => createMaterialRow(item)); } finally { materialTableLoading.value = false; } @@ -225,14 +258,16 @@ materialTableData.value.splice(index, 1); }; const handleProcessNameChange = (row, processName) => { const process = processOptions.value.find(item => item.name === processName); row.productProcessId = process?.id; const handleProcessNameChange = (row, operationName) => { const process = processOptions.value.find( item => item.name === operationName ); row.technologyOperationId = process?.technologyOperationId; }; const handleRequiredQtyChange = (row, val) => { const required = Number(val ?? 0); row.requiredQty = required; row.demandedQuantity = required; if (!row.pickQty || Number(row.pickQty) === 0) { row.pickQty = required; } @@ -246,6 +281,8 @@ }; const handleMaterialProductConfirm = products => { console.log(products, "products"); if (!products || products.length === 0) return; const index = currentMaterialSelectRowIndex.value; if (index < 0 || !materialTableData.value[index]) return; @@ -257,6 +294,7 @@ product.materialName || product.productName || product.name || ""; row.materialModel = product.materialModel || product.model || ""; row.unit = product.unit || product.measureUnit || ""; row.batchNoList = product.batchNoList; currentMaterialSelectRowIndex.value = -1; materialProductDialogVisible.value = false; }; @@ -266,22 +304,24 @@ return { valid: false, message: "请先新增领料数据" }; } const invalidNewRow = materialTableData.value.find( item => item.bom !== true && (!item.processName || !item.materialName) item => item.bom !== true && (!item.operationName || !item.materialName) ); if (invalidNewRow) { return { valid: false, message: "新增行的工序名称和原料名称为必填项" }; } const invalidRow = materialTableData.value.find( item => !item.processName || !item.operationName || !item.materialName || item.requiredQty === null || item.requiredQty === undefined || !item.batchNo || item.batchNo.length === 0 || item.demandedQuantity === null || item.demandedQuantity === undefined || item.pickQty === null || item.pickQty === undefined ); if (invalidRow) { return { valid: false, message: "请完善工序、原料和数量后再保存" }; return { valid: false, message: "请完善工序、原料、批号和数量后再保存" }; } return { valid: true, message: "" }; }; @@ -295,22 +335,49 @@ } materialSaving.value = true; try { await saveMaterialPickingLedger({ orderId: props.orderRow.id, items: materialTableData.value.map(item => ({ id: item.id, processId: item.processName, productProcessId: item.productProcessId, processName: item.processName, bom: item.bom === true, materialModelId: item.materialModelId, materialName: item.materialName, materialModel: item.materialModel, requiredQty: item.requiredQty, unit: item.unit, pickQty: item.pickQty, })), }); if (isDetail.value) { await updateMaterialPickingLedger({ productionOrderId: props.orderRow.id, productionOrderPickDto: materialTableData.value.map(item => ({ id: item.id, // processId: item.operationName, technologyOperationId: item.technologyOperationId, operationName: item.operationName, bom: item.bom === true, productModelId: item.materialModelId, // materialName: item.materialName, // materialModel: item.materialModel, demandedQuantity: item.demandedQuantity, unit: item.unit, pickQuantity: item.pickQty, batchNo: Array.isArray(item.batchNo) ? item.batchNo.join(",") : item.batchNo, })), }); } else { await saveMaterialPickingLedger({ productionOrderId: props.orderRow.id, productionOrderPickDto: materialTableData.value.map(item => ({ id: item.id, // processId: item.operationName, technologyOperationId: item.technologyOperationId, operationName: item.operationName, bom: item.bom === true, productModelId: item.materialModelId, // materialName: item.materialName, // materialModel: item.materialModel, demandedQuantity: item.demandedQuantity, unit: item.unit, pickQuantity: item.pickQty, batchNo: Array.isArray(item.batchNo) ? item.batchNo.join(",") : item.batchNo, })), }); } ElMessage({ message: "领料成功", type: "success" }); emit("saved"); dialogVisible.value = false; } finally { src/views/productionManagement/productionOrder/index.vue
@@ -304,7 +304,7 @@ label: "操作", align: "center", fixed: "right", width: 340, width: 360, operation: [ { name: "工艺路线", @@ -337,27 +337,20 @@ showSourceData(row); }, }, // { // name: "产品结构", // type: "text", // clickFun: row => { // showProductStructure(row); // }, // }, // { // name: "领料", // type: "text", // clickFun: row => { // openMaterialDialog(row); // }, // }, // { // name: "领料详情", // type: "text", // clickFun: row => { // openMaterialDetailDialog(row); // }, // }, { name: "领料", type: "text", clickFun: row => { openMaterialDialog(row); }, }, { name: "领料详情", type: "text", clickFun: row => { openMaterialDetailDialog(row); }, }, ], }, ]); src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue
@@ -1,86 +1,119 @@ <template> <div> <el-dialog v-model="dialogVisible" title="物料" width="1200px" @close="handleCloseMaterialDialog" > <el-table v-loading="materialTableLoading" :data="materialTableData" border row-key="id"> <el-table-column label="工序名称" prop="processName" min-width="140" /> <el-table-column label="原料名称" prop="materialName" min-width="140" /> <el-table-column label="原料型号" prop="materialModel" min-width="140" /> <el-table-column label="计量单位" prop="unit" min-width="100" /> <el-table-column label="线边仓数量" prop="pickQty" min-width="100" /> <el-table-column label="补料数量" prop="supplementQty" min-width="100" /> <el-table-column label="实际数量" min-width="140"> <el-dialog v-model="dialogVisible" title="物料" width="1200px" @close="handleCloseMaterialDialog"> <el-table v-loading="materialTableLoading" :data="materialTableData" border row-key="id"> <el-table-column label="工序名称" prop="processName" min-width="140" /> <el-table-column label="原料名称" prop="materialName" min-width="140" /> <el-table-column label="原料型号" prop="materialModel" min-width="140" /> <el-table-column label="计量单位" prop="unit" min-width="100" /> <el-table-column label="线边仓数量" prop="pickQty" min-width="100" /> <el-table-column label="补料数量" prop="supplementQty" min-width="100" /> <el-table-column label="实际数量" min-width="140"> <template #default="{ row }"> <el-input-number v-model="row.actualQty" :min="0" :precision="3" :step="1" controls-position="right" style="width: 100%;" /> <el-input-number v-model="row.actualQty" :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="180"> <el-table-column label="操作" align="center" fixed="right" width="180"> <template #default="{ row }"> <el-button type="primary" link @click="openSupplementDialog(row)">补料</el-button> <el-button type="info" link @click="openSupplementRecordDialog(row)">补料记录</el-button> <el-button type="primary" link @click="openSupplementDialog(row)">补料</el-button> <el-button type="info" link @click="openSupplementRecordDialog(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 type="primary" :loading="pickSubmitting" @click="handleSubmitPick">领用</el-button> <el-button @click="dialogVisible = false">取消</el-button> </span> </template> </el-dialog> <FormDialog v-model="supplementDialogVisible" title="补料" width="500px" @confirm="handleSubmitSupplement" > <el-form ref="supplementFormRef" :model="supplementForm" :rules="supplementRules" label-width="100px"> <el-form-item label="补料数量" prop="supplementQty"> <el-input-number v-model="supplementForm.supplementQty" :min="0.001" :precision="3" :step="1" style="width: 100%;" /> <FormDialog v-model="supplementDialogVisible" title="补料" width="500px" @confirm="handleSubmitSupplement"> <el-form ref="supplementFormRef" :model="supplementForm" :rules="supplementRules" label-width="100px"> <el-form-item label="补料数量" prop="supplementQty"> <el-input-number v-model="supplementForm.supplementQty" :min="0.001" :precision="3" :step="1" style="width: 100%;" /> </el-form-item> <el-form-item label="补料原因" prop="supplementReason"> <el-input v-model="supplementForm.supplementReason" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入补料原因" /> <el-form-item label="补料原因" prop="supplementReason"> <el-input v-model="supplementForm.supplementReason" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入补料原因" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" :loading="supplementSubmitting" @click="handleSubmitSupplement">确定</el-button> <el-button type="primary" :loading="supplementSubmitting" @click="handleSubmitSupplement">确定</el-button> <el-button @click="supplementDialogVisible = false">取消</el-button> </span> </template> </FormDialog> <el-dialog v-model="supplementRecordDialogVisible" title="补料记录" width="900px"> <el-table v-loading="supplementRecordLoading" :data="supplementRecordTableData" border row-key="id"> <el-table-column label="补料数量" prop="supplementQty" min-width="100" /> <el-table-column label="补料原因" prop="supplementReason" min-width="200" /> <el-table-column label="补料人" prop="supplementUserName" min-width="120" /> <el-table-column label="补料日期" prop="supplementTime" min-width="160" /> <el-dialog v-model="supplementRecordDialogVisible" title="补料记录" width="900px"> <el-table v-loading="supplementRecordLoading" :data="supplementRecordTableData" border row-key="id"> <el-table-column label="补料数量" prop="supplementQty" min-width="100" /> <el-table-column label="补料原因" prop="supplementReason" min-width="200" /> <el-table-column label="补料人" prop="supplementUserName" min-width="120" /> <el-table-column label="补料日期" prop="supplementTime" min-width="160" /> </el-table> <template #footer> <span class="dialog-footer"> @@ -92,187 +125,196 @@ </template> <script setup> import { computed, nextTick, reactive, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import FormDialog from "@/components/Dialog/FormDialog.vue"; import { listWorkOrderMaterialLedger, addWorkOrderMaterialSupplement, listWorkOrderMaterialSupplementRecord, pickWorkOrderMaterial, } from "@/api/productionManagement/workOrder.js"; import { computed, nextTick, reactive, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import FormDialog from "@/components/Dialog/FormDialog.vue"; import { listWorkOrderMaterialLedger, addWorkOrderMaterialSupplement, listWorkOrderMaterialSupplementRecord, pickWorkOrderMaterial, } from "@/api/productionManagement/workOrder.js"; 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); const materialTableData = ref([]); const currentMaterialRow = ref(null); const currentMaterialOrderRow = ref(null); const pickSubmitting = ref(false); const supplementDialogVisible = ref(false); const supplementSubmitting = ref(false); const supplementFormRef = ref(null); const supplementForm = reactive({ supplementQty: null, supplementReason: "", }); const supplementRecordDialogVisible = ref(false); const supplementRecordLoading = ref(false); const supplementRecordTableData = ref([]); const supplementRules = { supplementQty: [{ required: true, message: "请输入补料数量", trigger: "blur" }], supplementReason: [{ required: true, message: "请输入补料原因", trigger: "blur" }], }; const loadMaterialTable = async row => { if (!row?.id) return; currentMaterialOrderRow.value = row; materialTableLoading.value = true; materialTableData.value = []; try { const res = await listWorkOrderMaterialLedger({ workOrderId: row.id, processId: row.processId, productProcessRouteItemId: row.productProcessRouteItemId, }); materialTableData.value = res.data || []; } catch (e) { console.error("获取物料台账失败", e); ElMessage.error("获取物料台账失败"); } finally { materialTableLoading.value = false; } }; watch( () => props.modelValue, visible => { if (visible && props.rowData) { loadMaterialTable(props.rowData); } } ); const handleCloseMaterialDialog = () => { materialTableData.value = []; currentMaterialRow.value = null; currentMaterialOrderRow.value = null; }; const openSupplementDialog = row => { currentMaterialRow.value = row; supplementForm.supplementQty = null; supplementForm.supplementReason = ""; supplementDialogVisible.value = true; nextTick(() => { supplementFormRef.value?.clearValidate(); const props = defineProps({ modelValue: { type: Boolean, default: false, }, rowData: { type: Object, default: () => null, }, }); }; const handleSubmitSupplement = () => { supplementFormRef.value?.validate(async valid => { if (!valid || !currentMaterialRow.value?.id) { ElMessage.warning("缺少物料明细ID"); const emit = defineEmits(["update:modelValue", "refresh"]); const dialogVisible = computed({ get: () => props.modelValue, set: val => emit("update:modelValue", val), }); const materialTableLoading = ref(false); const materialTableData = ref([]); const currentMaterialRow = ref(null); const currentMaterialOrderRow = ref(null); const pickSubmitting = ref(false); const supplementDialogVisible = ref(false); const supplementSubmitting = ref(false); const supplementFormRef = ref(null); const supplementForm = reactive({ supplementQty: null, supplementReason: "", }); const supplementRecordDialogVisible = ref(false); const supplementRecordLoading = ref(false); const supplementRecordTableData = ref([]); const supplementRules = { supplementQty: [ { required: true, message: "请输入补料数量", trigger: "blur" }, ], supplementReason: [ { required: true, message: "请输入补料原因", trigger: "blur" }, ], }; const loadMaterialTable = async row => { if (!row?.id) return; currentMaterialOrderRow.value = row; materialTableLoading.value = true; materialTableData.value = []; try { const res = await listWorkOrderMaterialLedger({ workOrderId: row.id, processId: row.processId, productProcessRouteItemId: row.productProcessRouteItemId, }); materialTableData.value = res.data || []; } catch (e) { console.error("获取物料台账失败", e); ElMessage.error("获取物料台账失败"); } finally { materialTableLoading.value = false; } }; watch( () => props.modelValue, visible => { if (visible && props.rowData) { loadMaterialTable(props.rowData); } } ); const handleCloseMaterialDialog = () => { materialTableData.value = []; currentMaterialRow.value = null; currentMaterialOrderRow.value = null; }; const openSupplementDialog = row => { currentMaterialRow.value = row; supplementForm.supplementQty = null; supplementForm.supplementReason = ""; supplementDialogVisible.value = true; nextTick(() => { supplementFormRef.value?.clearValidate(); }); }; const handleSubmitSupplement = () => { supplementFormRef.value?.validate(async valid => { if (!valid || !currentMaterialRow.value?.id) { ElMessage.warning("缺少物料明细ID"); return; } supplementSubmitting.value = true; try { await addWorkOrderMaterialSupplement({ materialLedgerId: currentMaterialRow.value.id, supplementQty: Number(supplementForm.supplementQty), supplementReason: supplementForm.supplementReason, workOrderId: currentMaterialOrderRow.value?.id, }); supplementDialogVisible.value = false; await loadMaterialTable(currentMaterialOrderRow.value); ElMessage.success("补料成功"); emit("refresh"); } catch (e) { console.error("补料失败", e); ElMessage.error("补料失败"); } finally { supplementSubmitting.value = false; } }); }; const openSupplementRecordDialog = async row => { supplementRecordDialogVisible.value = true; supplementRecordLoading.value = true; supplementRecordTableData.value = []; try { const res = await listWorkOrderMaterialSupplementRecord({ materialLedgerId: row.id, }); supplementRecordTableData.value = res.data || []; } catch (e) { console.error("获取补料记录失败", e); ElMessage.error("获取补料记录失败"); } finally { supplementRecordLoading.value = false; } }; const validatePickRows = () => { if (materialTableData.value.length === 0) { return { valid: false, message: "暂无可领用物料" }; } const invalidRow = materialTableData.value.find( item => item.actualQty === null || item.actualQty === undefined || item.actualQty === "" ); if (invalidRow) { return { valid: false, message: "请填写实际数量后再领用" }; } const exceedRow = materialTableData.value.find(item => { const maxQty = Number(item.pickQty || 0) + Number(item.supplementQty || 0); return Number(item.actualQty || 0) > maxQty; }); if (exceedRow) { return { valid: false, message: "实际数量不能大于领用数量+补料数量" }; } return { valid: true, message: "" }; }; const handleSubmitPick = async () => { if (!currentMaterialOrderRow.value?.id) return; const validateResult = validatePickRows(); if (!validateResult.valid) { ElMessage.warning(validateResult.message); return; } supplementSubmitting.value = true; pickSubmitting.value = true; try { await addWorkOrderMaterialSupplement({ materialLedgerId: currentMaterialRow.value.id, supplementQty: Number(supplementForm.supplementQty), supplementReason: supplementForm.supplementReason, workOrderId: currentMaterialOrderRow.value?.id, await pickWorkOrderMaterial({ workOrderId: currentMaterialOrderRow.value.id, items: materialTableData.value.map(item => ({ materialLedgerId: item.id, actualQty: Number(item.actualQty || 0), })), }); supplementDialogVisible.value = false; ElMessage.success("领用成功"); await loadMaterialTable(currentMaterialOrderRow.value); ElMessage.success("补料成功"); emit("refresh"); } catch (e) { console.error("补料失败", e); ElMessage.error("补料失败"); console.error("领用失败", e); ElMessage.error("领用失败"); } finally { supplementSubmitting.value = false; pickSubmitting.value = false; } }); }; const openSupplementRecordDialog = async row => { supplementRecordDialogVisible.value = true; supplementRecordLoading.value = true; supplementRecordTableData.value = []; try { const res = await listWorkOrderMaterialSupplementRecord({ materialLedgerId: row.id, }); supplementRecordTableData.value = res.data || []; } catch (e) { console.error("获取补料记录失败", e); ElMessage.error("获取补料记录失败"); } finally { supplementRecordLoading.value = false; } }; const validatePickRows = () => { if (materialTableData.value.length === 0) { return { valid: false, message: "暂无可领用物料" }; } const invalidRow = materialTableData.value.find(item => item.actualQty === null || item.actualQty === undefined || item.actualQty === ""); if (invalidRow) { return { valid: false, message: "请填写实际数量后再领用" }; } const exceedRow = materialTableData.value.find(item => { const maxQty = Number(item.pickQty || 0) + Number(item.supplementQty || 0); return Number(item.actualQty || 0) > maxQty; }); if (exceedRow) { return { valid: false, message: "实际数量不能大于领用数量+补料数量" }; } return { valid: true, message: "" }; }; const handleSubmitPick = async () => { if (!currentMaterialOrderRow.value?.id) return; const validateResult = validatePickRows(); if (!validateResult.valid) { ElMessage.warning(validateResult.message); return; } pickSubmitting.value = true; try { await pickWorkOrderMaterial({ workOrderId: currentMaterialOrderRow.value.id, items: materialTableData.value.map(item => ({ materialLedgerId: item.id, actualQty: Number(item.actualQty || 0), })), }); ElMessage.success("领用成功"); await loadMaterialTable(currentMaterialOrderRow.value); emit("refresh"); } catch (e) { console.error("领用失败", e); ElMessage.error("领用失败"); } finally { pickSubmitting.value = false; } }; }; </script>