Jelajahi Sumber

超收-初始化

zhaohuanhuan 1 hari lalu
induk
melakukan
99c1560b8b
1 mengubah file dengan 252 tambahan dan 76 penghapusan
  1. 252 76
      src/views/inbound/takeDelivery/task/index.vue

+ 252 - 76
src/views/inbound/takeDelivery/task/index.vue

@@ -17,7 +17,12 @@
           </div>
           <div class="info-no-tips">
             <div>货主:<span style="color: #333;font-weight: bold;">{{ taskInfo.customerName || '--' }}</span></div>
-            <div>任务数:<span style="color: #0077ff;font-weight: bold;">{{ taskInfo.receivedQty || 0 }}/{{ taskInfo.expectedQty || 0}}</span></div>
+            <div>任务数:<span style="font-weight: bold"
+              ><span
+                :style="{
+                  color: (Number(taskInfo.receivedQty || 0) + Number(taskInfo.overReceiveQuantity || 0)) > Number(taskInfo.expectedQty || 0) ? '#ee0a24' : '#0077ff',
+                }"
+              >{{ Number(taskInfo.receivedQty || 0) + Number(taskInfo.overReceiveQuantity || 0) }}</span><span style="color: #0077ff">/{{ taskInfo.expectedQty || 0 }}</span></span></div>
           </div>
         </div>
         <div class="take-info-number">
@@ -48,7 +53,16 @@
           </div>
         </div>
       </div>
-      <van-progress v-if="taskInfo.receivedQty/taskInfo.expectedQty>=0" :percentage="((taskInfo.receivedQty/taskInfo.expectedQty)*100).toFixed(2)" stroke-width="4" />
+      <van-progress
+        v-if="Number(taskInfo.expectedQty) > 0"
+        :percentage="Number((((Number(taskInfo.receivedQty || 0) + Number(taskInfo.overReceiveQuantity || 0)) / Number(taskInfo.expectedQty)) * 100).toFixed(2))"
+        :color="(Number(taskInfo.receivedQty || 0) + Number(taskInfo.overReceiveQuantity || 0)) / Number(taskInfo.expectedQty) * 100 > 100 ? '#ee0a24' : '#1989fa'"
+        stroke-width="4"
+      />
+      <div v-if="showOverReceiveTag" class="over-receive-hint" role="status">
+        <span class="over-receive-hint__badge">超收</span>
+        <span class="over-receive-hint__text">无待收行明细的收货</span>
+      </div>
       <div class="take-barcode">
         <div class="barcode-input">
           <van-search
@@ -77,15 +91,24 @@
             autocomplete="off"
             show-action
             :min="1"
-            :max="asnInfo.asnNo?asnInfo.expectedQuantity-asnInfo.receivedQuantity:10000"
+            :max="999999"
             @search="isCheck()"
             :class="[scanType===4?'search-input-barcode':'','van-hairline--bottom']"
             @focus="scanType=4"
           >
+          <!-- :max="asnInfo.asnNo?asnInfo.expectedQuantity-asnInfo.receivedQuantity:10000" -->
             <template #action>
               <div style="display: flex; align-items: center;flex-direction: column;margin-left: 20px" v-if="asnInfo.asnNo">
                 <div style="height: 20px;font-size: 12px">已收/预计</div>
-                <div style="font-size: 16px;font-weight: bold">{{ asnInfo.receivedQuantity}}/{{ asnInfo.expectedQuantity }}
+                <div style="font-size: 16px;font-weight: bold;color: #323233">
+                  <span
+                    :style="{
+                      color:
+                        (Number(asnInfo.receivedQuantity || 0) + Number(asnInfo.overReceiveQuantity || 0)) > Number(asnInfo.expectedQuantity || 0)
+                          ? '#ee0a24'
+                          : '#323233',
+                    }"
+                  >{{ Number(asnInfo.receivedQuantity || 0) + Number(asnInfo.overReceiveQuantity || 0) }}</span>/{{ asnInfo.expectedQuantity }}
                 </div>
               </div>
             </template>
@@ -108,7 +131,7 @@
         </van-cell-group>
       </div>
       <div class="take-button">
-        <van-button type="primary" size="large" round style="height: 36px" @click="onConfirm">完成收货</van-button>
+        <van-button type="primary" size="large" round style="height: 36px" @click="onConfirm(false)">完成收货</van-button>
       </div>
     </div>
   </div>
@@ -116,7 +139,13 @@
   <!-- 条码输入组件 -->
   <input-barcode :back="back" @setBarcode="setBarcode" ref="inputBarcodeRef"  />
   <!--  单据选择-->
-  <van-action-sheet v-model:show="asnDetailsTrueFalseBy" cancel-text="取消" description="请选择具体单据" close-on-click-action>
+  <van-action-sheet
+    v-model:show="asnDetailsTrueFalseBy"
+    cancel-text="取消"
+    description="请选择具体单据"
+    close-on-click-action
+    @closed="onAsnActionSheetClosed"
+  >
     <van-cell-group>
       <van-cell v-for="item in asnDetailsList" @click="onDetailActive(item)">
         <template #title>
@@ -140,7 +169,7 @@
                      :combine-set-count="combineReceivingSetCount"
                      :asnInfo="asnInfo"
                      :resolve-panpass-codes="isCombinePanpass ? resolvePanpassScan : undefined"
-                     @setUniqueCode="onConfirm"
+                     @setUniqueCode="onConfirm(false)"
 
   />
   <van-action-sheet
@@ -177,7 +206,7 @@ import {
 import { getListCombineSku } from '@/api/picking'
 import { closeLoading, showLoading } from '@/utils/loading'
 import { useStore } from '@/store/modules/user'
-import { showNotify, showToast } from 'vant'
+import { showNotify, showToast,showConfirmDialog} from 'vant'
 import { isAttribute } from '@/views/inbound/takeDelivery/task/hooks/attribute'
 import Attribute from '@/views/inbound/takeDelivery/components/Attribute.vue'
 import LotDate from '@/views/inbound/takeDelivery/components/LotDate.vue'
@@ -275,50 +304,61 @@ const setBarcode = (code, type) => {
   }
   getIReceivingTask({ taskNo:code,version:'V6',warehouse }).then(res=>{
     back.value = true
-    if(res.data.receivedQty==res.data.expectedQty && res.data.expectedQty>0 ){
-      reset()
-      taskNo.value = ''
-      taskInfo.value = {}
-      containerNo.value = ''
-      stopTimer()
-      allAsnDetailList.value = []
-      ownerPanpassEnabled.value = false
+    const data = res.data
+    const applyTaskAfterFetch = () => {
+      if (!type) {
+        currentTime.value = getCurrentTime()
+        startTimer()
+        containerNo.value = ''
+      }
+      taskInfo.value = data
+      taskNo.value = code
       scanType.value = 2
       uniqueCodeList.value = []
-      if (type) {
-        switchTask()
+      const ownerCode = data?.customerId
+      if (ownerCode) {
+        getOwnerRule(ownerCode).then((ruleRes) => {
+          ownerPanpassEnabled.value = !!ruleRes.data?.panpassEnabled
+        }).catch(() => {
+          ownerPanpassEnabled.value = false
+        })
       } else {
-        inputBarcodeType.value = 'task'
-        back.value = true
-        inputBarcodeRef.value?.show('', '请扫描开单任务号', '')
+        ownerPanpassEnabled.value = false
       }
+      const params = { warehouse, asnNos: taskInfo.value?.asnNos.join(',') }
+      getReceivingAsnDetails(params).then((detailRes) => {
+        allAsnDetailList.value = detailRes.data
+      })
       scanSuccess()
-      return
     }
-    if(!type){//切换任务成功重启计时器
-      currentTime.value=getCurrentTime()
-      startTimer()
-      containerNo.value=''
-    }
-    taskInfo.value=res.data
-    taskNo.value=code
-    scanType.value=2
-    uniqueCodeList.value=[]
-    const ownerCode = res.data?.customerId
-    if (ownerCode) {
-      getOwnerRule(ownerCode).then((ruleRes) => {
-        ownerPanpassEnabled.value = !!ruleRes.data?.panpassEnabled
-      }).catch(() => {
-        ownerPanpassEnabled.value = false
+    if (data.receivedQty == data.expectedQty && data.expectedQty > 0) {
+      showConfirmDialog({
+        title: '温馨提示',
+        message: `${data.taskNo}任务已完成收货,是否继续收货?`,
       })
-    } else {
-      ownerPanpassEnabled.value = false
+        .then(applyTaskAfterFetch)
+        .catch(() => {
+          reset()
+          taskNo.value = ''
+          taskInfo.value = {}
+          containerNo.value = ''
+          stopTimer()
+          allAsnDetailList.value = []
+          ownerPanpassEnabled.value = false
+          scanType.value = 2
+          uniqueCodeList.value = []
+          if (type) {
+            switchTask()
+          } else {
+            inputBarcodeType.value = 'task'
+            back.value = true
+            inputBarcodeRef.value?.show('', '请扫描开单任务号', '')
+          }
+          scanSuccess()
+        })
+      return
     }
-    const params = { warehouse, asnNos: taskInfo.value?.asnNos.join(',') }
-    getReceivingAsnDetails(params).then(res => {
-      allAsnDetailList.value = res.data
-    })
-    scanSuccess()
+    applyTaskAfterFetch()
   }).catch(err=>{
     ownerPanpassEnabled.value = false
     inputBarcodeRef.value?.show('', '请扫描开单任务号',err.message)
@@ -360,8 +400,23 @@ function resetCombineProductState() {
   asnDetailsTrueFalseBy.value = false
 }
 
+/** 超收:多 ASN 选单已打开(无明细造行,选单中) */
+const overReceiveSheetActive = ref(false)
+const showOverReceiveTag = computed(
+  () =>
+    !!asnInfo.value?.isOverReceive
+    || (overReceiveSheetActive.value && !!asnDetailsTrueFalseBy.value),
+)
+
+const onAsnActionSheetClosed = () => {
+  if (overReceiveSheetActive.value && !asnInfo.value?.asnNo) {
+    overReceiveSheetActive.value = false
+  }
+}
+
 const reset = () => {
   resetCombineProductState()
+  overReceiveSheetActive.value = false
   asnInfo.value = {}
   lotData.value = []
   lotMap.value={}
@@ -376,6 +431,7 @@ const onDetailActive = (item) => {
     return
   }
   asnInfo.value = item
+  overReceiveSheetActive.value = false
   asnDetailsTrueFalseBy.value = false
   searchCount.value=1
   _getProductAttribute(item)
@@ -489,16 +545,86 @@ const isCombinePanpass = computed(
   () => ownerPanpassEnabled.value && combineReceivingData.value.length > 0,
 )
 
+/**
+ * 无 ASN 行明细时(allAsnDetailList 无匹配/为空)的「超收」占位行:不依赖待收货列表校验,由任务号 + 选中 ASN 构造。
+ */
+const buildOverReceiveAsnLine = (asnNo, code) => {
+  const wh = taskInfo.value.warehouse || warehouse
+  const skuU = barcodeToUpperCase(code)
+  return {
+    warehouse: wh,
+    customerId: taskInfo.value.customerId,
+    sku: skuU,
+    barcode: skuU,
+    asnNo,
+    expectedQuantity: 0,
+    receivedQuantity: 0,
+    /** 与正常 ASN 行区分,用于页面「超收」标记与逻辑 */
+    isOverReceive: true,
+  }
+}
+
+/**
+ * 组合接口查不到时
+ */
+const _fallbackReceiveByProductLotProbe = (code) => {
+  const { asnNos = [], warehouse, customerId } = taskInfo.value
+  const item = { warehouse, customerId, sku: barcodeToUpperCase(code) }
+  const continueOverReceiveLine = (line) => {
+    overReceiveSheetActive.value = false
+    asnInfo.value = line
+    searchCount.value = 1
+    _getProductAttribute(line)
+    _getProductLot(line)
+    _getCommodityRule(line)
+    closeLoading()
+    scanSuccess()
+  }
+  _getProductLot(item)
+    .then(() => {
+      if (lotData.value.length === 0) {
+        closeLoading()
+        scanError()
+        return
+      }
+      if (asnNos.length === 0) {
+        closeLoading()
+        scanError()
+        showNotify({ type: 'danger', duration: 3000, message: '任务无 ASN 单,无法超收' })
+        return
+      }
+      if (asnNos.length > 1) {
+        const synthetic = asnNos.map((asnNo) => buildOverReceiveAsnLine(asnNo, code))
+        asnDetailsList.value = synthetic
+        asnInfo.value = {}
+        lotData.value = []
+        lotMap.value = {}
+        searchCount.value = ''
+        uniqueCodeList.value = []
+        overReceiveSheetActive.value = true
+        asnDetailsTrueFalseBy.value = true
+        closeLoading()
+        scanSuccess()
+        return
+      }
+      continueOverReceiveLine(buildOverReceiveAsnLine(asnNos[0], code))
+    })
+    .catch(() => {
+      closeLoading()
+      scanError()
+    })
+}
+
 // 组合商品只支持1个
 const _handleCombineProduct = (code) => {
   showLoading()
   getListCombineSku({ combineSku: barcodeToUpperCase(code), workEnvironment: 'receiving' }).then((res) => {
     const _err = (msg) => { closeLoading(); scanError(); showNotify({ type: 'danger', duration: 3000, message: msg }); reset() }
-    if (!res.data?.length) return _err(`${code}-商品条码不匹配,请重新扫描`)
+    if (!res.data?.length) return _fallbackReceiveByProductLotProbe(code)
     if (res.data.length > 1) return _err('不支持多商品组合商品')
     const combineData = res.data
     const matchedList = receivingBarcodeCombine(allAsnDetailList.value, toMap(combineData, 'barcode'))
-    if (!matchedList.length) return _err('组合商品与待收货数据不匹配,请检查组合商品配置!')
+    if (!matchedList.length) return _fallbackReceiveByProductLotProbe(code)
     const asnGroupMap = matchedList.reduce((acc, detail) => {
       const key = detail.asnNo
       if (!acc[key]) acc[key] = { asnNo: detail.asnNo, customerId: detail.customerId, expectedQuantity: 0, list: [] }
@@ -562,17 +688,19 @@ let productLotGeneration = 0
 // 条码扫描:scanType 2商品/4数量/3唯一码/5容器
 const _handlerScan = (code) => {
   if (scanType.value == 2) {
-    searchBarcode.value = code
-    oldSearchBarcode.value = code
+      searchBarcode.value = code
+      oldSearchBarcode.value = code
       resetCombineProductState()
-      if ( allAsnDetailList.value.length > 0) {
-        const upperCode = barcodeToUpperCase(code) || ''
-        const clientMatched = allAsnDetailList.value.filter((detail) => {
-          const bars = [detail.barcode, detail.barcode2, detail.sku].filter(Boolean)
-          return bars.some((bar) => bar && barcodeToUpperCase(bar) === upperCode)
-        })
-        asnDetailsList.value = clientMatched
-      }
+      overReceiveSheetActive.value = false
+      const upperCode = barcodeToUpperCase(code) || ''
+      const clientMatched =
+        allAsnDetailList.value.length > 0
+          ? allAsnDetailList.value.filter((detail) => {
+              const bars = [detail.barcode, detail.barcode2, detail.sku].filter(Boolean)
+              return bars.some((bar) => bar && barcodeToUpperCase(bar) === upperCode)
+            })
+          : []
+      asnDetailsList.value = clientMatched
       uniqueCodeList.value=[]
 
       if (asnDetailsList.value.length > 0) {
@@ -692,7 +820,7 @@ const _getProductLot = (item) => {
   lotMap.value = {}
   lotData.value = []
   const params = { warehouse: item.warehouse, owner: item.customerId, barcode: item.sku }
-  getProductLot(params).then((res) => {
+  return getProductLot(params).then((res) => {
     if (gen !== productLotGeneration) return
     const rows = res.data
     if (!rows?.length) {
@@ -824,6 +952,9 @@ const _getCommodityRule = (item) => {
       }
     })
     uniqueRuleMap.value = toMap(res.data, 'type', 'uniqueRegExp')
+  }).catch(() => {
+    closeLoading()
+    scanError()
   })
 }
 const containerNoRef = ref(null)
@@ -844,8 +975,13 @@ const isCheck = () => {
     }
     return false
   }
-  // //商品批次属性判断
-  const incompleteLot = lotData.value.find(lot => lot.require && !lot.mapping)
+  // //商品批次属性判断(超收不校验 lotAtt05 必填)
+  const incompleteLot = lotData.value.find(
+    (lot) =>
+      lot.require &&
+      !lot.mapping &&
+      !(asnInfo.value.isOverReceive && lot.field === 'lotAtt05'),
+  )
   if (incompleteLot) {
     scanError()
     showToast({ duration: 3000, message: `请先补充${incompleteLot.label}` })
@@ -863,12 +999,6 @@ const isCheck = () => {
       showToast({ duration: 3000, message: `失效日期不能小于等于生产日期` })
       return false
     }
-    // // 检查失效日期是否小于当前日期
-    // if (expirationDate <= currentDate) {
-    //   scanError()
-    //   showToast({ duration: 3000, message: `失效日期不能小于等于当前日期` })
-    //   return false
-    // }
   }
   if(productionDate){
     // 如果有生产日期,进行有效性检查
@@ -908,10 +1038,10 @@ const isCheck = () => {
   return true
 }
 // 完成收货
-const onConfirm = () => {
+const onConfirm = (confirmOverReceive=false) => {
   if(isCheck()){
     const lotMap = toMap(lotData.value, 'field', 'mapping')
-    const { asnLineNo, asnNo, warehouse,customerId } = asnInfo.value
+    const { asnLineNo, asnNo, warehouse,customerId,sku} = asnInfo.value
     const {taskNo: taskCode } = taskInfo.value
     const data = {
       asnLineNo,
@@ -922,18 +1052,32 @@ const onConfirm = () => {
       customerId,
       serialNos: uniqueCodeList.value.length > 0 ? uniqueCodeList.value : undefined,
       ...lotMap,
-      taskNo:taskCode
+      taskNo:taskCode,
+      sku,
+      confirmOverReceive
     }
     showLoading()
     inputBarcodeType.value='task'
     setReceiving(data).then(res => {
-      scanSuccess()
-      showNotify({ type: 'success', duration: 3000, message: `${searchBarcode.value}收货完成,请继续收货!`})
-      setBarcode(taskNo.value, '2')
-      reset()
-      taskInfo.value={}
-      scanType.value=2
-      closeLoading()
+      console.log('res',res)
+      if(res.data.overReceive){
+        scanError()
+        showConfirmDialog({
+          title: '温馨提示',
+          message: '是否确认超收?',
+        }).then(() => {
+          onConfirm(true)
+        }).catch(() => {
+        })
+      }else{
+        scanSuccess()
+        showNotify({ type: 'success', duration: 3000, message: `${searchBarcode.value}收货完成,请继续收货!`})
+        setBarcode(taskNo.value, '2')
+        reset()
+        taskInfo.value={}
+        scanType.value=2
+        closeLoading()
+      }
     }).catch(err => {
       if(err.message.includes('序列号已存在')){
         scanType.value = 3
@@ -941,8 +1085,10 @@ const onConfirm = () => {
       }
       scanError()
       closeLoading()
+    }).finally(() => {
+      closeLoading()
     })
-    }
+  }
 }
 
 // 数据刷新
@@ -960,6 +1106,36 @@ window.onRefresh = loadData
 
 </script>
 <style scoped lang="scss">
+.over-receive-hint {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin: 8px 10px 0;
+  padding: 8px 10px;
+  background: #fff4e5;
+  border: 1px solid #f5d4a3;
+  border-radius: 6px;
+  font-size: 13px;
+  color: #8a5a00;
+  line-height: 1.4;
+}
+
+.over-receive-hint__badge {
+  flex-shrink: 0;
+  padding: 2px 8px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #fff;
+  background: linear-gradient(135deg, #e67e22, #d35400);
+  border-radius: 4px;
+}
+
+.over-receive-hint__text {
+  flex: 1;
+  min-width: 0;
+  text-align: left;
+}
+
 .take-delivery {
   .take-info {
     padding: 6px 10px;