diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts index 69d080b..a65d1f7 100644 --- a/src/modules/first-leg/api/firstLeg.ts +++ b/src/modules/first-leg/api/firstLeg.ts @@ -296,6 +296,8 @@ export interface MerchantFirstLegOrderRow { inboundedAt: string | null businessDate: string | null shipmentId: string | null + fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null + lxCode?: string | null createdAt: string cargoes?: Array<{ skuNo?: string | null }> } diff --git a/src/modules/first-leg/components/InboundBillingPanel.test.mjs b/src/modules/first-leg/components/InboundBillingPanel.test.mjs index d67fe10..649012a 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.test.mjs +++ b/src/modules/first-leg/components/InboundBillingPanel.test.mjs @@ -42,7 +42,8 @@ test('shows the published merchant inbound, measurement details and customer con assert.match(source, /latestFee\.rateCode/) assert.match(source, /latestFee\.chargeRuleVersion/) assert.match(source, /latestFee\.carrierRateVersion/) - assert.doesNotMatch(source, /箱规规则|calculationRuleVersion/) + assert.match(source, /箱规规则/) + assert.match(source, /getCalculationRuleLabel\(row\.calculationRuleVersion\)/) }) test('stacks core information for 720 and 360 pixel layouts', () => { diff --git a/src/modules/first-leg/components/InboundBillingPanel.vue b/src/modules/first-leg/components/InboundBillingPanel.vue index 1d67c29..c3858bf 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.vue +++ b/src/modules/first-leg/components/InboundBillingPanel.vue @@ -10,6 +10,7 @@ import { formatInboundMeasure, formatInboundMoney, formatInboundNumber, + getCalculationRuleLabel, getChargeModeLabel, getTaxModeLabel, } from '@/modules/first-leg/utils/inboundBilling' @@ -139,6 +140,7 @@ async function confirmFee() { + @@ -241,6 +243,7 @@ async function confirmFee() { + /) assert.match(apiSource, /apiRequest>/) assert.match(storeSource, /function mapFirstLegOrderRow/) - for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'pickupMethod', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'referenceId', 'createdAt']) { + for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'pickupMethod', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'fulfillmentType', 'lxCode', 'referenceId', 'createdAt']) { assert.match(storeSource, new RegExp(`row\\.${field}`)) } assert.match(storeSource, /row\.skuNos/) @@ -102,7 +102,9 @@ test('keeps unified order pagination valid when the store omits page and size', test('keeps drafts on the legacy read path without assigning a unified main status', () => { assert.match(storeSource, /getMerchantFirstLegOrders\(legacyQuery\(query, 'DRAFT'\)\)/) - assert.match(storeSource, /getById\(id\)\?\.status === 'DRAFT' \? loadLegacyDetail\(id\) : loadUnifiedDetail\(id\)/) + assert.match(storeSource, /const listOrder = getById\(id\)/) + assert.match(storeSource, /listOrder\?\.status === 'DRAFT' \? loadLegacyDetail\(id, listOrder\) : loadUnifiedDetail\(id\)/) + assert.match(storeSource, /detail\.order\.lxCode == null && listOrder\?\.lxCode/) assert.match(storeSource, /return loadLegacyDetail\(String\(createdId\)\)/) assert.match(storeSource, /draftLoadError/) }) diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index d202fc2..96ce9c5 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -359,7 +359,7 @@ function mapFirstLegOrderRow(row: MerchantFirstLegOrderRow): FirstLegOrder { const uniqueSkuNos = [...new Set(resolvedSkuNos)] return { id: String(row.orderId), orderNo: row.orderNo, referenceNo: row.referenceId ?? '', transportMode: null, - businessType: 'EXPORT', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '', + businessType: 'EXPORT', fulfillmentType: row.fulfillmentType ?? 'STANDARD', lxCode: row.lxCode ?? '', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '', receivingWarehouseName: '', receivingWarehouseSnapshot: null, expectedDepartureDate: '', etd: '', eta: '', shipperPartyId: '', shipperPartySnapshot: null, shipper: '', consigneePartyId: '', consigneePartySnapshot: null, consignee: row.receivingCompanyName ?? '', expressCompanyName: row.expressCompany ?? '', trackingNumber: row.trackingNo ?? '', @@ -542,9 +542,19 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () return mapped } - async function loadLegacyDetail(id: string) { + async function loadLegacyDetail(id: string, listOrder?: FirstLegOrder) { const detail = await getMerchantFirstLegOrder(id) - return upsertOrder(mapOrder(detail.order, warehouses.value, detail)) + const mapped = mapOrder(detail.order, warehouses.value, detail) + // Older detail responses may omit newly added dropship fields. Keep the + // values already returned by the list row so editing a draft does not + // clear the persisted LX code in the form. + if (detail.order.fulfillmentType == null && listOrder?.fulfillmentType) { + mapped.fulfillmentType = listOrder.fulfillmentType + } + if (detail.order.lxCode == null && listOrder?.lxCode) { + mapped.lxCode = listOrder.lxCode + } + return upsertOrder(mapped) } async function loadUnifiedDetail(id: string) { @@ -558,7 +568,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () } async function loadDetail(id: string) { - return getById(id)?.status === 'DRAFT' ? loadLegacyDetail(id) : loadUnifiedDetail(id) + const listOrder = getById(id) + return listOrder?.status === 'DRAFT' ? loadLegacyDetail(id, listOrder) : loadUnifiedDetail(id) } async function loadWarehouseDetail(id: string) { @@ -616,7 +627,7 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', () } async function loadDraftDetail(id: string) { - return loadLegacyDetail(id) + return loadLegacyDetail(id, getById(id)) } return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError, activeCount, pendingCount, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, saveDraft, saveDraftBatch, submitSaved, submit } diff --git a/src/modules/first-leg/utils/inboundBilling.test.mjs b/src/modules/first-leg/utils/inboundBilling.test.mjs index fec95b4..5751d98 100644 --- a/src/modules/first-leg/utils/inboundBilling.test.mjs +++ b/src/modules/first-leg/utils/inboundBilling.test.mjs @@ -19,6 +19,13 @@ test('translates backend rounding modes into customer-facing calculation labels' assert.match(source, /return value \? labels\[value\] \|\| value : '-'/) }) +test('translates carton calculation rules into customer-facing labels', () => { + assert.match(source, /export function getCalculationRuleLabel/) + assert.match(source, /ACTUAL_DIMENSION_V1/) + assert.match(source, /按实际箱规计算/) + assert.match(source, /CUSTOMER_DIMENSION_\(SMALL\|MEDIUM\|LARGE\|EXTRA_LARGE\)_V/) +}) + test('renders conversion metadata as one complete label or a single empty marker', () => { assert.match(source, /export function formatConversionPrecision/) assert.match(source, /roundingScale === null \|\| roundingScale === undefined/) diff --git a/src/modules/first-leg/utils/inboundBilling.ts b/src/modules/first-leg/utils/inboundBilling.ts index 600fcad..b880932 100644 --- a/src/modules/first-leg/utils/inboundBilling.ts +++ b/src/modules/first-leg/utils/inboundBilling.ts @@ -46,6 +46,20 @@ export function getTaxModeLabel(value: string) { return value || '-' } +export function getCalculationRuleLabel(value: string | null | undefined) { + if (!value) return '-' + if (value === 'ACTUAL_DIMENSION_V1' || value === 'DIMENSION_ACTUAL_V1') return '按实际箱规计算' + const customerRule = /^CUSTOMER_DIMENSION_(SMALL|MEDIUM|LARGE|EXTRA_LARGE)_V(\d+)$/.exec(value) + if (customerRule) { + const sizeLabel: Record = { + SMALL: '小号', MEDIUM: '中号', LARGE: '大号', EXTRA_LARGE: '超大号', + } + return `客户箱规规则(${sizeLabel[customerRule[1]]},V${customerRule[2]})` + } + const warehouseRule = /^WAREHOUSE_OUTER_OVERRIDE_(.+)$/.exec(value) + return warehouseRule ? `仓库外箱规覆盖(${warehouseRule[1]})` : value +} + export function getRoundingModeLabel(value: string | null | undefined) { const labels: Record = { UP: '向上舍入', diff --git a/src/modules/overseas-warehouse/api/returnOrders.ts b/src/modules/overseas-warehouse/api/returnOrders.ts index c6fd930..5374106 100644 --- a/src/modules/overseas-warehouse/api/returnOrders.ts +++ b/src/modules/overseas-warehouse/api/returnOrders.ts @@ -2,6 +2,7 @@ import { overseasApiRequest, overseasBlobRequest } from '@/api/overseasHttp' import type { PageResult } from './warehouseServices' export interface ReturnOrderSummary { + relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; createdAt: string }> returnNo: string; overseasWarehouseId: string; warehouseContactName?: string; warehouseContactPhone?: string; customerReferenceNo?: string; status: string skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; relabelNo?: string; relabelStatus?: string; createdAt: string } diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 590cd74..315bd2f 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -33,7 +33,7 @@ export interface RelabelInventoryCandidate { availablePackages: number | string; version: number positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }> sourceType?: 'INBOUND' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string - skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }> + skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }>; relabelledPackages?: number } export interface TransferCreate { diff --git a/src/modules/overseas-warehouse/permissions.test.mjs b/src/modules/overseas-warehouse/permissions.test.mjs index b5c9791..e76f422 100644 --- a/src/modules/overseas-warehouse/permissions.test.mjs +++ b/src/modules/overseas-warehouse/permissions.test.mjs @@ -32,7 +32,7 @@ test('opens warehouse service and return receipt details as dedicated protected assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/:returnNo\/detail'[\s\S]*?name: 'overseas-return-relabel-detail'[\s\S]*?props: \{ detailPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:list'/) assert.match(routes, /path: 'overseas-warehouse\/storage-transfer\/:detailNo\/detail'[\s\S]*?name: 'overseas-storage-transfer-detail'[\s\S]*?props: \{ service: 'transfer', detailPage: true \}/) assert.match(routes, /path: 'overseas-warehouse\/cargo-disposal\/:detailNo\/detail'[\s\S]*?name: 'overseas-cargo-disposal-detail'[\s\S]*?props: \{ service: 'disposal', detailPage: true \}/) - assert.match(services, /name: 'overseas-labeling-detail'/) + assert.match(services, /labeling: 'overseas-labeling-detail'/) assert.match(services, /transfer: 'overseas-storage-transfer-detail'/) assert.match(services, /disposal: 'overseas-cargo-disposal-detail'/) assert.match(services, /class="warehouse-detail-error"/) @@ -40,6 +40,11 @@ test('opens warehouse service and return receipt details as dedicated protected assert.doesNotMatch(returns, /title="退货签收详情"/) }) +test('returns to the labeling list after a labeling application is submitted', () => { + assert.match(services, /ElMessage\.success\(`\$\{config\.value\.title\}提交成功`\)[\s\S]*?router\.push\(\{ name: 'overseas-labeling' \}\)/) + assert.match(services, /uploadReturnRelabelFile\([\s\S]*?router\.push\(\{ name: 'overseas-labeling' \}\)/) +}) + test('keeps overseas warehouse writes behind operation-specific button permissions', () => { for (const permission of [ 'merchant:overseas-warehouse:relabel:create', diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 77975ae..eb2065d 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -42,7 +42,7 @@ test('退货列表和详情使用客户可读仓库、状态与时间', () => { assert.match(view, /return warehouse \? warehouseLabel\(warehouse\) : '未关联'/) assert.match(view, /PENDING_RECEIPT: '待签收'/) assert.match(view, /PARTIALLY_RECEIVED: '部分签收'/) - assert.match(view, /return needRelabel \? '已完成入库\/待换标' : '已完成入库'/) + assert.match(view, /if \(status === 'COMPLETED'\) return '已完成入库'/) assert.match(view, /function formatDateTime\(value\?: string \| null\)/) assert.match(view, /\{\{ operationWarehouseLabel\(row\.overseasWarehouseId\) \}\}/) assert.match(view, /\{\{ statusLabel\(row\.status, row\.needRelabel\) \}\}/) @@ -76,9 +76,9 @@ test('退货签收详情使用独立路由和紧凑连续页面', () => { test('待审核退货可编辑,完成入库后从独立新增页发起贴标', () => { assert.match(view, /row\.status === 'PENDING_REVIEW'.*key: 'edit'/) assert.match(view, /await updateReturnOrder\(editingReturnNo\.value, payload\)/) - assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true && !order\.relabelNo/) + assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true/) assert.match(view, /name: 'overseas-labeling-create'/) assert.match(view, /query: \{ returnNo: order\.returnNo, warehouseId: String\(order\.overseasWarehouseId\) \}/) - assert.match(view, /prop: 'relabelNo', label: '换标单号'/) - assert.match(view, /label: '换标单状态'/) + assert.match(view, /v-for="application in row\.relabelApplications \|\| \[\]"/) + assert.match(view, /relabelStatusLabel\(application\.status\)/) }) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index e8737cd..886a1d6 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -49,7 +49,7 @@ const tableColumns: DataTableColumn[] = [ { key: 'warehouse', label: '操作仓', slotName: 'warehouse', minWidth: 200 }, { key: 'relabel', label: '换标', slotName: 'relabel', width: 100 }, { prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报数量', width: 110 }, { prop: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 150 }, - { prop: 'relabelNo', label: '换标单号', minWidth: 190 }, { key: 'relabelStatus', label: '换标单状态', slotName: 'relabelStatus', width: 130 }, + { key: 'relabelApplications', label: '换标子单', slotName: 'relabelApplications', minWidth: 340 }, { key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 }, ] const itemColumns: DataTableColumn[] = [ @@ -191,15 +191,15 @@ async function submit() { } finally { creating.value = false } } function statusType(status: string, needRelabel = false) { - if (status === 'COMPLETED') return needRelabel ? 'warning' : 'success' + if (status === 'COMPLETED') return 'success' return ({ PENDING_REVIEW: 'warning', PENDING_RECEIPT: 'primary', PARTIALLY_RECEIVED: 'warning', REJECTED: 'danger', CANCELLED: 'info' } as Record)[status] || 'info' } function statusClass(status: string, needRelabel = false) { - if (status === 'COMPLETED') return needRelabel ? 'return-status--relabel' : 'return-status--completed' + if (status === 'COMPLETED') return 'return-status--completed' return `return-status--${status.toLowerCase().replaceAll('_', '-')}` } function statusLabel(status: string, needRelabel = false) { - if (status === 'COMPLETED') return needRelabel ? '已完成入库/待换标' : '已完成入库' + if (status === 'COMPLETED') return '已完成入库' return ({ PENDING_REVIEW: '待审核', PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', REJECTED: '已驳回', CANCELLED: '已取消' } as Record)[status] ?? (status || '未知状态') } function relabelStatusType(status?: string) { @@ -265,7 +265,7 @@ onBeforeUnmount(clearPhotoUrls) - + @@ -304,8 +304,6 @@ onBeforeUnmount(clearPhotoUrls)
实收 / 预报{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}
SKU 种类{{ selected.order.skuTypes }}
是否换标{{ selected.order.needRelabel ? '需要换标' : '否' }}
-
换标单号{{ selected.order.relabelNo || '—' }}
-
换标单状态{{ relabelStatusLabel(selected.order.relabelStatus) }}
客户参考号{{ selected.order.customerReferenceNo || '—' }}
海外仓联系人{{ selected.order.warehouseContactName || '—' }}
联系电话{{ selected.order.warehouseContactPhone || '—' }}
@@ -313,6 +311,14 @@ onBeforeUnmount(clearPhotoUrls)
备注{{ selected.remark }}
+
+

换标子单({{ selected.order.relabelApplications?.length || 0 }})

+ + + + + +

预报与实收

@@ -356,6 +362,7 @@ onBeforeUnmount(clearPhotoUrls)