From 2a38288e205cca708e1f7b80ad913020fa2c9b65 Mon Sep 17 00:00:00 2001
From: liuyihe <2513762445@qq.com>
Date: Thu, 15 Oct 2026 15:45:32 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/warehouse-transfers.js | 8 +
src/components/CartonCountFields.vue | 12 +
src/pages/outbound/outbound.vue | 2 +-
src/pages/return-counting/return-counting.vue | 6 +-
.../warehouse-transfer/warehouse-transfer.vue | 242 ++++++++++++++----
tests/return-counting-page.test.mjs | 2 +-
tests/warehouse-transfer-api.test.mjs | 10 +-
tests/warehouse-transfer-counting.test.mjs | 129 ++++++++++
8 files changed, 358 insertions(+), 53 deletions(-)
create mode 100644 src/components/CartonCountFields.vue
create mode 100644 tests/warehouse-transfer-counting.test.mjs
diff --git a/src/api/warehouse-transfers.js b/src/api/warehouse-transfers.js
index 90e7479..764ccc9 100644
--- a/src/api/warehouse-transfers.js
+++ b/src/api/warehouse-transfers.js
@@ -49,3 +49,11 @@ export function downloadWarehouseTransferPalletLabel(labelDownloadPath) {
if (!path) return Promise.reject(new Error('服务端未返回托盘标签下载地址'))
return overseasDownload(path)
}
+
+export function countWarehouseTransferCartons(transferNo, payload) {
+ return overseasRequest('POST', `${BASE}/${encoded(transferNo)}/cartons/count`, payload)
+}
+
+export function getWarehouseTransferCartonLabel(transferNo) {
+ return overseasRequest('GET', `${BASE}/${encoded(transferNo)}/carton-label`)
+}
diff --git a/src/components/CartonCountFields.vue b/src/components/CartonCountFields.vue
new file mode 100644
index 0000000..a5be130
--- /dev/null
+++ b/src/components/CartonCountFields.vue
@@ -0,0 +1,12 @@
+体积{{ boxVolume(box) }} cm³
+ {{ line.skuCode || '选择 SKU' }} ▾
+
+
diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue
index 65cdc72..26c728c 100644
--- a/src/pages/outbound/outbound.vue
+++ b/src/pages/outbound/outbound.vue
@@ -5,7 +5,7 @@
出库任务整托扫描托盘码,散箱扫描箱唛;扫描排车业务号会转入“排车拣货”
当前任务出库作业详情
-
+
任务号直接恢复;退货整托出库扫托盘码,散箱出库扫箱唛。
diff --git a/src/pages/return-counting/return-counting.vue b/src/pages/return-counting/return-counting.vue
index 335375f..4b6eb5c 100644
--- a/src/pages/return-counting/return-counting.vue
+++ b/src/pages/return-counting/return-counting.vue
@@ -13,8 +13,7 @@
{{ task.palletCode }}{{ task.status === 'COUNTING' ? '清点中' : '已完成' }}{{ task.returnNo }} · {{ task.boxes.length }} 箱{{ row.skuCode }}签收 {{ row.quantity }} / 已填 {{ row.actual }}
- 第 {{ index + 1 }} 箱{{ box.cartonMark }}体积{{ boxVolume(box) }} cm³
- {{ line.skuCode || '选择 SKU' }} ▾
+ 第 {{ index + 1 }} 箱{{ box.cartonMark }}
各箱 SKU 数量汇总必须与本托签收数量一致,确认后这些箱子才能换标、合箱或创建出库单。箱内清点已完成,可以继续换标或出库。
@@ -22,13 +21,14 @@
diff --git a/tests/return-counting-page.test.mjs b/tests/return-counting-page.test.mjs
index 577c226..f50cc7d 100644
--- a/tests/return-counting-page.test.mjs
+++ b/tests/return-counting-page.test.mjs
@@ -5,7 +5,7 @@ import { runInNewContext } from 'node:vm'
const source = readFileSync(new URL('../src/pages/return-counting/return-counting.vue', import.meta.url), 'utf8')
const script = source.match(/
diff --git a/tests/warehouse-transfer-api.test.mjs b/tests/warehouse-transfer-api.test.mjs
index bb9704d..1b062a9 100644
--- a/tests/warehouse-transfer-api.test.mjs
+++ b/tests/warehouse-transfer-api.test.mjs
@@ -36,7 +36,7 @@ test('warehouse transfer page loads detail without exposing internal identifiers
assert.match(page, /entryMode === 'work' && taskNo/)
assert.match(page, /mode=work&task=\$\{encodeURIComponent\(String\(item\.transferNo\)\)\}/)
assert.match(page, /v-if="!selected && entryMode === 'list'"/)
- assert.match(page, /作业详情/)
+ assert.match(page, /返回列表/)
assert.match(page, /backToCandidates\(\)/)
assert.match(page, /backToCandidates\(\) \{ if \(this\.submitting.*this\.entryMode = 'list'/)
assert.match(page, /存在未完结的测量单/)
diff --git a/tests/warehouse-transfer-mixed-cartons.test.mjs b/tests/warehouse-transfer-mixed-cartons.test.mjs
new file mode 100644
index 0000000..e625e00
--- /dev/null
+++ b/tests/warehouse-transfer-mixed-cartons.test.mjs
@@ -0,0 +1,117 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+
+const source = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
+const script = source.match(/')[0].replace(/^import .*$/gm, '').replace('export default', 'return')
function setup() {
const calls = []
- const component = new Function('ScanTargetInput', 'measureRelabelPallet', 'warehouseIdempotencyKey', 'releaseWarehouseKey', script)(
+ const component = new Function('ScanTargetInput', 'measureRelabelPallet', 'warehouseIdempotencyKey', 'releaseWarehouseKey', 'bindRelabelPallet', script)(
{}, async payload => { calls.push(payload); return [{ palletCode: payload.palletCode, volumeCbm: 0.25, grossWeightKg: payload.grossWeightKg }] },
- () => ({ value: 'stable-key', signature: 'signature' }), () => {})
+ () => ({ value: 'stable-key', signature: 'signature' }), () => {}, async payload => calls.push(payload))
const value = { ...component.data(), task: { relabelNo: 'RL-1' }, $emit() {} }
for (const [key, method] of Object.entries(component.methods)) value[key] = method.bind(value)
return { component, value, calls }
@@ -33,3 +35,74 @@ test('only whole physical pallet quantities offer original pallet reuse', () =>
ctx.plannedPackages = 10;ctx.isReturnBulk = true
assert.equal(canRetain.call(ctx), false)
})
+
+async function renderMeasurements(measurements) {
+ const { component } = setup()
+ const template = source.slice(source.indexOf('') + 10, source.indexOf('
+.compact-measure .count-line{padding-top:8px;border-top:1px solid #edf1f6}.compact-measure .sku-picker{line-height:18px}.compact-measure .quantity{width:76px;font-weight:600;background:#fff}.compact-measure .measure-grid{gap:10px}.compact-measure .measure-input{height:38px;background:#f8fafd}.compact-measure .volume-chip{margin:8px 0;padding:6px 10px;border-radius:7px;background:#f4f8ff}.compact-measure .volume-chip strong{font-size:13px}
diff --git a/src/pages/warehouse-transfer/warehouse-transfer.vue b/src/pages/warehouse-transfer/warehouse-transfer.vue
index ead8f28..64deff4 100644
--- a/src/pages/warehouse-transfer/warehouse-transfer.vue
+++ b/src/pages/warehouse-transfer/warehouse-transfer.vue
@@ -3,7 +3,7 @@
-
+
@@ -29,21 +29,23 @@
测量已完成该转运单已完成 PDA 测量,不再重复进入测量或托盘作业。
库存转运 · 无需测量该单来源于仓库现有库存,不需要重复测量或上传现场照片。请进入托盘作业,按来源托盘执行整托转运、拆托重打,或与相同目的仓合托。
-
+
当前测量任务{{ selected.transferNo }}
{{ cartonsCounted ? actualReceivedCartons : receivedCartonBoxes.length }}{{ cartonsCounted ? '实收箱' : '待确认箱' }}
+ {{ summaryCollapsed ? '展开 ⌄' : '收起 ⌃' }}
- {{ selected.packageCount || 0 }}客户预报{{ cartonsCounted ? actualReceivedCartons : '—' }}{{ cartonsCounted ? '当前实收' : '清点后确认' }}{{ !cartonsCounted ? '—' : cartonDifference > 0 ? `+${cartonDifference}` : cartonDifference }}{{ !cartonsCounted ? '差异待确认' : cartonDifference === 0 ? '无差异' : cartonDifference > 0 ? '超收差异' : '短收差异' }}
- {{ pallets.length }}托盘{{ totalVolume }}箱子总体积 m³{{ totalWeight }}箱子总重量 kg
+ {{ forecastSkuQuantity }}客户预报(件){{ cartonsCounted ? cartonQuantity : '—' }}SKU实收(件){{ !cartonsCounted ? '—' : hasSkuReceiptDifference ? skuReceiptDifferences.length + ' 种' : '无差异' }}{{ !cartonsCounted ? '差异待确认' : 'SKU签收差异' }}
+ {{ pallets.length }}托盘{{ totalVolume }}箱子总体积 m³{{ totalWeight }}箱子总重量 kg
{{ String(index + 1).padStart(2, '0') }}
- 托盘 {{ index + 1 }} · {{ pallet.bound ? '已绑定' : pallet.generated ? '待绑定' : '待创建' }}{{ pallet.collapsed && pallet.palletNo ? pallet.palletNo : '先创建托盘号,再录入并分配签收箱子' }}
-
+ 托盘 {{ index + 1 }}{{ pallet.bound ? '已绑定' : pallet.generated ? '待绑定' : '待创建' }}
+
- {{ palletSummary(pallet).cartonCount }} 箱{{ palletVolume(pallet) }} m³{{ palletSummary(pallet).weight.toFixed(2) }} kg
+ {{ palletSummary(pallet).cartonCount }} 箱实收箱数{{ palletVolume(pallet) }} m³体积{{ palletSummary(pallet).weight.toFixed(2) }} kg重量
+ {{ pallet.palletNo }}当前签收
本托箱子汇总由分配到本托的实收箱自动汇总,无需填写托盘尺寸和重量
{{ palletSummary(pallet).cartonCount }}实收箱数{{ palletVolume(pallet) }}箱子体积 m³{{ palletSummary(pallet).weight.toFixed(2) }}箱子重量 kg
@@ -57,7 +59,7 @@
请先生成托盘号,再填写签收箱子。
- 签收箱子
+ 签收箱子
@@ -67,27 +69,28 @@
批量所属托盘:{{ generatedPallets.find(pallet => String(pallet.draftId) === String(batchPalletId))?.palletNo || '请选择' }} ▾
-
+
已选 {{ batchTargetBoxes.length }} 箱 · 每箱 {{ Number(batchBox.items[0].quantity) || 0 }} 件 · 合计 {{ batchTotalQuantity }} 件
{{ batchCountMessage }}
-
- 第 {{ index + 1 }} 箱 · {{ box.cartonMark || '新增实收箱(确认后生成箱号)' }}
+
+ {{ box.cartonMark || '新增实收箱' }}
+
+ 第 {{ index + 1 }} 箱
+ {{ box.cartonMark || '新增实收箱' }}
- 客户箱唛:{{ box.customerCartonMark }}
- 可添加本单预报 SKU,数量按箱内实际件数填写
所属托盘:{{ cartonPalletLabel(box) }} ▾
-
+
+
+
-
-
-
+
测量现场照片完成测量并打托前至少拍摄 1 张{{ photoFileIds.length }} / 20 张
@@ -119,7 +122,7 @@ import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { session, requireSession } from '@/stores/session.js'
import { restoreWarehouseWork, resetWarehouseDomain, updateWarehouseWork, warehouseWork } from '@/stores/warehouse-work.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
-import { getWarehouseTransferCartonLabel, countWarehouseTransferCartons, bindWarehouseTransferPallet, deleteWarehouseTransferPallet, downloadWarehouseTransferMeasurementPhoto, downloadWarehouseTransferPalletLabel, generateWarehouseTransferPallet, getWarehouseTransferDetail, listWarehouseTransferCandidates, listWarehouseTransferMeasurementPhotos, palletizeWarehouseTransfer, saveWarehouseTransferPalletDraft, uploadWarehouseTransferMeasurementPhoto } from '@/api/warehouse-transfers.js'
+import { getWarehouseTransferCartonLabel, countWarehouseTransferCartons, reopenWarehouseTransferMeasurement, bindWarehouseTransferPallet, deleteWarehouseTransferPallet, downloadWarehouseTransferMeasurementPhoto, downloadWarehouseTransferPalletLabel, generateWarehouseTransferPallet, getWarehouseTransferDetail, listWarehouseTransferCandidates, listWarehouseTransferMeasurementPhotos, palletizeWarehouseTransfer, saveWarehouseTransferPalletDraft, uploadWarehouseTransferMeasurementPhoto } from '@/api/warehouse-transfers.js'
const newPallet = () => ({ key: `${Date.now()}-${Math.random()}`, cartonCount: 0, lengthCm: null, widthCm: null, heightCm: null, grossWeightKg: null, generated: false, bound: false, collapsed: false, palletNo: '', fbaWarehouseCode: '', labelDownloadPath: '', bindCode: '', busy: false })
const MAX_MEASUREMENT_PHOTOS = 20
@@ -135,7 +138,7 @@ const restoredPallet = (value, index) => ({
export default {
components: { PortalHeader, ScanTargetInput, CartonCountFields },
- data() { return { receivingPalletId: '', cartonPalletFilter: '', batchPalletId: '', batchCountOpen: false, batchSku: '', batchBox: newBatchBox(), batchSelectedIndexes: [], batchCountMessage: '', cartonScrollTarget: '', cartonBoxes: [], countDiscrepancyReason: '', countingCartons: false, cartonRequestKey: '', cartonSignature: '', user: session.currentUser, entryMode: 'work', pendingTaskNo: '', candidates: [], selected: null, pallets: [newPallet()], activePalletKey: '', photoFileIds: [], photoNames: [], photoUrls: {}, completedPallets: [], completedTransferNo: '', completedDestination: '', completedDestinationType: '', loading: false, detailLoading: false, submitting: false, uploadingPhoto: false, openingLabel: false, error: null, success: false, pendingKey: '' } },
+ data() { return { summaryCollapsed: false, receivingPalletId: '', cartonPalletFilter: '', batchPalletId: '', batchCountOpen: false, batchSku: '', batchBox: newBatchBox(), batchSelectedIndexes: [], batchCountMessage: '', cartonScrollTarget: '', cartonBoxes: [], countDiscrepancyReason: '', countingCartons: false, cartonRequestKey: '', cartonSignature: '', user: session.currentUser, entryMode: 'work', pendingTaskNo: '', candidates: [], selected: null, pallets: [newPallet()], activePalletKey: '', photoFileIds: [], photoNames: [], photoUrls: {}, completedPallets: [], completedTransferNo: '', completedDestination: '', completedDestinationType: '', loading: false, detailLoading: false, submitting: false, uploadingPhoto: false, openingLabel: false, error: null, success: false, pendingKey: '' } },
computed: {
generatedPallets() { return this.pallets.filter(pallet => pallet.generated && pallet.draftId) },
declaredSkuCodes() { return [...new Set((this.selected?.items || []).map(item => item.skuCode))] },
@@ -143,19 +146,30 @@ export default {
batchTargetBoxes() { return this.cartonBoxes.filter((box, index) => this.batchSelectedIndexes.includes(index) && box.received !== false && box.items.length === 1 && box.items[0]?.skuCode === this.batchSku) },
batchTotalQuantity() { const quantity = Number(this.batchBox.items[0].quantity); return Number.isSafeInteger(quantity) && quantity > 0 ? this.batchTargetBoxes.length * quantity : 0 },
batchCountReady() { return this.batchTargetBoxes.length > 0 && this.validCartonValues(this.batchBox) && this.generatedPallets.some(pallet => String(pallet.draftId) === String(this.batchPalletId)) },
+ // 确认快照与当前内容一致,才允许保存整单清点结果。
+ // 按 SKU 分别核对件数,不能让不同 SKU 的短收与超收相互抵消。
+ forecastSkuQuantity() { return (this.selected?.items || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0) },
+ skuReceiptDifferences() {
+ const expected = new Map(), received = new Map()
+ for (const item of this.selected?.items || []) expected.set(item.skuCode, (expected.get(item.skuCode) || 0) + Number(item.quantity || 0))
+ for (const box of this.receivedCartonBoxes) for (const line of box.items) received.set(line.skuCode, (received.get(line.skuCode) || 0) + Number(line.quantity || 0))
+ return [...new Set([...expected.keys(), ...received.keys()])].filter(sku => (received.get(sku) || 0) !== (expected.get(sku) || 0))
+ },
+ hasSkuReceiptDifference() { return this.skuReceiptDifferences.length > 0 },
+ allCartonsConfirmed() { return this.receivedCartonBoxes.length > 0 && this.receivedCartonBoxes.every(box => this.cartonConfirmed(box)) },
actualReceivedCartons() { return this.cartonsCounted ? this.receivedCartonBoxes.length : 0 },
cartonsCounted() { return Boolean(this.selected?.cartons?.length) && this.selected.cartons.every(box => box.countedAt) },
- cartonVolume() { return this.receivedCartonBoxes.reduce((sum, box) => sum + (Number(box.length) || 0) * (Number(box.width) || 0) * (Number(box.height) || 0) / 1000000, 0).toFixed(6) },
+ cartonVolume() { return this.receivedCartonBoxes.reduce((sum, box) => sum + (Number(box.length) || 0) * (Number(box.width) || 0) * (Number(box.height) || 0) / 1000000, 0).toFixed(3) },
cartonWeight() { return this.receivedCartonBoxes.reduce((sum, box) => sum + (Number(box.weight) || 0), 0).toFixed(2) },
cartonQuantity() { return this.receivedCartonBoxes.reduce((sum, box) => sum + box.items.reduce((total, line) => total + (Number(line.quantity) || 0), 0), 0) },
receivedCartonBoxes() { return this.cartonBoxes.filter(box => box.received !== false) },
- cartonCountReady() { return this.receivedCartonBoxes.length > 0 && this.pallets.every(pallet => pallet.generated && this.palletSummary(pallet).cartonCount > 0) && (this.receivedCartonBoxes.length === Number(this.selected?.packageCount) || Boolean(this.countDiscrepancyReason.trim())) && this.receivedCartonBoxes.every(box => this.generatedPallets.some(pallet => String(pallet.draftId) === String(box.transferPalletId)) && [box.length, box.width, box.height, box.weight].every(value => Number(value) >= 0.01) && this.validCartonSkuLines(box)) },
+ cartonCountReady() { return this.receivedCartonBoxes.length > 0 && this.pallets.every(pallet => pallet.generated && this.palletSummary(pallet).cartonCount > 0) && this.receivedCartonBoxes.every(box => this.generatedPallets.some(pallet => String(pallet.draftId) === String(box.transferPalletId)) && [box.length, box.width, box.height, box.weight].every(value => Number(value) >= 0.01) && this.validCartonSkuLines(box)) },
completedDestinationLabel() { return this.destinationLabel({ fbaWarehouseCode: this.completedDestination, destinationType: this.completedDestinationType }) },
totalCartons() { return this.pallets.reduce((sum, pallet) => sum + this.palletSummary(pallet).cartonCount, 0) },
cartonDifference() { return this.actualReceivedCartons - Number(this.selected?.packageCount || 0) },
isInventoryTransfer() { return Boolean(this.selected && this.selected.status === 'WAIT_PDA_RECEIVING' && this.selected.inboundCode && this.selected.inboundCode !== this.selected.transferNo) },
isMeasurementComplete() { return Boolean(this.selected && !['WAIT_PDA_RECEIVING', 'PARTIALLY_RECEIVED'].includes(String(this.selected.status))) },
- totalVolume() { return this.pallets.reduce((sum, pallet) => sum + this.rawPalletVolume(pallet), 0).toFixed(6) },
+ totalVolume() { return this.pallets.reduce((sum, pallet) => sum + this.rawPalletVolume(pallet), 0).toFixed(3) },
totalWeight() { return this.pallets.reduce((sum, pallet) => sum + this.palletSummary(pallet).weight, 0).toFixed(2) },
validationMessage() {
if (this.isInventoryTransfer) return ''
@@ -172,7 +186,7 @@ export default {
},
watch: {
countDiscrepancyReason() { this.saveCartonDraft() },
- cartonBoxes: { deep: true, handler() { if (this.selected && !this.cartonsCounted) uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature: this.cartonSignature }) } },
+ cartonBoxes: { deep: true, handler() { 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 }) } },
pallets: { deep: true, handler() { if (!this.submitting) this.pendingKey = '' } }
},
onShow() { if (requireSession()) { this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.startScanner(); this.loadCandidates().then(() => { const activeNo = String(warehouseWork.warehouseTransfer.currentNo || ''); const taskNo = this.pendingTaskNo || activeNo; if (this.entryMode === 'work' && taskNo) { const active = this.candidates.find((item) => String(item.transferNo) === taskNo); if (active) { const fromExplicitTask = Boolean(this.pendingTaskNo); this.pendingTaskNo = ''; void this.select(active, fromExplicitTask) } } }) } },
@@ -180,6 +194,31 @@ export default {
onHide() { this.stopScanner() },
beforeUnmount() { this.stopScanner() },
methods: {
+ // 快照只包含业务数据;复制、批量修改或改托后会自动失效并重新展开。
+ cartonConfirmationSignature(box) {
+ return JSON.stringify({ pallet: String(box.transferPalletId || ''), dimensions: [box.length, box.width, box.height, box.weight].map(Number), items: box.items.map(line => [line.skuCode, Number(line.quantity)]) })
+ },
+ cartonConfirmed(box) { return Boolean(box.confirmedSignature && box.confirmedSignature === this.cartonConfirmationSignature(box)) },
+ // 单箱先保存设备草稿,最后一箱确认后统一提交后端已有的整单清点事务。
+ async confirmSingleCarton(box) {
+ if (this.countingCartons || this.submitting || this.cartonsCounted || !this.validCartonValues(box) || !this.generatedPallets.some(pallet => String(pallet.draftId) === String(box.transferPalletId))) return
+ box.confirmedSignature = this.cartonConfirmationSignature(box)
+ this.error = null; this.saveCartonDraft()
+ if (this.allCartonsConfirmed && this.cartonCountReady) await this.confirmCartons()
+ },
+ // 已保存清点时先请求服务端重开;成功后只展开当前箱,其他箱继续保留确认状态。
+ async editCarton(box) {
+ if (this.countingCartons || this.submitting || this.pallets.some(pallet => pallet.busy)) return
+ if (!this.cartonsCounted) { box.confirmedSignature = ''; this.saveCartonDraft(); return }
+ const mark = box.cartonMark
+ this.countingCartons = true; this.error = null
+ try {
+ const detail = await reopenWarehouseTransferMeasurement(this.selected.transferNo)
+ uni.removeStorageSync(this.cartonDraftKey()); this.selected = detail; this.restoreCartons()
+ this.cartonBoxes.forEach(current => { current.confirmedSignature = current.cartonMark === mark ? '' : this.cartonConfirmationSignature(current) })
+ this.pendingKey = ''; this.saveCartonDraft()
+ } catch (error) { this.error = error } finally { this.countingCartons = false }
+ },
useReceivingPallet(pallet) {
if (this.cartonsCounted || this.countingCartons || !pallet.generated) return;
this.receivingPalletId = pallet.draftId; this.batchPalletId = pallet.draftId; this.cartonPalletFilter = pallet.draftId;
@@ -228,15 +267,17 @@ export default {
if (!target || target.received === false || !this.validCartonValues(source)) return;
this.copyCartonValues(source, target); this.saveCartonDraft();
},
- saveCartonDraft() { if (this.selected && !this.cartonsCounted) uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature: this.cartonSignature }) },
+ 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 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))) {
- this.cartonBoxes = draft.boxes; this.countDiscrepancyReason = draft.discrepancyReason || ''; this.cartonRequestKey = draft.key || ''; this.cartonSignature = draft.signature || '';
+ const knownMarks = new Set(draft.forecastMarks || draft.boxes.map(box => box.cartonMark));
+ this.cartonBoxes = [...draft.boxes, ...rows.filter(box => !knownMarks.has(box.cartonMark))]; this.countDiscrepancyReason = draft.discrepancyReason || ''; this.cartonRequestKey = draft.key || ''; this.cartonSignature = draft.signature || '';
} else this.cartonBoxes = rows;
+ this.cartonBoxes.forEach(box => { if (this.cartonsCounted || (box.status === 'COUNTED' && this.validCartonValues(box))) box.confirmedSignature = this.cartonConfirmationSignature(box) });
this.receivingPalletId = this.generatedPallets[0]?.draftId || ''; this.cartonPalletFilter = ''; this.batchPalletId = this.receivingPalletId; this.batchCountOpen = false; this.batchSelectedIndexes = []; this.batchCountMessage = ''; this.batchSku = this.batchSkuCodes[0] || ''; this.batchBox = newBatchBox(); this.batchBox.items[0].skuCode = this.batchSku;
},
async printCartonLabels() {
@@ -273,10 +314,10 @@ export default {
this.countingCartons = true; this.error = null;
try {
const boxes = this.receivedCartonBoxes.map(box => ({ cartonMark: box.cartonMark || null, transferItemId: box.cartonMark ? box.transferItemId : this.selected.items.find(item => item.skuCode === box.items[0]?.skuCode)?.id, transferPalletId: box.transferPalletId, length: Number(box.length), width: Number(box.width), height: Number(box.height), weight: Number(box.weight), items: box.items.map(line => ({ skuCode: line.skuCode, quantity: Number(line.quantity) })) }));
- const signature = JSON.stringify({ boxes, discrepancyReason: this.countDiscrepancyReason });
+ const signature = JSON.stringify({ boxes, discrepancyReason: this.countDiscrepancyReason, forecastPackageCount: Number(this.selected.packageCount) });
if (!this.cartonRequestKey || signature !== this.cartonSignature) { this.cartonRequestKey = `transfer-count-${Date.now()}-${Math.random().toString(36).slice(2)}`; this.cartonSignature = signature }
- uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature });
- const detail = await countWarehouseTransferCartons(this.selected.transferNo, { idempotencyKey: this.cartonRequestKey, boxes, discrepancyReason: this.countDiscrepancyReason });
+ uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature });
+ const detail = await countWarehouseTransferCartons(this.selected.transferNo, { idempotencyKey: this.cartonRequestKey, boxes, discrepancyReason: this.countDiscrepancyReason, forecastPackageCount: Number(this.selected.packageCount) });
if (!detail.cartons?.length || !detail.cartons.every(box => box.countedAt)) throw new Error('清点结果未确认,请刷新核实');
uni.removeStorageSync(this.cartonDraftKey()); this.selected = detail; this.pallets = detail.pallets.map(restoredPallet); this.restoreCartons();
} catch (error) { this.error = error } finally { this.countingCartons = false }
@@ -291,8 +332,9 @@ export default {
const boxes = this.receivedCartonBoxes.filter(box => pallet.draftId && String(box.transferPalletId) === String(pallet.draftId));
return { cartonCount: boxes.length, volume: boxes.reduce((sum,box) => sum + (Number(box.length)||0)*(Number(box.width)||0)*(Number(box.height)||0)/1000000,0), weight: boxes.reduce((sum,box) => sum + (Number(box.weight)||0),0) };
},
- rawPalletVolume(pallet) { return this.palletSummary(pallet).volume },
- palletVolume(pallet) { return this.rawPalletVolume(pallet).toFixed(6) },
+ // 逐托汇总后取三位,与服务端落库精度一致。
+ rawPalletVolume(pallet) { return Number(this.palletSummary(pallet).volume.toFixed(3)) },
+ palletVolume(pallet) { return this.rawPalletVolume(pallet).toFixed(3) },
cartonPalletLabel(box) { return this.generatedPallets.find(pallet => String(pallet.draftId) === String(box.transferPalletId))?.palletNo || '请选择托盘' },
assignCartonPallet(box, index) { if (!this.cartonsCounted && !this.countingCartons && box.received !== false) box.transferPalletId = this.generatedPallets[index]?.draftId || '' },
applyPallet(target, value) { Object.assign(target, restoredPallet(value, this.pallets.indexOf(target)), { key: target.key, busy: false, collapsed: false }) },
@@ -318,6 +360,16 @@ export default {
catch (error) { this.error = error; pallet.busy = false }
},
async printDraftPallet(pallet) { if (!pallet.labelDownloadPath) { this.error = new Error('服务端未返回托盘标签下载地址'); return }; await this.openLabelPdf(() => downloadWarehouseTransferPalletLabel(pallet.labelDownloadPath)) },
+ // 刷新本单时合并预报新增箱号,保留本机已填写的测量草稿。
+ async refreshCurrentTransfer() {
+ if (!this.selected || this.loading || this.submitting || this.countingCartons || this.pallets.some(pallet => pallet.busy)) return
+ this.saveCartonDraft(); this.loading = true; this.error = null
+ try {
+ const detail = await getWarehouseTransferDetail(this.selected.transferNo)
+ this.selected = detail; this.pallets = detail.pallets?.length ? detail.pallets.map(restoredPallet) : [newPallet()]
+ this.restoreCartons(); this.pendingKey = ''; await this.restorePhotos(detail.transferNo, detail.measurementPhotos)
+ } catch (error) { this.error = error } finally { this.loading = false }
+ },
async loadCandidates() {
this.loading = true; this.error = null
try {
@@ -352,13 +404,36 @@ export default {
this.candidates = this.candidates.map((candidate) => String(candidate.transferNo) === String(item.transferNo) ? { ...candidate, status: 'PARTIALLY_RECEIVED' } : candidate)
if (this.entryMode === 'list') { uni.navigateTo({ url: `/pages/warehouse-transfer/warehouse-transfer?mode=work&task=${encodeURIComponent(String(item.transferNo))}` }); return }
this.detailLoading = true; this.error = null; this.success = false; this.completedPallets = []; this.completedTransferNo = ''; this.completedDestination = ''; this.completedDestinationType = ''
- try { this.selected = await getWarehouseTransferDetail(item.transferNo); this.pendingKey = ''; this.pallets = this.selected.pallets?.length ? this.selected.pallets.map(restoredPallet) : [newPallet()]; this.restoreCartons(); await this.restorePhotos(item.transferNo, this.selected.measurementPhotos) }
+ try { this.selected = await getWarehouseTransferDetail(item.transferNo); this.summaryCollapsed = false; this.pendingKey = ''; this.pallets = this.selected.pallets?.length ? this.selected.pallets.map(restoredPallet) : [newPallet()]; this.restoreCartons(); await this.restorePhotos(item.transferNo, this.selected.measurementPhotos) }
catch (e) { this.selected = null; this.error = e }
finally { this.detailLoading = false }
},
backToCandidates() { if (this.submitting || this.detailLoading || this.countingCartons || this.pallets.some(pallet => pallet.busy)) return; this.entryMode = 'list'; this.selected = null; this.error = null; this.pallets = [newPallet()]; this.photoFileIds = []; this.photoUrls = {}; this.photoNames = [] },
addPallet() { if (this.cartonsCounted || this.countingCartons) return; this.pendingKey = ''; this.pallets.push(newPallet()) },
- async removePallet(index) { const pallet = this.pallets[index]; if (!pallet || pallet.generated || pallet.busy) return; pallet.busy = true; try { if (pallet.draftId) await deleteWarehouseTransferPallet(this.selected.transferNo, pallet.draftId); this.pallets.splice(index, 1); this.pendingKey = '' } catch (error) { pallet.busy = false; this.error = error } },
+ async remeasure() {
+ if (!this.selected || this.countingCartons || this.submitting || this.pallets.some(pallet => pallet.busy)) return;
+ this.countingCartons = true; this.error = null;
+ try {
+ const detail = await reopenWarehouseTransferMeasurement(this.selected.transferNo);
+ uni.removeStorageSync(this.cartonDraftKey()); this.selected = detail; this.restoreCartons(); this.pendingKey = '';
+ this.cartonScrollTarget = 'transfer-boxes';
+ } catch (error) { this.error = error } finally { this.countingCartons = false }
+ },
+ async removePallet(index) {
+ const pallet = this.pallets[index];
+ if (!pallet || this.countingCartons || this.submitting || this.pallets.some(item => item.busy)) return;
+ pallet.busy = true;
+ try {
+ const answer = await new Promise((resolve, reject) => uni.showModal({ title: '删除托盘', content: '删除后保留箱子和测量数据,本托箱子需重新选择托盘并确认清点。是否继续?', success: resolve, fail: reject }));
+ if (!answer.confirm) return;
+ this.error = null;
+ if (pallet.draftId) this.selected = await deleteWarehouseTransferPallet(this.selected.transferNo, pallet.draftId);
+ this.cartonBoxes.forEach(box => { if (pallet.draftId && String(box.transferPalletId) === String(pallet.draftId)) box.transferPalletId = '' });
+ this.pallets.splice(index, 1); if (!this.pallets.length) this.pallets = [newPallet()];
+ this.receivingPalletId = this.generatedPallets[0]?.draftId || ''; this.batchPalletId = this.receivingPalletId; this.cartonPalletFilter = ''; this.activePalletKey = '';
+ this.cartonRequestKey = ''; this.cartonSignature = ''; this.pendingKey = ''; this.batchSelectedIndexes = []; this.saveCartonDraft();
+ } catch (error) { this.error = error } finally { pallet.busy = false }
+ },
async restorePhotos(transferNo, knownPhotos) {
let photos
try {
@@ -416,8 +491,23 @@ export default {
if (this.validationMessage) { this.error = new Error(this.validationMessage); return }
this.submitting = true; this.error = null
try {
- if (!this.pendingKey) this.pendingKey = `pda-transfer-${this.selected.transferNo}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
- const payload = { idempotencyKey: this.pendingKey, photoFileIds: [...this.photoFileIds] }
+ // 清点、绑托和拍照完成后,仅 SKU 件数差异要求填写原因。
+ if (this.hasSkuReceiptDifference) {
+ const answer = await new Promise((resolve, reject) => uni.showModal({
+ title: 'SKU签收数量存在差异', content: this.countDiscrepancyReason || '', editable: true,
+ placeholderText: '请填写SKU签收差异原因(必填)', confirmText: '确认完成',
+ success: resolve, fail: reject
+ }))
+ if (!answer.confirm) return
+ const reason = String(answer.content || '').trim()
+ if (!reason) { this.error = new Error('请填写差异原因后再完成测量'); return }
+ if (reason.length > 1000) { this.error = new Error('差异原因不能超过1000字'); return }
+ if (reason !== this.countDiscrepancyReason) this.pendingKey = ''
+ this.countDiscrepancyReason = reason
+ this.saveCartonDraft()
+ }
+ if (!this.pendingKey) this.pendingKey = `pda-transfer-${this.selected.transferNo}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
+ const payload = { idempotencyKey: this.pendingKey, photoFileIds: [...this.photoFileIds], discrepancyReason: this.hasSkuReceiptDifference ? this.countDiscrepancyReason : null }
const transferNo = this.selected.transferNo; const result = await palletizeWarehouseTransfer(transferNo, payload); this.completedTransferNo = transferNo; this.completedDestination = result?.fbaWarehouseCode || this.selected.fbaWarehouseCode || ''; this.completedDestinationType = result?.destinationType || this.selected.destinationType || ''; this.completedPallets = (result?.pallets || []).filter((pallet) => pallet?.palletNo); this.success = true; resetWarehouseDomain('warehouseTransfer'); this.selected = null; this.pendingKey = ''; this.pallets = [newPallet()]; this.photoFileIds = []; this.photoUrls = {}; this.photoNames = []; await this.loadCandidates()
} catch (e) { this.error = e } finally { this.submitting = false }
},
@@ -444,7 +534,11 @@ export default {
diff --git a/tests/warehouse-transfer-api.test.mjs b/tests/warehouse-transfer-api.test.mjs
index 1b062a9..a4bf222 100644
--- a/tests/warehouse-transfer-api.test.mjs
+++ b/tests/warehouse-transfer-api.test.mjs
@@ -56,8 +56,8 @@ test('warehouse transfer removes completed work from the pending list', async ()
test('warehouse transfer shows forecast and actual variance and requires counted box reconciliation', async () => {
const page = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
- for (const label of ['客户预报', '当前实收', '短收差异', '超收差异']) assert.match(page, new RegExp(label))
- assert.match(page, /cartonDifference/)
+ for (const label of ['客户预报', 'SKU实收', 'SKU签收差异']) assert.match(page, new RegExp(label))
+ assert.match(page, /hasSkuReceiptDifference/)
assert.doesNotMatch(page, /全部托盘箱数必须等于订单计划箱数/)
assert.doesNotMatch(page, /本托 SKU 箱数|itemCounts/)
})
@@ -137,7 +137,7 @@ test('warehouse transfer pallet cards prepare generated and server-bound states'
assert.match(page, /handlePalletScan/)
assert.match(page, /扫描或手输新托盘标签号/)
assert.match(page, /已绑定实物托盘/)
- assert.match(page, /!pallet\.generated.*removePallet/)
+ assert.match(page, /@tap\.stop="removePallet\(index\)">×/)
assert.match(page, /请先生成托盘号,再填写签收箱子/)
assert.match(page, /collapsed: false/)
assert.match(page, /pallet\.collapsed = !pallet\.collapsed/)
@@ -149,5 +149,5 @@ test('warehouse transfer pallet cards prepare generated and server-bound states'
assert.match(page, /bindWarehouseTransferPallet/)
assert.match(page, /deleteWarehouseTransferPallet/)
assert.match(page, /请先生成并扫描确认绑定全部托盘/)
- assert.match(page, /const payload = \{ idempotencyKey: this\.pendingKey, photoFileIds: \[\.\.\.this\.photoFileIds\] \}/)
+ assert.match(page, /const payload = \{ idempotencyKey: this\.pendingKey, photoFileIds: \[\.\.\.this\.photoFileIds\], discrepancyReason:/)
})
diff --git a/tests/warehouse-transfer-counting.test.mjs b/tests/warehouse-transfer-counting.test.mjs
index 1bda1bf..21fb8f3 100644
--- a/tests/warehouse-transfer-counting.test.mjs
+++ b/tests/warehouse-transfer-counting.test.mjs
@@ -11,18 +11,63 @@ function setup() {
const value = scope.page.data()
for(const [key,method] of Object.entries(scope.page.methods)) value[key]=method.bind(value)
for(const [key,get] of Object.entries(scope.page.computed)) Object.defineProperty(value,key,{get:()=>get.call(value)})
- value.selected={transferNo:'WT1',packageCount:1,status:'WAIT_PDA_RECEIVING',items:[{id:'I1',skuCode:'A'}],cartons:[{cartonMark:'BOX1',transferItemId:'I1',status:'EXPECTED'}]}
+ value.selected={transferNo:'WT1',packageCount:1,status:'WAIT_PDA_RECEIVING',items:[{id:'I1',skuCode:'A',quantity:4}],cartons:[{cartonMark:'BOX1',transferItemId:'I1',status:'EXPECTED'}]}
value.pallets=[{key:'P1',draftId:'P1',palletNo:'PALLET1',generated:true,bound:false}];value.restoreCartons();Object.assign(value.cartonBoxes[0],{length:10,width:20,height:30,weight:2});value.cartonBoxes[0].items[0].quantity=4
return {scope,value,storage}
}
+test('summary toggle preserves measurements and volume uses three decimals',()=>{
+ const {value}=setup()
+ assert.equal(value.summaryCollapsed,false)
+ value.summaryCollapsed=true
+ assert.equal(value.cartonBoxes[0].weight,2)
+ Object.assign(value.cartonBoxes[0],{length:10,width:10,height:6})
+ value.addExtraCarton()
+ Object.assign(value.cartonBoxes[1],{length:10,width:10,height:6,weight:2})
+ assert.equal(value.palletVolume(value.pallets[0]),'0.001')
+ assert.equal(value.totalVolume,'0.001')
+ assert.match(source,/aria-label="添加 SKU"/)
+ assert.match(source,/aria-label="删除箱子"/)
+})
+
+test('remeasure preserves saved measurements and unlocks carton editing',async()=>{
+ const {value,scope}=setup();
+ const box={...value.cartonBoxes[0],countedAt:'2026-10-16',status:'COUNTED'};
+ value.selected.cartons=[box]; value.cartonRequestKey='old-key';
+ scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:[{...box,countedAt:null}]});
+ await value.remeasure();
+ assert.equal(value.cartonsCounted,false); assert.equal(value.cartonBoxes[0].weight,2);
+ assert.equal(value.cartonBoxes[0].transferPalletId,'P1'); assert.equal(value.cartonRequestKey,'');
+ assert.equal(value.cartonCountReady,true);
+})
+
+test('bound pallet deletion retains box values and requires reassignment',async()=>{
+ const {value,scope}=setup();
+ scope.uni.showModal=options=>options.success({confirm:true});
+ value.pallets[0].bound=true;
+ scope.deleteWarehouseTransferPallet=async()=>({...value.selected,pallets:[]});
+ await value.removePallet(0);
+ assert.equal(value.cartonBoxes[0].transferPalletId,''); assert.equal(value.cartonBoxes[0].weight,2);
+ assert.equal(value.generatedPallets.length,0); assert.equal(value.pallets.length,1);
+ assert.equal(value.cartonCountReady,false); assert.equal(value.cartonPalletFilter,'');
+})
+
+test('failed pallet deletion retains binding and measurements',async()=>{
+ const {value,scope}=setup();
+ scope.uni.showModal=options=>options.success({confirm:true});
+ scope.deleteWarehouseTransferPallet=async()=>{throw new Error('offline')};
+ await value.removePallet(0);
+ assert.equal(value.cartonBoxes[0].transferPalletId,'P1'); assert.equal(value.pallets[0].busy,false);
+ assert.equal(value.error.message,'offline');
+})
+
test('transfer counts require valid pieces and measurements and record variance reason',()=>{
const {value}=setup();assert.equal(value.cartonCountReady,true)
value.cartonBoxes[0].items[0].quantity=0;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].items[0].quantity=1.5;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].items[0].quantity=4;value.cartonBoxes[0].height=0;assert.equal(value.cartonCountReady,false)
value.cartonBoxes[0].height=30;value.addExtraCarton();Object.assign(value.cartonBoxes[1],{length:10,width:20,height:30,weight:2});value.cartonBoxes[1].items[0].quantity=5
- assert.equal(value.cartonCountReady,false);value.countDiscrepancyReason='多一箱';assert.equal(value.cartonCountReady,true)
- assert.equal(value.cartonQuantity,9);assert.equal(value.cartonVolume,'0.012000')
+ assert.equal(value.cartonCountReady,true);value.countDiscrepancyReason='多一箱';assert.equal(value.cartonCountReady,true)
+ assert.equal(value.cartonQuantity,9);assert.equal(value.cartonVolume,'0.012')
})
test('network retry preserves request key and changed content uses a new request',async()=>{
const {value,scope}=setup();const requests=[]
@@ -32,11 +77,9 @@ test('network retry preserves request key and changed content uses a new request
value.cartonBoxes[0].items[0].quantity=8;await value.confirmCartons();assert.notEqual(requests[1].idempotencyKey,requests[2].idempotencyKey)
assert.equal(value.countingCartons,false);assert.equal(value.error.message,'offline')
})
-test('draft restores additional cartons and discrepancy reason and scanning rejects foreign box',()=>{
+test('draft restores additional cartons and discrepancy reason without crossing tasks',()=>{
const {value}=setup();value.addExtraCarton();value.countDiscrepancyReason='超收';value.saveCartonDraft();value.restoreCartons()
assert.equal(value.cartonBoxes.length,2);assert.equal(value.countDiscrepancyReason,'超收')
- value.locateCarton('FOREIGN');assert.match(value.error.message,/不属于/)
- value.locateCarton('BOX1');assert.equal(value.selectedCartonMark,'BOX1');assert.equal(value.cartonScrollTarget,'transfer-box-0')
value.selected={...value.selected,transferNo:'WT2'};value.restoreCartons();assert.equal(value.cartonBoxes.length,1)
})
test('received count gates pallet completion and saving cannot switch active order',()=>{
@@ -47,6 +90,7 @@ test('received count gates pallet completion and saving cannot switch active ord
test('batch applies only selected received boxes of one SKU and preserves independent drafts',()=>{
const {value}=setup()
+ value.selected.items.push({id:'I2',skuCode:'B'})
value.addExtraCarton();value.addExtraCarton();value.addExtraCarton()
value.cartonBoxes[2].items[0].skuCode='B'
value.cartonBoxes[3].received=false
@@ -127,3 +171,83 @@ test('pallet number can be created before measurements and new boxes receive tha
assert.equal(value.cartonBoxes[0].transferPalletId,'CREATED')
assert.equal(value.pallets[0].palletNo,'NEWPALLET')
})
+
+// 最终完成入口需要区分无差异、取消、空白原因和网络重试。
+for (const answer of [{confirm:false}, {confirm:true,content:' '}, {confirm:true,content:'少到一箱'}]) {
+ test('completion handles discrepancy answer '+JSON.stringify(answer),async()=>{
+ const {value,scope}=setup();let requests=[];let prompts=0
+ value.selected.items[0].quantity=5;value.selected.cartons[0].countedAt='2026-10-16'
+ value.pallets[0].bound=true;value.photoFileIds=[91]
+ scope.uni.showModal=options=>{prompts++;options.success(answer)}
+ scope.palletizeWarehouseTransfer=async(no,payload)=>{requests.push(payload);throw new Error('offline')}
+ await value.submit()
+ assert.equal(prompts,1);assert.equal(value.submitting,false)
+ assert.equal(requests.length,answer.confirm && answer.content?.trim()?1:0)
+ if(requests.length){assert.equal(requests[0].discrepancyReason,'少到一箱');await value.submit();assert.equal(requests[0].idempotencyKey,requests[1].idempotencyKey)}
+ })
+}
+test('matching SKU counts with different carton count submit without prompting, and missing photos block before prompting',async()=>{
+ const {value,scope}=setup();let prompts=0;let requests=[]
+ value.selected.packageCount=9;value.selected.cartons[0].countedAt='2026-10-16';value.pallets[0].bound=true
+ scope.uni.showModal=()=>{prompts++}
+ scope.palletizeWarehouseTransfer=async(no,payload)=>{requests.push(payload);throw new Error('offline')}
+ await value.submit();assert.equal(requests.length,0);assert.equal(prompts,0)
+ value.photoFileIds=[91];await value.submit();assert.equal(prompts,0);assert.equal(requests.length,1);assert.equal(requests[0].discrepancyReason,null)
+})
+
+// 逐箱确认收起、再次编辑及草稿恢复必须保留其他箱的确认状态。
+test('individual confirmation collapses only that box and resumes from draft',async()=>{
+ const {value,scope}=setup();value.addExtraCarton()
+ Object.assign(value.cartonBoxes[1],{length:10,width:20,height:30,weight:2});value.cartonBoxes[1].items[0].quantity=5
+ let saves=0;scope.countWarehouseTransferCartons=async()=>{saves++;throw new Error('offline')}
+ await value.confirmSingleCarton(value.cartonBoxes[0])
+ assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true);assert.equal(value.allCartonsConfirmed,false);assert.equal(saves,0)
+ value.restoreCartons();assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true)
+ await value.editCarton(value.cartonBoxes[0]);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false)
+ await value.confirmSingleCarton(value.cartonBoxes[0]);await value.confirmSingleCarton(value.cartonBoxes[1])
+ assert.equal(value.allCartonsConfirmed,true);assert.equal(saves,1);assert.equal(value.error.message,'offline')
+ value.cartonBoxes[0].weight=3;assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false);assert.equal(value.allCartonsConfirmed,false)
+})
+test('saved boxes restore collapsed and editing reopens only selected box',async()=>{
+ const {value,scope}=setup()
+ const first={...value.cartonBoxes[0],quantity:4,countedAt:'2026-10-16',status:'COUNTED'}
+ const second={...first,cartonMark:'BOX2'}
+ value.selected.cartons=[first,second];value.restoreCartons()
+ assert.equal(value.cartonBoxes.every(box=>value.cartonConfirmed(box)),true)
+ scope.reopenWarehouseTransferMeasurement=async()=>({...value.selected,cartons:value.selected.cartons.map(box=>({...box,countedAt:null}))})
+ await value.editCarton(value.cartonBoxes[0])
+ assert.equal(value.cartonsCounted,false);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false);assert.equal(value.cartonConfirmed(value.cartonBoxes[1]),true)
+ value.restoreCartons();assert.equal(value.cartonConfirmed(value.cartonBoxes[1]),true)
+})
+test('failed reopening preserves confirmed data and invalid box cannot confirm',async()=>{
+ const {value,scope}=setup();value.cartonBoxes[0].weight=0
+ await value.confirmSingleCarton(value.cartonBoxes[0]);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),false)
+ value.cartonBoxes[0].weight=2;value.selected.cartons=[{...value.cartonBoxes[0],countedAt:'2026-10-16',status:'COUNTED'}];value.restoreCartons()
+ scope.reopenWarehouseTransferMeasurement=async()=>{throw new Error('offline')}
+ await value.editCarton(value.cartonBoxes[0]);assert.equal(value.cartonsCounted,true);assert.equal(value.cartonConfirmed(value.cartonBoxes[0]),true)
+})
+
+// 箱数变化不影响 SKU 门禁,异 SKU 正负差异不能按总件数抵消。
+test('SKU differences compare each SKU and ignore carton count',()=>{
+ const {value}=setup();value.selected.packageCount=9
+ assert.equal(value.hasSkuReceiptDifference,false)
+ value.selected.items.push({id:'I2',skuCode:'B',quantity:3})
+ value.cartonBoxes[0].items=[{skuCode:'A',quantity:3},{skuCode:'B',quantity:4}]
+ assert.equal(value.cartonQuantity,value.forecastSkuQuantity)
+ assert.equal(value.skuReceiptDifferences.length,2)
+ value.cartonBoxes[0].items=[{skuCode:'A',quantity:4},{skuCode:'B',quantity:3}]
+ assert.equal(value.hasSkuReceiptDifference,false)
+})
+
+// 客户补箱后,新增预报箱进入PDA,原本机草稿和删除标记不丢失。
+test('forecast additions merge into existing draft without losing edited boxes',()=>{
+ const {value}=setup();value.cartonBoxes[0].weight=7;value.saveCartonDraft()
+ value.selected.packageCount=2;value.selected.cartons.push({cartonMark:'BOX2',transferItemId:'I1',status:'EXPECTED'})
+ value.restoreCartons();assert.equal(value.cartonBoxes.length,2);assert.equal(value.cartonBoxes[0].weight,7);assert.equal(value.cartonBoxes[1].cartonMark,'BOX2')
+ value.removeCarton(0);value.restoreCartons();assert.equal(value.cartonBoxes.length,1);assert.equal(value.cartonBoxes[0].cartonMark,'BOX2')
+})
+test('count payload carries forecast version for stale snapshot rejection',async()=>{
+ const {value,scope}=setup();let payload
+ 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')
+})
diff --git a/tests/warehouse-transfer-mixed-cartons.test.mjs b/tests/warehouse-transfer-mixed-cartons.test.mjs
index e625e00..bdf13d4 100644
--- a/tests/warehouse-transfer-mixed-cartons.test.mjs
+++ b/tests/warehouse-transfer-mixed-cartons.test.mjs
@@ -58,14 +58,14 @@ test('delete only changes actual draft and preserves forecast; additions keep th
test('draft reload preserves deletions, including an empty actual list', () => {
const ctx = context()
ctx.selected.cartons = [{ cartonMark: 'BOX1', transferItemId: '1', status: 'EXPECTED' }, { cartonMark: 'BOX2', transferItemId: '2', status: 'EXPECTED' }]
- let saved = { boxes: [{ ...box('B'), cartonMark: 'BOX2', transferItemId: '2' }], discrepancyReason: '少到一箱' }
+ let saved = { forecastMarks: ['BOX1','BOX2'], boxes: [{ ...box('B'), cartonMark: 'BOX2', transferItemId: '2' }], discrepancyReason: '少到一箱' }
globalThis.uni = { getStorageSync: () => saved }
try {
ctx.restoreCartons()
assert.equal(ctx.cartonBoxes.length, 1)
assert.equal(ctx.cartonBoxes[0].cartonMark, 'BOX2')
assert.equal(ctx.countDiscrepancyReason, '少到一箱')
- saved = { boxes: [], discrepancyReason: '重新录入' }
+ saved = { forecastMarks: ['BOX1','BOX2'], boxes: [], discrepancyReason: '重新录入' }
ctx.restoreCartons()
assert.equal(ctx.cartonBoxes.length, 0)
assert.equal(ctx.selected.cartons.length, 2)
@@ -93,7 +93,7 @@ test('100 same SKU cartons accept two distinct specification groups and independ
assert.equal(ctx.palletSummary(ctx.pallets[1]).cartonCount, 40)
assert.equal(ctx.cartonQuantity, 1040)
assert.equal(ctx.totalWeight, '580.00')
- assert.equal(ctx.totalVolume, '3.840000')
+ assert.equal(ctx.totalVolume, '3.840')
assert.equal(ctx.cartonCountReady, true)
ctx.cartonBoxes[0].items[0].quantity = 11
ctx.cartonBoxes[0].weight = 6
From d4225ff5a1c98bb5394143be2fa934099268e1fd Mon Sep 17 00:00:00 2001
From: liuyihe <2513762445@qq.com>
Date: Fri, 16 Oct 2026 16:53:09 +0800
Subject: [PATCH 5/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
AGENTS.md | 2 +-
src/pages/warehouse-transfer/warehouse-transfer.vue | 6 +++---
tests/warehouse-transfer-counting.test.mjs | 10 ++++++++++
tests/warehouse-transfer-mixed-cartons.test.mjs | 10 ++++++++++
4 files changed, 24 insertions(+), 4 deletions(-)
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\)"/)
+})
From f8533772b754e882dbf9148aeb122ac24e245eca Mon Sep 17 00:00:00 2001
From: liuyihe <2513762445@qq.com>
Date: Fri, 16 Oct 2026 17:43:48 +0800
Subject: [PATCH 6/8] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../warehouse-transfer/warehouse-transfer.vue | 60 +++++++++++++++----
tests/warehouse-transfer-counting.test.mjs | 44 +++++++++++++-
.../warehouse-transfer-mixed-cartons.test.mjs | 6 +-
3 files changed, 93 insertions(+), 17 deletions(-)
diff --git a/src/pages/warehouse-transfer/warehouse-transfer.vue b/src/pages/warehouse-transfer/warehouse-transfer.vue
index 522f225..f76f5bd 100644
--- a/src/pages/warehouse-transfer/warehouse-transfer.vue
+++ b/src/pages/warehouse-transfer/warehouse-transfer.vue
@@ -90,6 +90,7 @@
+
@@ -138,8 +139,17 @@ const restoredPallet = (value, index) => ({
export default {
components: { PortalHeader, ScanTargetInput, CartonCountFields },
- data() { return { summaryCollapsed: false, receivingPalletId: '', cartonPalletFilter: '', batchPalletId: '', batchCountOpen: false, batchSku: '', batchBox: newBatchBox(), batchSelectedIndexes: [], batchCountMessage: '', cartonScrollTarget: '', cartonBoxes: [], countDiscrepancyReason: '', countingCartons: false, cartonRequestKey: '', cartonSignature: '', user: session.currentUser, entryMode: 'work', pendingTaskNo: '', candidates: [], selected: null, pallets: [newPallet()], activePalletKey: '', photoFileIds: [], photoNames: [], photoUrls: {}, completedPallets: [], completedTransferNo: '', completedDestination: '', completedDestinationType: '', loading: false, detailLoading: false, submitting: false, uploadingPhoto: false, openingLabel: false, error: null, success: false, pendingKey: '' } },
+ data() { return { deletedCartonBoxes: [], deletingCarton: false, summaryCollapsed: false, receivingPalletId: '', cartonPalletFilter: '', batchPalletId: '', batchCountOpen: false, batchSku: '', batchBox: newBatchBox(), batchSelectedIndexes: [], batchCountMessage: '', cartonScrollTarget: '', cartonBoxes: [], countDiscrepancyReason: '', countingCartons: false, cartonRequestKey: '', cartonSignature: '', user: session.currentUser, entryMode: 'work', pendingTaskNo: '', candidates: [], selected: null, pallets: [newPallet()], activePalletKey: '', photoFileIds: [], photoNames: [], photoUrls: {}, completedPallets: [], completedTransferNo: '', completedDestination: '', completedDestinationType: '', loading: false, detailLoading: false, submitting: false, uploadingPhoto: false, openingLabel: false, error: null, success: false, pendingKey: '' } },
computed: {
+ restorableCartons() {
+ const current = new Set(this.cartonBoxes.map(box => box.cartonMark || box.localKey));
+ const removed = new Map();
+ for (const box of [...(this.selected?.cartons || []), ...this.deletedCartonBoxes]) {
+ const key = box.cartonMark || box.localKey;
+ if (key && !current.has(key)) removed.set(key, box);
+ }
+ return [...removed.values()];
+ },
generatedPallets() { return this.pallets.filter(pallet => pallet.generated && pallet.draftId) },
declaredSkuCodes() { return [...new Set((this.selected?.items || []).map(item => item.skuCode))] },
batchSkuCodes() { return [...new Set(this.receivedCartonBoxes.map(box => box.items[0]?.skuCode).filter(Boolean))] },
@@ -186,7 +196,7 @@ export default {
},
watch: {
countDiscrepancyReason() { this.saveCartonDraft() },
- cartonBoxes: { deep: true, handler() { 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 }) } },
+ cartonBoxes: { deep: true, handler() { if (this.selected && !this.cartonsCounted) uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, deletedBoxes: this.deletedCartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature: this.cartonSignature }) } },
pallets: { deep: true, handler() { if (!this.submitting) this.pendingKey = '' } }
},
onShow() { if (requireSession()) { this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.startScanner(); this.loadCandidates().then(() => { const activeNo = String(warehouseWork.warehouseTransfer.currentNo || ''); const taskNo = this.pendingTaskNo || activeNo; if (this.entryMode === 'work' && taskNo) { const active = this.candidates.find((item) => String(item.transferNo) === taskNo); if (active) { const fromExplicitTask = Boolean(this.pendingTaskNo); this.pendingTaskNo = ''; void this.select(active, fromExplicitTask) } } }) } },
@@ -214,7 +224,7 @@ export default {
this.countingCartons = true; this.error = null
try {
const detail = await reopenWarehouseTransferMeasurement(this.selected.transferNo)
- uni.removeStorageSync(this.cartonDraftKey()); this.selected = detail; this.restoreCartons()
+ uni.setStorageSync(this.cartonDraftKey(), { deletedBoxes: this.deletedCartonBoxes }); this.selected = detail; this.restoreCartons()
this.cartonBoxes.forEach(current => { current.confirmedSignature = current.cartonMark === mark ? '' : this.cartonConfirmationSignature(current) })
this.pendingKey = ''; this.saveCartonDraft()
} catch (error) { this.error = error } finally { this.countingCartons = false }
@@ -267,13 +277,14 @@ export default {
if (!target || target.received === false || !this.validCartonValues(source)) return;
this.copyCartonValues(source, target); this.saveCartonDraft();
},
- 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 }) },
+ saveCartonDraft() { if (this.selected && !this.cartonsCounted) uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, deletedBoxes: this.deletedCartonBoxes, 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 || this.declaredSkuCodes[0] || '', quantity: box.quantity ?? '' }] }));
const draft = uni.getStorageSync(this.cartonDraftKey());
+ this.deletedCartonBoxes = Array.isArray(draft?.deletedBoxes) ? draft.deletedBoxes.filter(box => !box.cartonMark || (this.selected?.cartons || []).some(row => row.cartonMark === box.cartonMark)) : [];
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))) {
+ 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 || (this.selected?.cartons || []).some(row => row.cartonMark === box.cartonMark))) {
const knownMarks = new Set(draft.forecastMarks || draft.boxes.map(box => box.cartonMark));
this.cartonBoxes = [...draft.boxes, ...rows.filter(box => !knownMarks.has(box.cartonMark))]; this.countDiscrepancyReason = draft.discrepancyReason || ''; this.cartonRequestKey = draft.key || ''; this.cartonSignature = draft.signature || '';
} else this.cartonBoxes = rows;
@@ -291,9 +302,36 @@ export default {
this.error = new Error('当前设备未配置箱标签打印服务');
// #endif
},
- removeCarton(index) {
- if (this.cartonsCounted || this.countingCartons) return;
- this.cartonBoxes.splice(index, 1); this.batchSelectedIndexes = []; this.batchCountMessage = ''; this.saveCartonDraft();
+ async removeCarton(index) {
+ if (this.cartonsCounted || this.countingCartons || this.submitting || this.deletingCarton) return;
+ const box = this.cartonBoxes[index], transferNo = this.selected?.transferNo;
+ if (!box) return;
+ this.deletingCarton = true;
+ try {
+ const answer = await new Promise((resolve, reject) => uni.showModal({ title: '确认删除箱子', content: `确定删除${box.cartonMark || `第 ${index + 1} 箱`}吗?删除后本箱不计入本次实收,保存清点时会记为未收到。误删可通过“恢复已删除箱子”找回。`, confirmText: '确认删除', confirmColor: '#b74545', cancelText: '取消', success: resolve, fail: reject }));
+ if (!answer.confirm || transferNo !== this.selected?.transferNo || this.cartonsCounted || this.countingCartons || this.submitting) return;
+ const position = this.cartonBoxes.indexOf(box);
+ if (position < 0) return;
+ if (!box.cartonMark && !box.localKey) box.localKey = `removed-${Date.now()}-${Math.random()}`;
+ this.deletedCartonBoxes.push(box); this.cartonBoxes.splice(position, 1);
+ this.batchSelectedIndexes = []; this.batchCountMessage = ''; this.saveCartonDraft();
+ } catch (error) { this.error = error } finally { this.deletingCarton = false }
+ },
+ async restoreDeletedCarton() {
+ if (this.countingCartons || this.submitting || this.deletingCarton || !this.restorableCartons.length) return;
+ const candidates = this.restorableCartons, transferNo = this.selected?.transferNo;
+ try {
+ const choice = candidates.length === 1 ? 0 : await new Promise((resolve, reject) => uni.showActionSheet({ itemList: candidates.map((box, index) => box.cartonMark || `已删除实收箱 ${index + 1}`), success: result => resolve(result.tapIndex), fail: () => resolve(-1) }));
+ if (choice < 0 || transferNo !== this.selected?.transferNo) return;
+ const removed = candidates[choice], key = removed.cartonMark || removed.localKey;
+ if (this.cartonsCounted) { await this.remeasure(); if (this.cartonsCounted || transferNo !== this.selected?.transferNo) return }
+ if (this.cartonBoxes.some(box => (box.cartonMark || box.localKey) === key)) return;
+ const box = { ...removed, received: true, status: 'EXPECTED', countedAt: null, confirmedSignature: '',
+ items: removed.items?.length ? removed.items.map(line => ({ ...line })) : [{ skuCode: this.selected.items.find(item => String(item.id) === String(removed.transferItemId))?.skuCode || this.declaredSkuCodes[0] || '', quantity: removed.quantity ?? '' }] };
+ if (!this.generatedPallets.some(pallet => String(pallet.draftId) === String(box.transferPalletId))) box.transferPalletId = this.receivingPalletId || this.generatedPallets[0]?.draftId || '';
+ this.deletedCartonBoxes = this.deletedCartonBoxes.filter(item => (item.cartonMark || item.localKey) !== key);
+ this.cartonBoxes.push(box); this.cartonPalletFilter = ''; this.cartonRequestKey = ''; this.cartonSignature = ''; this.saveCartonDraft();
+ } catch (error) { this.error = error }
},
addExtraCarton() {
if (this.cartonsCounted || this.countingCartons || !this.selected?.items?.length) return;
@@ -316,10 +354,10 @@ export default {
const boxes = this.receivedCartonBoxes.map(box => ({ cartonMark: box.cartonMark || null, transferItemId: box.cartonMark ? box.transferItemId : this.selected.items.find(item => item.skuCode === box.items[0]?.skuCode)?.id, transferPalletId: box.transferPalletId, length: Number(box.length), width: Number(box.width), height: Number(box.height), weight: Number(box.weight), items: box.items.map(line => ({ skuCode: line.skuCode, quantity: Number(line.quantity) })) }));
const signature = JSON.stringify({ boxes, discrepancyReason: this.countDiscrepancyReason, forecastPackageCount: Number(this.selected.packageCount) });
if (!this.cartonRequestKey || signature !== this.cartonSignature) { this.cartonRequestKey = `transfer-count-${Date.now()}-${Math.random().toString(36).slice(2)}`; this.cartonSignature = signature }
- uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature });
+ uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, deletedBoxes: this.deletedCartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason, key: this.cartonRequestKey, signature });
const detail = await countWarehouseTransferCartons(this.selected.transferNo, { idempotencyKey: this.cartonRequestKey, boxes, discrepancyReason: this.countDiscrepancyReason, forecastPackageCount: Number(this.selected.packageCount) });
if (!detail.cartons?.length || !detail.cartons.every(box => box.countedAt)) throw new Error('清点结果未确认,请刷新核实');
- uni.removeStorageSync(this.cartonDraftKey()); this.selected = detail; this.pallets = detail.pallets.map(restoredPallet); this.restoreCartons();
+ uni.setStorageSync(this.cartonDraftKey(), { deletedBoxes: this.deletedCartonBoxes }); this.selected = detail; this.pallets = detail.pallets.map(restoredPallet); this.restoreCartons();
} catch (error) { this.error = error } finally { this.countingCartons = false }
},
openPalletOperations() { uni.navigateTo({ url: '/pages/pallet/pallet' }) },
@@ -415,7 +453,7 @@ export default {
this.countingCartons = true; this.error = null;
try {
const detail = await reopenWarehouseTransferMeasurement(this.selected.transferNo);
- uni.removeStorageSync(this.cartonDraftKey()); this.selected = detail; this.restoreCartons(); this.pendingKey = '';
+ uni.setStorageSync(this.cartonDraftKey(), { deletedBoxes: this.deletedCartonBoxes }); this.selected = detail; this.restoreCartons(); this.pendingKey = '';
this.cartonScrollTarget = 'transfer-boxes';
} catch (error) { this.error = error } finally { this.countingCartons = false }
},
diff --git a/tests/warehouse-transfer-counting.test.mjs b/tests/warehouse-transfer-counting.test.mjs
index d69202d..8046f3d 100644
--- a/tests/warehouse-transfer-counting.test.mjs
+++ b/tests/warehouse-transfer-counting.test.mjs
@@ -6,7 +6,7 @@ const source = readFileSync(new URL('../src/pages/warehouse-transfer/warehouse-t
const script = source.match(/