浏览代码

feat(替代料): 编码列增加"替"标识并展示替原物品编码

xieyong 2 周之前
父节点
当前提交
93f0087855

+ 62 - 1
src/BIZComponents/inventoryTableDetails.vue

@@ -31,6 +31,25 @@
           ></fileMain>
         </el-form-item>
       </template>
+      <!-- 编码:该产品存在替代料时给出“替”标识 -->
+      <template v-slot:productCode="{ row }">
+        <div class="replace-code-cell">
+          <span class="code-text" :title="row.productCode">{{
+            row.productCode
+          }}</span>
+          <el-tooltip v-if="hasReplace(row)" placement="top" effect="dark">
+            <el-tag type="warning" size="mini" effect="dark">替</el-tag>
+            <div slot="content" class="replace-tip">
+              <div
+                v-for="(item, index) in getReplaceTipLines(row)"
+                :key="'tip-' + index"
+              >
+                {{ item }}
+              </div>
+            </div>
+          </el-tooltip>
+        </div>
+      </template>
       <template v-slot:customerReqFiles="scope">
         <el-form-item
           style="margin-bottom: 20px"
@@ -207,6 +226,12 @@
         type: Boolean,
         default: false
       },
+      // 替代料信息:{ 产品编码: [{ docNo, replaceCode, replaceName }] }
+      // 订单详情等上层场景由父组件聚合下发,用于编码列的“替”标识
+      replaceInfoMap: {
+        type: Object,
+        default: () => ({})
+      },
       //
       isAllPrice: {
         type: Boolean,
@@ -325,7 +350,7 @@
             align: 'center'
           },
           {
-            width: 120,
+            width: 150,
             prop: 'productCode',
             label: '编码',
             slot: 'productCode',
@@ -984,6 +1009,28 @@
       this.requestDict('商品价格类型');
     },
     methods: {
+      // 取该产品的替代料信息(按产品编码聚合,可能对应多张发货单)
+      getReplaceList(row) {
+        const map = this.replaceInfoMap || {};
+        const list = row && row.productCode ? map[row.productCode] : null;
+        return Array.isArray(list) ? list : [];
+      },
+      // 父组件聚合到了替代关系,或行上直接带 isReplace,都算“有替代料”
+      hasReplace(row) {
+        return this.getReplaceList(row).length > 0 || row?.isReplace == 1;
+      },
+      // tooltip 文案:有明细就列出发货单与替代料,否则给通用提示
+      getReplaceTipLines(row) {
+        const list = this.getReplaceList(row);
+        if (list.length) {
+          return list.map(
+            (item) =>
+              `发货单 ${item.docNo || '-'}:由 ${item.replaceCode || '-'}` +
+              `${item.replaceName ? '(' + item.replaceName + ')' : ''} 替代`
+          );
+        }
+        return ['该产品存在替代料,发货时以对应发货单的替代料为准'];
+      },
       getSummaries(param) {
         return getSummaries(param, ['saleCount'], ' ');
       },
@@ -1162,6 +1209,20 @@
   };
 </script>
 <style lang="scss" scoped>
+  .replace-code-cell {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    line-height: 1.5;
+  }
+
+  .code-text {
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
   .headbox {
     display: flex;
     justify-content: space-between;

+ 70 - 1
src/views/saleManage/saleOrder/components/detailDialog.vue

@@ -260,7 +260,7 @@
         pageName="send"
         :cacheKeyUrl="cacheKeyUrl"
         :isProductionRequirements="true"
-
+        :replaceInfoMap="replaceInfoMap"
       ></inventoryTabledetail>
       <headerTitle title="类型清单" v-if="form.needProduce == 4"></headerTitle>
       <typeList
@@ -291,6 +291,10 @@
 
 <script>
   import { getSaleOrderDetail } from '@/api/saleManage/saleorder';
+  import {
+    getSendTableList,
+    getSendSaleOrderrecordDetailSplit
+  } from '@/api/saleManage/saleordersendrecord';
   import invoiceList from '@/views/saleManage/saleOrder/orderAssociation/invoiceList.vue';
   import returnGoodsList from '@/views/saleManage/saleOrder/orderAssociation/returnGoodsList.vue';
   import accountstatementList from '@/views/saleManage/saleOrder/orderAssociation/accountstatementList.vue';
@@ -327,6 +331,8 @@
         detailId: '',
         title: '详情',
         cacheKeyUrl: 'eos-saleManage-saleOrder-inventoryTableDetails',
+        // 替代料信息:{ 产品编码: [{ docNo, replaceCode, replaceName }] }
+        replaceInfoMap: {},
         form: {
           orderFiles: []
         }
@@ -343,6 +349,8 @@
         this.visible = true;
         this.getDetailData(row.id);
         this.detailId = row.id;
+        // 替代料是发货单层面的数据,订单详情需要从各发货单聚合
+        this.loadReplaceInfo(row.id);
       },
       toSendTab(tabName) {
         this.activeComp = tabName;
@@ -371,6 +379,67 @@
               this.$refs.typeListRef.putTableValue(data.typedetailList);
           });
         }
+      },
+
+      /**
+       * 汇总该订单下所有发货单的替代料关系,按产品编码聚合。
+       * 失败不影响详情本身,仅不展示“替”标识。
+       */
+      async loadReplaceInfo(orderId) {
+        if (!orderId) return;
+        this.replaceInfoMap = {};
+        try {
+          const page = await getSendTableList({
+            pageNum: 1,
+            size: 500,
+            orderId
+          });
+          const records = Array.isArray(page)
+            ? page
+            : page?.records || page?.list || [];
+          if (!records.length) return;
+          const details = await Promise.all(
+            records.map((item) =>
+              getSendSaleOrderrecordDetailSplit(item.id).catch(() => null)
+            )
+          );
+          const map = {};
+          details.forEach((detail, index) => {
+            const replaceList = (detail && detail.replaceList) || [];
+            if (!replaceList.length) return;
+            const docNo = records[index]?.docNo || detail?.docNo || '';
+            // relationId 指向该发货单自身产品清单的 id
+            const productById = {};
+            ((detail && detail.productList) || []).forEach((product) => {
+              if (product.id !== undefined && product.id !== null) {
+                productById[String(product.id)] = product;
+              }
+            });
+            replaceList.forEach((replace) => {
+              const origin = productById[String(replace.relationId)];
+              if (!origin || !origin.productCode) return;
+              const key = origin.productCode;
+              if (!map[key]) map[key] = [];
+              // 同一发货单同一产品多次替代只提示一次
+              if (
+                !map[key].some(
+                  (item) =>
+                    item.docNo === docNo &&
+                    item.replaceCode === replace.productCode
+                )
+              ) {
+                map[key].push({
+                  docNo,
+                  replaceCode: replace.productCode,
+                  replaceName: replace.productName
+                });
+              }
+            });
+          });
+          this.replaceInfoMap = map;
+        } catch (e) {
+          this.replaceInfoMap = {};
+        }
       }
     }
   };

+ 21 - 0
src/views/saleManage/saleOrder/invoice/components/addInvoiceDialog.vue

@@ -227,6 +227,7 @@
           :oldProductList="oldProductList"
           :sourceType="form.sourceType"
           :isReturnGoodsCreate="isReturnGoodsCreate"
+          :replaceRelationIds="replaceRelationIds"
         ></inventoryTable>
       </el-tab-pane>
       <el-tab-pane label="托盘清单" name="second">
@@ -247,6 +248,8 @@
       <el-tab-pane label="替代料" name="5">
         <replaceTable
           :pricingWay="form.pricingWay"
+          :getOriginList="getOriginProductList"
+          @relation-change="syncReplaceRelationIds"
           ref="replaceRef"
         ></replaceTable>
       </el-tab-pane>
@@ -418,6 +421,8 @@
         delDetailIds: [],
         carList: [],
         oldProductList: [],
+        // 替代料关联的原产品 id 集合,用于物品清单“已替代”标识
+        replaceRelationIds: [],
         visible: false,
         entrustedReceiveDialogFlag: false,
         processSubmitDialogFlag: false,
@@ -771,6 +776,7 @@
               this.$refs.palletTableRef.putTableValue(data.trayList);
             this.$refs.replaceRef &&
               this.$refs.replaceRef.putTableValue(data.replaceList);
+            this.syncReplaceRelationIds();
           });
         }
       },
@@ -988,6 +994,21 @@
       //替代料
       replaceTableChange(list) {
         this.$refs.replaceRef.putTableValue(list, 'add');
+        this.syncReplaceRelationIds();
+      },
+
+      // 替代料界面实时读取物品清单,用于展示“替代的是哪个产品”
+      getOriginProductList() {
+        const table = this.$refs.inventoryTableref;
+        return (table && table.form && table.form.datasource) || [];
+      },
+      // 替代料增删后同步关联关系,保证物品清单的“已替代”标识即时更新
+      syncReplaceRelationIds() {
+        const replace = this.$refs.replaceRef;
+        const rows = (replace && replace.form && replace.form.datasource) || [];
+        this.replaceRelationIds = rows
+          .map((item) => item.relationId)
+          .filter((v) => v !== undefined && v !== null && v !== '');
       },
 
       //选择订单弹框

+ 1 - 0
src/views/saleManage/saleOrder/invoice/components/detailDialog.vue

@@ -136,6 +136,7 @@
           <replaceTable
             :pricingWay="form.pricingWay"
             :disabled="true"
+            :originList="detailData.productList || []"
             ref="replaceRef"
           ></replaceTable>
         </el-tab-pane>

+ 51 - 1
src/views/saleManage/saleOrder/invoice/components/inventoryTable.vue

@@ -161,6 +161,20 @@
             </el-button>
           </el-form-item>
         </template>
+        <!-- 编码:该产品存在替代料时给出“替”标识 -->
+        <template v-slot:productCode="{ row }">
+          <div class="replace-code-cell">
+            <span class="code-text" :title="row.productCode">{{
+              row.productCode
+            }}</span>
+            <el-tooltip v-if="isRowReplaced(row)" placement="top" effect="dark">
+              <el-tag type="warning" size="mini" effect="dark">替</el-tag>
+              <div slot="content" class="replace-tip">
+                该产品存在替代料,详见“替代料”页签,发货时以替代料为准
+              </div>
+            </el-tooltip>
+          </div>
+        </template>
         <template v-slot:orderNo="scope">
           <el-form-item
             :prop="'datasource.' + scope.$index + '.orderNo'"
@@ -536,6 +550,11 @@
       isReturnGoodsCreate: {
         type: Boolean,
         default: false
+      },
+      // 替代料关联的原产品 id 集合,用于标记“已替代”的原产品
+      replaceRelationIds: {
+        type: Array,
+        default: () => []
       }
     },
     components: {
@@ -676,7 +695,7 @@
             align: 'center'
           },
           {
-            width: 120,
+            width: 150,
             prop: 'productCode',
             label: '编码',
             slot: 'productCode',
@@ -1554,6 +1573,23 @@
         });
         this.$emit('replaceTableChange', list);
       },
+      // 该产品是否已被替代料替代(新增时按 tempId,回显时按产品 id)
+      isRowReplaced(row) {
+        const keys = this.replaceRelationIds
+          .filter((v) => v !== undefined && v !== null && v !== '')
+          .map((v) => String(v));
+        if (!keys.length) {
+          return false;
+        }
+        if (row.id !== undefined && row.id !== null && row.id !== '') {
+          return keys.includes(String(row.id));
+        }
+        return (
+          row.tempId !== undefined &&
+          row.tempId !== null &&
+          keys.includes(String(row.tempId))
+        );
+      },
       // validateTotalCount(row) {
       //   return (rule, value, callback) => {
       //     if (isNaN(value) || Number(value) <= 0) {
@@ -1942,6 +1978,20 @@
     }
   }
 
+  .replace-code-cell {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    line-height: 1.5;
+  }
+
+  .code-text {
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
   .time-form .el-form-item {
     margin-bottom: 0 !important;
   }

+ 29 - 1
src/views/saleManage/saleOrder/invoice/components/inventoryTableDetails.vue

@@ -13,6 +13,20 @@
     :summary-method="getSummaries"
     @header-dragend="handleHeaderDragend"
   >
+    <!-- 编码:该产品存在替代料时给出“替”标识 -->
+    <template v-slot:productCode="{ row }">
+      <div class="replace-code-cell">
+        <span class="code-text" :title="row.productCode">{{
+          row.productCode
+        }}</span>
+        <el-tooltip v-if="row.isReplace == 1" placement="top" effect="dark">
+          <el-tag type="warning" size="mini" effect="dark">替</el-tag>
+          <div slot="content" class="replace-tip">
+            该产品存在替代料,详见“替代料”页签,发货时以替代料为准
+          </div>
+        </el-tooltip>
+      </div>
+    </template>
     <template v-slot:technicalDrawings="{ row }">
       <fileMain v-model="row.technicalDrawings" type="view"></fileMain>
     </template>
@@ -84,7 +98,7 @@
             align: 'center'
           },
           {
-            width: 120,
+            width: 150,
             prop: 'productCode',
             label: '编码',
             slot: 'productCode',
@@ -574,4 +588,18 @@
     align-items: center;
     font-weight: bold;
   }
+
+  .replace-code-cell {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    line-height: 1.5;
+  }
+
+  .code-text {
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
 </style>

+ 118 - 1
src/views/saleManage/saleOrder/invoice/components/replaceTable.vue

@@ -51,6 +51,33 @@
           />
         </el-form-item>
       </template>
+      <!-- 编码:本行编码 + 所替代的原物品编码,一眼看清替的是哪个物料 -->
+      <template v-slot:productCode="{ row, $index }">
+        <div
+          v-for="(origin, originIndex) in [getOriginInfo(row)]"
+          :key="(origin && origin.id) || 'code-' + $index + '-' + originIndex"
+          class="replace-code-cell"
+        >
+          <span class="code-text" :title="row.productCode">{{
+            row.productCode || '-'
+          }}</span>
+          <el-tooltip v-if="origin" placement="top" effect="dark">
+            <div class="replace-origin-code">
+              <el-tag type="warning" size="mini" effect="dark">替</el-tag>
+              <span class="origin-code-text">{{
+                origin.productCode || '-'
+              }}</span>
+            </div>
+            <div slot="content" class="relation-tip">
+              <div>替原物品名称:{{ origin.productName || '-' }}</div>
+              <div>替原物品编码:{{ origin.productCode || '-' }}</div>
+              <div>规格:{{ origin.specification || '-' }}</div>
+              <div v-if="origin.orderNo">订单编码:{{ origin.orderNo }}</div>
+            </div>
+          </el-tooltip>
+          <span v-else class="relation-empty">未关联</span>
+        </div>
+      </template>
       <template v-slot:stockLedger="scope">
         <el-form-item>
           <el-popover
@@ -379,12 +406,23 @@ import tabMixins from '@/mixins/tableColumnsMixin';
         type: Boolean,
         default: false
       },
+      // 原产品列表(只读详情场景直接传数组)
+      originList: {
+        type: Array,
+        default: () => []
+      },
+      // 编辑场景下由父组件实时提供物品清单,避免两边数据不同步
+      getOriginList: {
+        type: Function,
+        default: null
+      },
     },
     components: {
       // fileMain,
       innertboundDetailsDialog,
       stockLedgerDialog
     },
+    emits: ['relation-change'],
     data() {
       const defaultForm = {
         key: null,
@@ -490,7 +528,7 @@ import tabMixins from '@/mixins/tableColumnsMixin';
             align: 'center'
           },
           {
-            width: 120,
+            width: 190,
             prop: 'productCode',
             label: '编码',
             slot: 'productCode',
@@ -749,6 +787,46 @@ import tabMixins from '@/mixins/tableColumnsMixin';
       }
     },
     methods: {
+      // 取当前生效的原产品列表:优先实时 getter,其次静态数组
+      resolveOriginRows() {
+        if (typeof this.getOriginList === 'function') {
+          const rows = this.getOriginList();
+          if (Array.isArray(rows)) {
+            return rows;
+          }
+        }
+        return Array.isArray(this.originList) ? this.originList : [];
+      },
+      // relationId 指向被替代的原产品(新增时是 tempId,回显时是产品 id)
+      getOriginInfo(row) {
+        const relationId = row && row.relationId;
+        if (
+          relationId === undefined ||
+          relationId === null ||
+          relationId === ''
+        ) {
+          return null;
+        }
+        const key = String(relationId);
+        return (
+          this.resolveOriginRows().find((item) => {
+            if (
+              item.id !== undefined &&
+              item.id !== null &&
+              item.id !== '' &&
+              String(item.id) === key
+            ) {
+              return true;
+            }
+            return (
+              item.tempId !== undefined &&
+              item.tempId !== null &&
+              item.tempId !== '' &&
+              String(item.tempId) === key
+            );
+          }) || null
+        );
+      },
       // 子表表头拖拽列宽持久化(scope: sub1)
       onSub1HeaderDragend(newWidth, oldWidth, column) {
         this.persistScopedColumnWidth(
@@ -1178,15 +1256,18 @@ import tabMixins from '@/mixins/tableColumnsMixin';
             });
           });
         }
+        this.$emit('relation-change');
       },
 
       remove(i) {
         this.form.datasource.splice(i, 1);
         this.setSort();
+        this.$emit('relation-change');
       },
       // 清空表格
       restTable() {
         this.form.datasource = [];
+        this.$emit('relation-change');
       },
       // 重新排序
       setSort() {
@@ -1253,4 +1334,40 @@ import tabMixins from '@/mixins/tableColumnsMixin';
     align-items: center;
     font-weight: bold;
   }
+
+  .replace-code-cell {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    line-height: 1.5;
+  }
+
+  .code-text {
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .replace-origin-code {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    max-width: 100%;
+    margin-top: 2px;
+  }
+
+  .origin-code-text {
+    margin-left: 4px;
+    color: #e6a23c;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .relation-empty {
+    margin-top: 2px;
+    font-size: 12px;
+    color: #c0c4cc;
+  }
 </style>