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\)"/)
+})