188 lines
7.6 KiB
TypeScript
188 lines
7.6 KiB
TypeScript
import type { Prisma } from '@prisma/client'
|
||
import type { SupplyChainProductArchiveRow } from '@/lib/supply-chain-product-archive'
|
||
import { productArchiveDataTagText } from '@/lib/supply-chain-product-archive'
|
||
import { installMethodLabel, storageTypeLabel } from '@/lib/supply-chain-product-archive-values'
|
||
import { inferStorageTypeLabelFromWarehouseName } from '@/lib/supply-chain-product-archive-warehouse-config'
|
||
import type { ProductArchiveRecord } from '@/server/repositories/supply-chain-product-archive.repository'
|
||
|
||
/**
|
||
* 页面和导出拿到的是「已叠加周期业绩」的档案行:salesPerformance 会被换成按 日/月/年
|
||
* 从每日快照表聚合出来的对象,比 productArchiveSelect 的结果多出 salesAmount 和
|
||
* afterSaleAmount(每日快照表有这两列,旧的全周期汇总表没有);sabcPeriodRealPerformance
|
||
* 是 SABC 最新核算周期的真实业绩(月度冻结口径,「产品标签」兜底用它,不随 日/月/年 变)。
|
||
* 这里放宽入参类型,几种形状都接受。
|
||
*/
|
||
type MappableArchiveRecord = Omit<ProductArchiveRecord, 'salesPerformance'> & {
|
||
salesPerformance?:
|
||
| (NonNullable<ProductArchiveRecord['salesPerformance']> & {
|
||
salesAmount?: Prisma.Decimal | null
|
||
afterSaleAmount?: Prisma.Decimal | null
|
||
})
|
||
| null
|
||
sabcPeriodRealPerformance?: Prisma.Decimal | null
|
||
}
|
||
|
||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||
|
||
function calculateCreateDays(createdAt: Date, now: Date) {
|
||
return Math.max(0, Math.floor((now.getTime() - createdAt.getTime()) / DAY_IN_MS))
|
||
}
|
||
|
||
function decimalText(value: { toString(): string } | null) {
|
||
return value?.toString() ?? ''
|
||
}
|
||
|
||
function percentageText(
|
||
value: { mul(multiplier: number): { toFixed(digits: number): string } } | null,
|
||
) {
|
||
return value ? `${value.mul(100).toFixed(2)}%` : ''
|
||
}
|
||
|
||
// 结算价展示口径(2026-10-15 用户修正):结算价 = 成本价 ÷ 0.9(模块化),
|
||
// 非模块化原样展示(成本价即结算价)。模块化以产品标签含「模块化」为准
|
||
// (聚水潭标签里的“模块化成品/模块化部件”,其成本价按 9 折记)。查询映射层
|
||
// 实时计算,不落库。
|
||
const SETTLEMENT_MODULAR_TAG = '模块化'
|
||
const SETTLEMENT_MODULAR_DIVISOR = 0.9
|
||
|
||
export function settlementPriceText(
|
||
costPrice: string | null | undefined,
|
||
productTag: string | null | undefined,
|
||
) {
|
||
const cost = costPrice?.trim() ?? ''
|
||
if (!cost) return ''
|
||
if (!(productTag ?? '').includes(SETTLEMENT_MODULAR_TAG)) return cost
|
||
const value = Number(cost)
|
||
if (!Number.isFinite(value)) return ''
|
||
// 先取整到分再除,避免浮点误差让 123.45÷0.9 这类值四舍五入方向漂移。
|
||
const settled = Math.round(Math.round(value * 100) / SETTLEMENT_MODULAR_DIVISOR) / 100
|
||
return settled.toFixed(2)
|
||
}
|
||
|
||
export function normalizeSupplierName(value: string) {
|
||
return value.trim().toLocaleLowerCase('zh-CN')
|
||
}
|
||
|
||
export function normalizeWarehouseName(value: string) {
|
||
return value.trim().toLocaleLowerCase('zh-CN')
|
||
}
|
||
|
||
export function normalizeArchiveIdentity(value: string) {
|
||
return value.trim().toLocaleUpperCase('zh-CN')
|
||
}
|
||
|
||
export function mapProductArchiveRow({
|
||
record,
|
||
now,
|
||
}: {
|
||
record: MappableArchiveRecord
|
||
now: Date
|
||
}) {
|
||
return {
|
||
id: record.id,
|
||
image: record.image?.trim() ?? '',
|
||
firstSourceCode: record.firstSourceCode,
|
||
createTime: record.businessCreatedAt.toISOString(),
|
||
createDays: calculateCreateDays(record.businessCreatedAt, now),
|
||
updateTime: record.updatedAt.toISOString(),
|
||
categoryL1: record.categoryL1,
|
||
categoryL2: record.categoryL2,
|
||
spu: record.spu,
|
||
sku: record.sku,
|
||
barcode: record.barcode,
|
||
productName: record.productName,
|
||
designer: record.designer,
|
||
opsDirector: record.opsDirector,
|
||
opsDeptHead: record.opsDeptHead,
|
||
opsHead: record.opsHead,
|
||
productStatus: record.productStatus,
|
||
productionType: record.productionType,
|
||
material: record.material,
|
||
color: record.color,
|
||
weight: decimalText(record.weight),
|
||
length: decimalText(record.length),
|
||
width: decimalText(record.width),
|
||
height: decimalText(record.height),
|
||
volume: decimalText(record.volume),
|
||
packType: record.packType,
|
||
productTag: record.productTag,
|
||
installDocId: record.installDocId,
|
||
// 展示口径:定厂值优先,为空时由商品标签(整装/组装)推导,与筛选口径一致。
|
||
installMethod: installMethodLabel(record.installMethod, record.productTag),
|
||
hasDrawing: record.hasDrawing,
|
||
hasVideo: record.hasVideo,
|
||
// 是否有检测报告:由外部「检测报告」接口按 SPU 注入(见 listPage 的 withInspectionReport 选项)
|
||
inspectionReport: '',
|
||
spec: record.spec,
|
||
brand: record.brand,
|
||
orderType: record.orderType,
|
||
inOutWarehouse: record.inOutWarehouse,
|
||
warehouseType: record.warehouseType,
|
||
warehouseOwnership: '',
|
||
warehouseHead: record.warehouseHead,
|
||
// 档案列常为空:按仓库名兜底推导(简尚-* → 入仓;工厂-*代发 → 代发),与筛选口径保持一致。
|
||
storageType: storageTypeLabel(record.storageType) || inferStorageTypeLabelFromWarehouseName(record.inOutWarehouse),
|
||
isSeasonal: record.isSeasonal,
|
||
replaceCount: record.replaceCount,
|
||
region: record.region,
|
||
supplier: record.supplierName,
|
||
supplierType: record.supplierType,
|
||
supplierStatus: record.supplierStatus,
|
||
purchaser: record.purchaseOwner,
|
||
settleType: record.settlementMethod,
|
||
boardProvider: record.boardProvider,
|
||
penaltyStatus: record.penaltyStatus,
|
||
moq: record.minimumOrderQuantity,
|
||
firstLeadTime: record.firstLeadTime,
|
||
stableLeadTime: record.stableLeadTime,
|
||
palletSpec: record.palletSpec,
|
||
backupSupplier: record.backupSupplier,
|
||
dailySales: record.dailySales,
|
||
sales3d: record.sales3d,
|
||
sales7d: record.sales7d,
|
||
sales15d: record.sales15d,
|
||
sales30d: record.sales30d,
|
||
stockJianshang: record.stockJianshang,
|
||
stockClearance: record.stockClearance,
|
||
stockFactory: record.stockFactory,
|
||
purchasePrice: record.purchasePrice,
|
||
costPrice: record.costPrice,
|
||
costPriceSecondary: settlementPriceText(record.costPrice, record.productTag),
|
||
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),
|
||
grossMargin: percentageText(record.salesPerformance?.grossMargin ?? null),
|
||
feeRatio: percentageText(record.salesPerformance?.feeRatio ?? null),
|
||
operatingProfit: decimalText(record.salesPerformance?.operatingProfit ?? null),
|
||
returnQty: record.salesPerformance?.returnQty.toString() ?? '',
|
||
opsPriceLimit: '',
|
||
distributionIds: '',
|
||
priceType: '',
|
||
dataTag: productArchiveDataTagText({
|
||
sabcRating: record.sabcRating,
|
||
productStatus: record.productStatus,
|
||
realPerformanceText: decimalText(record.sabcPeriodRealPerformance ?? null),
|
||
}),
|
||
sales30dWithBig: record.sales30dWithBig,
|
||
jianshangStockDays: '',
|
||
stockDays: '',
|
||
stockCloud: '',
|
||
inTransitQty: '',
|
||
stockTurnoverDays: '',
|
||
stockValue: '',
|
||
slowStock: '',
|
||
slowStockValue: '',
|
||
afterSaleAmount: decimalText(record.salesPerformance?.afterSaleAmount ?? null),
|
||
afterSalesCount: '',
|
||
afterSalesRate: '',
|
||
afterSalesCost: '',
|
||
afterSalesCostRate: '',
|
||
returnRate: '',
|
||
partsResendCount: '',
|
||
partsRate: '',
|
||
} satisfies SupplyChainProductArchiveRow
|
||
}
|