feat(finished-product-progress): auto-import factory-confirmed SKUs on sample completion

This commit is contained in:
WuYongJie 2026-09-07 11:15:01 +08:00
parent 667438447a
commit 39d7fc02f6
6 changed files with 531 additions and 7 deletions

View File

@ -94,7 +94,12 @@ export function calculateFinishedProductInTransitQuantity(orderedQuantity: numbe
return Math.trunc(orderedQuantity) - Math.trunc(deliveryQuantity)
}
export const FINISHED_PRODUCT_CURRENT_NODE_OPTIONS = ['合同签订', '采购在途', '送货中', '已完结'] as const
export const FINISHED_PRODUCT_CURRENT_NODE_OPTIONS = ['合同待签', '合同签订', '采购在途', '送货中', '已完结'] as const
/** 同一 SPU 基号聚组排序用:去掉货号前缀字母(GBSFY1234/GBPM1234/1234 → 1234),前缀不同不影响归组。 */
export function finishedProductSpuBase(itemNumber: string) {
return String(itemNumber ?? '').trim().replace(/^[A-Za-z]+/, '')
}
export function resolveFinishedProductCurrentNode(contractNumber: string, orderedQuantity: number, deliveryQuantity: number) {
if (!String(contractNumber ?? '').trim()) return '合同签订'

View File

@ -0,0 +1,66 @@
import { sortFinishedProductRowsForList } from '@/server/repositories/finished-product-progress.repository'
type Row = {
id: string
orderPlacedDate: Date | null
orderSequenceNo: string
createdAt: Date
itemNumber: string
barcode: string | null
}
function row(id: string, overrides: Partial<Row> = {}): Row {
return {
id,
orderPlacedDate: null,
orderSequenceNo: '',
createdAt: new Date('2026-09-01T00:00:00Z'),
itemNumber: id,
barcode: null,
...overrides,
}
}
describe('sortFinishedProductRowsForList(成品进度列表聚组排序)', () => {
it('有下单日期的订单在前且保持原序,无日期的按 SPU 基号聚组', () => {
const rows = [
row('undated-gbsfy', { itemNumber: 'GBSFY1234' }),
row('dated-a', { orderPlacedDate: new Date('2026-09-01T00:00:00Z'), orderSequenceNo: 'SH003' }),
row('undated-plain', { itemNumber: '1234' }),
row('undated-gbpm', { itemNumber: 'GBPM1234' }),
row('dated-b', { orderPlacedDate: new Date('2026-08-01T00:00:00Z'), orderSequenceNo: 'SH002' }),
row('undated-other', { itemNumber: 'GBSFY999' }),
]
const sorted = sortFinishedProductRowsForList(rows)
expect(sorted.map((r) => r.id)).toEqual([
'dated-a', // 有下单日期,最近的在前
'dated-b',
'undated-other', // 基号 999 < 1234,组在前
'undated-plain', // 基号 1234 组:GBPM1234 / 1234 / GBSFY1234 按货号排序聚在一起
'undated-gbpm',
'undated-gbsfy',
])
})
it('同组内条码作为次级排序,创建时间兜底', () => {
const rows = [
row('x-b', { itemNumber: 'GBSFY1234', barcode: 'BBB' }),
row('x-a', { itemNumber: 'GBSFY1234', barcode: 'AAA' }),
row('x-c', { itemNumber: 'GBSFY1234', barcode: 'AAA', createdAt: new Date('2026-08-01T00:00:00Z') }),
]
const sorted = sortFinishedProductRowsForList(rows)
expect(sorted.map((r) => r.id)).toEqual(['x-a', 'x-c', 'x-b'])
})
it('无非日期行时保持原序不变', () => {
const rows = [
row('d1', { orderPlacedDate: new Date('2026-09-01T00:00:00Z') }),
row('d2', { orderPlacedDate: new Date('2026-08-01T00:00:00Z') }),
]
expect(sortFinishedProductRowsForList(rows).map((r) => r.id)).toEqual(['d1', 'd2'])
})
})

View File

@ -4,6 +4,7 @@ import {
FINISHED_PRODUCT_CURRENT_NODE_OPTIONS,
buildOrderSequenceNo,
buildOrderSequencePrefix,
finishedProductSpuBase,
resolveFinishedProductCurrentNode,
type FinishedProductProgressFilters,
type FinishedProductProgressInput,
@ -104,10 +105,57 @@ function dataValues(input: FinishedProductProgressInput) {
firstDueDate: input.firstDueDate ? new Date(`${input.firstDueDate}T00:00:00Z`) : null,
orderDueDate: input.orderDueDate ? new Date(`${input.orderDueDate}T00:00:00Z`) : null,
realtimeDate: input.realtimeDate ? new Date(`${input.realtimeDate}T00:00:00Z`) : null,
currentNode: resolveFinishedProductCurrentNode(input.contractNumber, input.orderedQuantity, input.deliveryQuantity),
// 有合同号的行按数量自动推进节点;空合同行(含定厂自动带入的待签行)保留表单所选节点
currentNode: String(input.contractNumber ?? '').trim()
? resolveFinishedProductCurrentNode(input.contractNumber, input.orderedQuantity, input.deliveryQuantity)
: (String(input.currentNode ?? '').trim() || '合同待签'),
}
}
type SortableFinishedProductRow = {
orderPlacedDate: Date | null
orderSequenceNo: string
createdAt: Date
itemNumber: string
barcode: string | null
}
/**
* 列表排序:有下单日期的订单保持原序在前;无下单日期的行(合同待签等)按 SPU 基号聚组——
* 同基号(GBSFY1234/GBPM1234/1234 → 1234)的行排在一起,组内按货号、条码排序。
*/
export function sortFinishedProductRowsForList<T extends SortableFinishedProductRow>(rows: T[]): T[] {
const dated = rows.filter((row) => row.orderPlacedDate)
const undated = rows.filter((row) => !row.orderPlacedDate)
if (undated.length === 0) return rows
const groups = new Map<string, T[]>()
for (const row of undated) {
const base = finishedProductSpuBase(row.itemNumber) || row.itemNumber || '~'
const bucket = groups.get(base)
if (bucket) bucket.push(row)
else groups.set(base, [row])
}
const compareBase = (left: string, right: string) => {
const leftNumeric = /^\d+$/.test(left)
const rightNumeric = /^\d+$/.test(right)
if (leftNumeric && rightNumeric) return Number(left) - Number(right)
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
return left.localeCompare(right, 'zh-Hans-CN')
}
const grouped = [...groups.entries()]
.sort(([left], [right]) => compareBase(left, right))
.flatMap(([, bucket]) => bucket.sort((left, right) => {
const byItemNumber = left.itemNumber.localeCompare(right.itemNumber, 'zh-Hans-CN')
if (byItemNumber !== 0) return byItemNumber
const leftBarcode = left.barcode ?? ''
const rightBarcode = right.barcode ?? ''
if (leftBarcode !== rightBarcode) return leftBarcode.localeCompare(rightBarcode)
return right.createdAt.getTime() - left.createdAt.getTime()
}))
return [...dated, ...grouped]
}
export class FinishedProductProgressRepository {
private async nextOrderSequenceNoInTransaction(tx: Prisma.TransactionClient, prefix: string) {
const normalized = prefix.trim() || 'DD'
@ -136,10 +184,11 @@ export class FinishedProductProgressRepository {
],
})
// 行身份重设计后每条记录即一笔独立订单,平铺返回
const pagedRows = rows.slice(params.skip, params.skip + params.take)
const sortedRows = sortFinishedProductRowsForList(rows)
const pagedRows = sortedRows.slice(params.skip, params.skip + params.take)
return {
groups: pagedRows.map((row) => [row]),
total: rows.length,
total: sortedRows.length,
}
}

View File

@ -0,0 +1,158 @@
import {
FinishedProductFactoryIntakeService,
FACTORY_INTAKE_LAUNCH_CUTOFF,
linkedProductIdFromFlowUrl,
type FinishedProductFactoryIntakeDeps,
} from '@/server/services/finished-product-factory-intake.service'
type CreatedRow = Record<string, unknown>
const FLOW_URL = 'https://cp.jiansun.vip/product-development/non-integrated/prod-1/detail'
function makeDeps() {
const created: CreatedRow[] = []
const deps: FinishedProductFactoryIntakeDeps = {
loadProduct: async () => ({
id: 'prod-1',
brand: '大货',
purchaser: '采购小王',
payload: {
uploadFiles: [
{ category: '设计卖点', mimeType: 'image/png', dataUrl: 'https://img.example/design.png' },
],
factoryRows: [
{
skuId: 'sku-1',
confirmed: true,
model: 'GBSFY1234',
barcode: 'GBSFY1234N1410V01',
specName: '款式五 100/14/2.5 碳纤维黑色 翻板',
supplierName: '三环',
},
{
skuId: 'sku-2',
confirmed: false,
model: 'GBSFY1235',
barcode: 'GBSFY1235N1410V01',
specName: '款式六',
supplierName: '三环',
},
{
skuId: 'sku-3',
confirmed: true,
model: '',
barcode: '',
specName: '缺货号条码',
supplierName: '三环',
},
{
skuId: 'sku-4',
confirmed: true,
model: 'GBPM1234',
barcode: 'GBPM1234N205V01',
specName: '款式七 120/60 白色',
supplierName: '瑞敏',
},
],
},
}),
findExistingAutoRow: async (itemNumber, barcode) => {
// 模拟真实持久化:本次测试会话内已创建的行也算已存在
if (created.some((c) => c.itemNumber === itemNumber && (c.barcode ?? null) === barcode)) {
return { id: 'created-in-session' }
}
if (itemNumber === 'GBSFY1234') return { id: 'existing-1' }
return null
},
createAutoRow: async (data) => {
created.push({ ...data })
return { id: `created-${created.length}` }
},
listCompletedSamplesSince: async () => [
{ openingCode: 'XP2026062500000226', productDevelopmentFlowUrl: FLOW_URL },
{ openingCode: 'XP2026070100000301', productDevelopmentFlowUrl: null },
],
}
return { deps, created }
}
describe('FinishedProductFactoryIntakeService(打样完结带入成品进度)', () => {
it('打样完结时:只带入定厂已确认的 SKU,幂等跳过已存在行,映射字段正确', async () => {
const { deps, created } = makeDeps()
const service = new FinishedProductFactoryIntakeService(deps)
const result = await service.syncCompletedSampleToFinishedProgress(
{ openingCode: 'XP2026062500000226', productDevelopmentFlowUrl: FLOW_URL },
{ id: 'user-1', name: '徐霞客' },
)
// sku-1 已存在跳过;sku-2 未确认跳过;sku-3 缺货号条码跳过;sku-4 创建
expect(result).toEqual({ created: 1, skipped: 1 })
expect(created).toHaveLength(1)
expect(created[0]).toMatchObject({
itemNumber: 'GBPM1234',
barcode: 'GBPM1234N205V01',
productName: '款式七 120/60 白色',
brand: '大货',
supplier: '瑞敏',
imageUrl: 'https://img.example/design.png',
followUpOwner: '采购小王',
createdById: 'user-1',
createdByName: '徐霞客',
})
expect(String(created[0].remarks)).toContain('定厂完成自动生成')
expect(String(created[0].remarks)).toContain('XP2026062500000226')
})
it('由壹点链接解析不出产品 ID 时返回零值', async () => {
const { deps, created } = makeDeps()
const service = new FinishedProductFactoryIntakeService(deps)
await expect(service.syncCompletedSampleToFinishedProgress({
openingCode: 'XP1',
productDevelopmentFlowUrl: 'https://cp.jiansun.vip/other/path',
})).resolves.toEqual({ created: 0, skipped: 0 })
expect(created).toHaveLength(0)
})
it('产品不存在时返回零值', async () => {
const deps: FinishedProductFactoryIntakeDeps = {
loadProduct: async () => null,
findExistingAutoRow: async () => null,
createAutoRow: async () => ({ id: 'x' }),
}
const service = new FinishedProductFactoryIntakeService(deps)
await expect(service.syncCompletedSampleToFinishedProgress({
openingCode: 'XP1',
productDevelopmentFlowUrl: FLOW_URL,
})).resolves.toEqual({ created: 0, skipped: 0 })
})
it('linkedProductIdFromFlowUrl 解析 /non-integrated/<id>', () => {
expect(linkedProductIdFromFlowUrl(FLOW_URL)).toBe('prod-1')
expect(linkedProductIdFromFlowUrl('not-a-url')).toBe('')
expect(linkedProductIdFromFlowUrl(null)).toBe('')
})
it('每日补抓:扫描上线后完结的打样单并重试,无链接的跳过、同天只跑一次', async () => {
const { deps, created } = makeDeps()
let now = new Date('2026-09-10T01:30:00Z')
const service = new FinishedProductFactoryIntakeService(deps, () => now)
const first = await service.sweepOncePerDay()
// 两条完结单:一条有链接(创建 1 条,1 条已存在跳过),一条无链接(抓不到,明天再试)
expect(first).toEqual({ scanned: 2, created: 1, skipped: 1 })
expect(created).toHaveLength(1)
// 同一天再刷列表:守卫生效,不再扫
now = new Date('2026-09-10T08:30:00Z')
await expect(service.sweepOncePerDay()).resolves.toEqual({ scanned: 0, created: 0, skipped: 0 })
expect(created).toHaveLength(1)
// 第二天:再次扫描,幂等跳过已抓到的
now = new Date('2026-09-11T01:30:00Z')
const third = await service.sweepOncePerDay()
expect(third.scanned).toBe(2)
expect(created).toHaveLength(1)
expect(FACTORY_INTAKE_LAUNCH_CUTOFF).toBe('2026-09-04')
})
})

View File

@ -0,0 +1,207 @@
import { prisma } from '@/lib/prisma'
const AUTO_CURRENT_NODE = '合同待签'
/** 功能上线日:打样完结时间早于该日的属于历史数据,每日补抓不扫描。 */
export const FACTORY_INTAKE_LAUNCH_CUTOFF = '2026-09-04'
export type FinishedProductFactoryIntakeActor = {
id?: string | null
name?: string | null
}
export type FinishedProductFactoryIntakeResult = {
created: number
skipped: number
}
export type FinishedProductFactoryAutoRowInput = {
itemNumber: string
barcode: string | null
productName: string
brand: string
supplier: string
imageUrl: string | null
followUpOwner: string
remarks: string
createdById: string | null
createdByName: string
}
export type FinishedProductFactoryIntakeDeps = {
loadProduct(productId: string): Promise<{
id: string
brand: string
purchaser: string
payload: unknown
} | null>
findExistingAutoRow(itemNumber: string, barcode: string | null): Promise<{ id: string } | null>
createAutoRow(data: FinishedProductFactoryAutoRowInput): Promise<{ id: string }>
/** 上线日后完结的打样单(开款编号+由壹点链接),每日补抓扫描用 */
listCompletedSamplesSince(cutoffDate: string): Promise<Array<{ openingCode: string; productDevelopmentFlowUrl: string | null }>>
}
function readText(value: unknown) {
return typeof value === 'string' ? value.trim() : ''
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
/** 由壹点链接 → 产品开发记录 ID(/non-integrated/<productId>) */
export function linkedProductIdFromFlowUrl(flowUrl: string | null | undefined) {
try {
const segments = new URL(flowUrl ?? '').pathname.split('/').filter(Boolean)
const index = segments.lastIndexOf('non-integrated')
return index >= 0 ? decodeURIComponent(segments[index + 1] || '') : ''
} catch {
return ''
}
}
// 与打样进度 OA 同步同口径:优先「设计卖点」图片,其次款式图
function extractProductImage(payload: unknown) {
if (!isRecord(payload)) return null
const files = Array.isArray(payload.uploadFiles) ? payload.uploadFiles : []
const designFile = files.find((value) => {
if (!isRecord(value)) return false
return value.category === '设计卖点'
&& String(value.mimeType || '').startsWith('image/')
&& typeof value.dataUrl === 'string'
&& Boolean(value.dataUrl.trim())
}) as { dataUrl?: string } | undefined
if (designFile?.dataUrl?.trim()) return designFile.dataUrl.trim()
const styleImages = Array.isArray(payload.styleImages) ? payload.styleImages : []
const styleImage = styleImages.find((value) => isRecord(value) && typeof value.url === 'string' && Boolean(value.url.trim())) as { url?: string } | undefined
return styleImage?.url?.trim() || null
}
export class FinishedProductFactoryIntakeService {
private lastSweepDate: string | null = null
constructor(
private readonly deps: FinishedProductFactoryIntakeDeps = {
loadProduct: async (productId) => prisma.intakeNonIntegratedProduct.findFirst({
where: { id: productId, deletedAt: null },
select: { id: true, brand: true, purchaser: true, payload: true },
}),
findExistingAutoRow: async (itemNumber, barcode) => prisma.finishedProductProgressItem.findFirst({
where: { contractNumber: '', itemNumber, barcode },
select: { id: true },
}),
createAutoRow: async (data) => prisma.finishedProductProgressItem.create({
data: {
contractDateNumber: '',
contractNumber: '',
orderSequenceNo: '',
orderPlacedDate: null,
followUpOwner: data.followUpOwner,
imageUrl: data.imageUrl,
itemNumber: data.itemNumber,
barcode: data.barcode,
productName: data.productName,
supplier: data.supplier,
brand: data.brand,
orderedQuantity: 0,
inTransitQuantity: 0,
firstDueDate: null,
orderDueDate: null,
realtimeDate: null,
deliveryQuantity: 0,
currentNode: AUTO_CURRENT_NODE,
remarks: data.remarks,
createdById: data.createdById,
createdByName: data.createdByName,
updatedById: data.createdById,
updatedByName: data.createdByName,
},
select: { id: true },
}),
listCompletedSamplesSince: async (cutoffDate) => prisma.sampleProgressItem.findMany({
where: { status: 'COMPLETED', actualSampleCompletedAt: { gte: new Date(`${cutoffDate}T00:00:00Z`) } },
select: { openingCode: true, productDevelopmentFlowUrl: true },
orderBy: { actualSampleCompletedAt: 'asc' },
}),
},
private readonly now: () => Date = () => new Date(),
) {}
/** 打样完结(实物审样完成,打样状态变为已完结)时调用:按开款编号+由壹点链接
* 关联产品开发记录,把已定厂确认的 SKU 幂等带入成品进度明细(合同待签)。
* 调用方负责只在「未完结 → 已完结」转换时触发,历史已完结产品不会被翻出。 */
async syncCompletedSampleToFinishedProgress(
input: { openingCode: string; productDevelopmentFlowUrl: string | null },
actor: FinishedProductFactoryIntakeActor = {},
): Promise<FinishedProductFactoryIntakeResult> {
const productId = linkedProductIdFromFlowUrl(input.productDevelopmentFlowUrl)
if (!productId) return { created: 0, skipped: 0 }
const product = await this.deps.loadProduct(productId)
if (!product) return { created: 0, skipped: 0 }
const payload = product.payload
const factoryRows = isRecord(payload) && Array.isArray(payload.factoryRows) ? payload.factoryRows : []
if (factoryRows.length === 0) return { created: 0, skipped: 0 }
const image = extractProductImage(payload)
const today = this.now().toISOString().slice(0, 10)
const actorId = actor.id ?? null
const actorName = actor.name || '系统'
let created = 0
let skipped = 0
for (const row of factoryRows) {
if (!isRecord(row) || row.confirmed !== true) continue
const itemNumber = readText(row.model)
const barcode = readText(row.barcode)
if (!itemNumber && !barcode) continue
const existing = await this.deps.findExistingAutoRow(itemNumber, barcode || null)
if (existing) {
skipped += 1
continue
}
await this.deps.createAutoRow({
itemNumber,
barcode: barcode || null,
productName: readText(row.specName),
brand: product.brand || '',
supplier: readText(row.supplierName),
imageUrl: image,
followUpOwner: product.purchaser || '',
remarks: `定厂完成自动生成 ${today}(打样单 ${input.openingCode})`,
createdById: actorId,
createdByName: actorName,
})
created += 1
}
return { created, skipped }
}
/** 每日补抓:扫描上线后完结的全部打样单并重试带入(幂等)。当时未定厂的会在
* 定厂确认后的某天补抓中被抓到,直到抓到为止。 */
async runDailyCatchUpSweep(actor: FinishedProductFactoryIntakeActor = {}): Promise<FinishedProductFactoryIntakeResult & { scanned: number }> {
const samples = await this.deps.listCompletedSamplesSince(FACTORY_INTAKE_LAUNCH_CUTOFF)
let created = 0
let skipped = 0
let scanned = 0
for (const sample of samples) {
if (!sample.openingCode) continue
scanned += 1
const result = await this.syncCompletedSampleToFinishedProgress(sample, actor)
created += result.created
skipped += result.skipped
}
this.lastSweepDate = this.now().toISOString().slice(0, 10)
return { scanned, created, skipped }
}
/** 每天最多执行一次补抓,其余调用直接跳过(进程内守卫)。 */
sweepOncePerDay(actor: FinishedProductFactoryIntakeActor = {}): Promise<FinishedProductFactoryIntakeResult & { scanned: number }> {
const today = this.now().toISOString().slice(0, 10)
if (this.lastSweepDate === today) return Promise.resolve({ scanned: 0, created: 0, skipped: 0 })
return this.runDailyCatchUpSweep(actor)
}
}
export const finishedProductFactoryIntakeService = new FinishedProductFactoryIntakeService()

View File

@ -17,6 +17,7 @@ import {
sampleProgressSupplierSnapshot,
} from '@/server/services/sample-progress-change-log'
import { serializeSampleProgress } from '@/server/services/sample-progress-serialization'
import { finishedProductFactoryIntakeService } from '@/server/services/finished-product-factory-intake.service'
import {
buildSampleProgressParentSummary,
resolveSupplierWrites,
@ -242,7 +243,8 @@ async function syncFromOaItems(items: OaBoardProjectListItemDTO[]) {
const endedAt = endedAtFromOa ?? (
current?.autoGenerated ? null : current?.actualSampleCompletedAt ?? null
)
return sampleProgressRepository.updateOaFields({
const previousStatus = current?.status ?? null
const updatePromise = sampleProgressRepository.updateOaFields({
openingCode,
brand: item.formSummary.plannedSalesBrands || item.formSummary.salesBrands || item.brand || '',
designer: structureDesignerByProductId.get(productId) || item.formSummary.structureDesigner || '',
@ -256,6 +258,24 @@ async function syncFromOaItems(items: OaBoardProjectListItemDTO[]) {
imageUrl: designImageByProductId.get(linkedProductId(workflowLink)) || item.styleImages[0]?.url || null,
productDevelopmentFlowUrl: workflowLink || null,
})
// 打样状态从未完结变为已完结(实物审样完成)时,把已定厂 SKU 自动带入成品进度明细
if (nextStatus === 'COMPLETED' && previousStatus !== 'COMPLETED') {
return updatePromise.then(async () => {
try {
const syncResult = await finishedProductFactoryIntakeService.syncCompletedSampleToFinishedProgress({
openingCode,
productDevelopmentFlowUrl: workflowLink || null,
})
if (syncResult.created > 0) {
console.log('[sample-progress] 打样完结自动带入成品进度明细:', { openingCode, ...syncResult })
}
} catch (error) {
console.error(`[sample-progress] 打样完结自动带入成品进度失败 (${openingCode}):`, error)
}
return { count: 1 }
})
}
return updatePromise
}))
}
@ -327,6 +347,10 @@ export const sampleProgressService = {
async listStored() { return (await sampleProgressRepository.list()).map(serializeSampleProgress) },
async list() {
await this.syncFromOaDashboard().catch(() => undefined)
// 每日一次:上线后完结但当时未定厂的打样单,补抓已定厂 SKU 直到抓到
await finishedProductFactoryIntakeService.sweepOncePerDay().catch((error) => {
console.error('[sample-progress] 定厂 SKU 每日补抓失败:', error)
})
return this.listStored()
},
getOaPrefill: readOaPrefill,
@ -393,7 +417,7 @@ export const sampleProgressService = {
const changeReason = changedDueSuppliers.map((supplier, index) =>
`${supplier.supplier || `供应商 ${index + 1}`}:${supplier.deliveryDateChangeReason?.trim()}`,
).join('\n') || deliveryDateChangeReason?.trim() || null
return serializeSampleProgress(await sampleProgressRepository.updateWithChangeLog({
const record = await sampleProgressRepository.updateWithChangeLog({
id,
data: { ...merged, openingCode: current.openingCode, autoGenerated: false, updatedBy: actor.id },
suppliers: supplierWrites,
@ -403,7 +427,22 @@ export const sampleProgressService = {
changeReason,
changedById: actor.id,
changedByName: actor.name || '未知用户',
}))
})
// 人工把打样状态改为已完结(实物审样完成)时,把已定厂 SKU 自动带入成品进度明细
if (current.status !== 'COMPLETED' && record.status === 'COMPLETED') {
try {
const syncResult = await finishedProductFactoryIntakeService.syncCompletedSampleToFinishedProgress({
openingCode: current.openingCode,
productDevelopmentFlowUrl: current.productDevelopmentFlowUrl ?? null,
}, { id: actor.id, name: actor.name })
if (syncResult.created > 0) {
console.log('[sample-progress] 打样完结(人工)自动带入成品进度明细:', { openingCode: current.openingCode, ...syncResult })
}
} catch (error) {
console.error(`[sample-progress] 打样完结(人工)自动带入成品进度失败 (${current.openingCode}):`, error)
}
}
return serializeSampleProgress(record)
},
async listChangeLogs(id: string) {
if (!(await sampleProgressRepository.findById(id))) throw new AppError(ErrorCode.VALIDATION_FAILED, '打样记录不存在')