fix(supply-chain): 限价明细「负责人限价」筛选补上候选值与筛选逻辑
- 负责人限价是 SupplyChainStyleLimitPrice.ownerLimitPrice 的派生列(按货号),此前 option 接口没有对应分支, 下拉框显示「没有可选值」、选了也不生效 - 候选值:无其他筛选时取限价表的全部去重价格(两位小数,与界面一致);有筛选时只取当前结果集里货号的限价; 无流程限价的商品提供「空值」选项 - 筛选:选中的价格先经 ROUND(price,2) 换回货号再按 sku 过滤;选中的价格无对应货号返回空结果; 「空值」= 货号不在限价表里 - 补 3 条单测(价格→货号、无匹配、空值)
This commit is contained in:
parent
ee983673d5
commit
c104a029c9
@ -11,17 +11,24 @@ import {
|
|||||||
} from '../supply-chain-product-archive-filters'
|
} from '../supply-chain-product-archive-filters'
|
||||||
|
|
||||||
jest.mock('@/lib/prisma', () => ({
|
jest.mock('@/lib/prisma', () => ({
|
||||||
prisma: { $queryRaw: jest.fn() },
|
prisma: {
|
||||||
|
$queryRaw: jest.fn(),
|
||||||
|
supplyChainStyleLimitPrice: { findMany: jest.fn() },
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockedQueryRaw = (jest.requireMock('@/lib/prisma') as {
|
const mockedQueryRaw = (jest.requireMock('@/lib/prisma') as {
|
||||||
prisma: { $queryRaw: jest.Mock }
|
prisma: { $queryRaw: jest.Mock }
|
||||||
}).prisma.$queryRaw
|
}).prisma.$queryRaw
|
||||||
|
const mockedStyleLimitFindMany = (jest.requireMock('@/lib/prisma') as {
|
||||||
|
prisma: { supplyChainStyleLimitPrice: { findMany: jest.Mock } }
|
||||||
|
}).prisma.supplyChainStyleLimitPrice.findMany
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clearNormalCodeValueCache()
|
clearNormalCodeValueCache()
|
||||||
mockedQueryRaw.mockReset()
|
mockedQueryRaw.mockReset()
|
||||||
mockedQueryRaw.mockResolvedValue([{ value: 'DNZ0411' }, { value: 'XG0022' }])
|
mockedQueryRaw.mockResolvedValue([{ value: 'DNZ0411' }, { value: 'XG0022' }])
|
||||||
|
mockedStyleLimitFindMany.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('normal archive code pattern', () => {
|
describe('normal archive code pattern', () => {
|
||||||
@ -241,4 +248,30 @@ describe('buildProductArchiveWhereWithTextRules normal code', () => {
|
|||||||
expect(serialized).toContain('整装')
|
expect(serialized).toContain('整装')
|
||||||
expect(serialized).toContain('组装')
|
expect(serialized).toContain('组装')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 「负责人限价」是 SupplyChainStyleLimitPrice 的派生列:按价格换回货号再过滤。
|
||||||
|
it('filters 负责人限价 by mapping the selected price back to 货号', async () => {
|
||||||
|
mockedQueryRaw.mockResolvedValueOnce([{ styleCode: 'SJ004833' }, { styleCode: 'SJ004834' }])
|
||||||
|
const where = await buildProductArchiveWhereWithTextRules({ opsPriceLimit: ['213.80'] }) as { AND?: unknown[] }
|
||||||
|
const serialized = JSON.stringify(where.AND)
|
||||||
|
|
||||||
|
expect(serialized).toContain('{"sku":{"in":["SJ004833","SJ004834"]}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns no rows when the selected 负责人限价 has no matching 货号', async () => {
|
||||||
|
mockedQueryRaw.mockResolvedValueOnce([])
|
||||||
|
const where = await buildProductArchiveWhereWithTextRules({ opsPriceLimit: ['123.45'] }) as { AND?: unknown[] }
|
||||||
|
|
||||||
|
expect(JSON.stringify(where.AND)).toContain('__NO_MATCH__')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats 负责人限价空值 as 货号不在限价表里', async () => {
|
||||||
|
mockedStyleLimitFindMany.mockResolvedValueOnce([{ styleCode: 'SJ004833' }, { styleCode: 'SJ004834' }])
|
||||||
|
const where = await buildProductArchiveWhereWithTextRules({
|
||||||
|
opsPriceLimit: [PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE],
|
||||||
|
}) as { AND?: unknown[] }
|
||||||
|
const serialized = JSON.stringify(where.AND)
|
||||||
|
|
||||||
|
expect(serialized).toContain('"NOT":{"sku":{"in":["SJ004833","SJ004834"]}}')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -605,6 +605,38 @@ export async function buildProductArchiveWhereWithTextRules(
|
|||||||
if (clause) extra.push(clause)
|
if (clause) extra.push(clause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 「负责人限价」:派生自 SupplyChainStyleLimitPrice.ownerLimitPrice(按货号),
|
||||||
|
// 先把选中的价格换回货号,再按 sku 过滤;「空值」= 不在限价表里的货号。
|
||||||
|
if (omitFilter !== 'opsPriceLimit') {
|
||||||
|
const rawValues = (filters.opsPriceLimit ?? []).filter(Boolean)
|
||||||
|
if (rawValues.length > 0) {
|
||||||
|
const includesEmpty = rawValues.includes(PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE)
|
||||||
|
const prices = rawValues
|
||||||
|
.filter((value) => value !== PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE)
|
||||||
|
.map((value) => Number(String(value).replace(/[^\d.-]/g, '')))
|
||||||
|
.filter((value) => Number.isFinite(value))
|
||||||
|
const priceClauses: Prisma.SupplyChainProductArchiveWhereInput[] = []
|
||||||
|
if (prices.length > 0) {
|
||||||
|
const rows = await prisma.$queryRaw<Array<{ styleCode: string }>>(Prisma.sql`
|
||||||
|
SELECT DISTINCT "styleCode"
|
||||||
|
FROM "SupplyChainStyleLimitPrice"
|
||||||
|
WHERE "ownerLimitPrice" IS NOT NULL
|
||||||
|
AND ROUND("ownerLimitPrice"::numeric, 2) IN (${Prisma.join(prices)})
|
||||||
|
`)
|
||||||
|
const styleCodes = rows.map((row) => row.styleCode).filter(Boolean)
|
||||||
|
// 选中的价格在限价表里没有对应货号时返回空结果(而不是忽略条件)
|
||||||
|
priceClauses.push({ sku: { in: styleCodes.length > 0 ? styleCodes : ['__NO_MATCH__'] } })
|
||||||
|
}
|
||||||
|
if (includesEmpty) {
|
||||||
|
const limitedStyles = await prisma.supplyChainStyleLimitPrice.findMany({ select: { styleCode: true } })
|
||||||
|
priceClauses.push({
|
||||||
|
NOT: { sku: { in: limitedStyles.map((row) => row.styleCode).filter(Boolean) } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (priceClauses.length === 1) extra.push(priceClauses[0])
|
||||||
|
else if (priceClauses.length > 1) extra.push({ OR: priceClauses })
|
||||||
|
}
|
||||||
|
}
|
||||||
const asyncClauses = [...extra, ...ownerClauses]
|
const asyncClauses = [...extra, ...ownerClauses]
|
||||||
return asyncClauses.length > 0 ? { AND: [base, ...asyncClauses] } : base
|
return asyncClauses.length > 0 ? { AND: [base, ...asyncClauses] } : base
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,6 +140,34 @@ class SupplyChainProductArchiveRepositoryClass implements SupplyChainProductArch
|
|||||||
const values = index.spuSet.size > 0 ? ['有', '无'] : ['无']
|
const values = index.spuSet.size > 0 ? ['有', '无'] : ['无']
|
||||||
return pageOptions(values, params)
|
return pageOptions(values, params)
|
||||||
}
|
}
|
||||||
|
// 「负责人限价」是派生列(SupplyChainStyleLimitPrice.ownerLimitPrice,按货号),
|
||||||
|
// 候选值 = 当前筛选范围内实际出现的限价(两位小数,与界面展示一致)。
|
||||||
|
if (params.field === 'opsPriceLimit') {
|
||||||
|
const query = await preparePeriodArchiveQuery(params.filters, params.field)
|
||||||
|
const unfiltered = query.ids === null && Object.keys(query.where).length === 0
|
||||||
|
if (unfiltered) {
|
||||||
|
const rows = await prisma.$queryRaw<Array<{ price: string }>>(Prisma.sql`
|
||||||
|
SELECT DISTINCT ROUND("ownerLimitPrice"::numeric, 2)::text AS price
|
||||||
|
FROM "SupplyChainStyleLimitPrice"
|
||||||
|
WHERE "ownerLimitPrice" IS NOT NULL
|
||||||
|
ORDER BY price
|
||||||
|
`)
|
||||||
|
// 没有流程限价的商品占多数,「空值」始终是一个有效候选
|
||||||
|
return pageOptions([...rows.map((row) => row.price), PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE], params)
|
||||||
|
}
|
||||||
|
const records = await readSelectedArchives(query, { sku: true })
|
||||||
|
const styleCodes = [...new Set(records.map((record) => String(record.sku ?? '').trim()).filter(Boolean))]
|
||||||
|
const limitRows = styleCodes.length
|
||||||
|
? await prisma.supplyChainStyleLimitPrice.findMany({
|
||||||
|
where: { styleCode: { in: styleCodes }, ownerLimitPrice: { not: null } },
|
||||||
|
select: { styleCode: true, ownerLimitPrice: true },
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
const values = [...new Set(limitRows.map((row) => Number(row.ownerLimitPrice).toFixed(2)))]
|
||||||
|
const coveredStyles = new Set(limitRows.map((row) => row.styleCode))
|
||||||
|
const hasMissingLimit = styleCodes.some((styleCode) => !coveredStyles.has(styleCode))
|
||||||
|
return pageOptions(hasMissingLimit ? [...values, PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE] : values, params)
|
||||||
|
}
|
||||||
const field = FILTER_OPTION_DATABASE_FIELDS[params.field as keyof typeof FILTER_OPTION_DATABASE_FIELDS]
|
const field = FILTER_OPTION_DATABASE_FIELDS[params.field as keyof typeof FILTER_OPTION_DATABASE_FIELDS]
|
||||||
if (!field) return { values: [], hasMore: false, total: 0 }
|
if (!field) return { values: [], hasMore: false, total: 0 }
|
||||||
const query = await preparePeriodArchiveQuery(params.filters, params.field)
|
const query = await preparePeriodArchiveQuery(params.filters, params.field)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user