You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

917 lines
36 KiB

11 months ago
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
11 months ago
​
11 months ago
11 months ago
​
11 months ago
11 months ago
​
11 months ago
11 months ago
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
9 months ago
11 months ago
11 months ago
​
​
10 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
11 months ago
11 months ago
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
8 months ago
11 months ago
​
​
​
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
11 months ago
11 months ago
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
1 month ago
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
8 months ago
​
7 months ago
8 months ago
7 months ago
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
11 months ago
11 months ago
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
11 months ago
1 month ago
11 months ago
​
​
11 months ago
11 months ago
​
11 months ago
​
1 month ago
​
​
​
11 months ago
​
​
11 months ago
11 months ago
​
11 months ago
​
​
​
​
11 months ago
​
​
​
​
​
11 months ago
​
​
​
​
​
11 months ago
​
​
​
​
​
1 month ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
11 months ago
​
​
11 months ago
11 months ago
​
11 months ago
11 months ago
10 months ago
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
1 month ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
8 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
8 months ago
8 months ago
​
​
​
​
8 months ago
​
​
11 months ago
​
8 months ago
8 months ago
7 months ago
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
8 months ago
7 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
1 month ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
10 months ago
​
11 months ago
​
​
10 months ago
11 months ago
​
​
​
​
​
11 months ago
​
​
11 months ago
11 months ago
11 months ago
​
​
11 months ago
​
​
​
​
​
​
11 months ago
11 months ago
11 months ago
​
​
​
​
​
​
11 months ago
​
​
​
​
11 months ago
​
​
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
​
​
​
11 months ago
​
​
​
​
​
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
​
​
​
​
11 months ago
​
​
​
​
​
11 months ago
​
​
​
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
11 months ago
11 months ago
​
​
11 months ago
​
​
​
​
​
11 months ago
​
11 months ago
​
​
​
11 months ago
​
11 months ago
​
11 months ago
​
​
11 months ago
​
11 months ago
11 months ago
​
11 months ago
​
​
11 months ago
​
11 months ago
​
​
11 months ago
​
11 months ago
11 months ago
11 months ago
​
​
11 months ago
​
​
​
11 months ago
​
​
11 months ago
​
​
​
11 months ago
​
11 months ago
​
11 months ago
  1. <template>
  2. <div>
  3. <div class="pda-container" v-loading.fullscreen.lock="fullscreenLoading"
  4. element-loading-background="rgba(255, 255, 255, 0.3)"
  5. element-loading-spinner="el-icon-loading"
  6. :element-loading-text="loadingText">
  7. <div class="status-bar">
  8. <div class="goBack" @click="handleBack"><i class="el-icon-arrow-left"></i>上一页</div>
  9. <div class="goBack">生产入库</div>
  10. <div class="network" style="color: #fff" @click="$router.push({ path: '/' })">🏠首页</div>
  11. </div>
  12. <div style="overflow-y: auto">
  13. <!-- Step 1: 扫描工单号 -->
  14. <div v-if="processFlag === 1">
  15. <div class="scan-box" style="margin: 2px;">
  16. <el-input clearable v-model="scanCode" placeholder="扫描或输入工单号"
  17. @keyup.enter.native="searchShopOrder" ref="scanCodeRef" />
  18. </div>
  19. <div class="item-list" v-if="shopOrderList.length > 0" style="margin: 2px;">
  20. <el-form label-position="top" style="margin: 3px;">
  21. <el-row :gutter="5"
  22. v-for="(orderDetail, index) in shopOrderList" :key="index" :class="index < shopOrderList.length - 1 ? 'bottom-line-row' : ''">
  23. <el-col :span="8">
  24. <el-form-item label="料号"><span>{{ orderDetail.partNo }}</span></el-form-item>
  25. </el-col>
  26. <el-col :span="8">
  27. <el-form-item label="行号/下达号"><span>{{ orderDetail.releaseNo }}/{{ orderDetail.sequenceNo }}</span></el-form-item>
  28. </el-col>
  29. <el-col :span="8">
  30. <el-form-item label="">
  31. <el-button type="text" class="recvButton" @click="selectShopOrder(orderDetail)"
  32. style="margin-top: 10px;margin-left: 20px" size="small">入库</el-button>
  33. </el-form-item>
  34. </el-col>
  35. <el-col :span="24">
  36. <el-form-item label="物料描述"><span>{{ orderDetail.partDesc }}</span></el-form-item>
  37. </el-col>
  38. <el-col :span="6" :class="{ mt10: getTextWidth(orderDetail.partDesc) > 34 }">
  39. <el-form-item label="订单数量"><span>{{ orderDetail.lotSize }}</span></el-form-item>
  40. </el-col>
  41. <el-col :span="6" :class="{ mt10: getTextWidth(orderDetail.partDesc) > 34 }">
  42. <el-form-item label="已完成数"><span>{{ orderDetail.qtyComplete }}</span></el-form-item>
  43. </el-col>
  44. <el-col :span="6" :class="{ mt10: getTextWidth(orderDetail.partDesc) > 34 }">
  45. <el-form-item label="状态">
  46. <el-tag :type="orderDetail.objstate === 'Started' ? 'success' : 'warning'" size="mini">
  47. {{ orderDetail.objstate }}
  48. </el-tag>
  49. </el-form-item>
  50. </el-col>
  51. <el-col :span="6" :class="{ mt10: getTextWidth(orderDetail.partDesc) > 34 }">
  52. <el-form-item style="margin-left: 20px" label="单位"><span>{{ orderDetail.uom }}</span></el-form-item>
  53. </el-col>
  54. </el-row>
  55. </el-form>
  56. </div>
  57. </div>
  58. <!-- Step 2: 入库明细 -->
  59. <div v-if="processFlag === 2">
  60. <el-form label-position="top" class="form-section" style="margin: 5px;">
  61. <el-row :gutter="20">
  62. <el-col :span="12"><el-form-item label="工单号"><el-input v-model="inboundItem.orderNo" disabled /></el-form-item></el-col>
  63. <el-col :span="12"><el-form-item label="下达号/序列号"><el-input v-model="displayReleaseSeq" disabled /></el-form-item></el-col>
  64. <el-col :span="12"><el-form-item label="料号"><el-input v-model="inboundItem.partNo" disabled /></el-form-item></el-col>
  65. <el-col :span="12"><el-form-item label="计量单位"><el-input v-model="inboundItem.uom" disabled /></el-form-item></el-col>
  66. <el-col :span="24"><el-form-item label="物料描述"><el-input v-model="inboundItem.partDesc" disabled /></el-form-item></el-col>
  67. <el-col :span="12"><el-form-item label="订单数量"><el-input v-model="inboundItem.lotSize" disabled /></el-form-item></el-col>
  68. <el-col :span="12"><el-form-item label="已完成数"><el-input v-model="inboundItem.qtyComplete" disabled /></el-form-item></el-col>
  69. <el-col :span="12">
  70. <el-form-item label="此次入库数量">
  71. <el-input v-model="inboundItem.transQty" type="number" @blur="checkQuantity" />
  72. </el-form-item>
  73. </el-col>
  74. <el-col :span="12">
  75. <el-form-item label=" ">
  76. <el-button type="text" @click.stop="handlingUnitStep"
  77. style="font-size: 16px;margin-top: -5px" size="small">包装记录</el-button>
  78. </el-form-item>
  79. </el-col>
  80. <el-col :span="12"><el-form-item label="库位">
  81. <el-autocomplete
  82. v-model.trim="inboundItem.locationNo"
  83. :fetch-suggestions="querySearchLocation"
  84. placeholder="请输入库位"
  85. :trigger-on-focus="true"
  86. clearable
  87. style="width: 100%;"
  88. @select="handleLocationSelect"
  89. @blur="handleLocationBlur"
  90. />
  91. </el-form-item></el-col>
  92. <el-col :span="12"><el-form-item label="批号">
  93. <el-input v-model="inboundItem.batchNo" placeholder="请输入批号" />
  94. </el-form-item></el-col>
  95. <el-col :span="12"><el-form-item label="WDR">
  96. <el-input v-model="inboundItem.wdr" disabled placeholder="请输入WDR" />
  97. </el-form-item></el-col>
  98. <el-col :span="12"><el-form-item label="高度(mm)">
  99. <el-input v-model="inboundItem.height" type="number" placeholder="请输入高度" />
  100. </el-form-item></el-col>
  101. <el-col :span="12"><el-form-item label="有效期">
  102. <el-date-picker
  103. v-model="inboundItem.ifsExpiryDate"
  104. type="date"
  105. placeholder="选择有效期"
  106. value-format="yyyy-MM-dd"
  107. style="width: 100%"
  108. />
  109. </el-form-item></el-col>
  110. <el-col :span="12"><el-form-item label="供应商批次号">
  111. <el-input
  112. v-model.trim="inboundItem.supplierBatchNo"
  113. placeholder="请扫描标签二维码(自动回车)或输入供应商批次号"
  114. @keyup.enter.native="querySupplierInfoByLabel"
  115. />
  116. </el-form-item></el-col>
  117. <el-col :span="12"><el-form-item label="供应商生产日期">
  118. <el-date-picker
  119. v-model="inboundItem.supplierManufactureDate"
  120. type="date"
  121. format="yyyy-MM-dd"
  122. value-format="yyyy-MM-dd"
  123. placeholder="请选择供应商生产日期"
  124. style="width: 100%"
  125. inputmode="none"
  126. autocomplete="off"
  127. autocorrect="off"
  128. spellcheck="false"
  129. />
  130. </el-form-item></el-col>
  131. <!-- 勾选框 -->
  132. <el-col :span="12" style="margin-top: 2px">
  133. <el-form-item>
  134. <el-checkbox v-model="inboundItem.ifsAutoReport">Auto Report of Operation</el-checkbox>
  135. </el-form-item>
  136. </el-col>
  137. <el-col :span="12" style="margin-top: 2px">
  138. <el-form-item>
  139. <el-checkbox v-model="inboundItem.ifsSimplifiedMaterial">Simplified Material Check</el-checkbox>
  140. </el-form-item>
  141. </el-col>
  142. </el-row>
  143. <el-row :gutter="20">
  144. <el-col :span="8" style="margin-top: 10px"><el-form-item><el-button type="text" style="font-size: 16px;margin-left: 30px" @click="processFlag = 1">回退</el-button></el-form-item></el-col>
  145. <el-col :span="8" style="margin-top: 10px"><el-form-item><el-button type="text" style="font-size: 16px;margin-left: 20px" @click="confirmInbound">确认</el-button></el-form-item></el-col>
  146. <el-col :span="8" style="margin-top: 10px"><el-form-item><el-button type="text" style="font-size: 16px;margin-left: 10px" @click="$router.push('/')">退出</el-button></el-form-item></el-col>
  147. </el-row>
  148. </el-form>
  149. </div>
  150. <!-- Step 3: 包装记录 -->
  151. <div v-if="processFlag === 3">
  152. <el-form label-position="top" class="form-section" style="margin: 5px;">
  153. <el-row :gutter="20">
  154. <el-col :span="12"><el-form-item label="料号"><el-input v-model="inboundItem.partNo" disabled /></el-form-item></el-col>
  155. <el-col :span="12"><el-form-item label="未完成数量"><el-input v-model="inboundItem.unCompleteQty" disabled /></el-form-item></el-col>
  156. <el-col :span="24"><el-form-item label="物料描述"><el-input v-model="inboundItem.partDesc" disabled /></el-form-item></el-col>
  157. <!-- 自动计算开关 -->
  158. <el-col :span="24" style="margin-bottom: 10px;">
  159. <el-form-item>
  160. <el-switch
  161. v-model="autoCalculate"
  162. active-text="自动计算模式"
  163. inactive-text="手动输入模式"
  164. active-color="#17B3A3"
  165. style="display: block;"
  166. >
  167. </el-switch>
  168. <span v-if="autoCalculate" style="font-size: 13px; color: #17B3A3; margin-top: 5px; display: block; font-weight: bold;">
  169. 💡 输入单包装数量后按回车,将根据未完成数量自动分配包装
  170. </span>
  171. <span v-else style="font-size: 12px; color: #999; margin-top: 5px; display: block;">
  172. 手动输入单包装数量和包装数,然后点击"创建"按钮
  173. </span>
  174. </el-form-item>
  175. </el-col>
  176. <el-col :span="8" style="margin-top: 5px"><el-form-item label="单包装数量">
  177. <el-input
  178. v-model="hanlingItem.perQty"
  179. type="number"
  180. :placeholder="autoCalculate ? '输入完按回车或失去焦点' : '请输入单包装数量'"
  181. @blur="autoCalculate && autoCreateHandlingUnits()"
  182. @keyup.enter.native="autoCalculate && autoCreateHandlingUnits()"
  183. />
  184. </el-form-item></el-col>
  185. <el-col :span="8" style="margin-top: 5px" v-if="!autoCalculate"><el-form-item label="包装数">
  186. <el-input v-model="hanlingItem.packageQty" type="number" placeholder="请输入包装数" />
  187. </el-form-item></el-col>
  188. <el-col :span="8" v-if="!autoCalculate" style="margin-top: 16px"><el-form-item>
  189. <el-button type="text" @click="createHandlingUnit" style="font-size: 16px">创建</el-button></el-form-item></el-col>
  190. <el-col :span="8" v-else style="margin-top: 16px"><el-form-item>
  191. <el-button type="text" style="font-size: 16px">确定</el-button></el-form-item></el-col>
  192. <el-table :data="handlingUnit" :row-style="{ height: '30px' }" style="width: 94%; margin-left: 10px;" highlight-current-row>
  193. <el-table-column prop="code" label="序号" />
  194. <el-table-column prop="packageQty" label="包装数" width="80" />
  195. <el-table-column prop="perQty" label="单包装数量" width="100" />
  196. <el-table-column label="操作" v-if="!autoCalculate">
  197. <template slot-scope="scope">
  198. <a @click="removeItem(scope.$index)">删除</a>
  199. </template>
  200. </el-table-column>
  201. </el-table>
  202. <el-col :span="24" v-if="autoCalculate && handlingUnit.length > 0" style="margin-top: 10px;">
  203. <span style="font-size: 12px; color: #999;">
  204. 💡 如需修改,请重新输入单包装数量
  205. </span>
  206. </el-col>
  207. <el-col :span="24"><el-form-item><span>合计</span><span style="margin-left: 38%">{{ totalQty }}</span></el-form-item></el-col>
  208. <el-col :span="12"><el-form-item><el-button type="text" @click="processFlag = 2" style="font-size: 18px;margin-left: 60px">回退</el-button></el-form-item></el-col>
  209. <el-col :span="12"><el-form-item><el-button type="text" @click="processFlag = 2" style="font-size: 18px;">确定</el-button></el-form-item></el-col>
  210. </el-row>
  211. </el-form>
  212. </div>
  213. </div>
  214. </div>
  215. </div>
  216. </template>
  217. <script>
  218. import { getShopOrderFromIfs, getNextSequenceNo, submitShopOrderInbound, validateMaterialIssued, printLabelCommon } from "@/api/production/production-inbound.js";
  219. import { getHandlingUnitDetail } from "@/api/handlingunit/handlingunit.js";
  220. import { filterRowsByAuthorizedSubSite, getAuthorizedSubSiteCodeList } from "@/utils/subSiteAuth.js";
  221. export default {
  222. data() {
  223. return {
  224. processFlag: 1,
  225. scanCode: '',
  226. shopOrderList: [],
  227. inboundItem: {},
  228. handlingUnit: [],
  229. hanlingItem: { code: '', qty: '', perQty: '', packageQty: '' },
  230. site: localStorage.getItem('site'),
  231. warehouseId: localStorage.getItem('selectedWarehouse'),
  232. fullscreenLoading: false,
  233. loadingText: '加载中...',
  234. autoCalculate: false,
  235. issueQty: '',
  236. locationHistoryList: [],
  237. locationHistoryLimit: 20
  238. };
  239. },
  240. computed: {
  241. totalQty() {
  242. // 使用整数运算避免浮点数精度问题
  243. const sumInCents = this.handlingUnit.reduce((sum, item) => {
  244. return sum + Math.round(Number(item.qty) * 10000);
  245. }, 0);
  246. const sum = sumInCents / 10000;
  247. // 仅在已有包装记录时,才回写此次入库数量,避免把扫描带入的transQty覆盖为0
  248. if (this.handlingUnit.length > 0) {
  249. this.inboundItem.transQty = sum;
  250. return sum;
  251. }
  252. return parseFloat(this.inboundItem.transQty) || 0;
  253. },
  254. huKey() {
  255. return `hu_shop_${this.inboundItem.orderNo}_${this.inboundItem.releaseNo}`;
  256. },
  257. displayReleaseSeq() {
  258. return `${this.inboundItem.releaseNo || ''}/${this.inboundItem.sequenceNo || ''}`;
  259. },
  260. locationHistoryKey() {
  261. const site = this.site || 'defaultSite';
  262. return `production_inbound_location_history_${site}`;
  263. }
  264. },
  265. watch: {
  266. autoCalculate(newVal, oldVal) {
  267. this.hanlingItem.perQty = '';
  268. this.hanlingItem.packageQty = '';
  269. this.handlingUnit = [];
  270. localStorage.setItem(this.huKey, JSON.stringify(this.handlingUnit));
  271. if (newVal) {
  272. this.$message.success('已切换到自动创建HU模式');
  273. } else {
  274. this.$message.success('已切换到手动输入模式');
  275. }
  276. }
  277. },
  278. methods: {
  279. // 计算"显示宽度"
  280. getTextWidth(text) {
  281. if (!text) return 0
  282. let len = 0
  283. for (let char of text) {
  284. if (/[\u4e00-\u9fa5\u3000-\u303F\uFF00-\uFFEF]/.test(char)) {
  285. len += 2
  286. } else {
  287. len += 1
  288. }
  289. }
  290. return len
  291. },
  292. handleBack() {
  293. if (this.processFlag === 1) this.$router.back();
  294. else if (this.processFlag === 3) this.processFlag = 2;
  295. else this.processFlag = 1;
  296. },
  297. // 搜索工单
  298. searchShopOrder() {
  299. if (!this.scanCode) return this.shopOrderList = [];
  300. this.loadingText = '搜索中...';
  301. this.fullscreenLoading = true;
  302. // 解析工单号:支持 "3038851"、"3038851-1-2" 或 "3038851-1-2-1000" 格式
  303. let orderNo = this.scanCode.trim();
  304. let releaseNo = "*";
  305. let sequenceNo = "*";
  306. this.issueQty = '';
  307. // 如果包含"-",则按照 orderNo-releaseNo-sequenceNo[-transQty] 格式解析
  308. const parts = orderNo.split('-');
  309. if (parts.length === 4) {
  310. // 结构: orderNo-releaseNo-sequenceNo-transQty
  311. orderNo = parts[0];
  312. releaseNo = parts[1] || "*";
  313. sequenceNo = parts[2] || "*";
  314. this.issueQty = parts[3];
  315. } else if (parts.length >= 2) {
  316. orderNo = parts[0];
  317. releaseNo = parts[1];
  318. if (parts.length >= 3) {
  319. sequenceNo = parts[2];
  320. }
  321. }
  322. const requestData = {
  323. ifsSiteID: this.site+'%',
  324. ifsOrderNo: orderNo,
  325. ifsReleaseNo: releaseNo,
  326. ifsSequenceNo: sequenceNo
  327. };
  328. console.log('工单查询参数:', requestData);
  329. getShopOrderFromIfs(requestData).then(({ data }) => {
  330. if (data.code === 0) {
  331. const sourceRows = data.data || [];
  332. const authorizedSubSiteCodeList = getAuthorizedSubSiteCodeList(this.$store.state.user.subSiteCodeList);
  333. // 接口按site前缀查询会返回同前缀下的所有子site,这里按当前用户授权子site做二次过滤
  334. this.shopOrderList = filterRowsByAuthorizedSubSite(sourceRows, authorizedSubSiteCodeList, ['contract']);
  335. if (this.shopOrderList.length === 0) {
  336. this.$message.warning('未找到匹配的工单');
  337. } else if (this.shopOrderList.length === 1) {
  338. this.selectShopOrder(this.shopOrderList[0]);
  339. }
  340. } else {
  341. this.$message.error(data.msg || '查询失败');
  342. this.shopOrderList = []
  343. }
  344. this.$nextTick(() => {
  345. if (this.$refs.scanCodeRef) {
  346. this.$refs.scanCodeRef.blur();
  347. }
  348. });
  349. }).catch(error => {
  350. console.error('搜索失败:', error);
  351. this.$message.error('搜索失败,请重试');
  352. this.shopOrderList = []
  353. }).finally(() => {
  354. this.fullscreenLoading = false;
  355. });
  356. },
  357. // 选择工单
  358. async selectShopOrder(row) {
  359. // 校验状态
  360. if (row.objstate !== 'Started') {
  361. return this.$message.warning(`工单状态为 ${row.objstate},只有Started状态才能入库`);
  362. }
  363. this.fullscreenLoading = true;
  364. this.loadingText = '检查物料发料状态...';
  365. try {
  366. // 1. 检查物料是否已发料
  367. const validateResult = await validateMaterialIssued({
  368. site: this.site+'%',
  369. orderNo: row.orderNo,
  370. releaseNo: row.releaseNo || '*',
  371. sequenceNo: row.sequenceNo || '*'
  372. });
  373. if (validateResult.data.code !== 0) {
  374. this.$alert(validateResult.data.msg || '物料发料检查失败', '操作提示', {
  375. confirmButtonText: '确定',
  376. type: 'warning'
  377. });
  378. return;
  379. }
  380. this.loadingText = '加载中...';
  381. // 2. 获取下一个序列号
  382. let nextSequence = 1;
  383. try {
  384. const { data } = await getNextSequenceNo({
  385. orderNo: row.orderNo,
  386. releaseNo: row.releaseNo || '*',
  387. sequenceNo: row.sequenceNo || '*'
  388. });
  389. if (data.code === 0) {
  390. nextSequence = data.data;
  391. }
  392. } catch (error) {
  393. console.error('获取序列号失败:', error);
  394. }
  395. this.inboundItem = {
  396. ...row,
  397. transQty: this.issueQty || '', // 如果扫描码中包含transQty,则优先使用,否则为空
  398. unCompleteQty: parseFloat(row.lotSize) - parseFloat(row.qtyComplete),
  399. sequenceNoNew: nextSequence,
  400. batchNo: `${row.orderNo}-${row.releaseNo}-${row.sequenceNo}-${nextSequence}`,
  401. locationNo: '',
  402. serialNo: '*',
  403. wdr: '*',
  404. ifsAutoReport: true, // Auto Report of Operation
  405. ifsSimplifiedMaterial: true, // Simplified Material Check默认选中
  406. height: this.parseHeightFromPartNo(row.partNo), // 根据物料编码自动填充高度
  407. ifsExpiryDate: '', // 有效期,传给IFS接口
  408. supplierManufactureDate: '',
  409. supplierBatchNo: ''
  410. };
  411. this.processFlag = 2;
  412. } catch (error) {
  413. console.error('操作失败:', error);
  414. this.$message.error(error.message || '操作失败');
  415. } finally {
  416. this.fullscreenLoading = false;
  417. }
  418. },
  419. // 扫描标签二维码(unit_id)后,回填供应商生产日期和供应商批次号
  420. async querySupplierInfoByLabel() {
  421. const unitId = this.extractUnitIdFromScanCode(this.inboundItem.supplierBatchNo);
  422. if (!unitId) {
  423. this.$message.warning('请先扫描标签二维码');
  424. return;
  425. }
  426. const previousLoadingText = this.loadingText;
  427. this.loadingText = '查询标签中...';
  428. this.fullscreenLoading = true;
  429. try {
  430. const { data } = await getHandlingUnitDetail({
  431. //site: this.site,
  432. unitId: unitId
  433. });
  434. if (!data || data.code !== 0) {
  435. this.$message.warning((data && data.msg) || '标签查询失败');
  436. return;
  437. }
  438. const labelInfo = data && data.data;
  439. if (!labelInfo) {
  440. this.$message.warning(`未查询到标签:${unitId}`);
  441. return;
  442. }
  443. // 标签值作为供应商字段来源,避免人工二次录入造成误差。
  444. this.inboundItem.supplierManufactureDate = this.normalizeDateToYmd(labelInfo.supplierManufactureDate);
  445. this.inboundItem.supplierBatchNo = labelInfo.supplierBatchNo || '';
  446. if (!this.inboundItem.supplierManufactureDate && !this.inboundItem.supplierBatchNo) {
  447. this.$message.warning('标签中未维护供应商生产日期/供应商批次号');
  448. return;
  449. }
  450. //this.$message.success('已回填供应商信息');
  451. } catch (error) {
  452. console.error('查询标签失败:', error);
  453. this.$message.error('查询标签失败,请重试');
  454. } finally {
  455. this.loadingText = previousLoadingText;
  456. this.fullscreenLoading = false;
  457. }
  458. },
  459. // PDA扫码内容可能是纯unit_id,也可能是带unit_id参数的URL,这里统一提取。
  460. extractUnitIdFromScanCode(scanValue) {
  461. if (!scanValue) return '';
  462. const rawText = String(scanValue).trim();
  463. if (!rawText) return '';
  464. const queryMatch = rawText.match(/(?:^|[?&])unit[_-]?id=([^&]+)/i);
  465. if (queryMatch && queryMatch[1]) {
  466. try {
  467. return decodeURIComponent(queryMatch[1]).trim();
  468. } catch (error) {
  469. return queryMatch[1].trim();
  470. }
  471. }
  472. const keyValueMatch = rawText.match(/unit[_-]?id[:=]\s*([^\s,;]+)/i);
  473. if (keyValueMatch && keyValueMatch[1]) {
  474. return keyValueMatch[1].trim();
  475. }
  476. return rawText;
  477. },
  478. // 统一转换为 yyyy-MM-dd,便于直接绑定到日期控件。
  479. normalizeDateToYmd(dateValue) {
  480. if (!dateValue) return '';
  481. if (typeof dateValue === 'string') {
  482. const text = dateValue.trim();
  483. if (!text) return '';
  484. const datePart = text.slice(0, 10);
  485. if (/^\d{4}-\d{2}-\d{2}$/.test(datePart)) {
  486. return datePart;
  487. }
  488. }
  489. const dateObj = new Date(dateValue);
  490. if (Number.isNaN(dateObj.getTime())) return '';
  491. const year = dateObj.getFullYear();
  492. const month = String(dateObj.getMonth() + 1).padStart(2, '0');
  493. const day = String(dateObj.getDate()).padStart(2, '0');
  494. return `${year}-${month}-${day}`;
  495. },
  496. loadLocationHistory() {
  497. const rawHistory = localStorage.getItem(this.locationHistoryKey);
  498. if (!rawHistory) {
  499. this.locationHistoryList = [];
  500. return;
  501. }
  502. try {
  503. const parsedHistory = JSON.parse(rawHistory);
  504. if (!Array.isArray(parsedHistory)) {
  505. this.locationHistoryList = [];
  506. return;
  507. }
  508. this.locationHistoryList = parsedHistory
  509. .map(item => (item || '').toString().trim())
  510. .filter((item, index, arr) => item && arr.indexOf(item) === index)
  511. .slice(0, this.locationHistoryLimit);
  512. } catch (error) {
  513. console.warn('解析库位历史缓存失败:', error);
  514. this.locationHistoryList = [];
  515. }
  516. },
  517. querySearchLocation(queryString, cb) {
  518. const normalizedKeyword = (queryString || '').trim().toLowerCase();
  519. const historyOptions = this.locationHistoryList.map(location => ({ value: location }));
  520. const result = normalizedKeyword
  521. ? historyOptions.filter(item => item.value.toLowerCase().includes(normalizedKeyword))
  522. : historyOptions;
  523. cb(result);
  524. },
  525. handleLocationSelect(selectedItem) {
  526. // 选中历史值后立即置顶,保证高频库位下次优先展示。
  527. this.saveLocationHistory(selectedItem && selectedItem.value);
  528. },
  529. handleLocationBlur() {
  530. // 失焦即缓存,覆盖“手输后直接切换焦点”的场景。
  531. this.saveLocationHistory(this.inboundItem.locationNo);
  532. },
  533. saveLocationHistory(locationNo) {
  534. const normalizedLocation = (locationNo || '').trim();
  535. if (!normalizedLocation) return;
  536. const dedupedHistory = [normalizedLocation, ...this.locationHistoryList.filter(item => item !== normalizedLocation)]
  537. .slice(0, this.locationHistoryLimit);
  538. this.locationHistoryList = dedupedHistory;
  539. localStorage.setItem(this.locationHistoryKey, JSON.stringify(dedupedHistory));
  540. },
  541. // 检查数量
  542. checkQuantity() {
  543. const transQty = parseFloat(this.inboundItem.transQty) || 0;
  544. const qtyComplete = parseFloat(this.inboundItem.qtyComplete) || 0;
  545. const lotSize = parseFloat(this.inboundItem.lotSize) || 0;
  546. if (qtyComplete + transQty > lotSize) {
  547. this.$confirm(
  548. `当前已完成数量(${qtyComplete}) + 本次入库数量(${transQty}) = ${qtyComplete + transQty},超过订单数量(${lotSize}),是否继续?`,
  549. '数量超出提示',
  550. {
  551. confirmButtonText: '继续',
  552. cancelButtonText: '取消',
  553. type: 'warning'
  554. }
  555. ).catch(() => {
  556. this.inboundItem.transQty = '';
  557. });
  558. }
  559. },
  560. handlingUnitStep() {
  561. this.processFlag = 3;
  562. this.autoCalculate = false; // 默认使用手动输入模式
  563. const saved = localStorage.getItem(this.huKey);
  564. this.handlingUnit = saved ? JSON.parse(saved) : [];
  565. },
  566. /**
  567. * 自动创建HU列表(自动模式)
  568. */
  569. autoCreateHandlingUnits() {
  570. const perQty = this.hanlingItem.perQty;
  571. if (!perQty || isNaN(perQty) || parseFloat(perQty) <= 0) {
  572. this.$message.warning('请输入有效的单包装数量');
  573. return;
  574. }
  575. // 优先使用扫描码中的transQty,其次使用页面上的此次入库数量,最后兜底未完成数量
  576. let qtyToReceive = parseFloat(this.issueQty) || parseFloat(this.inboundItem.transQty) || 0;
  577. if (qtyToReceive <= 0) {
  578. qtyToReceive = parseFloat(this.inboundItem.unCompleteQty) || 0;
  579. }
  580. if (qtyToReceive <= 0) {
  581. this.$message.warning('未完成数量为0,无法自动计算包装');
  582. return;
  583. }
  584. const perQtyValue = parseFloat(perQty);
  585. const packageQty = Math.floor(qtyToReceive / perQtyValue);
  586. // 使用整数运算避免浮点数精度问题
  587. const remainderInCents = Math.round(qtyToReceive * 10000) - Math.round(perQtyValue * 10000) * packageQty;
  588. const remainder = remainderInCents / 10000;
  589. this.handlingUnit = [];
  590. if (packageQty > 0) {
  591. for (let i = 1; i <= packageQty; i++) {
  592. const qty = perQtyValue;
  593. const code = String(i);
  594. this.handlingUnit.push({
  595. code: code,
  596. perQty: perQtyValue,
  597. packageQty: 1,
  598. qty: qty
  599. });
  600. }
  601. }
  602. if (remainder > 0) {
  603. const code = String(this.handlingUnit.length + 1);
  604. this.handlingUnit.push({
  605. code: code,
  606. perQty: remainder,
  607. packageQty: 1,
  608. qty: remainder
  609. });
  610. }
  611. localStorage.setItem(this.huKey, JSON.stringify(this.handlingUnit));
  612. this.hanlingItem.perQty = '';
  613. this.hanlingItem.packageQty = '';
  614. this.$message.success(`自动创建了${this.handlingUnit.length}个HU,合计数量:${qtyToReceive}`);
  615. },
  616. /**
  617. * 手动创建HU(手动模式)
  618. */
  619. createHandlingUnit() {
  620. const { perQty, packageQty } = this.hanlingItem;
  621. if (!perQty || !packageQty || isNaN(perQty) || isNaN(packageQty)) {
  622. return this.$message.warning("请填写有效的包装信息");
  623. }
  624. // 使用整数运算避免浮点数精度问题
  625. const qty = Math.round(parseFloat(perQty) * 10000) * parseInt(packageQty) / 10000;
  626. const code = String(this.handlingUnit.length + 1);
  627. const newItem = { ...this.hanlingItem, qty, code };
  628. this.handlingUnit.push(newItem);
  629. localStorage.setItem(this.huKey, JSON.stringify(this.handlingUnit));
  630. this.hanlingItem.perQty = '';
  631. this.hanlingItem.packageQty = '';
  632. },
  633. removeItem(index) {
  634. this.handlingUnit.splice(index, 1);
  635. localStorage.setItem(this.huKey, JSON.stringify(this.handlingUnit));
  636. },
  637. clearAllHandlingUnitCache() {
  638. this.hanlingItem = { code: '', qty: '', perQty: '', packageQty: '' };
  639. const keys = Object.keys(localStorage);
  640. keys.forEach(key => {
  641. if (key.startsWith('hu_shop_')) {
  642. localStorage.removeItem(key);
  643. }
  644. });
  645. },
  646. // 确认入库
  647. async confirmInbound() {
  648. if (this.fullscreenLoading) return;
  649. const item = this.inboundItem;
  650. if (!item.transQty || !item.locationNo || !item.batchNo) {
  651. return this.$message.error("请填写完整信息");
  652. }
  653. // 检查数量是否超出
  654. const transQty = parseFloat(item.transQty) || 0;
  655. const qtyComplete = parseFloat(item.qtyComplete) || 0;
  656. const lotSize = parseFloat(item.lotSize) || 0;
  657. if (qtyComplete + transQty > lotSize) {
  658. try {
  659. await this.$confirm(
  660. `当前已完成数量(${qtyComplete}) + 本次入库数量(${transQty}) = ${qtyComplete + transQty},超过订单数量(${lotSize}),是否继续?`,
  661. '数量超出提示',
  662. {
  663. confirmButtonText: '继续',
  664. cancelButtonText: '取消',
  665. type: 'warning'
  666. }
  667. );
  668. } catch (error) {
  669. // 用户取消
  670. return;
  671. }
  672. }
  673. this.loadingText = '提交中...';
  674. this.fullscreenLoading = true;
  675. // 构建提交数据
  676. const submitData = {
  677. // 基本字段
  678. site: item.contract,
  679. warehouseId: this.warehouseId,
  680. orderNo: item.orderNo,
  681. releaseNo: item.releaseNo,
  682. sequenceNo: item.sequenceNo,
  683. partNo: item.partNo,
  684. partDesc: item.partDesc,
  685. transQty: item.transQty,
  686. batchNo: item.batchNo,
  687. locationNo: item.locationNo,
  688. serialNo: item.serialNo || '*',
  689. engChgLevel: item.engChgLevel || '1',
  690. uom: item.uom,
  691. lotSize: item.lotSize,
  692. qtyComplete: item.qtyComplete,
  693. height: item.height || null, // 高度(mm)
  694. ifsExpiryDate: item.ifsExpiryDate || null, // 有效期
  695. supplierManufactureDate: item.supplierManufactureDate || null, // 供应商生产日期
  696. supplierBatchNo: item.supplierBatchNo || null, // 供应商批次号
  697. // IFS参数
  698. ifsAutoReport: item.ifsAutoReport ? 'Yes' : 'No',
  699. ifsSimplifiedMaterial: item.ifsSimplifiedMaterial ? 'Yes' : 'No',
  700. ifsAutoClose:item.close_code_db==='Y'?'TRUE':'FALSE',
  701. // 处理单元列表
  702. handlingUnitList: this.handlingUnit.map(hu => ({
  703. perQty: hu.perQty,
  704. packageQty: hu.packageQty
  705. }))
  706. };
  707. submitShopOrderInbound(submitData).then(({ data }) => {
  708. if (data.code === 0) {
  709. this.$message.success("入库成功");
  710. this.saveLocationHistory(item.locationNo);
  711. this.clearAllHandlingUnitCache();
  712. // 获取unitIds并打印标签
  713. const unitIds = data.unitIds || [];
  714. if (unitIds.length > 0) {
  715. let printLabelType;
  716. if (this.inboundItem.partNo && this.inboundItem.partNo.startsWith("80")) {
  717. printLabelType = '库存成品标签';
  718. } else {
  719. printLabelType = 'BIL标签';
  720. }
  721. this.printViaServer(unitIds,printLabelType);
  722. }
  723. this.processFlag = 1;
  724. this.scanCode = '';
  725. this.shopOrderList = [];
  726. this.inboundItem = {};
  727. this.handlingUnit = [];
  728. } else {
  729. this.$message.error(data.msg || '入库失败');
  730. }
  731. }).catch(error => {
  732. console.error('入库失败:', error);
  733. this.$message.error('网络错误,请重试');
  734. }).finally(() => {
  735. this.fullscreenLoading = false;
  736. });
  737. },
  738. /**
  739. * 通过服务器打印
  740. * @param {Array} unitIds - HU unitId列表
  741. * @param printLabelType
  742. */
  743. async printViaServer(unitIds, printLabelType) {
  744. if (!unitIds || unitIds.length === 0) {
  745. console.warn('没有可打印的标签');
  746. return;
  747. }
  748. this.$emit('print-start')
  749. try {
  750. const printRequest = {
  751. userId: localStorage.getItem('userName'),
  752. username: localStorage.getItem('userName'),
  753. site: localStorage.getItem('site'),
  754. unitIds: unitIds,
  755. labelType: printLabelType
  756. }
  757. console.log('打印请求:', printRequest);
  758. const { data } = await printLabelCommon(printRequest)
  759. if (data.code === 200 || data.code === 0) {
  760. this.$message.success(`打印任务已发送!共${unitIds.length}个标签`)
  761. } else {
  762. this.$message.error(data.msg || '打印失败')
  763. }
  764. } catch (error) {
  765. console.error('服务器打印失败:', error)
  766. this.$message.error(`打印失败: ${error.message || error}`)
  767. }
  768. },
  769. /**
  770. * 从物料编码中解析高度
  771. * 如果物料编码格式是 70004479-0030,则高度为30
  772. * 如果物料编码格式是 70004479,则高度为空
  773. */
  774. parseHeightFromPartNo(partNo) {
  775. if (!partNo) return '';
  776. // 检查是否包含横杠
  777. const parts = partNo.split('-');
  778. if (parts.length >= 2) {
  779. // 取横杠后面的数字部分
  780. const heightStr = parts[1];
  781. // 转换为数字,去掉前导0
  782. const height = parseInt(heightStr, 10);
  783. return isNaN(height) ? '' : height;
  784. }
  785. return '';
  786. }
  787. },
  788. mounted() {
  789. this.loadLocationHistory();
  790. this.$nextTick(() => this.$refs.scanCodeRef.focus());
  791. },
  792. };
  793. </script>
  794. <style scoped>
  795. .mt10 {
  796. margin-top: 10px;
  797. }
  798. .scan-box input {
  799. width: 100%;
  800. padding: 12px;
  801. font-size: 16px;
  802. }
  803. .item-list {
  804. flex: 1;
  805. overflow-y: auto;
  806. margin: 10px 0;
  807. border: 1px solid rgba(200, 200, 200, 0.8);
  808. }
  809. .item-list span {
  810. color: #000;
  811. font-size: 15px;
  812. }
  813. .bottom-line-row {
  814. border-bottom: 1px solid rgba(200, 200, 200, 0.8);
  815. }
  816. .recvButton {
  817. font-size: 16px;
  818. border-radius: 3px;
  819. color: #17b3a3;
  820. }
  821. .item-list .el-row {
  822. cursor: pointer;
  823. transition: background 0.3s;
  824. }
  825. .item-list .el-row:hover {
  826. background: #f5f7fa;
  827. }
  828. .disabled-button {
  829. color: #ccc !important;
  830. cursor: not-allowed !important;
  831. }
  832. .form-section >>> .el-col {
  833. margin-bottom: 2px;
  834. }
  835. .status-bar {
  836. display: flex;
  837. justify-content: space-between;
  838. align-items: center;
  839. background: #17b3a3;
  840. color: white;
  841. }
  842. /* 自定义loading样式 */
  843. .pda-container >>> .el-loading-mask {
  844. background-color: rgba(255, 255, 255, 0.3) !important;
  845. }
  846. .pda-container >>> .el-loading-spinner {
  847. margin-top: -25px;
  848. }
  849. .pda-container >>> .el-loading-spinner .circular {
  850. width: 35px;
  851. height: 35px;
  852. }
  853. .pda-container >>> .el-loading-text {
  854. color: #17b3a3 !important;
  855. font-size: 14px;
  856. font-weight: 500;
  857. margin-top: 10px;
  858. }
  859. </style>