diff --git a/AGENTS.md b/AGENTS.md index f3207ef..57a8218 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ ## 海外业务接口边界 -- 仓储转运按托盘分别保存箱数、长宽高(cm)和毛重(kg),体积按本托尺寸计算;SKU件数逐种核对差异,不以预报箱数强制核平。箱唛沿用客户预报编号,PDA不得另造新箱号;测量完成前客户可增加预报箱数,PDA刷新时合并新箱并保留本机草稿,清点提交携带forecastPackageCount拦截旧快照。生成托盘号后锁定该卡测量数据,必须取得服务端绑定确认且至少有一张现场照片才能完成打托;完成请求提交稳定幂等键、照片文件 ID,SKU实收件数有差异时在完成入口弹窗必填差异原因,不重复提交整份托盘测量。重新进入从详情恢复托盘和鉴权照片;标签使用服务端 `labelDownloadPath` 下载 PDF,打开文档不等于真机打印完成。 +- 仓储转运按托盘分别保存箱数、长宽高(cm)和毛重(kg),体积按本托尺寸计算;SKU件数逐种核对差异,不以预报箱数强制核平。箱唛沿用客户预报编号,PDA不得另造新箱号;测量完成前客户与平台可增加整单预报箱数,不选SKU,箱内SKU以PDA实点为准,PDA刷新时合并新箱并保留本机草稿,清点提交携带forecastPackageCount拦截旧快照。生成托盘号后锁定该卡测量数据,必须取得服务端绑定确认且至少有一张现场照片才能完成打托;完成请求提交稳定幂等键、照片文件 ID,SKU实收件数有差异时在完成入口弹窗必填差异原因,不重复提交整份托盘测量。重新进入从详情恢复托盘和鉴权照片;标签使用服务端 `labelDownloadPath` 下载 PDF,打开文档不等于真机打印完成。 - 换标目标确认即提交拆托合托,不再二次点击底部提交。`RelabelPalletWork.vue` 按当前轮次 `PALLET_BIND` 最新记录提供逐箱改绑,扫描新目标后弹窗确认,使用既有 `relabel-disassemble` 精确来源路径(不传 `actualSource`);同目标不发写请求。服务器持有库存、托盘版本与幂等约束,已排车或出库禁止直接改绑。离线回归 `tests/relabel-pallet-work.test.mjs`,不等同真机验收。 diff --git a/src/pages/warehouse-transfer/warehouse-transfer.vue b/src/pages/warehouse-transfer/warehouse-transfer.vue index 64deff4..522f225 100644 --- a/src/pages/warehouse-transfer/warehouse-transfer.vue +++ b/src/pages/warehouse-transfer/warehouse-transfer.vue @@ -85,7 +85,7 @@ 所属托盘:{{ cartonPalletLabel(box) }} ▾ - + @@ -270,7 +270,7 @@ export default { saveCartonDraft() { if (this.selected && !this.cartonsCounted) uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature: this.cartonSignature }) }, cartonDraftKey() { return `transfer-carton-count:${session.apiBase}:${this.user?.id}:${this.user?.warehouseId}:${this.selected?.transferNo}` }, restoreCartons() { - const rows = (this.selected?.cartons || []).filter(box => box.status !== 'NOT_RECEIVED').map(box => ({ ...box, transferPalletId: box.transferPalletId || (this.generatedPallets.length === 1 ? this.generatedPallets[0].draftId : ''), received: box.status !== 'NOT_RECEIVED', items: box.items?.length ? box.items.map(line => ({ skuCode: line.skuCode, quantity: line.quantity })) : [{ skuCode: this.selected.items.find(item => String(item.id) === String(box.transferItemId))?.skuCode || '', quantity: box.quantity ?? '' }] })); + const rows = (this.selected?.cartons || []).filter(box => box.status !== 'NOT_RECEIVED').map(box => ({ ...box, transferPalletId: box.transferPalletId || (this.generatedPallets.length === 1 ? this.generatedPallets[0].draftId : ''), received: box.status !== 'NOT_RECEIVED', items: box.items?.length ? box.items.map(line => ({ skuCode: line.skuCode, quantity: line.quantity })) : [{ skuCode: this.selected.items.find(item => String(item.id) === String(box.transferItemId))?.skuCode || this.declaredSkuCodes[0] || '', quantity: box.quantity ?? '' }] })); const draft = uni.getStorageSync(this.cartonDraftKey()); this.countDiscrepancyReason = this.selected?.cartons?.find(box => box.discrepancyReason)?.discrepancyReason || ''; this.cartonRequestKey = ''; this.cartonSignature = ''; if (!this.cartonsCounted && Array.isArray(draft?.boxes) && draft.boxes.every(box => box.items?.length && box.items.every(line => this.declaredSkuCodes.includes(line.skuCode)) && (box.cartonMark || this.selected.items.some(item => String(item.id) === String(box.transferItemId)))) && draft.boxes.every(box => !box.cartonMark || rows.some(row => row.cartonMark === box.cartonMark))) { @@ -306,7 +306,7 @@ export default { if (skuCode) box.items.push({ skuCode, quantity: '' }); }, validCartonSkuLines(box) { - return box.items.length > 0 && box.items.length <= 200 && (!box.cartonMark || box.items.some(line => line.skuCode === this.selected.items.find(item => String(item.id) === String(box.transferItemId))?.skuCode)) && new Set(box.items.map(line => line.skuCode)).size === box.items.length + return box.items.length > 0 && box.items.length <= 200 && new Set(box.items.map(line => line.skuCode)).size === box.items.length && box.items.every(line => this.declaredSkuCodes.includes(line.skuCode) && Number.isSafeInteger(Number(line.quantity)) && Number(line.quantity) > 0 && Number(line.quantity) <= 2147483647); }, async confirmCartons() { diff --git a/tests/warehouse-transfer-counting.test.mjs b/tests/warehouse-transfer-counting.test.mjs index 21fb8f3..d69202d 100644 --- a/tests/warehouse-transfer-counting.test.mjs +++ b/tests/warehouse-transfer-counting.test.mjs @@ -251,3 +251,13 @@ test('count payload carries forecast version for stale snapshot rejection',async scope.countWarehouseTransferCartons=async(no,request)=>{payload=request;throw new Error('offline')} await value.confirmCartons();assert.equal(payload.forecastPackageCount,1);assert.equal(payload.boxes[0].cartonMark,'BOX1') }) + +test('新增预报箱无SKU绑定时恢复为可编辑箱,保留已有草稿', () => { + const { value } = setup() + value.selected.cartons.push({ cartonMark: 'NEW-UNBOUND', cartonIndex: 2, status: 'EXPECTED', transferItemId: null }) + value.selected.packageCount = 2 + value.restoreCartons() + const added = value.cartonBoxes.find(box => box.cartonMark === 'NEW-UNBOUND') + assert.ok(added) + assert.ok(value.declaredSkuCodes.includes(added.items[0].skuCode)) +}) diff --git a/tests/warehouse-transfer-mixed-cartons.test.mjs b/tests/warehouse-transfer-mixed-cartons.test.mjs index bdf13d4..ccae108 100644 --- a/tests/warehouse-transfer-mixed-cartons.test.mjs +++ b/tests/warehouse-transfer-mixed-cartons.test.mjs @@ -115,3 +115,13 @@ test('copy previous matching SKU skips other SKUs and preserves the target palle ctx.cartonBoxes[2].items[0].quantity = 9 assert.equal(ctx.cartonBoxes[0].items[0].quantity, 12) }) + +test('预报箱可调整原SKU,新增无绑定箱也可清点', () => { + const ctx = context(); const current = { ...box('B', 4), cartonMark: 'FORECAST-1', transferItemId: '1' } + assert.equal(ctx.validCartonSkuLines(current), true) + current.transferItemId = null + assert.equal(ctx.validCartonSkuLines(current), true) + current.items[0].skuCode = 'OTHER' + assert.equal(ctx.validCartonSkuLines(current), false) + assert.doesNotMatch(source, /:lock-first-sku="Boolean\(box.cartonMark\)"/) +})