fix(lastleg): preserve mixed SKU balances when splitting return relabel stock

This commit is contained in:
2026-09-20 19:11:53 +08:00
parent bf99bf9048
commit 6211230564
3 changed files with 51 additions and 11 deletions
@@ -375,14 +375,20 @@ public class PdaPalletOperationService {
.eq("application_id", application.getId()).eq("return_carton_id", source.getId())).size() != 1)
throw conflict("来源托盘包含多项换标任务,不能按单 SKU 拆分,请核对库存");
List<ReturnInventoryCartonItem> lines = returnCartonItems.lockCarton(source.getId());
if (lines.size() != 1 || !item.getSkuCode().equalsIgnoreCase(lines.get(0).getSkuCode())
|| value(lines.get(0).getQuantity()) != value(source.getTotalQuantity()))
throw conflict("来源托盘包含其他 SKU,不能按单 SKU 拆分,请核对库存");
List<ReturnInventoryCartonItem> matching = lines.stream()
.filter(line -> item.getSkuCode().equalsIgnoreCase(line.getSkuCode())).toList();
if (matching.size() != 1 || quantity <= 0 || quantity > value(matching.get(0).getQuantity())
|| lines.stream().anyMatch(line -> value(line.getQuantity()) <= 0)
|| lines.stream().mapToLong(line -> value(line.getQuantity())).sum() != value(source.getTotalQuantity()))
throw conflict("来源托盘SKU数量不一致,请核对库存");
long remaining = value(source.getTotalQuantity()) - quantity;
ReturnInventoryCartonItem sourceLine = lines.get(0);
sourceLine.setQuantity(remaining);
if (returnCartonItems.updateById(sourceLine) != 1
|| returnCartons.releasePartial(source.getId(), application.getCustomerId(), application.getId(), remaining) != 1)
ReturnInventoryCartonItem sourceLine = matching.get(0);
long skuRemaining = value(sourceLine.getQuantity()) - quantity;
int remainingSkuTypes = lines.size() - (skuRemaining == 0 ? 1 : 0);
sourceLine.setQuantity(skuRemaining);
int changed = skuRemaining == 0 ? returnCartonItems.deleteById(sourceLine.getId()) : returnCartonItems.updateById(sourceLine);
if (changed != 1
|| returnCartons.releasePartial(source.getId(), application.getCustomerId(), application.getId(), remaining, remainingSkuTypes) != 1)
throw conflict("来源托盘数量已变化,请刷新后重试");
LocalDateTime now = LocalDateTime.now();
long targetId = IdWorker.getId();
@@ -403,7 +409,7 @@ public class PdaPalletOperationService {
targetLine.setProductNameZh(sourceLine.getProductNameZh()); targetLine.setQuantity(quantity);
if (returnCartonItems.insert(targetLine) != 1
|| returnBalances.adjustCartonQuantity(source.getCustomerId(), source.getOverseasWarehouseId(),
sourceLine.getSkuCode(), 1) != 1) throw conflict("退货库存拆分记账失败");
sourceLine.getSkuCode(), skuRemaining == 0 ? 0 : 1) != 1) throw conflict("退货库存拆分记账失败");
item.setReturnCartonId(targetId);
if (relabelItems.updateById(item) != 1) throw conflict("换标单来源托盘更新失败");
}
@@ -36,12 +36,13 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE")
ReturnInventoryCarton lockId(@Param("id") long id);
@Update("UPDATE ll_return_inventory_carton SET total_quantity=#{remaining},sku_types=1,status='AVAILABLE'," +
@Update("UPDATE ll_return_inventory_carton SET total_quantity=#{remaining},sku_types=#{skuTypes},status='AVAILABLE'," +
"relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL,updated_at=NOW(3) " +
"WHERE id=#{id} AND customer_id=#{customerId} AND relabel_application_id=#{applicationId} " +
"AND status='RELABEL_PENDING' AND total_quantity>#{remaining}")
int releasePartial(@Param("id") long id, @Param("customerId") String customerId,
@Param("applicationId") long applicationId, @Param("remaining") long remaining);
@Param("applicationId") long applicationId, @Param("remaining") long remaining,
@Param("skuTypes") int skuTypes);
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} LIMIT 1")
ReturnInventoryCarton findReceipt(@Param("receiptId") long receiptId);
@@ -102,7 +102,7 @@ class PdaPalletOperationServiceTest {
when(relabelItems.selectList(any())).thenReturn(List.of(item));
when(returnCartonItems.lockCarton(801L)).thenReturn(List.of(originalLine));
when(returnCartonItems.updateById(any(ReturnInventoryCartonItem.class))).thenReturn(1);
when(returnCartons.releasePartial(801L, "100", 600L, 70L)).thenReturn(1);
when(returnCartons.releasePartial(801L, "100", 600L, 70L, 1)).thenReturn(1);
when(returnCartons.insert(any(ReturnInventoryCarton.class))).thenReturn(1);
when(returnCartonItems.insert(any(ReturnInventoryCartonItem.class))).thenReturn(1);
when(returnBalances.adjustCartonQuantity("100", "88", "SKU-1", 1)).thenReturn(1);
@@ -128,6 +128,39 @@ class PdaPalletOperationServiceTest {
verifyNoInteractions(mapper);
}
@Test void mixedSkuSplitMovesOnlySelectedSkuAndPreservesOtherSkuOnSource() {
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100");
RelabelItem item = new RelabelItem(); item.setId(701L); item.setSkuCode("SKU-A");
item.setReturnCartonId(801L); item.setDestinationFbaWarehouseCode("SCK8");
ReturnInventoryCarton original = new ReturnInventoryCarton(); original.setId(801L);
original.setCustomerId("100"); original.setOverseasWarehouseId("88"); original.setTotalQuantity(2L);
original.setSourceReturnNo("RT-1"); original.setSourceBatchNo("RB-1,RB-2"); original.setPalletCode("OLD");
ReturnInventoryCartonItem a = new ReturnInventoryCartonItem(); a.setId(901L); a.setSkuCode("SKU-A"); a.setQuantity(1L);
ReturnInventoryCartonItem b = new ReturnInventoryCartonItem(); b.setId(902L); b.setSkuCode("SKU-B"); b.setQuantity(1L);
when(relabelItems.selectList(any())).thenReturn(List.of(item));
when(returnCartonItems.lockCarton(801L)).thenReturn(List.of(a, b));
when(returnCartonItems.deleteById(901L)).thenReturn(1);
when(returnCartons.releasePartial(801L, "100", 600L, 1L, 1)).thenReturn(1);
when(returnCartons.insert(any(ReturnInventoryCarton.class))).thenReturn(1);
when(returnCartonItems.insert(any(ReturnInventoryCartonItem.class))).thenReturn(1);
when(returnBalances.adjustCartonQuantity("100", "88", "SKU-A", 0)).thenReturn(1);
when(relabelItems.updateById(any(RelabelItem.class))).thenReturn(1);
ReflectionTestUtils.invokeMethod(service, "splitReturnRelabelCarton", app, item, original,
pallet(30L, "NEW", 88L, 0), 1L);
assertEquals(1L, b.getQuantity());
verify(returnCartonItems, never()).updateById(b);
var moved = ArgumentCaptor.forClass(ReturnInventoryCartonItem.class);
verify(returnCartonItems).insert(moved.capture());
assertEquals("SKU-A", moved.getValue().getSkuCode());
assertEquals(1L, moved.getValue().getQuantity());
var target = ArgumentCaptor.forClass(ReturnInventoryCarton.class);
verify(returnCartons).insert(target.capture());
assertEquals("RT-1", target.getValue().getSourceReturnNo());
assertEquals("RB-1,RB-2", target.getValue().getSourceBatchNo());
assertEquals("NEW", target.getValue().getPalletCode());
verify(returnBalances, never()).adjustCartonQuantity(eq("100"), eq("88"), eq("SKU-B"), anyLong());
}
@Test void resolveReportsMissingOnSitePalletAsNotFound() {
when(physical.resolve("MISSING")).thenReturn(null);