feat(overseas): select stock transfer sources and follow relabel
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { InventoryBalanceDto, InventoryPositionDto } from './inventory'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload, type OverseasPageResult } from './lastMileOrders'
|
||||
import type { WarehouseTransferDetailDto } from './warehouseTransfers'
|
||||
|
||||
export interface WarehouseTransferSourceCandidate {
|
||||
inventoryPositionId: string
|
||||
palletId: string
|
||||
inboundCode: string
|
||||
skuCode: string
|
||||
productNameZh: string
|
||||
palletNo: string
|
||||
warehouseLocationCode: string
|
||||
availablePackages: number
|
||||
canSelect: boolean
|
||||
blockReason: string
|
||||
}
|
||||
type ScopedPosition = InventoryPositionDto & { customerId: string }
|
||||
export async function listWarehouseTransferSources(customerId: string, transfer: WarehouseTransferDetailDto) {
|
||||
const sources = new Map((transfer.sourceOrders || []).map(row => [row.inboundOrderId, row]))
|
||||
if (!customerId || !transfer.operatingWarehouseId || !sources.size || [...sources.values()].some(row => !row.inboundOrderId || !row.inboundCode)) {
|
||||
throw new ApiResponseError('本票绑定入仓或操作仓资料不完整,需核查后选择库存来源。', 'TRANSFER_SOURCE_INCOMPLETE')
|
||||
}
|
||||
const oldSkus = new Set(transfer.relabelBindings.map(row => row.originalSku?.trim()).filter(Boolean))
|
||||
if (transfer.needRelabel && !oldSkus.size) throw new ApiResponseError('本票缺少明确旧 SKU,需核查换标要求后选择来源。', 'TRANSFER_SKU_MISSING')
|
||||
const candidates = new Map<string, WarehouseTransferSourceCandidate>(), issues: string[] = []
|
||||
for (const source of sources.values()) {
|
||||
const representedIds = new Set<string>()
|
||||
let currentPage = 1, total = -1, recordsRead = 0
|
||||
do {
|
||||
const page: OverseasPageResult<InventoryBalanceDto> = normalizeOverseasPage(unwrapApiResponse(await http.get<ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>, ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>>(
|
||||
'/platform/v1/last-leg/inventory/balances', { params: { customerId, overseasWarehouseId: transfer.operatingWarehouseId, inboundCode: source.inboundCode, page: currentPage, size: 200 } },
|
||||
)))
|
||||
if (page.page !== currentPage || page.size !== 200 || !Number.isSafeInteger(page.total) || page.total < 0 || page.pages !== Math.ceil(page.total / 200) || (total >= 0 && total !== page.total) || page.records.length !== Math.min(200, Math.max(0, page.total - (currentPage - 1) * 200))) {
|
||||
throw new ApiResponseError('来源库存分页发生变化或不完整,请重新加载,暂不能分配。', 'TRANSFER_SOURCE_PAGE_CHANGED')
|
||||
}
|
||||
total = page.total; recordsRead += page.records.length
|
||||
for (const representative of page.records) {
|
||||
if (!representative.id || representedIds.has(representative.id)) throw new ApiResponseError('来源库存分页含重复或缺失代表记录,请核查。', 'TRANSFER_SOURCE_AMBIGUOUS')
|
||||
representedIds.add(representative.id)
|
||||
const base = `/platform/v1/last-leg/inventory/balances/${encodeURIComponent(representative.id)}`
|
||||
const balance = unwrapApiResponse(await http.get<ApiEnvelope<InventoryBalanceDto>, ApiEnvelope<InventoryBalanceDto>>(base, { params: { customerId } }))
|
||||
if (balance.id !== representative.id || balance.customerId !== customerId || balance.overseasWarehouseId !== transfer.operatingWarehouseId || balance.inboundOrderId !== source.inboundOrderId || balance.inboundCode !== source.inboundCode || (representative.skuCode && balance.skuCode !== representative.skuCode)) {
|
||||
issues.push(`入仓码 ${source.inboundCode} 的库存代表与本票来源不一致,待核查;未使用该聚合行分配。`)
|
||||
continue
|
||||
}
|
||||
const positions = unwrapApiResponse(await http.get<ApiEnvelope<ScopedPosition[]>, ApiEnvelope<ScopedPosition[]>>(`${base}/positions`, { params: { customerId } }))
|
||||
for (const position of positions) {
|
||||
if (position.customerId !== customerId || position.overseasWarehouseId !== transfer.operatingWarehouseId || position.balanceId !== balance.id || position.inboundOrderId !== source.inboundOrderId || position.inboundCode !== source.inboundCode || !position.id || candidates.has(position.id)) {
|
||||
issues.push(`入仓码 ${source.inboundCode} 的库存位置归属不明确或重复,待核查。`)
|
||||
continue
|
||||
}
|
||||
const sku = balance.skuCode?.trim() || ''
|
||||
const reason = !position.palletId || !position.palletNo ? '缺少完整托盘业务码,待核查'
|
||||
: !Number.isSafeInteger(position.availablePackages) || position.availablePackages <= 0 ? '当前无可分配箱数'
|
||||
: transfer.needRelabel && (!sku || sku === 'MIXED' || !oldSkus.has(sku)) ? '来源不能证明本票具体旧 SKU,待核查' : ''
|
||||
candidates.set(position.id, { inventoryPositionId: position.id, palletId: position.palletId || '', inboundCode: source.inboundCode || '', skuCode: sku === 'MIXED' ? '—' : sku || '—', productNameZh: balance.productNameZh || '—', palletNo: position.palletNo || '—', warehouseLocationCode: position.warehouseLocationCode || '—', availablePackages: position.availablePackages, canSelect: !reason, blockReason: reason })
|
||||
}
|
||||
}
|
||||
currentPage++
|
||||
} while (recordsRead < total)
|
||||
}
|
||||
return { candidates: [...candidates.values()], issues }
|
||||
}
|
||||
Reference in New Issue
Block a user