feat: support mixed-SKU dropship cartons

This commit is contained in:
2026-09-11 15:38:19 +08:00
parent 0db11517db
commit cc7c2f0954
42 changed files with 1263 additions and 128 deletions
@@ -3,12 +3,15 @@ package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.CountCartonRequest;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.DiscrepancyAction;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.CountSkuLine;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.TaskRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelSkuRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonSkuRow;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -20,7 +23,7 @@ class DropshipPalletizationServiceTest {
private final FirstlegDocumentNumberGenerator numbers=mock(FirstlegDocumentNumberGenerator.class);
private final DropshipPalletizationMapper mapper=mock(DropshipPalletizationMapper.class);
private final DropshipPalletizationService service=new DropshipPalletizationService();
DropshipPalletizationServiceTest(){ ReflectionTestUtils.setField(service,"mapper",mapper); ReflectionTestUtils.setField(service,"documentNumbers",numbers); }
DropshipPalletizationServiceTest(){ ReflectionTestUtils.setField(service,"mapper",mapper); ReflectionTestUtils.setField(service,"documentNumbers",numbers); when(mapper.labelSkus(anyLong(),anyLong())).thenAnswer(call -> java.util.List.of(labelSku())); when(mapper.cartonSkus(anyLong())).thenAnswer(call -> java.util.List.of(cartonSku())); }
@Test void reviewCreatesExpectedNumberOfPrintablePalletCodes(){
InboundOrderEntity inbound=dropshipInbound(); when(mapper.insertTask(anyLong(),eq(1L),eq(30L),eq(100L),eq(8))).thenReturn(1);
@@ -38,20 +41,21 @@ class DropshipPalletizationServiceTest {
@Test void resolvesUncountedCartonBeforeQuantityConfirmation(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label());
var result=service.resolveCarton(10L,100L,"BOX-1",7L);
assertThat(result.inboundCode()).isEqualTo("IN-1"); assertThat(result.expectedSkuQuantity()).isEqualTo(12); assertThat(result.canCount()).isTrue();
assertThat(result.inboundCode()).isEqualTo("IN-1"); assertThat(result.skuLines()).extracting("expectedQuantity").containsExactly(12); assertThat(result.canCount()).isTrue();
}
@Test void cartonCanBindDirectlyToAPreGeneratedPallet(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(pallet());
when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton());
when(mapper.pallet(10L,20L,100L)).thenReturn(pallet()); when(mapper.cartons(20L)).thenReturn(java.util.List.of());
service.countCarton(10L,new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,"DP-260911-0001","op-1"),7L,"张三");
service.countCarton(10L,countRequest("DP-260911-0001"),7L,"张三");
verify(mapper).insertCarton(anyLong(),eq(10L),eq(20L),eq(1L),eq(30L),eq(40L),eq(50L),eq("SKU-1"),any(),eq("BOX-1"),eq(12),eq(12),eq("MATCH"),isNull(),eq("op-1"),eq(7L),eq("张三"));
verify(mapper).insertCartonSku(anyLong(),anyLong(),eq(40L),eq("SKU-1"),any(),eq(12),eq(12),eq("MATCH"),isNull(),eq(1));
}
@Test void unknownPalletCodeCannotCreateAnAdHocPallet(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label());
var request=new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,"DP-260911-9999","op-1");
var request=countRequest("DP-260911-9999");
assertThatThrownBy(()->service.countCarton(10L,request,7L,"张三")).isInstanceOf(BizException.class).hasMessageContaining("测量审核后生成");
verify(mapper,never()).insertPallet(anyLong(),anyLong(),anyLong(),anyLong(),anyLong(),anyString(),anyString(),anyLong(),anyString());
}
@@ -61,12 +65,15 @@ class DropshipPalletizationServiceTest {
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(0); when(mapper.label(10L,"BOX-1")).thenReturn(label());
var result=service.resolveCarton(10L,100L,"BOX-1",7L);
assertThat(result.canCount()).isFalse(); assertThat(result.blockReason()).contains("预计 2 托").contains("当前仅生成 0 个托盘码");
assertThatThrownBy(()->service.countCarton(10L,new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,null,"op-1"),7L,"张三"))
assertThatThrownBy(()->service.countCarton(10L,countRequest(null),7L,"张三"))
.isInstanceOf(BizException.class).hasMessageContaining("预计 2 托");
}
private static TaskRow task(){TaskRow t=new TaskRow();t.setId(10L);t.setTenantId(1L);t.setInboundId(30L);t.setWarehouseId(100L);t.setInboundCode("IN-1");t.setTaskStatus("CLAIMED");t.setAssigneeId(7L);t.setProcessStatus("WAIT_PALLETIZE");t.setTotalCartons(2);t.setCountedCartons(0);t.setAllocatedCartons(0);t.setDiscrepancyCartons(0);t.setExpectedPalletCount(2);t.setVersion(1);return t;}
private static LabelRow label(){LabelRow l=new LabelRow();l.setCartonLabelId(50L);l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setUnitsPerCarton(12);l.setInboundCode("IN-1");l.setBoxCode("BOX-1");return l;}
private static LabelSkuRow labelSku(){LabelSkuRow l=new LabelSkuRow();l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setExpectedQuantity(12);l.setLineNo(1);return l;}
private static CartonSkuRow cartonSku(){CartonSkuRow l=new CartonSkuRow();l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setExpectedQuantity(12);l.setActualQuantity(12);l.setDiscrepancyStatus("MATCH");l.setLineNo(1);return l;}
private static CountCartonRequest countRequest(String palletNo){return new CountCartonRequest(100L,"BOX-1",java.util.List.of(new CountSkuLine(40L,"SKU-1",12,DiscrepancyAction.MATCH,null)),palletNo,"op-1");}
private static PalletRow pallet(){PalletRow p=new PalletRow();p.setId(20L);p.setTaskId(10L);p.setTenantId(1L);p.setInboundId(30L);p.setWarehouseId(100L);p.setPalletNo("DP-260911-0001");p.setPalletStatus("OPEN");p.setVersion(1);return p;}
private static CartonRow carton(){CartonRow c=new CartonRow();c.setId(60L);c.setTaskId(10L);c.setPalletId(20L);c.setInboundId(30L);c.setInboundDetailId(40L);c.setPalletNo("DP-260911-0001");c.setSkuCode("SKU-1");c.setCargoName("货物");c.setBoxCode("BOX-1");c.setExpectedSkuQuantity(12);c.setActualSkuQuantity(12);c.setDiscrepancyStatus("MATCH");return c;}
}
@@ -42,13 +42,31 @@ class InboundCartonLabelPrintServiceTest {
Pallet open = dropshipPallet("OPEN");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.eq("request-open"), org.mockito.ArgumentMatchers.any()))
.thenReturn(new ResponseEntity<>(new byte[] {7, 8, 9}, headers,
org.springframework.http.HttpStatus.OK));
ResponseEntity<byte[]> response = service.generateDropshipPallet(open, "PCHK2608040174", "request-open");
assertThat(response.getBody()).containsExactly(7, 8, 9);
ArgumentCaptor<PrintPdfClient.DropshipPalletA4PdfRequest> request =
ArgumentCaptor.forClass(PrintPdfClient.DropshipPalletA4PdfRequest.class);
verify(print).generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.eq("request-open"), request.capture());
assertThat(request.getValue().templateCode()).isEqualTo("DROPSHIP_PALLET_A4_V1");
assertThat(request.getValue().pageSpec().widthMm()).isEqualByComparingTo("210");
assertThat(request.getValue().pageSpec().heightMm()).isEqualByComparingTo("297");
assertThat(request.getValue().documents()).singleElement().satisfies(document -> {
assertThat(document.palletNo()).isEqualTo("DP-260911-0001");
assertThat(document.inboundCodes()).isEqualTo("PCHK2608040174");
assertThat(document.skuRows()).singleElement().satisfies(row -> {
assertThat(row.skuCode()).isEqualTo("SKU-1");
assertThat(row.cargoName()).isEqualTo("音箱");
assertThat(row.cartonCount()).isEqualTo(1);
assertThat(row.skuQuantity()).isEqualTo(19);
});
});
}
@Test
@@ -7,6 +7,7 @@ import com.baiyue.firstleg.client.PrintableOrderInboundClient;
import com.baiyue.firstleg.entity.InboundCartonLabelEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundCartonLabelRepository;
import com.baiyue.firstleg.mapper.InboundCartonLabelSkuMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,6 +33,7 @@ class InboundCartonLabelServiceTest {
@Mock private OrderCargoClient orderCargoClient;
@Mock private PrintableOrderInboundClient printableOrderInboundClient;
@Mock private InboundOrderCreationService inboundOrderCreationService;
@Mock private InboundCartonLabelSkuMapper labelSkus;
private InboundCartonLabelService service;
@BeforeEach
@@ -41,6 +43,7 @@ class InboundCartonLabelServiceTest {
ReflectionTestUtils.setField(service, "orderCargoClient", orderCargoClient);
ReflectionTestUtils.setField(service, "printableOrderInboundClient", printableOrderInboundClient);
ReflectionTestUtils.setField(service, "inboundOrderCreationService", inboundOrderCreationService);
ReflectionTestUtils.setField(service, "labelSkus", labelSkus);
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
}
@@ -75,6 +78,26 @@ class InboundCartonLabelServiceTest {
verify(repository).updatePackageCount(10001L, 70001L, 5);
}
@Test
void mixedCartonGroupCreatesOnePhysicalBoxSeriesWithEverySkuLine() {
InboundOrderEntity inbound = inbound();
inbound.setFulfillmentType("ONE_CLICK_DROPSHIP");
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
when(repository.findActiveByInbound(10001L, 70001L)).thenReturn(new ArrayList<>());
when(repository.findByInbound(10001L, 70001L)).thenReturn(new ArrayList<>());
var skuA = mixedCargo(80001L, 1, "SKU-A", 2, 3, "A");
var skuB = mixedCargo(80002L, 2, "SKU-B", 5, 3, "A");
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
.thenReturn(ApiResponse.success(List.of(skuA, skuB)));
var batch = service.getOrCreate(10001L, 70001L);
assertThat(batch.totalCount()).isEqualTo(3);
verify(labelSkus, org.mockito.Mockito.times(6)).insert(any(Long.class),
org.mockito.ArgumentMatchers.eq(10001L), org.mockito.ArgumentMatchers.eq(70001L), any(Long.class),
any(Long.class), any(String.class), any(), any(Integer.class), any(Integer.class));
}
@Test
void repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation() {
InboundOrderEntity inbound = inbound();
@@ -283,6 +306,12 @@ class InboundCartonLabelServiceTest {
new BigDecimal(length), new BigDecimal(width), new BigDecimal(height));
}
private OrderCargoClient.CargoLabelBinding mixedCargo(long id, int itemNo, String sku,
int units, int count, String group) {
return new OrderCargoClient.CargoLabelBinding(id, 60001L, itemNo, "货物" + itemNo, sku, count,
units, new BigDecimal("60"), new BigDecimal("40"), new BigDecimal("30"), null, group);
}
private List<InboundCartonLabelEntity> labels(InboundOrderEntity inbound, int total) {
List<InboundCartonLabelEntity> values = new ArrayList<>();
for (int index = 1; index <= total; index++) {