发布:将已验 TEST 功能更新至既有生产灰度(2026-10-23) #15
@@ -1,12 +1,19 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
|
||||
export interface RelabelQuantity {
|
||||
sourcePalletCount?: number | string | null
|
||||
targetPalletCount?: number | string | null
|
||||
sourceUnitCounts?: Record<string, number | string> | null
|
||||
labelPieceCount: number | string | null
|
||||
cartonCount: number | string | null
|
||||
skuCount: number | string
|
||||
duplicateCarton: boolean
|
||||
}
|
||||
export interface RelabelQuantityTotals {
|
||||
knownSourcePalletCount?: number | string
|
||||
unknownSourcePalletOrders?: number | string
|
||||
knownTargetPalletCount?: number | string
|
||||
unknownTargetPalletOrders?: number | string
|
||||
orderCount: number | string
|
||||
skuCount: number | string
|
||||
knownLabelPieceCount: number | string
|
||||
|
||||
@@ -2,6 +2,10 @@ import { apiRequest, apiBlobRequest } from '@/api/http'
|
||||
import type { PageResult } from './warehouseServices'
|
||||
|
||||
export interface ReturnOrderSummary {
|
||||
remainingQuantity?: number
|
||||
receivedBoxCount?: number
|
||||
receivedParcelCount?: number
|
||||
receivedPalletCount?: number
|
||||
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; quantityUnit?: string; 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
|
||||
@@ -30,6 +34,13 @@ export interface ReturnOrderCreate {
|
||||
export function listReturnOrders(params: Record<string, unknown>) {
|
||||
return apiRequest<PageResult<ReturnOrderSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders', params })
|
||||
}
|
||||
export interface ReturnOrderStatistics {
|
||||
totals: { orderCount: number; skuTypes: number; expectedQuantity: number; receivedQuantity: number; remainingQuantity: number; receivedBoxCount: number; receivedParcelCount: number; receivedPalletCount: number }
|
||||
pageDimensions: Array<{ returnNo: string; remainingQuantity: number; receivedBoxCount: number; receivedParcelCount: number; receivedPalletCount: number }>
|
||||
}
|
||||
export function getReturnOrderStatistics(params: Record<string, unknown>) {
|
||||
return apiRequest<ReturnOrderStatistics>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders/statistics', params })
|
||||
}
|
||||
export function getReturnOrder(returnNo: string) {
|
||||
return apiRequest<ReturnOrderDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}` })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
|
||||
export interface TransferQuantityTotals {
|
||||
orderCount: number | string
|
||||
declaredPieceCount: number | string | null
|
||||
knownDeclaredPieceCount: number | string
|
||||
unknownDeclaredOrderCount: number | string
|
||||
receivedPieceCount: number | string | null
|
||||
knownReceivedPieceCount: number | string
|
||||
unknownReceivedOrderCount: number | string
|
||||
declaredCartonCount: number | string
|
||||
receivedCartonCount: number | string
|
||||
receivedPalletCount: number | string
|
||||
availableCartonCount: number | string
|
||||
shippedCartonCount: number | string
|
||||
remainingPalletCount: number | string
|
||||
shippedPalletCount: number | string
|
||||
}
|
||||
|
||||
/** 客户范围由会话决定,汇总使用本次列表完整筛选。 */
|
||||
export function getTransferQuantities(params: Record<string, unknown>) {
|
||||
return apiRequest<TransferQuantityTotals>({ method: 'GET', url: '/merchant/v1/last-leg/warehouse-transfers/quantities', params })
|
||||
}
|
||||
@@ -25,8 +25,8 @@ export interface RelabelDetail {
|
||||
tailOrders?: Array<{ id: string; orderNo: string; overseasWarehouseId: string; packageCount: number; status: string }>
|
||||
completionEvidence?: { taskId: string; photoFileIds: string[]; completionRemark?: string; reviewerName?: string; reviewRemark?: string; completedAt?: string; customerConfirmedAt?: 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; customerConfirmedAt?: string }
|
||||
export interface DiscardBox { cartonId: string; cartonMark: string; palletNo?: string; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
|
||||
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; customerConfirmedAt?: string; skuCodes: string[]; skuCount: number; requestedPackages: number; actualPackages: number | null }
|
||||
export interface DiscardBox { cartonId: string; cartonMark: string; palletNo?: string; skus: Array<{ skuCode: string; productName: string; availablePackages: number; availablePieces?: number | null }> }
|
||||
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
|
||||
|
||||
@@ -26,6 +26,8 @@ watch(() => [props.query, props.relabelNo], async () => {
|
||||
<span>{{ data.orderCount }} 票</span><span>{{ data.skuCount }} 种 SKU</span>
|
||||
<span>贴标件数:{{ dimension(Number(data.knownLabelPieceCount), Number(data.unknownPieceOrders), '件') }}</span>
|
||||
<span>箱数:{{ dimension(Number(data.knownCartonCount), Number(data.unknownCartonOrders), '箱') }}</span>
|
||||
<span>原托盘:{{ dimension(Number(data.knownSourcePalletCount ?? 0), Number(data.unknownSourcePalletOrders ?? data.orderCount), '托') }}</span>
|
||||
<span>本轮目标托盘:{{ dimension(Number(data.knownTargetPalletCount ?? 0), Number(data.unknownTargetPalletOrders ?? data.orderCount), '托') }}</span>
|
||||
<p v-if="relabelNo && data.orders[relabelNo]?.duplicateCarton" role="status">存在重复箱唛关联,箱数已去重,贴标件数待核查。</p>
|
||||
</template>
|
||||
<span v-else>数量加载中…</span>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { getTransferQuantities, type TransferQuantityTotals } from '../api/transferQuantities'
|
||||
const props = defineProps<{ query: Record<string, unknown> }>()
|
||||
const data = ref<TransferQuantityTotals>()
|
||||
const error = ref('')
|
||||
let generation = 0
|
||||
watch(() => JSON.stringify(props.query), async () => {
|
||||
const current = ++generation
|
||||
data.value = undefined; error.value = ''
|
||||
try { const result = await getTransferQuantities(props.query); if (current === generation) data.value = result }
|
||||
catch { if (current === generation) error.value = '转运数量汇总加载失败,请重新查询' }
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
<template>
|
||||
<section class="transfer-quantity-panel" aria-label="仓储转运全筛选汇总">
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<template v-else-if="data">
|
||||
<strong>全部筛选结果 · {{ data.orderCount }} 票</strong>
|
||||
<span>预报:{{ data.declaredPieceCount ?? `未记录(已知 ${data.knownDeclaredPieceCount} 件,${data.unknownDeclaredOrderCount} 票未知)` }}{{ data.declaredPieceCount == null ? '' : ' 件' }} / {{ data.declaredCartonCount }} 箱</span>
|
||||
<span>历史实收:{{ data.receivedPieceCount ?? `未记录(已知 ${data.knownReceivedPieceCount} 件,${data.unknownReceivedOrderCount} 票未知)` }}{{ data.receivedPieceCount == null ? '' : ' 件' }} / {{ data.receivedCartonCount }} 箱 / {{ data.receivedPalletCount }} 托</span>
|
||||
<span>当前已清点 {{ data.availableCartonCount }} 箱 · 剩余 {{ data.remainingPalletCount }} 托 · 已出库 {{ data.shippedCartonCount }} 箱 / {{ data.shippedPalletCount }} 托(托盘可重叠)</span>
|
||||
</template>
|
||||
<span v-else>转运数量加载中…</span>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>
|
||||
.transfer-quantity-panel { display: flex; flex-wrap: wrap; gap: 12px; padding: 12px 16px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface, white); }
|
||||
.transfer-quantity-panel p { margin: 0; }
|
||||
</style>
|
||||
@@ -49,3 +49,14 @@ test('相同仓位重复关联只计一次,零值与未知可用量不进入
|
||||
assert.equal(groupDiscardDocuments([source, source])[0].availablePackages, 2)
|
||||
for (const availablePackages of [0, null, undefined, 'invalid']) assert.equal(groupDiscardDocuments([{ ...source, availablePackages }]).length, 0)
|
||||
})
|
||||
|
||||
test('相同来源的混箱分片保留全部SKU和实箱,且不修改接口原始对象', () => {
|
||||
const a = { ...returned('1'), sourceType: 'INVENTORY', inventoryPositionId: 'P', skus: [{ skuCode: 'A', availablePackages: 1 }] }
|
||||
const b = { ...a, skus: [{ skuCode: 'B', availablePackages: 1 }], cartons: [{ ...a.cartons[0], skus: [{ skuCode: 'B', availablePackages: 1 }] }] }
|
||||
const doc = groupDiscardDocuments([a, b])[0]
|
||||
assert.equal(doc.availablePackages, 1)
|
||||
assert.equal(doc.sources.length, 1)
|
||||
assert.deepEqual(doc.sources[0].skus.map(sku => sku.skuCode), ['A', 'B'])
|
||||
assert.deepEqual(doc.sources[0].cartons[0].skus.map(sku => sku.skuCode), ['A', 'B'])
|
||||
assert.equal(a.skus.length, 1)
|
||||
})
|
||||
|
||||
@@ -7,15 +7,28 @@ export interface DiscardDocument extends DiscardCandidate {
|
||||
/** 下拉按来源单据和操作仓汇总;原始库存身份保留给选箱与提交。 */
|
||||
export function groupDiscardDocuments(candidates: DiscardCandidate[]): DiscardDocument[] {
|
||||
const groups = new Map<string, DiscardDocument>()
|
||||
const identities = new Set<string>()
|
||||
const identities = new Map<string, DiscardCandidate>()
|
||||
for (const candidate of candidates) {
|
||||
const available = Number(candidate.availablePackages)
|
||||
if (candidate.availablePackages == null || !Number.isFinite(available) || available <= 0) continue
|
||||
// 按真实来源身份去重;缺身份的旧接口行仍独立保留,不合并不同库存。
|
||||
const identity = candidate.sourceType === 'RETURN_CARTON' ? candidate.returnCartonId : candidate.inventoryPositionId || candidate.orderId
|
||||
const sourceKey = identity ? JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, identity]) : null
|
||||
if (sourceKey && identities.has(sourceKey)) continue
|
||||
if (sourceKey) identities.add(sourceKey)
|
||||
const existing = sourceKey ? identities.get(sourceKey) : undefined
|
||||
if (existing) {
|
||||
// 同一仓位的分片响应合并 SKU 和箱,重复关联不会丢掉箱内商品。
|
||||
existing.skus = [...new Map([...existing.skus, ...candidate.skus].map(sku => [sku.skuCode, sku])).values()]
|
||||
const cartons = new Map((existing.cartons || []).map(box => [box.cartonId, box]))
|
||||
for (const box of candidate.cartons || []) {
|
||||
const old = cartons.get(box.cartonId)
|
||||
cartons.set(box.cartonId, old ? { ...old, skus: [...new Map([...old.skus, ...box.skus].map(sku => [sku.skuCode, sku])).values()] } : { ...box })
|
||||
}
|
||||
existing.cartons = [...cartons.values()]
|
||||
existing.availablePackages = Math.max(Number(existing.availablePackages), available)
|
||||
continue
|
||||
}
|
||||
const source = { ...candidate, skus: [...candidate.skus], cartons: candidate.cartons?.map(box => ({ ...box, skus: [...box.skus] })) }
|
||||
if (sourceKey) identities.set(sourceKey, source)
|
||||
const documentNo = candidate.inboundOrderNo || candidate.inboundCode
|
||||
const key = JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, documentNo || candidate.orderId])
|
||||
let document = groups.get(key)
|
||||
@@ -23,7 +36,7 @@ export function groupDiscardDocuments(candidates: DiscardCandidate[]): DiscardDo
|
||||
document = { ...candidate, orderId: key, sources: [], availablePackages: 0 }
|
||||
groups.set(key, document)
|
||||
}
|
||||
document.sources.push(candidate)
|
||||
document.sources.push(source)
|
||||
}
|
||||
for (const document of groups.values()) {
|
||||
const boxes = new Set<string>()
|
||||
|
||||
@@ -6,6 +6,45 @@ import ts from 'typescript'
|
||||
const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
||||
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
|
||||
|
||||
function listHarness(list, summary) {
|
||||
const state = { query: { page: 1, size: 1, skuCode: 'FILTER' }, loading: { value: false }, listError: { value: '' }, statistics: { value: undefined }, rows: { value: [] }, total: { value: 0 } }
|
||||
const source = view.slice(view.indexOf('async function load()'), view.indexOf('\nfunction changePage('))
|
||||
const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const load = new Function(...Object.keys(state), 'listReturnOrders', 'getReturnOrderStatistics', `let listRequest=0; ${js}; return load`)(...Object.values(state), list, summary)
|
||||
return { ...state, load }
|
||||
}
|
||||
|
||||
test('全筛选统计使用服务器总计,分页行只按单号映射载体,不重算SKU', async () => {
|
||||
const calls = []
|
||||
const totals = { orderCount: 3, skuTypes: 3, expectedQuantity: 19, receivedQuantity: 14, remainingQuantity: 5 }
|
||||
const h = listHarness(async p => { calls.push(p); return { total: '3', records: [{ returnNo: 'RT-1', receivedQuantity: 8 }] } }, async p => { calls.push(p); return { totals, pageDimensions: [{ returnNo: 'RT-1', receivedParcelCount: 1, receivedBoxCount: 1 }] } })
|
||||
await h.load()
|
||||
assert.deepEqual(h.statistics.value, totals)
|
||||
assert.equal(h.rows.value[0].receivedParcelCount, 1)
|
||||
h.query.page = 2
|
||||
await h.load()
|
||||
assert.equal(h.statistics.value.receivedQuantity, 14)
|
||||
assert.deepEqual(calls[2], calls[3])
|
||||
assert.equal(calls[3].page, 2)
|
||||
})
|
||||
|
||||
test('统计失败清空旧列表和汇总,并忽略较旧查询的迟到结果', async () => {
|
||||
let fail = false, finish
|
||||
const h = listHarness(async () => ({ total: 1, records: [{ returnNo: 'RT-1' }] }), async p => {
|
||||
if (fail) throw Error('统计服务不可用')
|
||||
if (p.page === 1) return new Promise(resolve => { finish = resolve })
|
||||
return { totals: { orderCount: 2 }, pageDimensions: [] }
|
||||
})
|
||||
const old = h.load(); h.query.page = 2; await h.load()
|
||||
finish({ totals: { orderCount: 999 }, pageDimensions: [] }); await old
|
||||
assert.equal(h.statistics.value.orderCount, 2)
|
||||
fail = true; await h.load()
|
||||
assert.equal(h.statistics.value, undefined)
|
||||
assert.deepEqual(h.rows.value, [])
|
||||
assert.equal(h.listError.value, '统计服务不可用')
|
||||
assert.equal(h.loading.value, false)
|
||||
})
|
||||
|
||||
function submitHarness(failure) {
|
||||
const source = view.slice(view.indexOf('async function submit()'), view.indexOf('\nfunction statusType('))
|
||||
const state = { requests: [], loads: 0, routes: [], errors: [], successes: [] }
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-l
|
||||
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||||
import { getReturnOrderStatistics, type ReturnOrderStatistics } from '../api/returnOrders'
|
||||
import { getReturnInventoryLabel } from '../api/returnInventory'
|
||||
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
|
||||
|
||||
@@ -40,6 +41,9 @@ const printableBoxCount = computed(() => receiptPrintableBoxCount(printableRecei
|
||||
const photoUrls = reactive<Record<string, string>>({})
|
||||
const rows = ref<ReturnOrderSummary[]>([])
|
||||
const total = ref(0)
|
||||
const statistics = ref<ReturnOrderStatistics['totals']>()
|
||||
const listError = ref('')
|
||||
let listRequest = 0
|
||||
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||||
const operationWarehousesLoading = ref(false)
|
||||
const operationWarehousesError = ref('')
|
||||
@@ -62,8 +66,13 @@ const tableColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 220, fixed: 'left' },
|
||||
{ prop: 'returnNo', label: '退货单号', minWidth: 190 }, { prop: 'customerReferenceNo', label: '客户参考号', minWidth: 160 },
|
||||
{ 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: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报(件)', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收(件)', width: 110 },
|
||||
{ prop: 'remainingQuantity', label: '待收(件)', width: 110 },
|
||||
{ prop: 'receivedBoxCount', label: '签收箱数', width: 110 },
|
||||
{ prop: 'receivedParcelCount', label: '签收包裹数', width: 115 },
|
||||
{ prop: 'receivedPalletCount', label: '签收托数', width: 110 },
|
||||
{ key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||
{ key: 'relabelApplications', label: '换标子单', slotName: 'relabelApplications', minWidth: 340 },
|
||||
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
||||
]
|
||||
@@ -147,9 +156,21 @@ async function openEdit(returnNo: string) {
|
||||
function addItem() { form.items.push(emptySku()) }
|
||||
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
||||
async function load() {
|
||||
const request = ++listRequest
|
||||
loading.value = true
|
||||
try { const response = await listReturnOrders({ ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }); rows.value = response.records; total.value = Number(response.total); if (!Number.isFinite(total.value)) total.value = response.records.length }
|
||||
finally { loading.value = false }
|
||||
listError.value = ''; statistics.value = undefined
|
||||
try {
|
||||
const params = { ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }
|
||||
const [response, summary] = await Promise.all([listReturnOrders(params), getReturnOrderStatistics(params)])
|
||||
if (request !== listRequest) return
|
||||
const dimensions = new Map(summary.pageDimensions.map(row => [row.returnNo, row]))
|
||||
rows.value = response.records.map(row => ({ ...row, ...dimensions.get(row.returnNo) }))
|
||||
total.value = Number(response.total); statistics.value = summary.totals
|
||||
} catch (error) {
|
||||
if (request !== listRequest) return
|
||||
rows.value = []; total.value = 0
|
||||
listError.value = error instanceof Error ? error.message : '退货列表及汇总加载失败'
|
||||
} finally { if (request === listRequest) loading.value = false }
|
||||
}
|
||||
function changePage(page: number) {
|
||||
if (page === query.page) return
|
||||
@@ -323,6 +344,11 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
|
||||
<template v-else-if="!props.detailPage">
|
||||
<QueryToolbar v-model="queryModel" class="return-query" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
|
||||
<el-alert v-if="listError" type="error" :closable="false" :title="listError"><el-button @click="load">重新加载</el-button></el-alert>
|
||||
<div v-if="statistics && !loading" class="return-filter-summary" role="status" aria-label="退货全筛选汇总">
|
||||
全筛选:{{ statistics.orderCount }} 票 · {{ statistics.skuTypes }} 种 SKU · 预报 {{ statistics.expectedQuantity }} 件 · 实收 {{ statistics.receivedQuantity }} 件 · 待收 {{ statistics.remainingQuantity }} 件 · 签收 {{ statistics.receivedBoxCount }} 箱 / {{ statistics.receivedParcelCount }} 包裹 / {{ statistics.receivedPalletCount }} 托
|
||||
<span>预报箱/包裹/托数:未提供</span>
|
||||
</div>
|
||||
<div class="return-table-card">
|
||||
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
|
||||
@@ -364,7 +390,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<section class="return-detail-section">
|
||||
<h3>签收概览</h3>
|
||||
<div class="return-detail-grid">
|
||||
<div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</strong></div>
|
||||
<div><span>预报(件)</span><strong>{{ selected.order.expectedQuantity }}</strong></div><div><span>实收(件)</span><strong>{{ selected.order.receivedQuantity }}</strong></div><div><span>待收(件)</span><strong>{{ Math.max(selected.order.expectedQuantity - selected.order.receivedQuantity, 0) }}</strong></div>
|
||||
<div><span>SKU 种类</span><strong>{{ selected.order.skuTypes }}</strong></div>
|
||||
<div><span>是否换标</span><strong>{{ selected.order.needRelabel ? '需要换标' : '否' }}</strong></div>
|
||||
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
||||
@@ -475,6 +501,8 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.return-filter-summary { padding:12px 16px; margin-bottom:12px; border:1px solid var(--client-border); border-radius:8px; background:var(--client-surface,#fff); line-height:1.8; }
|
||||
.return-filter-summary span { margin-left:16px; color:var(--client-text-secondary,#64748b); }
|
||||
.return-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,280px)) max-content; gap:14px 18px; align-items:end; }
|
||||
.return-query :deep(.query-toolbar__filters) { display:contents; }
|
||||
.return-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,7 +28,7 @@ test('废弃来源包含通用仓位,选择后带出全部库存且不把选
|
||||
function selectionRows(boxes, selectedKeys) {
|
||||
const body = view.match(/function syncDiscardSelection\(\) \{([\s\S]*?)\n\}/)[1].replace('new Map<string, CargoRow>()', 'new Map()')
|
||||
const rows = { value: [] }
|
||||
new Function('discardSelectableBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({}))
|
||||
new Function('discardSourceBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({}))
|
||||
return rows.value
|
||||
}
|
||||
test('部分废弃只提交勾选的箱子,数量由箱子汇总,清空后无明细', () => {
|
||||
@@ -48,3 +48,12 @@ test('混合SKU箱按实物箱只计一次,退货箱仍整箱携带全部SKU',
|
||||
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))
|
||||
})
|
||||
|
||||
test('通用混箱同仓位保留全部SKU,跨仓位相同SKU保持独立来源', () => {
|
||||
const skus = [{ skuCode: 'A', availablePackages: 1 }, { skuCode: 'B', availablePackages: 1 }]
|
||||
const source = { orderId: null, inventoryPositionId: '42', sourceType: 'INVENTORY', availablePackages: 1, skus }
|
||||
const boxes = [{ key: 'INVENTORY:1', cartonId: '1', source, skus }, { key: 'INVENTORY:2', cartonId: '2', source: { ...source, inventoryPositionId: '43' }, skus: [skus[0]] }]
|
||||
const rows = selectionRows(boxes, ['INVENTORY:1', 'INVENTORY:2'])
|
||||
assert.equal(rows.length, 3)
|
||||
assert.deepEqual(rows.map(row => [row.sourceInventoryPositionId, row.sku, row.quantity, row.discardCartonIds]), [['42', 'A', 1, ['1']], ['42', 'B', 1, ['1']], ['43', 'A', 1, ['2']]])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user