52 lines
6.1 KiB
TypeScript
52 lines
6.1 KiB
TypeScript
import type { PlatformProcurement, ProcurementFile, ProcurementRemaining, ProcurementRemainingKind } from './procurementTypes'
|
|
import type { ProcurementCommand, ProcurementCommandBody } from './api/procurementCommands'
|
|
export const controlCommands = new Set<ProcurementCommand>(['control-notes', 'control-notes/resolve', 'actual-warehouse/correct', 'exception-close'])
|
|
export const remainingLabels: Record<ProcurementRemainingKind, string> = { PURCHASE_REMAINDER: '采购未到货', TRANSIT_REMAINDER: '国际段未到货', GOOD: '合格货物', DAMAGED: '破损货物', EXTRA: '多到货物' }
|
|
export const decisionLabels: Record<string, string> = { SUPPLIER_RETAINED: '供应商保留', RETURNED_TO_SUPPLIER: '已退回供应商', RETURNED_TO_SENDER: '已退回寄件方', LOSS_CONFIRMED: '已确认丢失', CUSTOMER_AUTHORIZED_RETENTION: '客户授权留存', DISPOSED: '已处置' }
|
|
export const allowedDecisions: Record<ProcurementRemainingKind, string[]> = { 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() }
|
|
}
|