Explorar el Código

fix(报价单审批): 修复审批页明细修改不落库,并完善指派技术员流程

审批通过时按整单对象回写报价单(含 quoteProductList / totalPrice),详情按节点选择可编辑或只读表格取数并校验生产交付交期;生产主管指派技术员改为选人后传 userId;审批成功码改为只认 0;修正 changeCount 的 \ 用法、客户代号改为取 contactDetail 的 base.serialNo;补充有效期输入组与多选 tag 样式
lihaiting hace 5 días
padre
commit
e033302a3a

+ 16 - 5
src/BIZComponents/inventoryTable.vue

@@ -760,6 +760,8 @@
           datasource: []
         },
         pricingWayList,
+        // 标记:用户是否手动改过表格数据(单价/数量),审批前据此判断是否需要落库
+        singlePriceChanged: false,
         rules: {},
         dictList: {}
       };
@@ -1073,7 +1075,8 @@
               },
 
           {
-            width: 200,
+            // 有效期 = 数字输入框 + 单位下拉,两个控件拼一起
+            width: 150,
             prop: 'guaranteePeriod',
             label: '有效期',
             slot: 'guaranteePeriod',
@@ -1281,6 +1284,10 @@
       getPrice() {
         return [this.allPrice, this.form.discountTotalPrice];
       },
+      // 供父组件查询:用户在表格里是否手动调整过数据
+      isSinglePriceChanged() {
+        return this.singlePriceChanged;
+      },
       orderNoChange(row, item) {
         row['orderId'] = item.orderId;
       },
@@ -1288,16 +1295,20 @@
       changeCount(row, index) {
         if (!row) {
           this.form.datasource.forEach((item, index) => {
+            // 必须替换数组里的行对象:写成 this.$set(this.form, 'datasource[i]')
+            // 只会在 form 上挂一个字符串键,数组元素不会更新,合计/总计永远是旧值
             this.$set(
-              this.form,
-              'datasource[' + index + ']',
+              this.form.datasource,
+              index,
               changeCount(item, this.countObj)
             );
           });
         } else {
+          // 用户在界面上改动了某行的单价/数量,置位"已改动"标记
+          this.singlePriceChanged = true;
           this.$set(
-            this.form,
-            'datasource[' + index + ']',
+            this.form.datasource,
+            index,
             changeCount(row, this.countObj)
           );
         }

+ 4 - 0
src/BIZComponents/inventoryTableDetails.vue

@@ -995,6 +995,10 @@
         });
         return comitDatasource;
       },
+      // 返回 [总计, 优惠后总金额],与可编辑表格的接口保持一致
+      getPrice() {
+        return [this.allPrice, this.form.discountTotalPrice];
+      },
       setDeliveryDays() {
         console.log(this.form.datasource, 'this.form.datasource');
         this.form.datasource.forEach((item, i) => {

+ 86 - 0
src/styles/index.scss

@@ -203,3 +203,89 @@ table th .is-required::before {
 .repair_infoLogs .el-steps:last-child .el-step__line {
   display: none !important;
 }
+
+/* 下拉触发器(如审批按钮区的“更多”):可点击文字统一显示手型光标 */
+.el-dropdown-link {
+  cursor: pointer;
+}
+
+/*
+ * 物品清单「有效期」列:数字输入框 + 有效期单位下拉 拼成一个输入组
+ * 问题:左侧输入框包在 el-form-item 里,内容区默认 40px 行高,输入框按基线排版
+ * 会被往下压 2px,导致和右侧下拉框上下错位;同时两个框各自带边框,拼接处成了双线。
+ * 全项目有 6 份 inventoryTable 副本(报价单/商机/合同/采购/销售/退货),统一在这里修。
+ */
+.period {
+  display: flex;
+  align-items: center;
+  width: 100%;
+
+  .borderleftnone {
+    flex: 1 1 0;
+    min-width: 0;
+
+    .el-form-item {
+      margin-bottom: 0;
+    }
+
+    /* 去掉行高/基线影响,让输入框与右侧下拉框顶边对齐 */
+    .el-form-item__content {
+      display: flex;
+      line-height: normal;
+    }
+
+    .el-input__inner {
+      border-top-right-radius: 0;
+      border-bottom-right-radius: 0;
+    }
+  }
+
+  .borderrightnone {
+    /* 和左侧输入框等分整列宽度 */
+    flex: 1 1 0;
+    min-width: 0;
+
+    /* 拼接处只留左侧输入框的右边框,避免两条边框叠成双线 */
+    .el-input__inner {
+      border-top-left-radius: 0;
+      border-bottom-left-radius: 0;
+      border-left: 0;
+    }
+  }
+
+  /* 单元格很窄,默认 15px 内边距会把"请输入/请选择"截断 */
+  .el-input__inner {
+    padding-left: 6px;
+    padding-right: 6px;
+  }
+}
+
+/*
+ * 表格内的多选(如报价单物品清单的「产地」):
+ * el-tag 是 flex 子项,单元格一窄就被压扁,文字被裁掉、关闭按钮溢出到 tag 外面。
+ * 这里让 tag 不被压缩,并把 tag / 关闭图标收小一号,保证 2~3 个字的选项能完整显示。
+ */
+.time-form .el-select .el-tag {
+  flex: 0 0 auto;
+  max-width: 100%;
+  height: 22px;
+  line-height: 20px;
+  padding: 0 4px;
+  font-size: 12px;
+}
+
+.time-form .el-select .el-tag .el-icon-close {
+  flex: none;
+  position: static;
+  width: 14px;
+  height: 14px;
+  line-height: 14px;
+  font-size: 10px;
+  margin-left: 3px;
+}
+
+.time-form .el-select__tags-text {
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}

+ 31 - 4
src/views/bpm/handleTask/components/quotation/addDialog.vue

@@ -259,6 +259,7 @@
         :isDeliveryDeadline="false"
         :isGuaranteePeriod="false"
         :isDiscount="false"
+        :is-produce-delivery-deadline="isProduceDeliveryDeadlineRequired"
       ></inventoryTable>
       <!-- <el-row style="margin-top: 20px">
         <el-col :span="6">
@@ -338,6 +339,7 @@
     getcontactlink,
     getDetail
   } from '@/api/bpm/components/saleManage/quotation';
+  import { contactDetail } from '@/api/bpm/components/saleManage/contact';
   import {
     getDetail as getOpportunityDetail
   } from '@/api/bpm/components/saleManage/businessOpportunity';
@@ -357,6 +359,25 @@
     props: {
       businessId: {
         default: ''
+      },
+      // 当前审批节点(发起时为空),保留透传,便于后续按节点控制字段
+      taskDefinitionKey: {
+        default: ''
+      }
+    },
+    computed: {
+      /**
+       * 「生产交付交期」是否必填:
+       * - 发起报价单(没有审批节点,taskDefinitionKey 为空)→ 不必填
+       * - 生产主管指派节点只是选人往下传,不排产,不校验
+       * - 其余审批节点(技术员、销售员补充等真正排产/确认交期的节点)→ 必填
+       * 驳回/转办不走回写,不会触发这条校验
+       */
+      isProduceDeliveryDeadlineRequired() {
+        return (
+          !!this.taskDefinitionKey &&
+          this.taskDefinitionKey !== 'productionSupervisorApprove1'
+        );
       }
     },
     data() {
@@ -587,21 +608,27 @@
           contactLinkId: '',
           contactLinkName: '',
           contactEmail: '',
-          contactTel: '',
-          customerMark: base.serialNo
-
+          contactTel: ''
         });
         this.getLinkInfo(obj.id);
         this.getBusinessList(obj.name);
+        this.getCustomerMark(obj.id);
         this.$nextTick(() => {
           if(this.form.opportunityType==1){
             this.$refs.inventoryTable.putTableValue([])
             this.form.opportunityId=''
           }
-          this.$refs.inventoryTable.setCustomerMark(base.serialNo);
         });
       },
 
+      // 选完客户后取客户代号(base.serialNo),填充到明细行的 customerMark
+      async getCustomerMark(contactId) {
+        const { base } = await contactDetail(contactId);
+        this.$set(this.form, 'customerMark', base.serialNo);
+        this.$refs.inventoryTable &&
+          this.$refs.inventoryTable.setCustomerMark(base.serialNo);
+      },
+
       handParent() {
         let item = {
           id: this.form.contactId

+ 42 - 1
src/views/bpm/handleTask/components/quotation/detailDialog.vue

@@ -83,6 +83,7 @@
       :isDeliveryDeadline="false"
       :isGuaranteePeriod="false"
       :isDiscount="false"
+      :isProduceDeliveryDeadline="true"
       v-if="taskDefinitionKey === 'technicianApprove'"
     ></inventoryTable>
     <inventoryTabledetail
@@ -296,8 +297,48 @@
           this.form = data;
         }
       },
+      /**
+       * 取当前明细数据,供审批组件在「技术主管审批 / 销售员补充 / 发起人」节点回写报价单。
+       * 1. 技术员节点渲染可编辑的 inventoryTable,其余节点渲染只读的 inventoryTabledetail,
+       *    必须按当前真正挂载的表格取,否则另一个 ref 是 undefined,会报 "reading 'getTableValue'"。
+       * 2. /eom/quote/update 接收的是报价单对象(含 quoteProductList),不是裸数组,
+       *    所以这里返回整单数据,明细和总价都用当前表格的最新值覆盖。
+       */
       getTableValue() {
-        return this.$refs.inventoryTable.getTableValue();
+        const table =
+          this.taskDefinitionKey === 'technicianApprove'
+            ? this.$refs.inventoryTable
+            : this.$refs.inventoryTabledetailRef;
+        if (!table) {
+          throw new Error(
+            `报价单明细表格未渲染(taskDefinitionKey=${this.taskDefinitionKey}),无法取明细数据`
+          );
+        }
+        // 审批提交前不跑表格的 el-form 校验,必填项在这里兜住:
+        // 可编辑表格(isProduceDeliveryDeadline 开启)里生产交付交期必填
+        if (table === this.$refs.inventoryTable) {
+          const rows = table.getTableValue() || [];
+          const missing = rows.some((row) => !row.produceDeliveryDeadline);
+          if (missing) {
+            throw new Error('请选择生产交付交期');
+          }
+        }
+        const payload = Object.assign({}, this.form, {
+          quoteProductList: table.getTableValue()
+        });
+        // 明细被改过之后 form 里的旧总额已失效,总价以表格当前计算结果为准
+        if (typeof table.getPrice === 'function') {
+          payload.totalPrice = table.getPrice()[0];
+        }
+        return payload;
+      },
+      // 转发:用户在明细表里是否手动改过单价/数量,审批前据此判断是否需要落库
+      isSinglePriceChanged() {
+        const table =
+          this.taskDefinitionKey === 'technicianApprove'
+            ? this.$refs.inventoryTable
+            : this.$refs.inventoryTabledetailRef;
+        return (table && table.isSinglePriceChanged && table.isSinglePriceChanged()) || false;
       }
     }
   };

+ 71 - 134
src/views/bpm/handleTask/components/quotation/submit.vue

@@ -1,31 +1,6 @@
 <template>
   <el-col :span="16" :offset="6">
     <el-form label-width="100px" ref="formRef" :model="form">
-      <!-- <el-form-item
-        label="技术员"
-        prop="technicianId"
-        style="margin-bottom: 20px"
-        :rules="{
-          required: true,
-          message: '请选择',
-          trigger: 'change'
-        }"
-        v-if="taskDefinitionKey == 'productionSupervisorApprove1'"
-      >
-        <el-select
-          v-model="form.technicianId"
-          clearable
-          style="width: 100%"
-          :filterable="true"
-        >
-          <el-option
-            v-for="item in userOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item> -->
       <el-form-item
         label="审批意见"
 
@@ -40,33 +15,32 @@
       </el-form-item>
     </el-form>
     <div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
+      <!-- 生产主管指派节点:通过没有任何意义,必须指派技术员才能流转到下一节点 -->
       <el-button
-        icon="el-icon-edit-outline"
+        v-if="taskDefinitionKey === 'productionSupervisorApprove1'"
+        icon="el-icon-user"
         type="success"
         size="mini"
         v-click-once
-        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
-        >通过
+        @click="$refs.approvalOpinion.validate(1) && openAssign()"
+        >指派技术员
       </el-button>
-      <!-- <el-button
+      <el-button
+        v-else
         icon="el-icon-edit-outline"
         type="success"
         size="mini"
-        v-if="taskDefinitionKey === 'productionSupervisorApprove1'"
-        @click="head"
-        >指派技术员
-      </el-button> -->
+        v-click-once
+        @click="$refs.approvalOpinion.validate(1) && handleAudit(1)"
+        >通过
+      </el-button>
       <el-button
         icon="el-icon-circle-close"
         type="danger"
         size="mini"
         v-click-once
         @click="$refs.approvalOpinion.validate(0) && handleAudit(0)"
-        v-if="
-          !['starter'].includes(
-            taskDefinitionKey
-          )
-        "
+        v-if="!['starter'].includes(taskDefinitionKey)"
         >驳回
       </el-button>
 
@@ -76,43 +50,14 @@
           <el-dropdown-item command="cancel">作废</el-dropdown-item>
         </el-dropdown-menu>
       </el-dropdown>
-
-      <!-- <el-button
-        icon="el-icon-circle-close"
-        type="danger"
-        size="mini"
-        @click="handleBackList"
-        >退回
-      </el-button> -->
-      <!-- <el-button
-        icon="el-icon-circle-close"
-        type="danger"
-        size="mini"
-        @click="handleAudit(0)"
-        v-if="taskDefinitionKey != 'productionSupervisorApprove1'"
-        >不通过
-      </el-button>
-      <el-button
-        icon="el-icon-edit-outline"
-        type="primary"
-        size="mini"
-        v-if="taskDefinitionKey != 'productionSupervisorApprove1'"
-        @click="handleUpdateAssignee"
-        >转办
-      </el-button> -->
     </div>
     <head-list ref="headRef" @changeParent="changePersonel"></head-list>
   </el-col>
 </template>
 
 <script>
-  import {
-    updateTech,
-    UpdateInformation,
-    cancel
-  } from '@/api/bpm/components/saleManage/quotation';
+  import { UpdateInformation } from '@/api/bpm/components/saleManage/quotation';
   import {approveTaskWithVariables, rejectTask,cancelTask} from '@/api/bpm/task';
-  import { listAllUserBind } from '@/api/system/organization';
   import headList from '@/components/headList';
   // 流程实例的详情页,可用于审批
   export default {
@@ -137,99 +82,91 @@
     data() {
       return {
         form: {
-          technicianId: '',
           reason: '同意'
         },
-        userOptions: []
+        // 指派技术员时,人员选择弹窗里选中的人
+        pickedUserId: ''
       };
     },
-    created() {
-      this.userOptions = [];
-      listAllUserBind().then((data) => {
-        this.userOptions.push(...data);
-      });
-    },
     methods: {
-      /** 处理转办审批人 */
-      handleUpdateAssignee() {
-        this.$emit('handleUpdateAssignee');
-      },
-      /** 退回 */
-      handleBackList() {
-        this.$emit('handleBackList');
-      },
-      head() {
-        this.$refs.headRef.open();
+      /** 指派技术员:选人后提交(流转变量 userId) */
+      openAssign() {
+        this.pickedUserId = '';
+        this.$refs.headRef.open(null, 0, '指派技术员');
       },
+      /** 人员选择回调 */
       changePersonel(data) {
-        this.form.technicianId = data.id;
-        this.handleAudit(1, 'zp');
+        if (!data || !data.id) return;
+        this.pickedUserId = data.id;
+        this.handleAudit(1, 'assign');
       },
       async handleAudit(status, type) {
-        //生产主管审批选择技术员
-        // if (this.taskDefinitionKey === 'productionSupervisorApprove1') {
-        //   if (!this.form.technicianId && type == 'zp') {
-        //     this.$message.warning(`请选择技术人员!`);
-        //     return;
-        //   }
-        // }
-        //技术员修改
-        // if (this.taskDefinitionKey === 'technicianApprove' && status === 1) {
-        //   let data = await this.getTableValue();
-        //   if (!arr) {
-        //     return;
-        //   }
-        //   let arr = data.map((item) => {
-        //     return {
-        //       id: item.id,
-        //       technicalAnswerId: item.technicalAnswerId,
-        //       technicalAnswerName: item.technicalAnswerName,
-        //       technicalDrawings: item.technicalDrawings,
-        //       technicalParams: item.technicalParams
-        //     };
-        //   });
-        //   try {
-        //     await updateTech(arr);
-        //   } catch (error) {}
-        // }
-        //销售员补充
-        if ((this.taskDefinitionKey === 'salesmanApprove'||this.taskDefinitionKey === 'starter')&&status === 1) {
-          let arr = await this.getTableValue();
-          if (!arr) {
-            return;
-          }
-          let data = await UpdateInformation(arr);
-          if (data.code != '0') {
+        // 指派技术员必须选人:后端靠 variables.userId 决定任务交给谁
+        if (type === 'assign' && !this.pickedUserId) {
+          this.$message.warning('请选择技术人员!');
+          return;
+        }
+        // 通过时必须回写报价单:明细表里的单价/备注/附件/工艺路线等修改都要落库,
+        // 不能靠“是否改过单价”来判断,否则改了备注、附件这些字段就会漏调 UpdateInformation
+        if (status === 1) {
+          try {
+            const payload = await this.getTableValue();
+            // 详情组件在必填校验不通过时会返回空值(如生产交付交期没选),
+            // 此时表格已经弹过具体是哪个必填项,这里直接拦下不再提交审批
+            if (!payload) {
+              console.warn('报价单取值为空(校验未通过),已阻止提交审批');
+              return;
+            }
+            const data = await UpdateInformation(payload);
+            if (data.code != '0') {
+              // 回写失败不能静默跳过,否则点通过后流程动了、修改却没保存
+              this.$message.error(data.message || '报价单回写失败,未提交审批');
+              return;
+            }
+          } catch (error) {
+            this.$message.error(error.message || '报价单回写失败,未提交审批');
             return;
           }
         }
-        this._approveTaskWithVariables(status, this.form.technicianId, type);
+        this._approveTaskWithVariables(status, type);
       },
-      async _approveTaskWithVariables(status, technicianId, type) {
+      async _approveTaskWithVariables(status, type) {
         let variables = {
           pass: !!status
         };
-        if (technicianId && type == 'zp') {
-          variables['technicianId'] = technicianId;
+        // 后端流程变量名是 userId(不是 technicianId):指派技术员时把选中的人带上
+        if (status && type === 'assign' && this.pickedUserId) {
+          variables['userId'] = this.pickedUserId;
         }
 
+        const failText = status === 0 ? '驳回失败' : '审批失败';
+
         let API = !!status ? approveTaskWithVariables : rejectTask;
         API({
           id: this.taskId,
           reason: this.form.reason,
           variables
-        }).then((res) => {
-          if (res.data.code != '-1') {
-            this.$emit('handleAudit', {
-              status,
-              title: status === 0 ? '驳回' : ''
-            });
-          }
-        });
+        })
+          .then((res) => {
+            // 后端成功码是 0(其余一律视为失败),不能再只判断 -1,
+            // 否则遇到 -2 / 500 之类的错误码会误报成功并关掉弹窗
+            if (res?.data?.code == 0) {
+              this.$emit('handleAudit', {
+                status,
+                title: status === 0 ? '驳回' : ''
+              });
+            } else {
+              this.$message.error(res?.data?.message || failText);
+            }
+          })
+          .catch(() => {
+            this.$message.error(failText);
+          });
       },
 
       getTableValue() {
         return new Promise((resolve, reject) => {
+          // 父组件回调:data 为报价单整单数据(含 quoteProductList)
           this.$emit('getTableValue', async (data) => {
             resolve(await data);
           });