import type { PlatformProcurement, ProcurementFile, ProcurementRemaining, ProcurementRemainingKind } from './procurementTypes' import type { ProcurementCommand, ProcurementCommandBody } from './api/procurementCommands' export const controlCommands = new Set(['control-notes', 'control-notes/resolve', 'actual-warehouse/correct', 'exception-close']) export const remainingLabels: Record = { PURCHASE_REMAINDER: '采购未到货', TRANSIT_REMAINDER: '国际段未到货', GOOD: '合格货物', DAMAGED: '破损货物', EXTRA: '多到货物' } export const decisionLabels: Record = { SUPPLIER_RETAINED: '供应商保留', RETURNED_TO_SUPPLIER: '已退回供应商', RETURNED_TO_SENDER: '已退回寄件方', LOSS_CONFIRMED: '已确认丢失', CUSTOMER_AUTHORIZED_RETENTION: '客户授权留存', DISPOSED: '已处置' } export const allowedDecisions: Record = { PURCHASE_REMAINDER: ['SUPPLIER_RETAINED', 'RETURNED_TO_SUPPLIER', 'LOSS_CONFIRMED'], TRANSIT_REMAINDER: ['LOSS_CONFIRMED', 'RETURNED_TO_SENDER'], GOOD: ['RETURNED_TO_SUPPLIER', 'CUSTOMER_AUTHORIZED_RETENTION', 'DISPOSED'], DAMAGED: ['DISPOSED', 'RETURNED_TO_SUPPLIER'], EXTRA: ['RETURNED_TO_SUPPLIER'] } export interface ResolutionDraft extends ProcurementRemaining { decision: string; destination: string; voucherFileId: string } export interface ControlForm { targetType: 'PURCHASE' | 'WAREHOUSE' | 'RECEIPT' | 'SHIPMENT'; targetId: string; blocking: boolean; clientSummary: string; noteId: string; customerVoucherFileId: string; financialConclusion: string; resolutions: ResolutionDraft[] } export function controlForm(detail: PlatformProcurement): ControlForm { return { targetType: 'PURCHASE', targetId: '', blocking: true, clientSummary: '', noteId: '', customerVoucherFileId: '', financialConclusion: '', resolutions: detail.controls.remaining.map(row => ({ ...row, decision: '', destination: '', voucherFileId: '' })) } } function proof(files: ProcurementFile[], id: string) { if (!files.some(row => row.fileId === id && row.category === 'EXCEPTION')) throw new Error('请选择本单异常或客户确认凭证') } function sourceKey(row: { kind: string; sourceId: string | null }) { return `${row.kind}:${row.sourceId ?? ''}` } export function buildControl(command: ProcurementCommand, form: ControlForm, detail: PlatformProcurement, files: ProcurementFile[], base: ProcurementCommandBody, warehouseId: string, voucherFileId: string): ProcurementCommandBody { if (command === 'control-notes') { let targetId: string | undefined if (form.targetType === 'PURCHASE') { if (!detail.purchaseFacts.purchased_at) throw new Error('尚无真实购买事实,不能关联购买说明') } else if (form.targetType === 'WAREHOUSE') { if (!detail.purchaseFacts.actual_warehouse_id) throw new Error('尚无实际收货仓'); targetId = detail.purchaseFacts.actual_warehouse_id } else if (form.targetType === 'RECEIPT') { if (!detail.physical.receipts.some(row => row.id === form.targetId)) throw new Error('请选择本单实收记录'); targetId = form.targetId } else { if (!detail.transport.shipments.some(row => row.id === form.targetId)) throw new Error('请选择本单实际交寄记录'); targetId = form.targetId } proof(files, voucherFileId) if (form.clientSummary.trim().length > 500) throw new Error('对客摘要最多 500 字') return { ...base, targetType: form.targetType, ...(targetId ? { targetId } : {}), blocking: form.blocking, voucherFileId, ...(form.clientSummary.trim() ? { clientSummary: form.clientSummary.trim() } : {}) } } if (command === 'control-notes/resolve') { if (!detail.controls.notes.some(note => note.id === form.noteId && note.blocking && !note.resolved_at)) throw new Error('请选择本单尚未处理的阻断异常') proof(files, voucherFileId); return { ...base, noteId: form.noteId, voucherFileId } } if (command === 'actual-warehouse/correct') { if (!detail.purchaseFacts.actual_warehouse_id) throw new Error('尚无实际仓,请使用计划仓调整') if (!warehouseId || warehouseId === detail.purchaseFacts.actual_warehouse_id) throw new Error('请选择与原实际仓不同的有效海外收货仓') proof(files, voucherFileId); return { ...base, warehouseId, voucherFileId } } const action = detail.actions.find(row => row.key === 'EXCEPTION_CLOSE') if (!action?.allowed) throw new Error(action?.reason || '尚不满足异常关闭条件') if (!detail.purchaseFacts.purchased_at) throw new Error('购买前请使用取消订单') if (detail.controls.notes.some(note => note.blocking && !note.resolved_at)) throw new Error('请先处理本单未解除的阻断异常') proof(files, form.customerVoucherFileId) const expected = new Map(detail.controls.remaining.map(row => [sourceKey(row), row])) const resolutions = form.resolutions.map(row => { const source = expected.get(sourceKey(row)) if (!source || !Number.isSafeInteger(row.quantity) || row.quantity <= 0 || source.quantity !== row.quantity) throw new Error('处置必须恰好覆盖每份剩余来源,不可重复、跨单或改数量') expected.delete(sourceKey(row)) if (!allowedDecisions[row.kind].includes(row.decision)) throw new Error('处置方式与剩余货物来源不匹配') if (!row.destination.trim() || row.destination.trim().length > 500) throw new Error('请填写每份货物的实际去向及接收方,最多 500 字') proof(files, row.voucherFileId) return { kind: row.kind, sourceId: row.sourceId, quantity: row.quantity, decision: row.decision, destination: row.destination.trim(), voucherFileId: row.voucherFileId } }) if (expected.size) throw new Error('仍有剩余货物去向未明确') if (!form.financialConclusion.trim() || form.financialConclusion.trim().length > 1000) throw new Error('请填写财务处理结论,最多 1000 字') if (!form.clientSummary.trim() || form.clientSummary.trim().length > 500) throw new Error('请填写对客关闭摘要,最多 500 字') return { ...base, resolutions, customerVoucherFileId: form.customerVoucherFileId, financialConclusion: form.financialConclusion.trim(), clientSummary: form.clientSummary.trim() } }