furniture-3d-system-Wu/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts

188 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}