|
|
@@ -0,0 +1,236 @@
|
|
|
+<template>
|
|
|
+ <el-dialog v-model="visible" title="物料分配" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
|
|
|
+ <el-form ref="formRef" :model="form" label-width="120">
|
|
|
+ <el-collapse v-model="activeNames">
|
|
|
+ <el-collapse-item title="订单信息" name="order">
|
|
|
+ <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
|
|
|
+ <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <el-descriptions :column="2" label-width="140" border>
|
|
|
+ <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.code }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.orderDate }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="客户名称" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.customerName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="预计交期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.planReceiveDate }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="订单产品" :span="ismobile ? 2 : 1" label-align="right">
|
|
|
+ <div v-for="item in orderData.childrenList" :key="item.id" class="order-product">{{ item.material.name }}({{ item.materialCode }})
|
|
|
+ <template v-if="item.material.needType == 'self_made'">
|
|
|
+ <el-tag v-if="!item.bomList" type="warning">请维护BOM清单</el-tag>
|
|
|
+ <el-button v-else type="primary" link @click="show_bom(item)">查看BOM清单</el-button>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="需求数量" :span="ismobile ? 2 : 1" label-align="right">
|
|
|
+ <div v-for="item in orderData.childrenList" :key="item.id" class="order-product">{{ item.materialQuantity }}({{ item.material.unit }})</div>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <el-row>
|
|
|
+ <el-col :md="8" :xs="24">
|
|
|
+ <el-form-item label="计划周期" prop="planBeginDate" label-width="140" :rules="{ required: true, message: '请选择计划周期' }">
|
|
|
+ <vxe-date-range-picker v-model:start-value="form.planBeginDate" v-model:end-value="form.planEndDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划周期"></vxe-date-range-picker>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-collapse-item>
|
|
|
+
|
|
|
+ <el-collapse-item v-for="(item, name) in XEUtils.omit(cardDic, (_, key) => !form[key].length)" v-bind:key="name" :name="name">
|
|
|
+ <template #title="{ isActive }">
|
|
|
+ <div :class="['title-wrapper', { 'is-active': isActive }]">{{ item.title }}<span>(共{{ formatEditableCount(name) }}项)</span></div>
|
|
|
+ </template>
|
|
|
+ <el-row>
|
|
|
+ <el-col :md="8" :xs="24">
|
|
|
+ <el-form-item :label="`${item.title}主题`" required>
|
|
|
+ <el-input v-model="form[`${item.fieldPrefix}Name`]" clearable placeholder="不填将自动生成"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <sc-form-table v-model="form[name]" v-bind="item.tableOptions">
|
|
|
+ <template #stock="{ row }">
|
|
|
+ <template v-if="!row.disabled">
|
|
|
+ <template v-if="!row.warehouse.length">无库存</template>
|
|
|
+ <template v-else>
|
|
|
+ <span>{{ formatWarehouseCount(row.warehouse, "number") }}</span>/
|
|
|
+ <span>{{ formatWarehouseCount(row.warehouse, "normalNumber") }}</span>/
|
|
|
+ <span>{{ formatWarehouseCount(row.warehouse, "lockedNumber") }}</span>
|
|
|
+ <el-button type="primary" link @click="table_stock(name, row)">查看</el-button>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ <template #stockUse="{ row }">
|
|
|
+ <el-button type="primary" link @click="table_stock(name, row, 'stockUse')">分配</el-button>
|
|
|
+ <el-button type="info" link :disabled="!row.stockUseNum || row.stockUseNum == 0" @click="table_clear(row)">清空</el-button>
|
|
|
+ </template>
|
|
|
+ </sc-form-table>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <el-button auto-insert-space @click="visible = false">取消</el-button>
|
|
|
+ <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <multiple-drawer v-if="drawer" ref="drawerRef" @success="editSuccess" @closed="drawer = false"></multiple-drawer>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import moment from "moment";
|
|
|
+import XEUtils from "xe-utils";
|
|
|
+
|
|
|
+import API from "@/api";
|
|
|
+import store from "@/store";
|
|
|
+import { cardDic } from "./main";
|
|
|
+import multipleDrawer from "./drawer";
|
|
|
+
|
|
|
+const $emit = defineEmits(["success", "closed"]);
|
|
|
+const visible = ref(false);
|
|
|
+const drawer = ref(false);
|
|
|
+const isSaving = ref(false);
|
|
|
+
|
|
|
+const ismobile = computed(() => store.state.global.ismobile);
|
|
|
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == orderData.tenantId), "name"));
|
|
|
+
|
|
|
+const activeNames = ref(["order", "self_made", "out_purchase", "outsourcing"]);
|
|
|
+const orderData = reactive({
|
|
|
+ id: null,
|
|
|
+ tenantId: store.state.tenant.tenantId,
|
|
|
+ code: null,
|
|
|
+ orderDate: null,
|
|
|
+ customerName: null,
|
|
|
+ planReceiveDate: null,
|
|
|
+ childrenList: []
|
|
|
+});
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ productPlanName: null,
|
|
|
+ purchasePlanName: null,
|
|
|
+ outsourcingPlanName: null,
|
|
|
+ planBeginDate: null,
|
|
|
+ planEndDate: null,
|
|
|
+ self_made: [],
|
|
|
+ out_purchase: [],
|
|
|
+ outsourcing: []
|
|
|
+});
|
|
|
+
|
|
|
+const formatEditableCount = name => XEUtils.filter(XEUtils.toTreeArray(form[name]), item => item.material.needType == name).length;
|
|
|
+const formatWarehouseCount = (array, key) => XEUtils.sum(XEUtils.map(array, item => XEUtils.get(item, key)));
|
|
|
+const setData = data => {
|
|
|
+ visible.value = true;
|
|
|
+ XEUtils.objectEach(orderData, (_, key) => {
|
|
|
+ if (key == "childrenList") XEUtils.set(orderData, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, bomList: item.bomList || [{ parentId: "0", ...XEUtils.pick(item, "material", "warehouseMaterialList") }] })));
|
|
|
+ else XEUtils.set(orderData, key, XEUtils.get(data, key));
|
|
|
+ });
|
|
|
+
|
|
|
+ XEUtils.objectEach(form, (_, key) => {
|
|
|
+ if (XEUtils.has(cardDic, key)) {
|
|
|
+ const tableData = XEUtils.map(orderData.childrenList, product => {
|
|
|
+ // 三种类型 bomtree
|
|
|
+ const bomTree = XEUtils.searchTree(XEUtils.toArrayTree(product.bomList), item => item.material.needType == key, { isEvery: true });
|
|
|
+
|
|
|
+ return XEUtils.mapTree(bomTree, item => {
|
|
|
+ // 获取父节点quantity 求积
|
|
|
+ const quantity = XEUtils.reduce(XEUtils.map(XEUtils.get(XEUtils.findTree(bomTree, b => b.id == item.id), "nodes", []), b => b.parentId === "0" ? product.materialQuantity : b.quantity), (p, v) => XEUtils.multiply(p, v));
|
|
|
+ // 控制冗余节点(展示层级关系) 不可编辑
|
|
|
+ const disabled = item.material.needType != key;
|
|
|
+ return {
|
|
|
+ disabled,
|
|
|
+ bomId: item.id,
|
|
|
+ routeId: item.routeId,
|
|
|
+ material: item.material,
|
|
|
+ warehouse: item.warehouseMaterialList || [],
|
|
|
+ quantity,
|
|
|
+ stockUseNum: disabled ? undefined: 0,
|
|
|
+ allocateNum: disabled ? undefined: quantity
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ XEUtils.set(form, key, XEUtils.flatten(tableData));
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+const drawerRef = ref();
|
|
|
+const show_bom = ({ materialQuantity, bomList }) => {
|
|
|
+ const bomTree = XEUtils.toArrayTree(bomList);
|
|
|
+ const tableData = XEUtils.mapTree(bomTree, item => ({ ...item, total: XEUtils.reduce(XEUtils.map(XEUtils.get(XEUtils.findTree(bomTree, b => b.id == item.id), "nodes", []), b => b.parentId === "0" ? materialQuantity : b.quantity), (p, v) => XEUtils.multiply(p, v)) }));
|
|
|
+
|
|
|
+ drawer.value = true;
|
|
|
+ nextTick(() => drawerRef.value?.setData({ tableData }));
|
|
|
+}
|
|
|
+
|
|
|
+const table_stock = (table_Key, row, mode = "stock") => {
|
|
|
+ drawer.value = true;
|
|
|
+ nextTick(() => drawerRef.value?.setData({ table_Key, ...XEUtils.omit(row, "warehouse"), tableData: row.warehouse }, mode));
|
|
|
+}
|
|
|
+
|
|
|
+const editSuccess = ({ table_Key, id, tableData }) => {
|
|
|
+ const treeItem = XEUtils.get(XEUtils.findTree(form[table_Key], item => item.id == id), "item");
|
|
|
+ treeItem.stockUseNum = XEUtils.sum(tableData, item => item.useNum);
|
|
|
+ treeItem.allocateNum = XEUtils.subtract(treeItem.quantity, XEUtils.sum(tableData, item => item.useNum));
|
|
|
+ treeItem.warehouse = tableData;
|
|
|
+}
|
|
|
+
|
|
|
+const table_clear = row => {
|
|
|
+ row.stockUseNum = 0;
|
|
|
+ row.allocateNum = row.quantity;
|
|
|
+ XEUtils.arrayEach(row.warehouse, item => item.useNum = undefined);
|
|
|
+}
|
|
|
+
|
|
|
+const formRef = ref();
|
|
|
+const submit = () => {
|
|
|
+ formRef.value.validate(async valid => {
|
|
|
+ if (valid) {
|
|
|
+ const data = XEUtils.omit(form, (_, key) => XEUtils.has(cardDic, key));
|
|
|
+ XEUtils.set(data, "saleOrderId", orderData.id);
|
|
|
+ XEUtils.objectEach(cardDic, ({ fieldPrefix }, key) => {
|
|
|
+ XEUtils.set(data,
|
|
|
+ `${fieldPrefix}VoList`,
|
|
|
+ XEUtils.map(XEUtils.filter(XEUtils.toTreeArray(form[key]), item => item.material.needType == key), item => XEUtils.omit({
|
|
|
+ bomId: item.bomId,
|
|
|
+ routeId: item.routeId,
|
|
|
+ allocateNum: item.allocateNum,
|
|
|
+ materialCode: item.material.code,
|
|
|
+ warehouseMaterialVoList: XEUtils.map(item.warehouse, w => ({ warehouseId: w.warehouseId, lockedNumber: w.useNum || 0 }))
|
|
|
+ }, key != "self_made" && ["bomId", "routeId"]))
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ isSaving.value = true;
|
|
|
+ API.production.prePlan.allocate(data).then(res => {
|
|
|
+ ElMessage.success("操作成功");
|
|
|
+ isSaving.value = false;
|
|
|
+ visible.value = false;
|
|
|
+ $emit("success");
|
|
|
+ }).catch(() => isSaving.value = false);
|
|
|
+ } else {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ setData
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.el-form {padding-left: 16px;padding-right: 22px;}
|
|
|
+
|
|
|
+.el-collapse {border: none;}
|
|
|
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__header) .title-wrapper {display: flex;align-items: center;gap: 4px;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__header) .title-wrapper span {font-size: var(--el-collapse-header-font-size);color: var(--el-text-color-secondary);}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row {margin-top: 20px;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item {margin-bottom: 0;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker .vxe-date-range-picker--suffix {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker .vxe-date-range-picker--control-icon {padding-left: .5em;padding-right: 0;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 260px;}
|
|
|
+.el-collapse-item :deep(.el-collapse-item__content) .order-product + .order-product {margin-top: 4px;padding-top: 4px;border-top: var(--el-descriptions-table-border);}
|
|
|
+</style>
|