diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index e488686..f050848 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -76,6 +76,10 @@ export function updateTransfer(transferNo: string, payload: TransferCreate) { export function submitTransfer(transferNo: string) { return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/submit` }) } +/** 补充既有SKU的预报箱数,原箱数快照用于并发核对与安全重试。 */ +export function increaseTransferForecastCartons(transferNo: string, payload: { expectedPackageCount: number; transferItemId: string; expectedItemPackageCount: number; packageCount: number }) { + return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/increase-forecast-cartons`, data: payload }) +} export function listRelabels(params: Record) { return apiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications', params }) } diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 21cccf8..15cea5b 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -11,7 +11,7 @@ import CompletionConfirmationDialog from '../components/CompletionConfirmationDi import { returnRelabelCandidate } from '../utils/returnRelabelCandidates' import { createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel, - getTransferCartonLabel, submitTransfer, updateTransfer, getTransferSourceCartons, type TransferSourceCarton, + getTransferCartonLabel, submitTransfer, updateTransfer, increaseTransferForecastCartons, getTransferSourceCartons, type TransferSourceCarton, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, uploadRelabelFile, listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, @@ -148,6 +148,34 @@ interface ServiceDetail extends Record { cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string } const selectedDetail = ref(null) +// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。 +const showForecastCartons = ref(false) +const increaseForecastOpen = ref(false) +const increasingForecast = ref(false) +const forecastSnapshot = ref(null) +const increaseForecastForm = reactive({ itemId: '', additionalCount: 1 }) +const canIncreaseForecast = computed(() => canCreate.value && selectedDetail.value?.transferNo === selectedDetail.value?.inboundCode && ['PENDING_REVIEW', 'WAIT_PDA_RECEIVING'].includes(selectedDetail.value?.status || '')) +/** 保存原预报快照,防止弹窗打开后其他设备的修改被覆盖。 */ +function openIncreaseForecast() { + if (!canIncreaseForecast.value || !selectedDetail.value) return + forecastSnapshot.value = selectedDetail.value + increaseForecastForm.itemId = String(selectedDetail.value.items?.[0]?.id || '') + increaseForecastForm.additionalCount = 1; increaseForecastOpen.value = true +} +/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */ +async function saveIncreaseForecast() { + if (increasingForecast.value || !forecastSnapshot.value?.transferNo) return + const snapshot = forecastSnapshot.value + const item = snapshot.items?.find(row => String(row.id) === increaseForecastForm.itemId) + if (!item || !Number.isSafeInteger(increaseForecastForm.additionalCount) || increaseForecastForm.additionalCount < 1) { ElMessage.warning('请选择SKU并填写新增箱数'); return } + increasingForecast.value = true + try { + const result = await increaseTransferForecastCartons(String(snapshot.transferNo), { expectedPackageCount: Number(snapshot.packageCount), transferItemId: String(item.id), expectedItemPackageCount: Number(item.packageCount), packageCount: Number(item.packageCount) + increaseForecastForm.additionalCount }) + selectedDetail.value = result as ServiceDetail; increaseForecastOpen.value = false + ElMessage.success('预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点') + } catch (error) { ElMessage.error(error instanceof Error ? error.message : '增加预报箱数失败') } + finally { increasingForecast.value = false } +} const showTransferCartons = ref(false) function transferCartonItem(carton: TransferCarton) { return selectedDetail.value?.items?.find((item) => carton.transferItemId != null && String(item.id) === String(carton.transferItemId)) @@ -612,36 +640,6 @@ async function selectDiscardWarehouse() { } function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, expectedPackageCount: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } } function addCargoRow() { cargoRows.value.push(createCargoRow()) } -function escapePrintHtml(value: string) { - return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''') -} -function printTransferDraftCartonMarks() { - if (props.service !== 'transfer') return - const validRows = cargoRows.value.filter((row) => row.sku.trim() && Number(row.expectedPackageCount) > 0) - if (!validRows.length || validRows.some((row) => !Number.isInteger(Number(row.expectedPackageCount)))) { - ElMessage.warning('请先填写商品 SKU 和对应的整数箱数'); return - } - const totalCartons = validRows.reduce((sum, row) => sum + Number(row.expectedPackageCount), 0) - if (totalCartons > 500) { ElMessage.warning('单次最多打印 500 张箱唛'); return } - const batch = new Date().toISOString().replace(/\D/g, '').slice(2, 14) - const labels: Array<{ mark: string; sku: string; index: number }> = [] - let cartonIndex = 0 - for (const [rowIndex, row] of validRows.entries()) { - let marks = transferCustomerMarks(row) - if (marks.length !== Number(row.expectedPackageCount)) { - marks = Array.from({ length: Number(row.expectedPackageCount) }, (_, index) => `CT-${batch}-${String(rowIndex + 1).padStart(2, '0')}-${String(index + 1).padStart(3, '0')}`) - row.customerCartonMarks = marks.join('\n') - } - for (const mark of marks) labels.push({ mark, sku: row.sku.trim(), index: ++cartonIndex }) - } - const printWindow = window.open('', '_blank') - if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return } - printWindow.opener = null - const sections = labels.map((label) => `
仓储转运客户箱唛
${escapePrintHtml(label.mark)}
SKU:${escapePrintHtml(label.sku)}
${label.index} / ${labels.length}
`).join('') - printWindow.document.open() - printWindow.document.write(`仓储转运客户箱唛${sections}`) - printWindow.document.close(); printWindow.focus(); printWindow.print() -} function addTransferRelabelRow() { const source = cargoRows.value[Math.min(transferRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))] transferRelabelRows.value.push({ ...createCargoRow(), sku: source?.sku || '' }) @@ -1787,7 +1785,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
商品 SKU中文品名库存箱数原目的仓
{{ row.sku || '—' }}{{ row.name || '—' }}{{ row.expectedPackageCount }}{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}
-

1货物明细

打印唛头添加行清空
+

1货物明细

添加行清空
商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}中文品名{{ props.service === 'transfer' ? '' : ' *' }}可用箱数FNSKU / 箱唛SKU 数量(件)*{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *操作
{{ row.availableQuantity }} 箱
{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}
@@ -1833,6 +1831,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {

本次废弃箱子

入仓码SKU品名申请数量所选箱唛
{{ item.inboundCode || '—' }}{{ item.skuCode || '—' }}{{ item.productName || '—' }}{{ item.discardPackages ?? '—' }}{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}

废弃现场照片

照片加载中…
{{ formatDateTime(photo.submittedAt) }}
暂无已提交的废弃现场照片
+
+

预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)

增加预报箱数打印预报箱唛查看预报箱唛
+ + +

保留原箱唛,仅补生成新增箱唛;SKU预报件数保持不变。

+ +
+

预报与实际签收

商品 SKU中文品名预报数量(件)预计箱数实际签收件数实际签收箱数
{{ item.skuCode || '—' }}{{ item.productNameZh || '—' }}{{ item.quantity ?? '—' }}{{ item.packageCount ?? '—' }}{{ receivedItemQuantity(item.id) }}{{ receivedItemCartons(item.id).length }}
暂无货物明细
@@ -1855,7 +1861,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
测量打托现场照片
照片 {{ index + 1 }} 加载中…
仓库尚未上传测量打托照片
-

签收箱号明细({{ countedTransferCartons().length }} 箱)

打印箱唛查看箱号明细
+

签收箱号明细({{ countedTransferCartons().length }} 箱)

查看箱号明细
暂无实际签收箱,等待 PDA 清点。
diff --git a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs index c55636f..96fe8dc 100644 --- a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs @@ -102,15 +102,10 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换 assert.doesNotMatch(view, /FNSKU \/ 箱唛<\/th>/) }) -test('仓储转运货物明细可按 SKU 和箱数打印客户预贴箱唛', () => { - assert.match(view, /printTransferDraftCartonMarks/) - assert.match(view, /请先填写商品 SKU 和对应的整数箱数/) - assert.match(view, /Array\.from\(\{ length: Number\(row\.expectedPackageCount\) \}/) - assert.match(view, /row\.customerCartonMarks = marks\.join\('\\n'\)/) - assert.match(view, /仓储转运客户箱唛/) - assert.match(view, /@click="printTransferDraftCartonMarks"/) - assert.match(view, /打印唛头/) - assert.match(view, /单次最多打印 500 张箱唛/) +test('仓储转运提交后统一打印服务端预报箱唛', () => { + assert.doesNotMatch(view, /printTransferDraftCartonMarks|CT-\$\{batch\}/) + assert.match(view, /getTransferCartonLabel\(transferNo\)/) + assert.match(view, /打印预报箱唛/) }) test('仓储转运换标资料按步骤分组展示', () => { @@ -330,7 +325,7 @@ test('仓储转运可在新建页直接提交,也可保存草稿后编辑提 assert.match(view, /await updateTransfer\(editingTransferNo\.value, payload\)/) assert.match(view, /await submitTransfer\(transferNo\)/) assert.match(view, /
]*@submit\.prevent="submit\('submit'\)"/) - assert.match(view, /selectedDetail\?\.status !== 'DRAFT'[^>]*[\s\S]*?printTransferCartons[^>]*[\s\S]*?打印箱唛/) + assert.match(view, /selectedDetail\?\.status !== 'DRAFT'[^>]*[\s\S]*?printTransferCartons[^>]*[\s\S]*?打印预报箱唛/) assert.doesNotMatch(view, /row\.__status !== 'DRAFT'.*printTransferCartons\(row\['0'\]\)/s) assert.doesNotMatch(view, /selectedDetail\?\.status === 'DRAFT'[^>]*[\s\S]*?>提交订单/) assert.match(view, /native-type="button"[^>]*:loading="saving"[^>]*@click="submit\('draft'\)"/) @@ -407,7 +402,7 @@ test('预报实收同区对照、托盘卡片换行、签收按钮紧邻标题', assert.ok(section.includes('class="warehouse-transfer-pallet-sku"')) assert.doesNotMatch(section, /打印箱唛/) const receipt = view.slice(view.indexOf('warehouse-transfer-received-card', start)).split('