feat(product-archive): 新增「销售金额」表头字段(取每日快照 sales_amount)
- 列表「数据信息」分组新增「销售金额」,位置固定在「真实业绩」前一列 - 取值来自每日快照表 SupplyChainProductArchiveSalesPerformanceDaily.sales_amount, 与发货金额/退货金额/真实业绩走同一链路,随页面 日/月/年 周期聚合 - 表头漏斗支持按销售金额筛选(可搜索多选标准弹层),列设置/详情弹窗/资料导出随列配置自动生效, 字段总数 89 → 90 - 新增一次性 localStorage 列序迁移,把上线前保存过的列设置里的新列挪回「真实业绩」前 - 不改数据库结构、不改每日同步脚本;旧的全周期汇总表不加列、不回填
This commit is contained in:
parent
0fde0bfe4b
commit
8b76cd57db
@ -95,6 +95,7 @@ export const ARCHIVE_COLUMNS: ArchiveColumnGroup[] = [
|
||||
{ prop: 'costPriceSecondary', label: '成本价', width: 90 },
|
||||
{ prop: 'shipAmount', label: '发货金额', width: 100 },
|
||||
{ prop: 'returnAmount', label: '退货金额', width: 100 },
|
||||
{ prop: 'salesAmount', label: '销售金额', width: 100 },
|
||||
{ prop: 'realPerformance', label: '真实业绩', width: 100 },
|
||||
{ prop: 'shipQty', label: '发货件数', width: 90 },
|
||||
{ prop: 'grossProfit', label: '毛利额', width: 90 },
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import {
|
||||
migrateSalesAmountColumnOrder,
|
||||
migrateSalesAmountColumnOrderOnce,
|
||||
migrateWarehouseColumnOrder,
|
||||
migrateWarehouseColumnOrderOnce,
|
||||
type ColumnSetting,
|
||||
@ -92,3 +94,110 @@ describe('product archive warehouse column order migration', () => {
|
||||
expect(storage.setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('product archive sales amount column order migration', () => {
|
||||
it('moves the appended 销售金额 back in front of 真实业绩', () => {
|
||||
const settings: ColumnSetting[] = [{
|
||||
group: '数据信息',
|
||||
columns: [
|
||||
{ prop: 'purchasePrice', label: '采购价', width: 90, visible: true },
|
||||
{ prop: 'shipAmount', label: '发货金额', width: 100, visible: true },
|
||||
{ prop: 'returnAmount', label: '退货金额', width: 100, visible: true },
|
||||
{ prop: 'realPerformance', label: '真实业绩', width: 100, visible: true },
|
||||
{ prop: 'salesAmount', label: '销售金额', width: 100, visible: true },
|
||||
{ prop: 'shipQty', label: '发货件数', width: 90, visible: true },
|
||||
],
|
||||
}]
|
||||
|
||||
const migrated = migrateSalesAmountColumnOrder(settings)
|
||||
|
||||
expect(migrated[0]?.columns.map((column) => column.prop)).toEqual([
|
||||
'purchasePrice',
|
||||
'shipAmount',
|
||||
'returnAmount',
|
||||
'salesAmount',
|
||||
'realPerformance',
|
||||
'shipQty',
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps hidden state and width of the migrated column', () => {
|
||||
const settings: ColumnSetting[] = [{
|
||||
group: '数据信息',
|
||||
columns: [
|
||||
{ prop: 'realPerformance', label: '真实业绩', width: 140, visible: true },
|
||||
{ prop: 'salesAmount', label: '销售金额', width: 210, visible: false },
|
||||
],
|
||||
}]
|
||||
|
||||
const migrated = migrateSalesAmountColumnOrder(settings)
|
||||
|
||||
expect(migrated[0]?.columns.find((column) => column.prop === 'salesAmount')).toEqual(
|
||||
expect.objectContaining({ width: 210, visible: false }),
|
||||
)
|
||||
})
|
||||
|
||||
it('does not change other groups or settings without the column', () => {
|
||||
const otherGroup: ColumnSetting = {
|
||||
group: '仓库信息',
|
||||
columns: [
|
||||
{ prop: 'warehouseHead', label: '仓库负责人', width: 110, visible: true },
|
||||
{ prop: 'salesAmount', label: '销售金额', width: 100, visible: true },
|
||||
],
|
||||
}
|
||||
const dataGroup: ColumnSetting = {
|
||||
group: '数据信息',
|
||||
columns: [
|
||||
{ prop: 'shipAmount', label: '发货金额', width: 100, visible: true },
|
||||
{ prop: 'realPerformance', label: '真实业绩', width: 100, visible: true },
|
||||
],
|
||||
}
|
||||
|
||||
const migrated = migrateSalesAmountColumnOrder([otherGroup, dataGroup])
|
||||
|
||||
expect(migrated[0]).toBe(otherGroup)
|
||||
expect(migrated[1]).toBe(dataGroup)
|
||||
})
|
||||
|
||||
it('persists the migrated order before marking the migration complete', () => {
|
||||
const settings: ColumnSetting[] = [{
|
||||
group: '数据信息',
|
||||
columns: [
|
||||
{ prop: 'realPerformance', label: '真实业绩', width: 100, visible: true },
|
||||
{ prop: 'salesAmount', label: '销售金额', width: 100, visible: true },
|
||||
],
|
||||
}]
|
||||
const storage = {
|
||||
getItem: jest.fn(() => null),
|
||||
setItem: jest.fn(),
|
||||
}
|
||||
|
||||
migrateSalesAmountColumnOrderOnce(settings, storage)
|
||||
|
||||
expect(storage.setItem.mock.calls.map(([key]) => key)).toEqual([
|
||||
'caigou_archive_columns',
|
||||
'caigou_archive_columns_sales_amount_v1',
|
||||
])
|
||||
expect(JSON.parse(storage.setItem.mock.calls[0]?.[1] ?? '[]')[0].columns.map(
|
||||
(column: { prop: string }) => column.prop,
|
||||
)).toEqual(['salesAmount', 'realPerformance'])
|
||||
})
|
||||
|
||||
it('preserves later manual sorting after the one-time migration', () => {
|
||||
const settings: ColumnSetting[] = [{
|
||||
group: '数据信息',
|
||||
columns: [
|
||||
{ prop: 'realPerformance', label: '真实业绩', width: 100, visible: true },
|
||||
],
|
||||
}]
|
||||
const storage = {
|
||||
getItem: jest.fn(() => '1'),
|
||||
setItem: jest.fn(),
|
||||
}
|
||||
|
||||
const migrated = migrateSalesAmountColumnOrderOnce(settings, storage)
|
||||
|
||||
expect(migrated).toBe(settings)
|
||||
expect(storage.setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -68,6 +68,47 @@ export function migrateWarehouseColumnOrderOnce(
|
||||
return migrated
|
||||
}
|
||||
|
||||
const SALES_AMOUNT_ORDER_MIGRATION_KEY = 'caigou_archive_columns_sales_amount_v1'
|
||||
const SALES_AMOUNT_COLUMN_PROP = 'salesAmount'
|
||||
const SALES_AMOUNT_ANCHOR_PROP = 'realPerformance'
|
||||
const DATA_INFO_GROUP_NAME = '数据信息'
|
||||
|
||||
/**
|
||||
* 新增「销售金额」后,已保存过列设置的浏览器会把新字段追加到分组末尾。
|
||||
* 这里把它挪回「真实业绩」前面,和默认表头顺序一致。
|
||||
*/
|
||||
export function migrateSalesAmountColumnOrder(settings: ColumnSetting[]): ColumnSetting[] {
|
||||
return settings.map((group) => {
|
||||
if (group.group !== DATA_INFO_GROUP_NAME) return group
|
||||
|
||||
const column = group.columns.find((entry) => entry.prop === SALES_AMOUNT_COLUMN_PROP)
|
||||
if (!column) return group
|
||||
|
||||
const rest = group.columns.filter((entry) => entry.prop !== SALES_AMOUNT_COLUMN_PROP)
|
||||
const anchor = rest.findIndex((entry) => entry.prop === SALES_AMOUNT_ANCHOR_PROP)
|
||||
if (anchor < 0) return group
|
||||
|
||||
const columns = [...rest]
|
||||
columns.splice(anchor, 0, column)
|
||||
const unchanged = columns.every((entry, index) => entry === group.columns[index])
|
||||
return unchanged ? group : { ...group, columns }
|
||||
})
|
||||
}
|
||||
|
||||
export function migrateSalesAmountColumnOrderOnce(
|
||||
settings: ColumnSetting[],
|
||||
storage: Pick<Storage, 'getItem' | 'setItem'>,
|
||||
): ColumnSetting[] {
|
||||
if (storage.getItem(SALES_AMOUNT_ORDER_MIGRATION_KEY) === '1') {
|
||||
return settings
|
||||
}
|
||||
|
||||
const migrated = migrateSalesAmountColumnOrder(settings)
|
||||
storage.setItem(STORAGE_KEY, JSON.stringify(migrated))
|
||||
storage.setItem(SALES_AMOUNT_ORDER_MIGRATION_KEY, '1')
|
||||
return migrated
|
||||
}
|
||||
|
||||
export function persistColumnSettings(settings: ColumnSetting[]) {
|
||||
if (typeof window === 'undefined') return
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
|
||||
@ -144,7 +185,10 @@ function loadSettings(): ColumnSetting[] {
|
||||
columns: [...orderedCols, ...customColumns],
|
||||
}
|
||||
})
|
||||
return migrateWarehouseColumnOrderOnce(merged, localStorage)
|
||||
return migrateSalesAmountColumnOrderOnce(
|
||||
migrateWarehouseColumnOrderOnce(merged, localStorage),
|
||||
localStorage,
|
||||
)
|
||||
} catch {
|
||||
return defaults
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ export type SupplyChainProductArchiveRow = {
|
||||
costPriceSecondary: string
|
||||
shipAmount: string
|
||||
returnAmount: string
|
||||
salesAmount: string
|
||||
realPerformance: string
|
||||
shipQty: string
|
||||
grossProfit: string
|
||||
@ -213,6 +214,7 @@ export type ProductArchiveFilters = {
|
||||
returnQty?: string[]
|
||||
shipAmount?: string[]
|
||||
returnAmount?: string[]
|
||||
salesAmount?: string[]
|
||||
realPerformance?: string[]
|
||||
shipQty?: string[]
|
||||
grossProfit?: string[]
|
||||
@ -244,7 +246,7 @@ export const PRODUCT_ARCHIVE_FILTER_OPTION_KEYS = [
|
||||
'afterSalesRate', 'afterSalesCost', 'afterSalesCostRate', 'returnQty', 'returnRate',
|
||||
'partsResendCount', 'partsRate', 'installDocId', 'hasDrawing', 'hasVideo',
|
||||
'distributionIds', 'opsPriceLimit', 'createDays', 'shipAmount', 'returnAmount',
|
||||
'realPerformance', 'shipQty', 'grossProfit', 'grossMargin', 'feeRatio',
|
||||
'salesAmount', 'realPerformance', 'shipQty', 'grossProfit', 'grossMargin', 'feeRatio',
|
||||
'operatingProfit',
|
||||
] as const
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import sql from 'mssql'
|
||||
|
||||
export type ProductArchiveSalesPerformanceAggregate = {
|
||||
barcode: string
|
||||
salesAmount: string
|
||||
shipAmount: string
|
||||
returnAmount: string
|
||||
realPerformance: string
|
||||
@ -24,7 +25,6 @@ export type ProductArchiveSalesPerformanceDailyAggregate =
|
||||
ProductArchiveSalesPerformanceAggregate & {
|
||||
statDate: string
|
||||
sourceRowCount: number
|
||||
salesAmount: string
|
||||
// Operator and unit-cost enrichment is sourced for DF barcodes only.
|
||||
skuOperator: string
|
||||
unitCost: string | null
|
||||
@ -138,6 +138,7 @@ class ProductArchiveSalesPerformanceSourceRepositoryClass
|
||||
.query<ProductArchiveSalesPerformanceAggregate>(`
|
||||
SELECT
|
||||
UPPER(LTRIM(RTRIM(CAST(skuoid AS nvarchar(100))))) AS barcode,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(sales_amount, 0) AS decimal(38, 4)))) AS salesAmount,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(clear_goods_cost, 0) AS decimal(38, 4)))) AS shipAmount,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(refund_amount, 0) AS decimal(38, 4)))) AS returnAmount,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(real_performance, 0) AS decimal(38, 4)))) AS realPerformance,
|
||||
|
||||
@ -4,7 +4,8 @@ import { Prisma } from '@prisma/client'
|
||||
import { matchesPerformanceFilters, performanceText, type PeriodPerformance } from './supply-chain-product-archive-sales-period.repository'
|
||||
import { PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE as empty } from '@/lib/supply-chain-product-archive'
|
||||
const value: PeriodPerformance = {
|
||||
shipAmount: new Prisma.Decimal('8.8400'), returnAmount: new Prisma.Decimal(0), realPerformance: new Prisma.Decimal(20),
|
||||
shipAmount: new Prisma.Decimal('8.8400'), returnAmount: new Prisma.Decimal(0), salesAmount: new Prisma.Decimal('10.5'),
|
||||
realPerformance: new Prisma.Decimal(20),
|
||||
shipQty: 1n, returnQty: 0n, grossProfit: new Prisma.Decimal(4), grossMargin: new Prisma.Decimal('.12345678'),
|
||||
feeRatio: null, operatingProfit: new Prisma.Decimal(-1),
|
||||
}
|
||||
@ -13,6 +14,14 @@ it('matches decimal amounts by value and exposes rates as the table shows them',
|
||||
expect(performanceText(value, 'grossMargin')).toBe('12.35%')
|
||||
expect(matchesPerformanceFilters(value, { grossMargin: ['0.12345678'] })).toBe(true)
|
||||
})
|
||||
|
||||
it('filters 销售金额 with the same decimal rule as other period amounts', () => {
|
||||
expect(performanceText(value, 'salesAmount')).toBe('10.5')
|
||||
expect(matchesPerformanceFilters(value, { salesAmount: ['10.50'] })).toBe(true)
|
||||
expect(matchesPerformanceFilters(value, { salesAmount: ['10.5'] })).toBe(true)
|
||||
expect(matchesPerformanceFilters(value, { salesAmount: ['11'] })).toBe(false)
|
||||
expect(matchesPerformanceFilters(null, { salesAmount: [empty] })).toBe(true)
|
||||
})
|
||||
it('treats missing rows and undefined ratios as empty, including when other filters apply', () => {
|
||||
expect(matchesPerformanceFilters(null, { shipAmount: [empty] })).toBe(true)
|
||||
expect(matchesPerformanceFilters(value, { feeRatio: [empty] })).toBe(true)
|
||||
|
||||
@ -8,11 +8,19 @@ import { buildProductArchiveWhereWithTextRules } from './supply-chain-product-ar
|
||||
import type { ProductArchiveRecord } from './supply-chain-product-archive-select'
|
||||
|
||||
export const PERFORMANCE_KEYS = [
|
||||
'shipAmount', 'returnAmount', 'realPerformance', 'shipQty', 'grossProfit',
|
||||
'shipAmount', 'returnAmount', 'salesAmount', 'realPerformance', 'shipQty', 'grossProfit',
|
||||
'grossMargin', 'feeRatio', 'operatingProfit', 'returnQty',
|
||||
] as const
|
||||
export type PerformanceKey = typeof PERFORMANCE_KEYS[number]
|
||||
export type PeriodPerformance = NonNullable<ProductArchiveRecord['salesPerformance']>
|
||||
/**
|
||||
* 周期业绩:按页面选中的 日/月/年 从每日快照表聚合出来的值。
|
||||
*
|
||||
* 基础字段与 productArchiveSelect.salesPerformance 对齐;salesAmount 只有每日快照表有,
|
||||
* 旧的全周期汇总表没有这一列,所以在这里扩展类型,避免为旧表加列和做迁移。
|
||||
*/
|
||||
export type PeriodPerformance = NonNullable<ProductArchiveRecord['salesPerformance']> & {
|
||||
salesAmount: Prisma.Decimal
|
||||
}
|
||||
|
||||
export async function readSalesCoverage(): Promise<SalesCoverage> {
|
||||
const [row] = await prisma.$queryRaw<SalesCoverage[]>(Prisma.sql`
|
||||
@ -58,7 +66,7 @@ export async function listPeriodPerformance(filters: ProductArchiveFilters = {})
|
||||
const period = resolveSalesPeriod(filters, coverage)
|
||||
const performanceByBarcode = new Map<string, PeriodPerformance>()
|
||||
if (!period.hasCoverage) return { period, performanceByBarcode }
|
||||
const money = ['shipAmount', 'returnAmount', 'realPerformance', 'clearSalesCost', 'sendFreight',
|
||||
const money = ['shipAmount', 'returnAmount', 'salesAmount', 'realPerformance', 'clearSalesCost', 'sendFreight',
|
||||
'platformCommission', 'xyCost', 'afterSaleAmount', 'pushFeeTotal', 'marketingFeeTotal',
|
||||
'advertisementFeeTotal', 'sharedWarehousingSum', 'otherFee', 'operatingFee'] as const
|
||||
const aggregates = await prisma.$queryRaw<ProductArchiveSalesPerformanceAggregate[]>(Prisma.sql`
|
||||
@ -73,6 +81,7 @@ export async function listPeriodPerformance(filters: ProductArchiveFilters = {})
|
||||
const value = calculateProductArchiveSalesPerformance(aggregate)
|
||||
performanceByBarcode.set(value.barcode, {
|
||||
shipAmount: new Prisma.Decimal(value.shipAmount), returnAmount: new Prisma.Decimal(value.returnAmount),
|
||||
salesAmount: new Prisma.Decimal(value.salesAmount),
|
||||
realPerformance: new Prisma.Decimal(value.realPerformance), shipQty: value.shipQty, returnQty: value.returnQty,
|
||||
grossProfit: new Prisma.Decimal(value.grossProfit),
|
||||
grossMargin: value.grossMargin === null ? null : new Prisma.Decimal(value.grossMargin),
|
||||
|
||||
@ -4,6 +4,7 @@ describe('calculateProductArchiveSalesPerformance', () => {
|
||||
it('calculates gross profit and gross margin from the confirmed components', () => {
|
||||
expect(calculateProductArchiveSalesPerformance({
|
||||
barcode: ' sku-001 ',
|
||||
salesAmount: '80',
|
||||
shipAmount: '70',
|
||||
returnAmount: '5.5',
|
||||
realPerformance: '100',
|
||||
@ -22,6 +23,7 @@ describe('calculateProductArchiveSalesPerformance', () => {
|
||||
operatingFee: '2',
|
||||
})).toEqual({
|
||||
barcode: 'SKU-001',
|
||||
salesAmount: '80.0000',
|
||||
shipAmount: '70.0000',
|
||||
returnAmount: '5.5000',
|
||||
realPerformance: '100.0000',
|
||||
@ -37,6 +39,7 @@ describe('calculateProductArchiveSalesPerformance', () => {
|
||||
it('leaves gross margin empty when real performance is zero', () => {
|
||||
const result = calculateProductArchiveSalesPerformance({
|
||||
barcode: 'SKU-002',
|
||||
salesAmount: '0',
|
||||
shipAmount: '0',
|
||||
returnAmount: '0',
|
||||
realPerformance: '0',
|
||||
|
||||
@ -77,8 +77,8 @@ describe('product archive full export', () => {
|
||||
|
||||
expect(repository.openRowSource).toHaveBeenCalledWith({ productName: ['测试桌'] })
|
||||
expect(result.total).toBe(1)
|
||||
expect(result.fieldCount).toBe(89)
|
||||
expect(headers).toHaveLength(89)
|
||||
expect(result.fieldCount).toBe(90)
|
||||
expect(headers).toHaveLength(90)
|
||||
expect(headers).toContain('商品名称')
|
||||
expect(headers).toContain('配件率')
|
||||
expect(worksheet.getCell(2, headers.indexOf('商品名称') + 1).value).toBe('测试桌')
|
||||
@ -98,7 +98,7 @@ describe('product archive full export', () => {
|
||||
expect(worksheet.views).toEqual([
|
||||
expect.objectContaining({ state: 'frozen', ySplit: 1 }),
|
||||
])
|
||||
expect(ARCHIVE_COLUMNS.flatMap((group) => group.columns)).toHaveLength(89)
|
||||
expect(ARCHIVE_COLUMNS.flatMap((group) => group.columns)).toHaveLength(90)
|
||||
})
|
||||
|
||||
it('streams rows in batches instead of materialising them all', async () => {
|
||||
|
||||
@ -20,6 +20,7 @@ describe('SupplyChainProductArchiveSalesPerformanceSyncService', () => {
|
||||
listAggregates: jest.fn().mockResolvedValue([
|
||||
{
|
||||
barcode: ' sku-001 ',
|
||||
salesAmount: '80',
|
||||
shipAmount: '70',
|
||||
returnAmount: '5',
|
||||
realPerformance: '100',
|
||||
@ -39,6 +40,7 @@ describe('SupplyChainProductArchiveSalesPerformanceSyncService', () => {
|
||||
},
|
||||
{
|
||||
barcode: 'NOT-IN-ARCHIVE',
|
||||
salesAmount: '20',
|
||||
shipAmount: '20',
|
||||
returnAmount: '0',
|
||||
realPerformance: '20',
|
||||
|
||||
@ -3,6 +3,7 @@ import type { ProductArchiveSalesPerformanceAggregate } from '@/server/repositor
|
||||
|
||||
export type ProductArchiveSalesPerformanceValues = {
|
||||
barcode: string
|
||||
salesAmount: string
|
||||
shipAmount: string
|
||||
returnAmount: string
|
||||
realPerformance: string
|
||||
@ -40,6 +41,7 @@ export function calculateProductArchiveSalesPerformance(
|
||||
const barcode = normalizeSalesPerformanceBarcode(aggregate.barcode)
|
||||
if (!barcode) throw new Error('Sales performance barcode is required')
|
||||
|
||||
const salesAmount = decimal(aggregate.salesAmount, 'salesAmount')
|
||||
const shipAmount = decimal(aggregate.shipAmount, 'shipAmount')
|
||||
const returnAmount = decimal(aggregate.returnAmount, 'returnAmount')
|
||||
const realPerformance = decimal(aggregate.realPerformance, 'realPerformance')
|
||||
@ -69,6 +71,7 @@ export function calculateProductArchiveSalesPerformance(
|
||||
|
||||
return {
|
||||
barcode,
|
||||
salesAmount: salesAmount.toFixed(4),
|
||||
shipAmount: shipAmount.toFixed(4),
|
||||
returnAmount: returnAmount.toFixed(4),
|
||||
realPerformance: realPerformance.toFixed(4),
|
||||
|
||||
@ -170,7 +170,7 @@ export class SupplyChainProductArchiveExportService {
|
||||
* 返回时只做完了「确定要导哪些行」这步(查 id 快照、装载仓库配置),xlsx 字节
|
||||
* 还没开始产;调用方拿到 stream 立刻响应,边生成边下发,堆占用与总行数无关。
|
||||
*
|
||||
* 这条路径取代了原来的 `createWorkbook`(整包 writeBuffer)——旧写法在 89 列下
|
||||
* 这条路径取代了原来的 `createWorkbook`(整包 writeBuffer)——旧写法在 90 列下
|
||||
* 约 60MB / 千行,生产 768MB 堆实际只能撑约 5000 行,99434 行量级的全量导出必崩。
|
||||
*/
|
||||
async openWorkbook(params: { filters?: ProductArchiveFilters }): Promise<ProductArchiveExportStream> {
|
||||
|
||||
@ -107,6 +107,7 @@ const productArchiveFiltersSchema = z.object({
|
||||
createDays: archiveValueArraySchema.optional(),
|
||||
shipAmount: archiveValueArraySchema.optional(),
|
||||
returnAmount: archiveValueArraySchema.optional(),
|
||||
salesAmount: archiveValueArraySchema.optional(),
|
||||
realPerformance: archiveValueArraySchema.optional(),
|
||||
shipQty: archiveValueArraySchema.optional(),
|
||||
grossProfit: archiveValueArraySchema.optional(),
|
||||
|
||||
@ -1,7 +1,21 @@
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import type { SupplyChainProductArchiveRow } from '@/lib/supply-chain-product-archive'
|
||||
import { storageTypeLabel } from '@/lib/supply-chain-product-archive-values'
|
||||
import type { ProductArchiveRecord } from '@/server/repositories/supply-chain-product-archive.repository'
|
||||
|
||||
/**
|
||||
* 页面和导出拿到的是「已叠加周期业绩」的档案行:salesPerformance 会被换成按 日/月/年
|
||||
* 从每日快照表聚合出来的对象,比 productArchiveSelect 的结果多一个 salesAmount
|
||||
* (每日快照表有这一列,旧的全周期汇总表没有)。这里放宽入参类型,两种形状都接受。
|
||||
*/
|
||||
type MappableArchiveRecord = Omit<ProductArchiveRecord, 'salesPerformance'> & {
|
||||
salesPerformance?:
|
||||
| (NonNullable<ProductArchiveRecord['salesPerformance']> & {
|
||||
salesAmount?: Prisma.Decimal | null
|
||||
})
|
||||
| null
|
||||
}
|
||||
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
function calculateCreateDays(createdAt: Date, now: Date) {
|
||||
@ -34,7 +48,7 @@ export function mapProductArchiveRow({
|
||||
record,
|
||||
now,
|
||||
}: {
|
||||
record: ProductArchiveRecord
|
||||
record: MappableArchiveRecord
|
||||
now: Date
|
||||
}) {
|
||||
return {
|
||||
@ -102,6 +116,7 @@ export function mapProductArchiveRow({
|
||||
costPriceSecondary: record.costPriceSecondary,
|
||||
shipAmount: decimalText(record.salesPerformance?.shipAmount ?? null),
|
||||
returnAmount: decimalText(record.salesPerformance?.returnAmount ?? null),
|
||||
salesAmount: decimalText(record.salesPerformance?.salesAmount ?? null),
|
||||
realPerformance: decimalText(record.salesPerformance?.realPerformance ?? null),
|
||||
shipQty: record.salesPerformance?.shipQty.toString() ?? '',
|
||||
grossProfit: decimalText(record.salesPerformance?.grossProfit ?? null),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user