fix(logistics-arrival): 换货单不再把已换货的 0 数量行算成条码

内部单号 9627695(赔偿换货单)看板显示 X00005、SJ0061100N430V01 两个条码,
但聚水潭里 SJ0061100N430V01 是 qty=0 / item_status=Replaced 的"已换货"原始行,
实际发出的是手工换货替换品 X00005,看板应只显示 X00005。

- 条码匹配时跳过 qty<=0 的明细行(已换货/取消的原始行不产生条码);
- 明细查询补上 oi.qty 字段;
- 补单测:换货单只保留实际发出条码、0/负数行不计入,并把原有两条条码用例补上 qty。

实测(10-01~10-09 窗口):jst_OrderItem 2436 行、jst_qmcust_orderlist_item 3007 行
数量为 0 且全部是 Replaced;过滤后没有任何订单会变成无条码。

注:快照里的条码是生成时固化的,快照区间需重跑当天批次后才会更新。
任务名:物流到货签收时长-deepseek
This commit is contained in:
apple 2026-10-10 13:48:03 +08:00
parent 158995b71b
commit 253c803be6
3 changed files with 52 additions and 14 deletions

View File

@ -108,19 +108,35 @@ describe('物流到货签收时长明细', () => {
expect(logisticsArrivalSigningDurationArchiveFallbackBarcodes('')).toEqual([])
})
it('订单条码只用本单明细,不并集同一物流单号上的其它订单', () => {
// 真实案例:9627695 的物流单号是 75627908174893,同单号下还有 9593499 的两项变体条码。
it('换货单只保留实际发出的条码(qty=0 的已换货行不计)', () => {
// 真实案例:9627695 是赔偿换货单——SJ0061100N430V01 是 qty=0 / item_status=Replaced 的原始行,
// 实际发出的是手工换货替换品 X00005;同一物流单号 75627908174893 上还挂着补发单 9593499。
const rows = [{
orderKeys: ['9627695', '351207604012'],
logisticsPayKey: '75627908174893|2026-10-03 17:35:36',
logisticsId: '75627908174893',
}]
const items = [
{ orderKeys: ['9627695', '351207604012'], logisticsPayKey: '@75627908174893|2026-10-03 17:35:36', logisticsId: '@75627908174893', barcode: 'SJ0061100N430V01' },
{ orderKeys: ['9627695', '351207604012'], logisticsPayKey: '@75627908174893|2026-10-03 17:35:36', logisticsId: '@75627908174893', barcode: 'X00005' },
{ orderKeys: ['9627695', '351207604012'], logisticsPayKey: '@75627908174893|2026-10-03 17:35:36', logisticsId: '@75627908174893', barcode: 'SJ0061100N430V01', qty: 0 },
{ orderKeys: ['9627695', '351207604012'], logisticsPayKey: '@75627908174893|2026-10-03 17:35:36', logisticsId: '@75627908174893', barcode: 'X00005', qty: 1 },
// 同物流单号的另一张订单(合并发货)——不能串进本单
{ orderKeys: ['9593499', '351207604012-9'], logisticsPayKey: '75627908174893|2026-10-03 18:00:00', logisticsId: '75627908174893', barcode: 'SJ0061100N430V01-5' },
{ orderKeys: ['9593499', '351207604012-9'], logisticsPayKey: '75627908174893|2026-10-03 18:00:00', logisticsId: '75627908174893', barcode: 'SJ0061100N430V01-7' },
{ orderKeys: ['9593499', '3634438015247023'], logisticsPayKey: '75627908174893|2026-09-30 16:49:13', logisticsId: '75627908174893', barcode: 'SJ0061100N430V01-5', qty: 5 },
{ orderKeys: ['9593499', '3634438015247023'], logisticsPayKey: '75627908174893|2026-09-30 16:49:13', logisticsId: '75627908174893', barcode: 'SJ0061100N430V01-7', qty: 4 },
]
expect(matchLogisticsArrivalSigningDurationBarcodes(rows, items)).toEqual(['X00005'])
})
it('订单条码只用本单明细,不并集同一物流单号上的其它订单', () => {
const rows = [{
orderKeys: ['9627695', '351207604012'],
logisticsPayKey: '75627908174893|2026-10-03 17:35:36',
logisticsId: '75627908174893',
}]
const items = [
{ orderKeys: ['9627695', '351207604012'], logisticsPayKey: '@75627908174893|2026-10-03 17:35:36', logisticsId: '@75627908174893', barcode: 'SJ0061100N430V01', qty: 1 },
{ orderKeys: ['9627695', '351207604012'], logisticsPayKey: '@75627908174893|2026-10-03 17:35:36', logisticsId: '@75627908174893', barcode: 'X00005', qty: 1 },
{ orderKeys: ['9593499', '3634438015247023'], logisticsPayKey: '75627908174893|2026-09-30 16:49:13', logisticsId: '75627908174893', barcode: 'SJ0061100N430V01-5', qty: 5 },
{ orderKeys: ['9593499', '3634438015247023'], logisticsPayKey: '75627908174893|2026-09-30 16:49:13', logisticsId: '75627908174893', barcode: 'SJ0061100N430V01-7', qty: 4 },
]
expect(matchLogisticsArrivalSigningDurationBarcodes(rows, items)).toEqual(['SJ0061100N430V01, X00005'])
})
@ -133,12 +149,12 @@ describe('物流到货签收时长明细', () => {
{ orderKeys: ['7777777'], logisticsPayKey: '', logisticsId: '79000000000003' },
]
const items = [
{ orderKeys: ['8888888'], logisticsPayKey: '', logisticsId: '79000000000001', barcode: 'A1' },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000001', barcode: 'A1' },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000001', barcode: 'B2' },
{ orderKeys: [], logisticsPayKey: '79000000000002|2026-10-01 10:00:00', logisticsId: '79000000000002', barcode: 'C3' },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000003', barcode: 'D4' },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000003', barcode: 'D4' },
{ orderKeys: ['8888888'], logisticsPayKey: '', logisticsId: '79000000000001', barcode: 'A1', qty: 1 },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000001', barcode: 'A1', qty: 1 },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000001', barcode: 'B2', qty: 1 },
{ orderKeys: [], logisticsPayKey: '79000000000002|2026-10-01 10:00:00', logisticsId: '79000000000002', barcode: 'C3', qty: 1 },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000003', barcode: 'D4', qty: 1 },
{ orderKeys: [], logisticsPayKey: '', logisticsId: '79000000000003', barcode: 'D4', qty: 1 },
]
expect(matchLogisticsArrivalSigningDurationBarcodes(rows, items)).toEqual([
// 第一行精确命中 A1(去重后只剩一条),不再叠加物流单号上的 B2
@ -150,6 +166,16 @@ describe('物流到货签收时长明细', () => {
])
})
it('数量为 0 或负数的明细不计入条码', () => {
const rows = [{ orderKeys: ['777'], logisticsPayKey: '', logisticsId: '' }]
const items = [
{ orderKeys: ['777'], logisticsPayKey: '', logisticsId: '', barcode: '已换货行', qty: 0 },
{ orderKeys: ['777'], logisticsPayKey: '', logisticsId: '', barcode: '负数行', qty: -1 },
{ orderKeys: ['777'], logisticsPayKey: '', logisticsId: '', barcode: '正常行', qty: 2 },
]
expect(matchLogisticsArrivalSigningDurationBarcodes(rows, items)).toEqual(['正常行'])
})
it('按中国日期生成默认月份范围并规范页码', () => {
expect(normalizeLogisticsArrivalSigningDurationFilters({ page: 0, pageSize: 100 }, new Date('2026-09-24T01:00:00Z'))).toMatchObject({
dateFrom: '2026-09-01',

View File

@ -213,6 +213,11 @@ export type LogisticsArrivalSigningDurationBarcodeItemKeys = {
logisticsPayKey: string
logisticsId: string
barcode: string
/**
* 明细数量。聚水潭把"已换货"的原始商品行留在明细里但数量为 0
* (`item_status = Replaced`),那不是实际发出的商品,不产生条码。
*/
qty: number
}
function uniqueNonEmpty(values: string[]) {
@ -226,6 +231,9 @@ function uniqueNonEmpty(values: string[]) {
* 物流单号是**最后兜底**——一个物流单号可能合并发货多个订单(也可能被两套
* 订单表分别登记),无脑并集会把别单的条码串到本单,且同一张单在不同分页
* 会显示不同条码。只有精确键一个都匹配不到时才退到物流单号。
*
* 另外数量 <= 0 的明细("已换货 Replaced"的原始行)直接跳过:
* 例:换货单 9627695 里 `SJ0061100N430V01` 是 qty=0 的被换行,实际发出的是 `X00005`。
*/
export function matchLogisticsArrivalSigningDurationBarcodes(
rows: LogisticsArrivalSigningDurationBarcodeRowKeys[],
@ -239,6 +247,8 @@ export function matchLogisticsArrivalSigningDurationBarcodes(
index.set(key, current)
}
for (const item of items) {
// 已换货/取消的原始行数量为 0,不是实际发出的商品。
if (!(item.qty > 0)) continue
for (const key of item.orderKeys) push(key, item.barcode)
push(item.logisticsPayKey, item.barcode)
push(item.logisticsId, item.barcode)

View File

@ -64,6 +64,7 @@ type SqlItemRow = {
i_id: string | null
l_id: string | null
pay_date: string | null
qty: number | string | null
}
type ArchiveRecord = { barcode: string; productName: string; supplier: string; purchaser: string }
@ -372,7 +373,7 @@ async function listItemRows(rows: SqlExpressRow[]) {
const orderParams = batch.map((_, index) => `@orderId${index}`)
batch.forEach((value, index) => request.input(`orderId${index}`, sql.BigInt, value))
const batchResult = await request.query<SqlItemRow>(`
SELECT oi.[o_id], oi.[outer_so_id], oi.[raw_so_id], oi.[sku_id], oi.[i_id],
SELECT oi.[o_id], oi.[outer_so_id], oi.[raw_so_id], oi.[sku_id], oi.[i_id], oi.[qty],
o.[l_id], CONVERT(varchar(19), o.[pay_date], 120) AS [pay_date]
FROM ${sourceObject(ITEM_OBJECT)} AS oi
LEFT JOIN ${sourceObject('jst_qmcust_orderlist')} AS o ON o.[o_id] = oi.[o_id]
@ -384,7 +385,7 @@ async function listItemRows(rows: SqlExpressRow[]) {
otherRequest.timeout = EXPORT_REQUEST_TIMEOUT_MS
batch.forEach((value, index) => otherRequest.input(`orderId${index}`, sql.BigInt, value))
const otherResult = await otherRequest.query<SqlItemRow>(`
SELECT oi.[o_id], CONVERT(nvarchar(120), o.[so_id]) AS [outer_so_id], oi.[raw_so_id], oi.[sku_id], oi.[i_id],
SELECT oi.[o_id], CONVERT(nvarchar(120), o.[so_id]) AS [outer_so_id], oi.[raw_so_id], oi.[sku_id], oi.[i_id], oi.[qty],
o.[l_id], CONVERT(varchar(19), o.[pay_date], 120) AS [pay_date]
FROM ${sourceObject(ITEM_OBJECT_OTHER)} AS oi
LEFT JOIN ${sourceObject(ORDER_OBJECT_OTHER)} AS o ON o.[o_id] = oi.[o_id]
@ -437,6 +438,7 @@ function buildBarcodeMap(rows: SqlExpressRow[], items: SqlItemRow[]) {
logisticsPayKey: itemLogisticId && item.pay_date ? `${itemLogisticId}|${text(item.pay_date)}` : '',
logisticsId: itemLogisticId,
barcode,
qty: Number(item.qty ?? 0),
}
})
.filter((item) => Boolean(item.barcode)),