import type { PlatformProcurement } from './procurementTypes' import type { ProcurementCommand, ProcurementCommandBody, ProcurementPackingCarton } from './api/procurementCommands' export type Leg = 'OVERSEAS' | 'DOMESTIC' export const physicalCommands = new Set(['overseas-receipts', 'domestic-receipts', 'overseas-receipts/complete', 'domestic-receipts/complete', 'packing', 'packing/confirm', 'packing/withdraw', 'dispositions', 'receipts/correct', 'dispositions/revoke']) export interface PackingDraft { cartonNo: string; lengthCm: string; widthCm: string; heightCm: string; grossWeightKg: string; sources: { receiptId: string; quantity: number | undefined }[] } export interface PhysicalForm { leg: Leg; sourceCartonId: string; receiptId: string; dispositionId: string; goodQuantity: number | undefined; damagedQuantity: number | undefined; extraQuantity: number | undefined; receivedAt: string; kind: 'SHORTAGE' | 'DAMAGED' | 'EXTRA'; quantity: number | undefined; cartons: PackingDraft[] } export function newCarton(): PackingDraft { return { cartonNo: '', lengthCm: '', widthCm: '', heightCm: '', grossWeightKg: '', sources: [{ receiptId: '', quantity: undefined }] } } export function physicalForm(detail?: PlatformProcurement): PhysicalForm { return { leg: 'OVERSEAS', sourceCartonId: '', receiptId: '', dispositionId: '', goodQuantity: 0, damagedQuantity: 0, extraQuantity: 0, receivedAt: '', kind: 'SHORTAGE', quantity: undefined, cartons: detail?.physical.cartons.length ? detail.physical.cartons.map(row => ({ cartonNo: row.cartonNo, lengthCm: String(row.lengthCm), widthCm: String(row.widthCm), heightCm: String(row.heightCm), grossWeightKg: String(row.grossWeightKg), sources: row.sources.map(source => ({ ...source })) })) : [newCarton()] } } export function shippedCartons(detail: PlatformProcurement) { const numbers = new Set(detail.customerView.shipments.filter(row => row.leg === 'OVERSEAS').map(row => row.cartonNo)); return detail.physical.cartons.filter(row => numbers.has(row.cartonNo)) } export function whole(value: number | undefined, label: string, min = 0): number { if (value === undefined || !Number.isSafeInteger(value) || value < min) throw new Error(`${label}必须为${min ? '正' : '非负'}整数`); return value } export function dimension(value: string, label: string, scale: number, digits: number) { if (!new RegExp(`^\\d{1,${digits}}(?:\\.\\d{1,${scale}})?$`).test(value.trim()) || Number(value) <= 0) throw new Error(`${label}必须为正数,最多 ${scale} 位小数`); return value.trim() } export function buildPacking(cartons: PackingDraft[], detail: PlatformProcurement): ProcurementPackingCarton[] { if (!cartons.length) throw new Error('请至少建立一个装货箱') const used = new Map(); const names = new Set() return cartons.map(carton => { const cartonNo = carton.cartonNo.trim(); if (!cartonNo || cartonNo.length > 64 || names.has(cartonNo)) throw new Error('箱号必填、不可重复且不超过 64 个字符'); names.add(cartonNo) if (!carton.sources.length) throw new Error('每箱必须分配实收来源') const inCarton = new Set() const sources = carton.sources.map(source => { const receipt = detail.physical.receipts.find(row => row.id === source.receiptId && row.active && row.leg === 'OVERSEAS') if (!receipt || inCarton.has(source.receiptId)) throw new Error('请选择有效海外实收来源,同箱来源不可重复') inCarton.add(source.receiptId) const quantity = whole(source.quantity, '装箱件数', 1); const total = (used.get(source.receiptId) || 0) + quantity if (!Number.isSafeInteger(total) || total > receipt.goodQuantity) throw new Error(`装箱件数超过实收 ${receipt.receiptNo} 的合格数量`) used.set(source.receiptId, total); return { receiptId: source.receiptId, quantity } }) return { cartonNo, lengthCm: dimension(carton.lengthCm, '长', 2, 10), widthCm: dimension(carton.widthCm, '宽', 2, 10), heightCm: dimension(carton.heightCm, '高', 2, 10), grossWeightKg: dimension(carton.grossWeightKg, '毛重', 3, 9), sources } }) } export function buildPhysical(command: ProcurementCommand, form: PhysicalForm, detail: PlatformProcurement, base: ProcurementCommandBody, warehouseId: string, voucherFileId: string): ProcurementCommandBody { const closed = ['REJECTED', 'CANCELLED', 'EXCEPTION_CLOSED', 'SHIPPED_TO_CUSTOMER'].includes(detail.customerView.order.status) if (closed) throw new Error('订单已结束,不能改写实物记录') if (command === 'packing') return { ...base, cartons: buildPacking(form.cartons, detail) } if (command === 'packing/confirm') { if (detail.physical.packingStatus !== 'DRAFT' || !detail.physical.packingVersion) throw new Error('请先保存当前装箱草稿') return { ...base, packingVersion: detail.physical.packingVersion } } if (command === 'packing/withdraw') { if (detail.physical.packingStatus !== 'CONFIRMED' || detail.purchaseFacts.overseas_shipped_at) throw new Error('仅允许撤回尚未交寄的已确认清单') return base } if (command === 'dispositions/revoke') { if (!detail.physical.dispositions.some(row => row.id === form.dispositionId && row.active)) throw new Error('请选择有效差异处置') return { ...base, dispositionId: form.dispositionId, voucherFileId } } if (command.endsWith('/complete')) return { ...base, leg: command.startsWith('overseas') ? 'OVERSEAS' : 'DOMESTIC' } const correction = command === 'receipts/correct' const old = correction ? detail.physical.receipts.find(row => row.id === form.receiptId && row.active) : undefined if (correction && !old) throw new Error('请选择有效原实收记录') const leg: Leg = old?.leg ?? (command === 'overseas-receipts' ? 'OVERSEAS' : command === 'domestic-receipts' ? 'DOMESTIC' : form.leg) const sourceCartonId = old?.sourceCartonId || form.sourceCartonId if (leg === 'DOMESTIC' && !shippedCartons(detail).some(row => row.id === sourceCartonId)) throw new Error('请选择本单已国际交寄的原箱') if (command === 'dispositions') { const quantity = whole(form.quantity, '处置件数', 1) const source = leg === 'DOMESTIC' ? sourceCartonId : null const receipt = detail.physical.receipts.find(row => row.id === form.receiptId && row.active && row.leg === leg && row.sourceCartonId === source) const records = detail.physical.receipts.filter(row => row.active && row.leg === leg && row.sourceCartonId === source) const resolved = detail.physical.dispositions.filter(row => row.active && row.leg === leg && row.source_carton_id === source && row.kind === form.kind && (form.kind === 'SHORTAGE' || row.receipt_id === form.receiptId)).reduce((sum, row) => sum + row.quantity, 0) const expected = leg === 'OVERSEAS' ? detail.purchaseFacts.purchased_quantity : shippedCartons(detail).find(row => row.id === source)?.quantity const available = form.kind === 'SHORTAGE' ? Number(expected) - records.reduce((sum, row) => sum + row.goodQuantity + row.damagedQuantity, 0) - resolved : receipt ? (form.kind === 'DAMAGED' ? receipt.damagedQuantity : receipt.extraQuantity) - resolved : -1 if (!Number.isSafeInteger(available) || quantity > available) throw new Error('处置件数超过当前来源的未处理差异') const decisions = { SHORTAGE: 'ACCEPT_SHORTAGE', DAMAGED: 'DISPOSE_DAMAGED', EXTRA: 'RETURN_EXTRA' } as const return { ...base, leg, ...(leg === 'DOMESTIC' ? { sourceCartonId } : {}), ...(form.kind === 'SHORTAGE' ? {} : { receiptId: form.receiptId }), kind: form.kind, quantity, decision: decisions[form.kind], voucherFileId } } const goodQuantity = whole(form.goodQuantity, '合格件数'), damagedQuantity = whole(form.damagedQuantity, '破损件数'), extraQuantity = whole(form.extraQuantity, '多货件数') if (!Number.isSafeInteger(goodQuantity + damagedQuantity + extraQuantity) || goodQuantity + damagedQuantity + extraQuantity <= 0) throw new Error('本次实收总件数必须大于零') if (!form.receivedAt || !Number.isFinite(Date.parse(form.receivedAt)) || Date.parse(form.receivedAt) > Date.now()) throw new Error('请填写不晚于当前时间的真实实收时间') const origin = leg === 'OVERSEAS' ? detail.purchaseFacts.purchased_at : detail.purchaseFacts.overseas_shipped_at if (!origin || Date.parse(form.receivedAt) < Date.parse(origin)) throw new Error('实收时间不能早于购买或国际交寄时间') const source = leg === 'DOMESTIC' ? sourceCartonId : null const counted = detail.physical.receipts.filter(row => row.active && row.leg === leg && row.sourceCartonId === source && row.id !== old?.id).reduce((sum, row) => sum + row.goodQuantity + row.damagedQuantity, 0) const expected = leg === 'OVERSEAS' ? detail.purchaseFacts.purchased_quantity : shippedCartons(detail).find(row => row.id === sourceCartonId)?.quantity if (expected == null || goodQuantity + damagedQuantity + counted > expected) throw new Error('合格及破损实收超过来源件数,多到货物请填入多货') if (leg === 'OVERSEAS' && (!warehouseId || (detail.purchaseFacts.actual_warehouse_id && detail.purchaseFacts.actual_warehouse_id !== warehouseId))) throw new Error('实际收货仓必须与本单已冻结的仓库一致') return { ...base, leg, ...(leg === 'OVERSEAS' ? { warehouseId } : { sourceCartonId }), goodQuantity, damagedQuantity, extraQuantity, receivedAt: form.receivedAt, ...(correction ? { receiptId: form.receiptId, voucherFileId } : {}) } }