优化
This commit is contained in:
@@ -113,6 +113,13 @@ export async function inventorySnapshot(context, customerId, warehouseId, inboun
|
||||
blocker(context, name, '需要本轮正式PDA Task.positions冻结的全部真实balance IDs;分组代表ID不能冒充完整来源')
|
||||
return null
|
||||
}
|
||||
const inventoryEvidence = context.createdResources?.phase5OverseasFulfillment
|
||||
if (inventoryEvidence) {
|
||||
inventoryEvidence.inventoryReadEvidence ||= {}
|
||||
inventoryEvidence.inventoryReadEvidence[name] = { customerId, warehouseId, inboundCodes: [...codes],
|
||||
sourceBalanceIds: sourceIds, complete: false, globalGroups: null, sourceBalances: [] }
|
||||
}
|
||||
const readEvidence = inventoryEvidence?.inventoryReadEvidence?.[name]
|
||||
const groups = await allPages(context, `${LASTLEG_BASE}/inventory/balances`,
|
||||
{ customerId, overseasWarehouseId: warehouseId }, `${name}-客户仓全部分组余额`)
|
||||
if (!groups) return null
|
||||
@@ -121,6 +128,7 @@ export async function inventorySnapshot(context, customerId, warehouseId, inboun
|
||||
&& new Set(groups.map(balanceDimension)).size === groups.length
|
||||
context.assert(MODULE, `${name}完整分组客户仓数量及维度唯一`, null, groupValid, `groups=${groups.length}`, OPTIONS)
|
||||
if (!groupValid) return null
|
||||
if (readEvidence) readEvidence.globalGroups = inventoryQuantitySnapshot(groups)
|
||||
context.context ||= {}
|
||||
context.context.phase5InventorySources ||= {}
|
||||
const frozen = context.context.phase5InventorySources
|
||||
@@ -167,6 +175,7 @@ export async function inventorySnapshot(context, customerId, warehouseId, inboun
|
||||
if (!included) return null
|
||||
frozen[sourceKey] = { balance: inventoryQuantitySnapshot([balance])[0], group: inventoryQuantitySnapshot([group])[0] }
|
||||
}
|
||||
if (readEvidence) readEvidence.sourceBalances.push({ balance: inventoryQuantitySnapshot([balance])[0], positions: inventoryQuantitySnapshot(positions) })
|
||||
const reconciled = POSITION_BUCKETS.every(field => positions.reduce((sum, row) => sum + Number(row[field]), 0) === Number(balance[field]))
|
||||
context.assert(MODULE, `${name}全部位置独立核平原余额`, current.response, reconciled,
|
||||
`balanceId=${balanceId}, balanceTotal=${balance.totalPackages}`, OPTIONS)
|
||||
@@ -183,6 +192,7 @@ export async function inventorySnapshot(context, customerId, warehouseId, inboun
|
||||
.reduce((sum, row) => sum + Number(row.balance[field]), 0) <= Number(group[field])))
|
||||
context.assert(MODULE, `${name}固定原始余额合计不超过独立分组`, null, jointlyIncluded,
|
||||
`rawBalances=${snapshot.length}, groups=${groups.length}`, OPTIONS)
|
||||
if (readEvidence) readEvidence.complete = jointlyIncluded
|
||||
return jointlyIncluded ? snapshot : null
|
||||
}
|
||||
|
||||
@@ -218,10 +228,20 @@ async function shipmentLedger(context, customerId, outboundNo, name, businessTyp
|
||||
&& (!businessOrderNo || row.businessOrderNo === businessOrderNo))
|
||||
context.assert(MODULE, `${name}只属于本轮实际出库来源`, null, exact.length === rows.length,
|
||||
`rows=${rows.length}, exact=${exact.length}, outboundNo=${outboundNo}`, OPTIONS)
|
||||
return exact.map(row => ({ id: id(row.id), inboundCode: row.inboundCode,
|
||||
palletNo: row.palletNo, deltaPackages: Number(row.deltaPackages),
|
||||
beforePackages: Number(row.beforePackages), afterPackages: Number(row.afterPackages) }))
|
||||
const quantities = rows.map(row => ({ id: id(row.id), customerId: id(row.customerId),
|
||||
overseasWarehouseId: id(row.overseasWarehouseId), inboundOrderId: id(row.inboundOrderId),
|
||||
inboundOrderNo: id(row.inboundOrderNo), inboundCode: id(row.inboundCode), palletNo: id(row.palletNo),
|
||||
batchNo: id(row.batchNo), skuCode: id(row.skuCode), businessType: id(row.businessType),
|
||||
businessOrderNo: id(row.businessOrderNo), outboundOrderNo: id(row.outboundOrderNo),
|
||||
deltaPackages: Number(row.deltaPackages), beforePackages: Number(row.beforePackages), afterPackages: Number(row.afterPackages) }))
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
const owned = context.createdResources?.phase5OverseasFulfillment
|
||||
if (owned) {
|
||||
owned.ledgerReadEvidence ||= {}
|
||||
owned.ledgerReadEvidence[name] = { customerId, outboundNo, businessType, businessOrderNo: businessOrderNo || null,
|
||||
complete: exact.length === rows.length, rows: quantities }
|
||||
}
|
||||
return exact.length === rows.length ? quantities : null
|
||||
}
|
||||
|
||||
const quantityTotal = rows => (rows || []).reduce((sum, row) => sum + Number(row.totalPackages), 0)
|
||||
@@ -229,6 +249,56 @@ const groupQuantityTotal = rows => (rows?.[0]?.globalGroups || []).reduce((sum,
|
||||
const balanceQuantityTotal = rows => (rows || []).reduce((sum, row) => sum + Number(row.balance.totalPackages), 0)
|
||||
const sameSnapshot = (left, right) => left !== null && right !== null && JSON.stringify(left) === JSON.stringify(right)
|
||||
|
||||
export function dispatchStockConservation(context, { customerId, warehouseId, dispatchNo, expectedPackages,
|
||||
sourcePositionBalances, stockBefore, stockAfter, ledgerBefore, ledgerAfter }) {
|
||||
const positions = snapshot => (snapshot || []).flatMap(row => row.positions || [])
|
||||
const referenceKeys = new Set(sourcePositionBalances.map(row => `${row.balanceId}:${row.positionId}`))
|
||||
const selected = snapshot => positions(snapshot).filter(row => referenceKeys.has(`${row.balanceId}:${row.id}`))
|
||||
const positionBefore = selected(stockBefore)
|
||||
const positionAfter = selected(stockAfter)
|
||||
const newLedger = ledgerAfter?.filter(row => !ledgerBefore?.some(before => before.id === row.id)) || []
|
||||
const rowChecks = newLedger.map(row => ({ id: row.id,
|
||||
identity: row.customerId === customerId && row.overseasWarehouseId === warehouseId
|
||||
&& row.businessType === 'SHIP' && row.businessOrderNo === dispatchNo && row.outboundOrderNo === dispatchNo
|
||||
&& positionBefore.some(position => position.palletNo === row.palletNo),
|
||||
quantities: [row.beforePackages, row.deltaPackages, row.afterPackages].every(Number.isSafeInteger)
|
||||
&& row.deltaPackages < 0 && row.afterPackages - row.beforePackages === row.deltaPackages }))
|
||||
const identity = row => Object.fromEntries(INVENTORY_IDENTITIES.filter(field => row[field] !== undefined).map(field => [field, row[field]]))
|
||||
const sourcesStable = positionBefore.every(row => {
|
||||
const after = positionAfter.find(value => value.id === row.id && value.balanceId === row.balanceId)
|
||||
return after && JSON.stringify(identity(row)) === JSON.stringify(identity(after))
|
||||
})
|
||||
const unselectedBefore = positions(stockBefore).filter(row => !referenceKeys.has(`${row.balanceId}:${row.id}`))
|
||||
const unselectedAfter = positions(stockAfter).filter(row => !referenceKeys.has(`${row.balanceId}:${row.id}`))
|
||||
const totals = { positionBefore: quantityTotal(positionBefore), positionAfter: quantityTotal(positionAfter),
|
||||
rawBefore: balanceQuantityTotal(stockBefore), rawAfter: balanceQuantityTotal(stockAfter),
|
||||
groupBefore: groupQuantityTotal(stockBefore), groupAfter: groupQuantityTotal(stockAfter),
|
||||
ledgerShipped: -newLedger.reduce((sum, row) => sum + row.deltaPackages, 0), expectedPackages }
|
||||
const checks = {
|
||||
fixedSourcesPresent: inventorySourcesPresent(stockBefore, sourcePositionBalances)
|
||||
&& inventorySourcesPresent(stockAfter, sourcePositionBalances),
|
||||
sourceIdentitiesStable: sourcesStable,
|
||||
unselectedPositionsUnchanged: sameSnapshot(inventoryQuantitySnapshot(unselectedBefore), inventoryQuantitySnapshot(unselectedAfter)),
|
||||
positionConservation: expectedPackages > 0 && totals.positionBefore - totals.positionAfter === expectedPackages,
|
||||
rawConservation: stockBefore !== null && stockAfter !== null && totals.rawBefore - totals.rawAfter === expectedPackages,
|
||||
groupConservation: stockBefore !== null && stockAfter !== null && totals.groupBefore - totals.groupAfter === expectedPackages,
|
||||
ledgerSourceIdentities: ledgerBefore !== null && ledgerAfter !== null && newLedger.length > 0
|
||||
&& new Set(newLedger.map(row => row.id)).size === newLedger.length && rowChecks.every(row => row.identity),
|
||||
ledgerQuantities: newLedger.length > 0 && totals.ledgerShipped === expectedPackages && rowChecks.every(row => row.quantities),
|
||||
}
|
||||
const proof = { sourcePositionBalances, totals, checks, ledgerRowChecks: rowChecks,
|
||||
snapshots: { beforePick: stockBefore, afterPick: stockAfter }, ledgers: { beforePick: ledgerBefore, afterPick: ledgerAfter } }
|
||||
if (context.createdResources?.phase5OverseasFulfillment) {
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, proof)
|
||||
}
|
||||
for (const [name, passed] of Object.entries(checks)) {
|
||||
context.assert(MODULE, `本排车库存独立核对-${name}`, null, passed, JSON.stringify(totals), OPTIONS)
|
||||
}
|
||||
const passed = Object.values(checks).every(Boolean)
|
||||
context.assert(MODULE, 'PDA扫码及重放实际库存只扣本出库数量一次', null, passed, JSON.stringify(totals), OPTIONS)
|
||||
return { passed, proof, newLedger }
|
||||
}
|
||||
|
||||
// A dedicated multi-fragment producer must supply these codes. Multiple FBA destinations alone are not proof.
|
||||
export async function verifyPartialOrderCompletion(context, { orderId, customerId, deliveredInboundCode, pendingInboundCodes }) {
|
||||
if (!id(orderId) || !id(customerId) || !nonBlank(deliveredInboundCode)
|
||||
@@ -820,22 +890,20 @@ export async function run(context) {
|
||||
const { sourceBalanceIds, sourcePositionIds, sourcePositionBalances } = sourceIds
|
||||
context.createdResources.phase5OverseasFulfillment.inventoryProof = { sourceBalanceIds, sourcePositionIds, sourcePositionBalances }
|
||||
const stockBeforePick = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '扫码前本排车来源库存', sourceBalanceIds)
|
||||
const ledgerBeforePick = await shipmentLedger(context, customerId, dispatchNo, '扫码前本排车SHIP流水')
|
||||
const ledgerBeforePick = await shipmentLedger(context, customerId, dispatchNo, '扫码前本排车SHIP流水', 'SHIP', dispatchNo)
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, { snapshots: { beforePick: stockBeforePick }, ledgers: { beforePick: ledgerBeforePick } })
|
||||
const sourcesBeforeValid = inventorySourcesPresent(stockBeforePick, sourcePositionBalances)
|
||||
context.assert(MODULE, '扫码前库存完整覆盖任务冻结的每个原位置关联', null, sourcesBeforeValid, `positions=${sourcePositionIds.length}`, OPTIONS)
|
||||
if (!sourcesBeforeValid || !ledgerBeforePick || !await pickAll(context, picker.token, dispatchNo)) return
|
||||
if (!sourcesBeforeValid || !ledgerBeforePick) return
|
||||
const pickedSuccessfully = await pickAll(context, picker.token, dispatchNo)
|
||||
const stockAfterPick = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '扫码及重放后库存', sourceBalanceIds)
|
||||
const ledgerAfterPick = await shipmentLedger(context, customerId, dispatchNo, '扫码及重放后SHIP流水')
|
||||
const shippedRows = ledgerAfterPick?.filter(row => !ledgerBeforePick.some(before => before.id === row.id))
|
||||
const ledgerAfterPick = await shipmentLedger(context, customerId, dispatchNo, '扫码及重放后SHIP流水', 'SHIP', dispatchNo)
|
||||
const expectedPackages = Number(transferred.data.packageCount)
|
||||
context.assert(MODULE, 'PDA扫码及重放实际库存只扣本出库数量一次', null,
|
||||
inventorySourcesPresent(stockAfterPick, sourcePositionBalances) && quantityTotal(stockBeforePick) - quantityTotal(stockAfterPick) === expectedPackages
|
||||
&& balanceQuantityTotal(stockBeforePick) - balanceQuantityTotal(stockAfterPick) === expectedPackages
|
||||
&& groupQuantityTotal(stockBeforePick) - groupQuantityTotal(stockAfterPick) === expectedPackages
|
||||
&& expectedPackages > 0 && shippedRows?.length > 0
|
||||
&& -shippedRows.reduce((sum, row) => sum + row.deltaPackages, 0) === expectedPackages
|
||||
&& shippedRows.every(row => row.deltaPackages < 0 && row.afterPackages - row.beforePackages === row.deltaPackages),
|
||||
`before=${quantityTotal(stockBeforePick)}, after=${quantityTotal(stockAfterPick)}, expected=${expectedPackages}, SHIPRows=${shippedRows?.length ?? 'missing'}`, OPTIONS)
|
||||
const conservation = dispatchStockConservation(context, { customerId, warehouseId, dispatchNo, expectedPackages,
|
||||
sourcePositionBalances, stockBefore: stockBeforePick, stockAfter: stockAfterPick,
|
||||
ledgerBefore: ledgerBeforePick, ledgerAfter: ledgerAfterPick })
|
||||
const shippedRows = conservation.newLedger
|
||||
if (!pickedSuccessfully) return
|
||||
const ready = await verifyDepartureReadiness(context, picker.token, dispatchNo)
|
||||
if (!ready) return
|
||||
const departBody = { idempotencyKey: key(context, 'DISPATCH-DEPART'), remark: `${context.marker}-depart` }
|
||||
@@ -852,16 +920,19 @@ export async function run(context) {
|
||||
&& Number(departReplay.data?.shippedPackages) === Number(departed.data?.shippedPackages),
|
||||
trace(departReplay.response), OPTIONS)
|
||||
const stockAfterDepart = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '发车及重放后库存', sourceBalanceIds)
|
||||
const ledgerAfterDepart = await shipmentLedger(context, customerId, dispatchNo, '发车及重放后SHIP流水')
|
||||
const ledgerAfterDepart = await shipmentLedger(context, customerId, dispatchNo, '发车及重放后SHIP流水', 'SHIP', dispatchNo)
|
||||
const inventoryProof = context.createdResources.phase5OverseasFulfillment.inventoryProof
|
||||
inventoryProof.snapshots.afterDepart = stockAfterDepart
|
||||
inventoryProof.ledgers.afterDepart = ledgerAfterDepart
|
||||
context.assert(MODULE, '发车及重放只推进履约且库存和SHIP流水零额外扣减', departReplay.response,
|
||||
inventorySourcesPresent(stockAfterDepart, sourcePositionBalances)
|
||||
&& sameSnapshot(stockAfterPick, stockAfterDepart) && sameSnapshot(ledgerAfterPick, ledgerAfterDepart), trace(departReplay.response), OPTIONS)
|
||||
context.createdResources.phase5OverseasFulfillment.inventoryProof = {
|
||||
sourceBalanceIds, sourcePositionIds, sourcePositionBalances, inboundCodes: dispatchInboundCodes, beforePackages: quantityTotal(stockBeforePick),
|
||||
afterPackages: quantityTotal(stockAfterDepart), expectedPackages,
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, {
|
||||
sourceBalanceIds, sourcePositionIds, sourcePositionBalances, inboundCodes: dispatchInboundCodes, beforePackages: conservation.proof.totals.positionBefore,
|
||||
afterPackages: quantityTotal((stockAfterDepart || []).flatMap(row => row.positions.filter(position => sourcePositionBalances.some(reference => reference.positionId === position.id && reference.balanceId === position.balanceId)))), expectedPackages,
|
||||
shipmentLedgerIds: (shippedRows || []).map(row => row.id),
|
||||
canDepart: ready.data?.canDepart === true,
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
await disablePicker(context, picker, 5)
|
||||
if (picker) picker.token = null
|
||||
|
||||
Reference in New Issue
Block a user