diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 6a3ca8f..d8eaf02 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -1,7 +1,7 @@ import { apiRequest, apiBlobRequest } from '@/api/http' import { createIdempotencyKey } from '@/utils/idempotencyKey' -export type RelabelDeliveryMethod = 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' +export type RelabelDeliveryMethod = 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' export interface PageResult { records: T[]; page: number; size: number; total: number; pages: number } export interface LastlegOrderSummary { id: string; orderNo: string; inboundCode: string; orderType: string; packageCount: number; overseasWarehouseId?: string } @@ -26,7 +26,8 @@ export interface RelabelDetail { completionEvidence?: { taskId: string; photoFileIds: string[]; completionRemark?: string; reviewerName?: string; reviewRemark?: string; completedAt?: string } } export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string } -export interface DiscardCandidate { orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> } +export interface DiscardBox { cartonId: string; cartonMark: string; palletNo?: string; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> } +export interface DiscardCandidate { cartons?: DiscardBox[]; cartonSelectionSupported?: boolean; cartonSelectionBlockReason?: string; orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'INVENTORY'; inventoryPositionId?: string; palletNo?: string; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> } export interface RelabelInventoryCandidate { cartonSkuItems: Array<{ skuCode: string; quantity: number }>; cartonAvailablePackages: number | string skuConversions?: import('./returnInventory').ReturnSkuConversion[] @@ -127,7 +128,7 @@ export function getDiscard(discardNo: string) { export function listDiscardCandidates(inboundCode?: string) { return apiRequest({ method: 'GET', url: '/merchant/v1/last-leg/cargo-discards/inventory-candidates', params: { inboundCode: inboundCode || undefined } }) } -export function createDiscard(payload: { items: Array<{ orderId?: string; returnCartonId?: string; skuCode: string; discardPackages: number }>; idempotencyKey: string; remark?: string }) { +export function createDiscard(payload: { items: Array<{ orderId?: string; returnCartonId?: string; inventoryPositionId?: string; cartonIds?: string[]; skuCode: string; discardPackages: number }>; idempotencyKey: string; remark?: string }) { return apiRequest>({ method: 'POST', url: '/merchant/v1/last-leg/cargo-discards', data: payload }) } export function cancelDiscard(discardNo: string) { diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 8a746a0..319ea63 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -443,24 +443,6 @@ onBeforeUnmount(clearPhotoUrls) -
-

当前关联实物箱

- - - -

来源退货单:{{ current.carton.sourceReturnNo }} · 我方箱唛:{{ current.labelStatus === 'CONFIRMED' ? '已确认贴标' : '待贴标确认' }}

- - - - - -

换箱唛以当前实物箱为单位;跨来源单同 SKU 的部分贴标须先现场分拣确认。

- 选择此箱贴标 -

{{ current.blockReason }}

-
-
- -

退货打托照片({{ selected.photos?.length || 0 }} 张)

diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 6fcf1eb..375f4a3 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -13,7 +13,7 @@ import { getTransferCartonLabel, submitTransfer, updateTransfer, confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, uploadRelabelFile, - listDiscards, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, + listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, type RelabelInventoryCandidate, type RelabelCreate, type RelabelDetail, @@ -33,7 +33,7 @@ type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel' type PageMode = 'list' | 'create' | 'edit' | 'detail' type LabelingMode = 'carton' | 'sku' | 'carton_and_sku' type RelabelTemplateType = 'carton' | 'sku' | 'mixed' -interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } +interface CargoRow { discardCartonIds?: string[]; id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string } interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; shipmentNo: string; destinationWarehouseId: string; remark: string } interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string } interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] } @@ -130,7 +130,7 @@ interface ServiceDetail extends Record { relabelContactPhone?: string; relabelDeliveryMethod?: string; relabelNo?: string relabelStatus?: string; relabelFulfillmentStatus?: string; createdAt?: string inboundCode?: string; sourceOrders?: Array<{ lastlegOrderId?: string | number }> - items?: Array<{ skuCode?: string; productNameZh?: string; packageCount?: number; shippingMark?: string }> + items?: Array<{ cartonMarks?: string[]; inboundCode?: string; productName?: string; discardPackages?: number; skuCode?: string; productNameZh?: string; packageCount?: number; shippingMark?: string }> relabelBindings?: Array<{ operationType?: string; customerCartonMark?: string; originalSku?: string; newSku?: string; quantity?: number; fbaCartonMark?: string; destinationWarehouseCode?: string; remark?: string }> cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string } @@ -201,7 +201,48 @@ const listColumns = computed(() => config.value.columns.map(( : { key: String(index - 1), prop: String(index - 1), label, minWidth: label === '关联业务单号' ? 300 : index === 1 || label === '换标单号' ? 190 : 130, singleLine: label !== '关联业务单号', className: label === '关联业务单号' ? 'related-business-number-cell' : undefined, slotName: props.service === 'labeling' && label === '换标类型' ? 'labelingType' : props.service === 'labeling' && label === '状态' ? 'labelingStatus' : props.service === 'transfer' && label === '换标状态' ? 'transferRelabelStatus' : props.service === 'transfer' && label === '状态' ? 'transferStatus' : undefined })) +const discardSelectedBoxKeys = ref([]) +const discardBoxSearch = ref('') const discardCandidates = ref([]) +const discardSelectedCandidate = computed(() => discardCandidates.value.find(item => item.orderId === selectedDiscardOrderId.value)) +const discardSelectionSources = computed(() => { + const current = discardSelectedCandidate.value + if (!current) return [] + if (current.sourceType !== 'RETURN_CARTON') return [current] + return discardCandidates.value.filter(item => item.sourceType === current.sourceType && item.inboundCode === current.inboundCode && item.overseasWarehouseId === current.overseasWarehouseId) +}) +const discardSelectableBoxes = computed(() => discardSelectionSources.value.flatMap(source => (source.cartons || []).map(box => ({ ...box, key: `${source.sourceType}:${box.cartonId}`, source })))) +const discardVisibleBoxes = computed(() => discardSelectableBoxes.value.filter(box => !discardBoxSearch.value.trim() || box.cartonMark.includes(discardBoxSearch.value.trim()) || box.skus.some(sku => sku.skuCode.includes(discardBoxSearch.value.trim())))) +function toggleDiscardBox(key: string, selected: boolean) { + discardSelectedBoxKeys.value = selected ? [...new Set([...discardSelectedBoxKeys.value, key])] : discardSelectedBoxKeys.value.filter(value => value !== key) + syncDiscardSelection() +} +function selectAllDiscardBoxes(selected: boolean) { + discardSelectedBoxKeys.value = selected ? discardSelectableBoxes.value.map(box => box.key) : [] + syncDiscardSelection() +} +function syncDiscardSelection() { + const selected = discardSelectableBoxes.value.filter(box => discardSelectedBoxKeys.value.includes(box.key)) + const groups = new Map() + for (const box of selected) { + const source = box.source + const skus = source.sourceType === 'RETURN_CARTON' ? source.skus : source.sourceType === 'INVENTORY' ? source.skus.slice(0, 1) : box.skus.slice(0, 1) + for (const sku of skus) { + const key = `${source.orderId}:${sku.skuCode}` + let row = groups.get(key) + if (!row) { + row = { ...createCargoRow(), sku: sku.skuCode, name: sku.productName, quantity: 0, availableQuantity: source.sourceType === 'RETURN_CARTON' ? sku.availablePackages : source.availablePackages, + sourceOrderId: source.orderId, sourceType: source.sourceType, sourceInventoryPositionId: source.inventoryPositionId, returnCartonId: source.returnCartonId, + palletNo: source.palletNo, discardCartonIds: source.sourceType === 'RETURN_CARTON' ? undefined : [] } + groups.set(key, row) + } + row.quantity += source.sourceType === 'RETURN_CARTON' ? sku.availablePackages : 1 + row.discardCartonIds?.push(box.cartonId) + } + } + cargoRows.value = [...groups.values()] +} + const selectedDiscardOrderId = ref('') const discardCandidatesLoading = ref(false) const relabelCandidates = ref([]) @@ -459,6 +500,8 @@ async function openEdit(transferNo: string) { } catch (error) { ElMessage.error(error instanceof Error ? error.message : '草稿加载失败') } } async function selectDiscardWarehouse() { + discardSelectedBoxKeys.value = [] + discardBoxSearch.value = '' selectedDiscardOrderId.value = '' form.inboundNo = '' discardCandidates.value = [] @@ -467,37 +510,13 @@ async function selectDiscardWarehouse() { if (!form.warehouse) return discardCandidatesLoading.value = true try { - const inventory = await listRelabelInventoryCandidates(form.warehouse) - const orderByInbound = new Map() - const inboundCodes = [...new Set(inventory.map((candidate) => String(candidate.inboundCode || '')).filter(Boolean))] - const historicalGroups = await Promise.all(inboundCodes.map(async (inboundCode) => ({ inboundCode, orders: await listRelabelHistoricalOrders(form.warehouse, inboundCode) }))) - for (const { inboundCode, orders } of historicalGroups) { - for (const order of orders) { - if (order.id) orderByInbound.set(inboundCode, String(order.id)) - } - } - const grouped = new Map() - for (const candidate of inventory) { - const returnCartonId = String(candidate.returnCartonId || '') - const sourceType = candidate.sourceType === 'RETURN_CARTON' ? 'RETURN_CARTON' : 'DROPSHIP' - const inboundCode = String(candidate.inboundCode || '') - const orderId = sourceType === 'RETURN_CARTON' ? `return:${returnCartonId}` : (orderByInbound.get(inboundCode) || '') - if (!orderId || (sourceType === 'RETURN_CARTON' && !returnCartonId)) continue - const current = grouped.get(orderId) || { orderId, inboundCode, inboundOrderNo: candidate.inboundOrderNo || '', overseasWarehouseId: String(candidate.overseasWarehouseId || form.warehouse), overseasWarehouseName: candidate.overseasWarehouseName || '', availablePackages: 0, skus: [] } - current.sourceType = sourceType - current.returnCartonId = returnCartonId || undefined - current.cartonMark = candidate.cartonMark || undefined - const packages = inventoryQuantity(candidate.availablePackages) - current.availablePackages += packages - const skuCode = String(candidate.skuCode || '').trim() - if (skuCode) { - const sku = current.skus.find((item) => item.skuCode === skuCode) - if (sku) sku.availablePackages += packages - else current.skus.push({ skuCode, productName: candidate.productNameZh || '', availablePackages: packages }) - } - grouped.set(orderId, current) - } - discardCandidates.value = [...grouped.values()].filter((candidate) => String(candidate.overseasWarehouseId) === form.warehouse) + const warehouse = form.warehouse + const candidates = await listDiscardCandidates() + if (form.warehouse !== warehouse) return + discardCandidates.value = candidates + .filter((candidate) => String(candidate.overseasWarehouseId) === warehouse) + .filter((candidate) => candidate.sourceType === 'DROPSHIP' ? Boolean(candidate.orderId) : candidate.sourceType === 'RETURN_CARTON' ? Boolean(candidate.returnCartonId) : candidate.sourceType === 'INVENTORY' && Boolean(candidate.inventoryPositionId)) + .map((candidate) => ({ ...candidate, orderId: candidate.sourceType === 'RETURN_CARTON' ? `return:${candidate.returnCartonId}` : candidate.sourceType === 'INVENTORY' ? `inventory:${candidate.inventoryPositionId}` : String(candidate.orderId) })) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '可废弃入库单加载失败') } finally { discardCandidatesLoading.value = false } } @@ -865,14 +884,17 @@ function importSelectedBindingsToDetails() { ElMessage.success(`已将 ${selected.length} 条绑定关系导入本次换标明细`) } function selectDiscardInbound(orderId: string) { + discardSelectedBoxKeys.value = [] + discardBoxSearch.value = '' const candidate = discardCandidates.value.find((item) => item.orderId === orderId) if (!candidate) { cargoRows.value = []; return } form.inboundNo = candidate.inboundCode || candidate.inboundOrderNo form.warehouse = candidate.overseasWarehouseId + if (candidate.cartonSelectionSupported) { selectAllDiscardBoxes(true); return } cargoRows.value = candidate.skus.map((sku) => ({ - id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: 0, + id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: sku.availablePackages, availableQuantity: sku.availablePackages, sourceOrderId: candidate.orderId, sourceType: candidate.sourceType, - returnCartonId: candidate.returnCartonId, cartonMark: candidate.cartonMark || '', + returnCartonId: candidate.returnCartonId, sourceInventoryPositionId: candidate.inventoryPositionId, palletNo: candidate.palletNo, cartonMark: candidate.cartonMark || '', fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '', })) } @@ -981,7 +1003,7 @@ function transferRelabelStatusTagType(status: string): StatusTagType { } function deliveryMethodLabel(value: string) { const labels: Record = { - TRUCK: '卡车派送', EXPRESS: '快递派送', TRUCK_PICKUP: '卡车自提', EXPRESS_PICKUP: '快递自提', + TRUCK: '卡车派送', EXPRESS: '快递派送', AMAZON_PICKUP: '亚马逊自提', CUSTOMER_PICKUP: '客户自提', WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓', // Keep the legacy value readable for historical applications created before the four-way split. PICKUP: '仓库自提', @@ -1067,7 +1089,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.destination) { ElMessage.warning('请选择本张贴标申请统一使用的 FBA 目的仓'); return } - if (props.service === 'labeling' && labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') + if (props.service === 'labeling' && labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel && !shippingLabelFile.value) { ElMessage.warning('选择打印面单时必须上传面单文件'); return } @@ -1119,8 +1141,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') { const selectedOperationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' if (items.some(item => item.operationType !== selectedOperationType)) throw new Error('一张申请只能选择一种换标类型,请核对绑定明细') const created = await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, remark: form.remark || undefined, idempotencyKey, items, - deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP', - printShippingLabel: labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel }) + deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP', + printShippingLabel: labelingMode.value !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel }) try { if (cartonTotalFile.value) await uploadRelabelFile(created.application.relabelNo, 'CARTON_TOTAL', cartonTotalFile.value) if (skuTotalFile.value) await uploadRelabelFile(created.application.relabelNo, 'SKU_TOTAL', skuTotalFile.value) @@ -1137,12 +1159,13 @@ async function submit(action: 'draft' | 'submit' = 'submit') { return } else if (props.service === 'disposal') { const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => { - if (!item.sourceOrderId || item.sourceOrderId !== selectedDiscardOrderId.value || item.quantity > Number(item.availableQuantity || 0)) { + if (!item.sourceOrderId || !discardSelectionSources.value.some(source => source.orderId === item.sourceOrderId) || item.quantity > Number(item.availableQuantity || 0)) { throw new Error(`SKU ${item.sku} 的可废弃库存不足`) } - return { orderId: item.sourceType === 'RETURN_CARTON' ? undefined : item.sourceOrderId, + return { orderId: item.sourceType === 'DROPSHIP' ? item.sourceOrderId : undefined, + inventoryPositionId: item.sourceType === 'INVENTORY' ? item.sourceInventoryPositionId : undefined, returnCartonId: item.sourceType === 'RETURN_CARTON' ? item.returnCartonId : undefined, - skuCode: item.sku, discardPackages: item.quantity } + skuCode: item.sku, discardPackages: item.quantity, cartonIds: item.discardCartonIds } }) await createDiscard({ items, idempotencyKey, remark: [form.reason, form.remark].filter(Boolean).join(';') || undefined }) } else { @@ -1151,7 +1174,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { const transferRemark = [form.remark, form.relabel && form.requirement ? `换标要求:${form.requirement}` : ''].filter(Boolean).join(';') const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination) - const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } + const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: form.volumeCbm, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, packageCount: item.quantity, shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } if (props.service === 'transfer' && selectedTransferInventory.value) { ;(payload as TransferCreate).inboundCode = selectedTransferInventory.value ;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value @@ -1536,7 +1559,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
- + @@ -1560,9 +1583,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { - -
是否打印面单需要仓库打印面单
- + +
是否打印面单需要仓库打印面单
+ @@ -1639,13 +1662,22 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *FBA 货件编号 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”

2换标申请

-
+
换标类型只换箱唛只换 SKU 标签SKU 和箱唛都换
@@ -1682,6 +1714,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {

订单概览

操作仓{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}
客户参考号{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}
SKU 数(种){{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}
总箱数{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}
贴标截止时间{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}
仓库联系人{{ selectedRelabelDetail?.application.contactName || '—' }}
联系电话{{ selectedRelabelDetail?.application.contactPhone || '—' }}
创建时间{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}
状态{{ statusLabel(selectedDetail?.status || '') }}
目的去向{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}
FBA 目的仓{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}
FBA 货件号{{ selectedDetail?.fbaShipmentNo || '—' }}
私人仓地址{{ selectedDetail?.privateDestinationAddress || '—' }}
邮编{{ selectedDetail?.privateDestinationPostalCode || '—' }}
私人仓联系人{{ selectedDetail?.privateDestinationContactName || '—' }}
联系电话{{ selectedDetail?.privateDestinationContactPhone || '—' }}
海外仓{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}
箱数{{ selectedDetail?.packageCount ?? '—' }}
入仓方式{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}
期望发货时间{{ formatDateTime(selectedDetail?.estimatedShipAt) }}
实际发货时间{{ formatDateTime(selectedDetail?.actualShipAt) }}
是否换标{{ selectedDetail?.needRelabel ? '是' : '否' }}
换标状态{{ transferRelabelStatus(selectedDetail || {}).label }}
换标单号{{ selectedDetail?.relabelNo || '—' }}
创建时间{{ formatDateTime(selectedDetail?.createdAt) }}
转运单号{{ selectedDetail?.transferNo || '—' }}
{{ column }}{{ selectedDetail?.[String(index)] || '—' }}
+

本次废弃箱子

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

废弃现场照片

照片加载中…
{{ formatDateTime(photo.submittedAt) }}
暂无已提交的废弃现场照片
@@ -1897,3 +1930,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { .warehouse-transfer-form .warehouse-choice-field--relabel { grid-template-columns: 1fr; align-items: start; } } + + diff --git a/src/modules/overseas-warehouse/views/relabelAttachments.test.mjs b/src/modules/overseas-warehouse/views/relabelAttachments.test.mjs index f789374..145fc2c 100644 --- a/src/modules/overseas-warehouse/views/relabelAttachments.test.mjs +++ b/src/modules/overseas-warehouse/views/relabelAttachments.test.mjs @@ -129,9 +129,9 @@ test('贴标货物明细通过单一换标类型选择三种模式', () => { test('箱唛相关换标必须选择四选一派送方式', () => { assert.match(view, /label="卡车派送" value="TRUCK"/) assert.match(view, /label="快递派送" value="EXPRESS"/) - assert.match(view, /label="卡车自提" value="TRUCK_PICKUP"/) - assert.match(view, /label="快递自提" value="EXPRESS_PICKUP"/) - assert.match(api, /RelabelDeliveryMethod = 'TRUCK' \| 'EXPRESS' \| 'TRUCK_PICKUP' \| 'EXPRESS_PICKUP'/) + assert.match(view, /label="亚马逊自提" value="AMAZON_PICKUP"/) + assert.match(view, /label="客户自提" value="CUSTOMER_PICKUP"/) + assert.match(api, /RelabelDeliveryMethod = 'TRUCK' \| 'EXPRESS' \| 'AMAZON_PICKUP' \| 'CUSTOMER_PICKUP'/) assert.match(view, /labelingMode\.value !== 'sku' && !form\.deliveryMethod/) }) diff --git a/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs index 678ee90..b5be9fb 100644 --- a/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs @@ -7,6 +7,43 @@ const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url) test('货物废弃先选择海外操作仓,再加载并选择该仓入库单', () => { assert.match(view, /props\.service === 'disposal'.*操作仓.*v-model="form\.warehouse".*请选择海外仓.*@change="selectDiscardWarehouse"/s) assert.match(view, /props\.service === 'disposal'.*选择入库单.*:disabled="!form\.warehouse".*请先选择操作仓/s) - assert.match(view, /async function selectDiscardWarehouse\(\).*selectedDiscardOrderId\.value = ''.*cargoRows\.value = \[\].*listRelabelInventoryCandidates\(form\.warehouse\).*grouped/s) + assert.match(view, /async function selectDiscardWarehouse\(\).*selectedDiscardOrderId\.value = ''.*cargoRows\.value = \[\].*listDiscardCandidates\(\)/s) assert.match(view, /\|\| !primaryWarehouse \|\| invalidCargo/) }) + + +test('废弃来源包含通用仓位,选择后带出全部库存且不把选择键提交为订单ID', () => { + const body = view.match(/function selectDiscardInbound\(orderId: string\) \{([\s\S]*?)\n\}/)[1] + const select = new Function('discardSelectedBoxKeys', 'discardBoxSearch', 'selectAllDiscardBoxes', 'discardCandidates', 'cargoRows', 'form', 'cargoSequence', 'orderId', body) + const cargoRows = { value: [] }; const form = {} + select({ value: [] }, { value: '' }, () => {}, { value: [{ orderId: 'inventory:42', sourceType: 'INVENTORY', inventoryPositionId: '42', overseasWarehouseId: '99', inboundCode: 'WT-1', skus: [{ skuCode: 'A', productName: '商品', availablePackages: 100 }] }] }, cargoRows, form, 0, 'inventory:42') + assert.equal(cargoRows.value[0].quantity, 100) + assert.equal(cargoRows.value[0].sourceInventoryPositionId, '42') + assert.equal(form.inboundNo, 'WT-1') + assert.match(view, /orderId: item.sourceType === 'DROPSHIP' \? item.sourceOrderId : undefined/) + assert.match(view, /inventoryPositionId: item.sourceType === 'INVENTORY'/) +}) + +function selectionRows(boxes, selectedKeys) { + const body = view.match(/function syncDiscardSelection\(\) \{([\s\S]*?)\n\}/)[1].replace('new Map()', 'new Map()') + const rows = { value: [] } + new Function('discardSelectableBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({})) + return rows.value +} +test('部分废弃只提交勾选的箱子,数量由箱子汇总,清空后无明细', () => { + const source = { orderId: 'inventory:42', inventoryPositionId: '42', sourceType: 'INVENTORY', availablePackages: 3, skus: [{ skuCode: 'SKU', productName: '商品' }] } + const boxes = ['1', '2', '3'].map(id => ({ key: `INVENTORY:${id}`, cartonId: id, cartonMark: `BOX-${id}`, source, skus: source.skus })) + const rows = selectionRows(boxes, ['INVENTORY:2']) + assert.equal(rows.length, 1); assert.equal(rows[0].quantity, 1) + assert.deepEqual(rows[0].discardCartonIds, ['2']); assert.equal(rows[0].sourceInventoryPositionId, '42') + assert.equal(selectionRows(boxes, []).length, 0) +}) +test('混合SKU箱按实物箱只计一次,退货箱仍整箱携带全部SKU', () => { + const skus = [{ skuCode: 'A', availablePackages: 2 }, { skuCode: 'B', availablePackages: 3 }] + const dropship = { orderId: '42', sourceType: 'DROPSHIP', availablePackages: 1, skus } + const rows = selectionRows([{ key: 'DROPSHIP:1', cartonId: '1', source: dropship, skus }], ['DROPSHIP:1']) + assert.equal(rows.length, 1); assert.equal(rows[0].quantity, 1); assert.deepEqual(rows[0].discardCartonIds, ['1']) + const returned = { orderId: 'return:2', returnCartonId: '2', sourceType: 'RETURN_CARTON', availablePackages: 5, skus } + const returnRows = selectionRows([{ key: 'RETURN_CARTON:2', cartonId: '2', source: returned, skus }], ['RETURN_CARTON:2']) + assert.deepEqual(returnRows.map(row => row.quantity), [2, 3]); assert.ok(returnRows.every(row => row.returnCartonId === '2' && row.discardCartonIds === undefined)) +})