feat(consignment): 优化寄售仓库与接单工单双向手动匹配,支持工单号与提报单号检索与强制匹配
1. 寄售仓库手动匹配订单扩充支持 open 状态工单,增强 WO 工单号与纯数字的模糊/前缀搜索,修复工单搜不到的问题并在卡片高亮展示内部工单号与状态; 2. 接单工单操作列全面支持【手动匹配寄售】,允许从接单工单主动调起寄售匹配弹窗; 3. 寄售现货筛选支持通过 SUB 提报单号、INV 仓库号、供货打手等快速搜索; 4. 完善规格冲突下的强制匹配确认与必填原因提交机制,确保履约全流程留痕合规。
This commit is contained in:
@@ -846,13 +846,13 @@ export function buildWorkOrderWhere({
|
||||
params.push(normalizedKeyword)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
`(wo.platform_order_id = ${paramIndex} OR wo.platform_order_id LIKE ${paramIndex} || '%')`,
|
||||
`(wo.platform_order_id = ${paramIndex} OR wo.platform_order_id LIKE ${paramIndex} || '%' OR wo.work_order_no ILIKE '%' || ${paramIndex} || '%')`,
|
||||
)
|
||||
} else if (isWorkOrderNoLookupKeyword(normalizedKeyword)) {
|
||||
params.push(normalizedKeyword)
|
||||
const paramIndex = `$${params.length}`
|
||||
filters.push(
|
||||
`(wo.work_order_no = ${paramIndex} OR wo.work_order_no ILIKE ${paramIndex} || '%' OR LOWER(wo.work_order_no) = LOWER(${paramIndex}))`,
|
||||
`(wo.work_order_no = ${paramIndex} OR wo.work_order_no ILIKE ${paramIndex} || '%' OR wo.work_order_no ILIKE '%' || ${paramIndex} || '%' OR LOWER(wo.work_order_no) = LOWER(${paramIndex}))`,
|
||||
)
|
||||
} else {
|
||||
params.push(`%${normalizedKeyword}%`)
|
||||
|
||||
@@ -250,7 +250,11 @@ export async function listInventoryMatchableOrders(
|
||||
}
|
||||
|
||||
const { items } = await listWorkOrders({
|
||||
statuses: [WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.PENDING_MATERIAL],
|
||||
statuses: [
|
||||
WORK_ORDER_STATUS.UNASSIGNED,
|
||||
WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
WORK_ORDER_STATUS.OPEN,
|
||||
],
|
||||
keyword: String(input.keyword || '').trim(),
|
||||
pageSize: Math.min(Math.max(Number(input.pageSize || 50), 1), 100),
|
||||
sort: 'id_desc',
|
||||
@@ -358,11 +362,13 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
||||
throw createHttpError('工单不存在', { statusCode: 404, errorCode: 'work_order_not_found' })
|
||||
}
|
||||
|
||||
// 仅未分配或待补资料工单支持选用库存履约
|
||||
// 未分配、待补资料或大厅待接单工单支持选用库存履约
|
||||
if (
|
||||
![WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.PENDING_MATERIAL].includes(
|
||||
workOrder.status as any,
|
||||
)
|
||||
![
|
||||
WORK_ORDER_STATUS.UNASSIGNED,
|
||||
WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
WORK_ORDER_STATUS.OPEN,
|
||||
].includes(workOrder.status as any)
|
||||
) {
|
||||
throw createHttpError('当前工单状态不允许直接使用内部库存履约', {
|
||||
statusCode: 409,
|
||||
|
||||
@@ -141,6 +141,10 @@ export default function ConsignmentManualMatchModal({
|
||||
item.inventory.system,
|
||||
item.inventory.serverZone,
|
||||
item.inventory.series,
|
||||
item.inventory.workerName,
|
||||
item.inventory.buyerName,
|
||||
item.inventory.sellerGameId,
|
||||
item.inventory.id,
|
||||
]),
|
||||
[options, keyword],
|
||||
)
|
||||
@@ -148,10 +152,12 @@ export default function ConsignmentManualMatchModal({
|
||||
() =>
|
||||
filterManualMatchOptions(orders, keyword, (item) => [
|
||||
item.workOrderNo,
|
||||
item.workOrderId,
|
||||
item.productName,
|
||||
item.requiredSpec.system,
|
||||
item.requiredSpec.serverZone,
|
||||
item.requiredSpec.series,
|
||||
item.status,
|
||||
]),
|
||||
[orders, keyword],
|
||||
)
|
||||
@@ -179,8 +185,8 @@ export default function ConsignmentManualMatchModal({
|
||||
workOrderId: selected.workOrderId,
|
||||
inventoryId: selected.inventoryId,
|
||||
workerId: selected.workerId,
|
||||
allowPartial: selected.matchType !== 'exact',
|
||||
allowSpecMismatch: selected.matchType === 'conflict',
|
||||
allowPartial: selected.matchType !== 'exact' || confirmedPartial,
|
||||
allowSpecMismatch: selected.matchType === 'conflict' || confirmedMismatch,
|
||||
matchNote: matchNote.trim(),
|
||||
})
|
||||
|
||||
@@ -259,8 +265,8 @@ export default function ConsignmentManualMatchModal({
|
||||
<Input
|
||||
placeholder={
|
||||
mode === 'order'
|
||||
? '搜索物品名 / 寄卖单号 / 仓库号 / 规格'
|
||||
: '搜索工单号 / 平台单号 / 标题 / 规格'
|
||||
? '搜索物品名 / 寄卖单号 (SUB...) / 仓库号 (INV...) / 规格 / 供货打手'
|
||||
: '搜索工单号 (WO...) / 平台单号 / 标题 / 规格'
|
||||
}
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
@@ -296,6 +302,9 @@ export default function ConsignmentManualMatchModal({
|
||||
>
|
||||
<Space direction="vertical" size={2}>
|
||||
<Space size={6} wrap>
|
||||
<Tag color="cyan">
|
||||
{option.inventory.sourceSubmissionNo || option.inventory.inventoryNo}
|
||||
</Tag>
|
||||
<Text strong>{option.inventory.itemName}</Text>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
</Space>
|
||||
@@ -369,9 +378,16 @@ export default function ConsignmentManualMatchModal({
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }} align="start">
|
||||
<Space direction="vertical" size={2}>
|
||||
<Space size={6} wrap>
|
||||
<Text strong>#{order.workOrderId}</Text>
|
||||
<Text>{order.productName}</Text>
|
||||
<Tag color="purple">{order.workOrderNo || `#${order.workOrderId}`}</Tag>
|
||||
<Text strong>{order.productName}</Text>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
{order.status === 'open' ? (
|
||||
<Tag color="blue">大厅待抢</Tag>
|
||||
) : order.status === 'unassigned' ? (
|
||||
<Tag color="orange">待指派</Tag>
|
||||
) : order.status === 'pending_material' ? (
|
||||
<Tag color="gold">待补资料</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
要求:{order.requiredSpec.system || '未指定'}/
|
||||
@@ -398,7 +414,7 @@ export default function ConsignmentManualMatchModal({
|
||||
onClick={() => {
|
||||
setSelected({
|
||||
workOrderId: order.workOrderId,
|
||||
workOrderLabel: `#${order.workOrderId} ${order.productName}`,
|
||||
workOrderLabel: `${order.workOrderNo || `#${order.workOrderId}`} · ${order.productName}`,
|
||||
inventoryId: inventory?.inventoryId || 0,
|
||||
inventoryLabel,
|
||||
matchType: order.matchType,
|
||||
|
||||
@@ -1644,7 +1644,7 @@ export default function WorkOrdersPanel() {
|
||||
onClick={() => setManualMatchOrder(row)}
|
||||
title="从全部在库现货里人工指定一件(支持规格冲突强制匹配,需填原因)"
|
||||
>
|
||||
手动匹配现货
|
||||
手动匹配寄售
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
|
||||
|
||||
@@ -74,9 +74,9 @@ export function filterManualMatchOptions<T>(
|
||||
/**
|
||||
* 哪些订单状态可以手动指定现货。
|
||||
*
|
||||
* 与后端 `fulfillWorkOrderWithConsignment` 的前置校验保持一致(只允许 unassigned /
|
||||
* pending_material):入口先按同一口径收敛,避免"按钮点了才报错"。
|
||||
* 与后端 `fulfillWorkOrderWithConsignment` 的前置校验保持一致(允许 unassigned /
|
||||
* pending_material / open):入口先按同一口径收敛,避免"按钮点了才报错"。
|
||||
*/
|
||||
export function isManuallyMatchableOrderStatus(status: string): boolean {
|
||||
return status === 'unassigned' || status === 'pending_material'
|
||||
return status === 'unassigned' || status === 'pending_material' || status === 'open'
|
||||
}
|
||||
|
||||
@@ -60,11 +60,12 @@ test('关键词过滤:现货能按寄卖单号/物品名搜到,大小写与
|
||||
assert.equal(filterManualMatchOptions(list, '不存在', pick).length, 0)
|
||||
})
|
||||
|
||||
test('只有待指派/待补资料的订单能手动指定现货(与后端前置校验同口径)', async () => {
|
||||
test('待指派/待补资料/大厅待抢的订单均能手动指定现货(与后端前置校验同口径)', async () => {
|
||||
const { isManuallyMatchableOrderStatus } =
|
||||
await import('../src/utils/consignment-manual-match.ts')
|
||||
assert.equal(isManuallyMatchableOrderStatus('unassigned'), true)
|
||||
assert.equal(isManuallyMatchableOrderStatus('pending_material'), true)
|
||||
assert.equal(isManuallyMatchableOrderStatus('open'), true)
|
||||
for (const status of ['in_progress', 'pending_acceptance', 'accepted', 'cancelled', 'disputed']) {
|
||||
assert.equal(isManuallyMatchableOrderStatus(status), false, `${status} 不该给手动匹配入口`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user