From 15bbaec3e1a9ea477679d993e8de4ce79727fdfc Mon Sep 17 00:00:00 2001 From: michaldev0 <125133223+michaldev0@users.noreply.github.com> Date: Fri, 12 Jun 2026 02:15:54 +0200 Subject: [PATCH 1/5] OLMIS-4412: Prevent orders with all zero quantities Requisitions with nothing to ship are now released without creating an order. --- .../RequisitionServiceIntegrationTest.java | 45 +++++++++ .../domain/requisition/Requisition.java | 11 +++ .../service/RequisitionService.java | 38 +++++-- .../domain/requisition/RequisitionTest.java | 64 ++++++++++++ .../service/RequisitionServiceTest.java | 99 ++++++++++++++++++- 5 files changed, 248 insertions(+), 9 deletions(-) diff --git a/src/integration-test/java/org/openlmis/requisition/service/RequisitionServiceIntegrationTest.java b/src/integration-test/java/org/openlmis/requisition/service/RequisitionServiceIntegrationTest.java index a74f9c702..fb0006403 100644 --- a/src/integration-test/java/org/openlmis/requisition/service/RequisitionServiceIntegrationTest.java +++ b/src/integration-test/java/org/openlmis/requisition/service/RequisitionServiceIntegrationTest.java @@ -45,6 +45,7 @@ import guru.nidi.ramltester.RamlDefinition; import guru.nidi.ramltester.RamlLoaders; import guru.nidi.ramltester.restassured.RestAssuredClient; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -64,6 +65,8 @@ import org.openlmis.requisition.domain.RequisitionTemplateColumnDataBuilder; import org.openlmis.requisition.domain.requisition.Requisition; import org.openlmis.requisition.domain.requisition.RequisitionBuilder; +import org.openlmis.requisition.domain.requisition.RequisitionLineItem; +import org.openlmis.requisition.domain.requisition.RequisitionLineItemDataBuilder; import org.openlmis.requisition.domain.requisition.RequisitionStatus; import org.openlmis.requisition.dto.FacilityDto; import org.openlmis.requisition.dto.OrderDto; @@ -78,6 +81,7 @@ import org.openlmis.requisition.repository.RequisitionTemplateRepository; import org.openlmis.requisition.service.fulfillment.OrderFulfillmentService; import org.openlmis.requisition.service.referencedata.FacilityReferenceDataService; +import org.openlmis.requisition.service.referencedata.OrderableReferenceDataService; import org.openlmis.requisition.service.referencedata.SupplyLineReferenceDataService; import org.openlmis.requisition.service.referencedata.UserFulfillmentFacilitiesReferenceDataService; import org.openlmis.requisition.testutils.DtoGenerator; @@ -137,6 +141,9 @@ public class RequisitionServiceIntegrationTest { @MockBean private OrderFulfillmentService orderFulfillmentService; + @MockBean + private OrderableReferenceDataService orderableReferenceDataService; + @Autowired private AvailableRequisitionColumnRepository availableColumnRepository; @@ -202,6 +209,9 @@ public void setUp() { any(UUID.class), eq(right.getId()))).willReturn(managedFacilities); doNothing().when(orderFulfillmentService).create(ordersCaptor.capture()); + + given(orderableReferenceDataService.findByIdentities(any())) + .willReturn(new ArrayList<>()); } @Test @@ -279,6 +289,30 @@ public void shouldNotBuildOrdersIfSaveFailed() { verify(orderFulfillmentService, times(0)).create(anyList()); } + @Test + public void shouldReleaseWithoutOrderWhenAllPacksToShipAreZero() { + final long reqsCountBefore = requisitionRepository.count(); + Requisition requisition = mockAndSaveRequisition(generateRequisition(0L)); + ReleasableRequisitionBatchDto releaseDto = generateReleaseRequisitionDto( + Collections.singletonList( + getReleasableRequisitionDto(requisition) + )); + + restAssured.given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(releaseDto) + .when() + .post(BATCH_RELEASES_URL) + .then() + .statusCode(HttpStatus.CREATED.value()); + + Requisition released = requisitionRepository.findById(requisition.getId()).orElse(null); + assertEquals(RequisitionStatus.RELEASED_WITHOUT_ORDER, released.getStatus()); + verify(orderFulfillmentService, times(0)).create(anyList()); + assertEquals(reqsCountBefore + 1, requisitionRepository.count()); + } + /** * https://openlmis.atlassian.net/browse/OLMIS-6876 indicates that there is some exception * after saving the requisitions and before the return statement of @@ -318,6 +352,10 @@ private Requisition saveRequisition(Requisition requisition) { } private Requisition generateRequisition() { + return generateRequisition(5L); + } + + private Requisition generateRequisition(Long packsToShip) { Requisition requisition = RequisitionBuilder.newRequisition(UUID.randomUUID(), UUID.randomUUID(), false); requisition.setTemplate(template); @@ -326,6 +364,13 @@ private Requisition generateRequisition() { requisition.setStatus(RequisitionStatus.APPROVED); requisition.setSupervisoryNodeId(supervisoryNodeId); + RequisitionLineItem lineItem = new RequisitionLineItemDataBuilder() + .withRequisition(requisition) + .withPacksToShip(packsToShip) + .withSkippedFlag(false) + .buildAsNew(); + requisition.setRequisitionLineItems(new ArrayList<>(Collections.singletonList(lineItem))); + return requisition; } diff --git a/src/main/java/org/openlmis/requisition/domain/requisition/Requisition.java b/src/main/java/org/openlmis/requisition/domain/requisition/Requisition.java index a6f28814f..7e2369c34 100644 --- a/src/main/java/org/openlmis/requisition/domain/requisition/Requisition.java +++ b/src/main/java/org/openlmis/requisition/domain/requisition/Requisition.java @@ -816,6 +816,17 @@ public void reject(Map products, UUID rejector statusChanges.add(StatusChange.newStatusChange(this, rejector)); } + /** + * Returns true if at least one non-skipped line item has a positive packsToShip value. + * Returns false when all non-skipped line items have zero or null packsToShip, + * meaning the requisition results in a blank order with nothing to ship. + */ + public boolean hasNonZeroPacksToShip() { + return getNonSkippedRequisitionLineItems() + .stream() + .anyMatch(line -> line.getPacksToShip() != null && line.getPacksToShip() > 0L); + } + /** * Release the requisition. */ diff --git a/src/main/java/org/openlmis/requisition/service/RequisitionService.java b/src/main/java/org/openlmis/requisition/service/RequisitionService.java index 6d8aafa9d..bd02dd025 100644 --- a/src/main/java/org/openlmis/requisition/service/RequisitionService.java +++ b/src/main/java/org/openlmis/requisition/service/RequisitionService.java @@ -553,14 +553,17 @@ public ValidationResult validateCanSaveRequisition(Requisition requisition) { /** * Releases the list of given requisitions as order. + * Requisitions with all zero packsToShip are automatically released without order + * and collected in autoReleasedWithoutOrder instead of the returned list. * - * @param convertToOrderDtos list of Requisitions with their supplyingDepots to be released as - * order - * @return list of released requisitions + * @param convertToOrderDtos list of Requisitions with their supplyingDepots + * @param autoReleasedWithoutOrder output list populated with requisitions auto-released + * without order (all packsToShip zero or null) + * @return list of requisitions released as order (non-zero packsToShip only) */ private List releaseRequisitionsAsOrder( List convertToOrderDtos, UserDto user, - Boolean isLocallyFulfilled) { + Boolean isLocallyFulfilled, List autoReleasedWithoutOrder) { Profiler profiler = new Profiler("RELEASE_REQUISITIONS_AS_ORDER"); profiler.setLogger(LOGGER); @@ -581,6 +584,13 @@ private List releaseRequisitionsAsOrder( .orElseThrow(() -> new ContentNotFoundMessageException(ERROR_REQUISITION_NOT_FOUND, requisitionId)); isEligibleForConvertToOrder(loadedRequisition).throwExceptionIfHasErrors(); + + if (!loadedRequisition.hasNonZeroPacksToShip()) { + loadedRequisition.releaseWithoutOrder(authenticationHelper.getCurrentUser().getId()); + autoReleasedWithoutOrder.add(loadedRequisition); + continue; + } + loadedRequisition.release(authenticationHelper.getCurrentUser().getId()); UUID facilityId = convertToOrderDto.getSupplyingDepotId(); @@ -713,6 +723,7 @@ public List convertToOrder(List list, Use /** * Converting Requisition list to Orders. + * Requisitions with all zero packsToShip are automatically released without order. */ public List convertToOrder(List list, UserDto user, Boolean isLocallyFulfilled) { @@ -720,17 +731,28 @@ public List convertToOrder(List list, Use profiler.setLogger(LOGGER); profiler.start("RELEASE_REQUISITIONS_AS_ORDER"); - List releasedRequisitions = - releaseRequisitionsAsOrder(list, user, isLocallyFulfilled); + List autoReleasedWithoutOrder = new ArrayList<>(); + final List releasedRequisitions = + releaseRequisitionsAsOrder(list, user, isLocallyFulfilled, autoReleasedWithoutOrder); + + profiler.start("SAVE_AUTO_RELEASED_WITHOUT_ORDER"); + for (Requisition requisition : autoReleasedWithoutOrder) { + requisitionRepository.save(requisition); + requisitionStatusProcessor.statusChange(requisition, LocaleContextHolder.getLocale()); + } profiler.start("BUILD_ORDER_DTOS_AND_SAVE_REQUISITION"); List orders = buildOrders(releasedRequisitions, user); profiler.start("CREATE_ORDER_IN_FULFILLMENT"); - orderFulfillmentService.create(orders); + if (!orders.isEmpty()) { + orderFulfillmentService.create(orders); + } profiler.stop().log(); - return releasedRequisitions; + List allReleased = new ArrayList<>(releasedRequisitions); + allReleased.addAll(autoReleasedWithoutOrder); + return allReleased; } /** diff --git a/src/test/java/org/openlmis/requisition/domain/requisition/RequisitionTest.java b/src/test/java/org/openlmis/requisition/domain/requisition/RequisitionTest.java index 863d653b3..7299595f3 100644 --- a/src/test/java/org/openlmis/requisition/domain/requisition/RequisitionTest.java +++ b/src/test/java/org/openlmis/requisition/domain/requisition/RequisitionTest.java @@ -934,6 +934,70 @@ public void shouldReturnSkippedRequisitionLineItems() { assertEquals(skipped.getId(), requisitionLineItem.getId()); } + @Test + public void shouldHaveNonZeroPacksToShipWhenAnyNonSkippedLineIsPositive() { + RequisitionLineItem zeroLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(false).withPacksToShip(0L).build(); + RequisitionLineItem positiveLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(false).withPacksToShip(5L).build(); + + Requisition requisition = new RequisitionDataBuilder() + .addLineItem(zeroLine, false) + .addLineItem(positiveLine, false) + .build(); + + assertTrue(requisition.hasNonZeroPacksToShip()); + } + + @Test + public void shouldNotHaveNonZeroPacksToShipWhenAllNonSkippedLinesAreZero() { + RequisitionLineItem firstZeroLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(false).withPacksToShip(0L).build(); + RequisitionLineItem secondZeroLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(false).withPacksToShip(0L).build(); + + Requisition requisition = new RequisitionDataBuilder() + .addLineItem(firstZeroLine, false) + .addLineItem(secondZeroLine, false) + .build(); + + assertFalse(requisition.hasNonZeroPacksToShip()); + } + + @Test + public void shouldNotHaveNonZeroPacksToShipWhenAllNonSkippedLinesAreNull() { + RequisitionLineItem nullLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(false).withPacksToShip(null).build(); + + Requisition requisition = new RequisitionDataBuilder() + .addLineItem(nullLine, false) + .build(); + + assertFalse(requisition.hasNonZeroPacksToShip()); + } + + @Test + public void shouldIgnoreSkippedLinesWhenCheckingPacksToShip() { + RequisitionLineItem zeroNonSkippedLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(false).withPacksToShip(0L).build(); + RequisitionLineItem positiveSkippedLine = new RequisitionLineItemDataBuilder() + .withSkippedFlag(true).withPacksToShip(5L).build(); + + Requisition requisition = new RequisitionDataBuilder() + .addLineItem(zeroNonSkippedLine, false) + .addLineItem(positiveSkippedLine, false) + .build(); + + assertFalse(requisition.hasNonZeroPacksToShip()); + } + + @Test + public void shouldNotHaveNonZeroPacksToShipWhenThereAreNoLineItems() { + Requisition requisition = new RequisitionDataBuilder().build(); + + assertFalse(requisition.hasNonZeroPacksToShip()); + } + @Test public void shouldUpdatePacksToShipOnSubmit() { // given diff --git a/src/test/java/org/openlmis/requisition/service/RequisitionServiceTest.java b/src/test/java/org/openlmis/requisition/service/RequisitionServiceTest.java index 3c467925a..461722a6c 100644 --- a/src/test/java/org/openlmis/requisition/service/RequisitionServiceTest.java +++ b/src/test/java/org/openlmis/requisition/service/RequisitionServiceTest.java @@ -1711,7 +1711,10 @@ private List setUpReleaseRequisitionsAsOrder( requisition.setId(UUID.randomUUID()); requisition.setSupervisoryNodeId(UUID.randomUUID()); requisition.setSupplyingFacilityId(facility.getId()); - requisition.setRequisitionLineItems(Lists.newArrayList()); + RequisitionLineItem nonZeroLine = new RequisitionLineItemDataBuilder() + .withPacksToShip(5L) + .build(); + requisition.setRequisitionLineItems(Lists.newArrayList(nonZeroLine)); requisition.setTemplate(requisitionTemplate); when(requisitionRepository.findById(requisition.getId())) @@ -1724,6 +1727,38 @@ private List setUpReleaseRequisitionsAsOrder( return result; } + private List setUpZeroPacksToShipRequisitions( + int amount, RequisitionStatus status) { + if (amount < 1) { + throw new IllegalArgumentException("Amount must be a positive number"); + } + + List result = new ArrayList<>(); + + for (int i = 0; i < amount; i++) { + FacilityDto facility = mock(FacilityDto.class); + when(facility.getId()).thenReturn(UUID.randomUUID()); + + Requisition requisition = new Requisition(UUID.randomUUID(), UUID.randomUUID(), + UUID.randomUUID(), status, false); + requisition.setId(UUID.randomUUID()); + requisition.setSupervisoryNodeId(UUID.randomUUID()); + requisition.setSupplyingFacilityId(facility.getId()); + RequisitionLineItem zeroLine = new RequisitionLineItemDataBuilder() + .withPacksToShip(0L) + .build(); + requisition.setRequisitionLineItems(Lists.newArrayList(zeroLine)); + requisition.setTemplate(requisitionTemplate); + + when(requisitionRepository.findById(requisition.getId())) + .thenReturn(Optional.of(requisition)); + + result.add(new ReleasableRequisitionDto(requisition.getId(), facility.getId())); + } + + return result; + } + private Requisition generateRequisition() { lineItem1 = new RequisitionLineItemDataBuilder().build(); lineItem2 = new RequisitionLineItemDataBuilder().build(); @@ -1997,4 +2032,66 @@ private void mockSupportedProgramStartDateNotSet() { when(supportedProgram.getSupportStartDate()).thenReturn(null); } + @Test + public void shouldAutoReleaseWithoutOrderWhenAllPacksToShipAreZero() { + // given + List list = setUpZeroPacksToShipRequisitions(3, APPROVED); + when(fulfillmentFacilitiesReferenceDataService.getFulfillmentFacilities( + user.getId(), convertToOrderRight.getId())).thenReturn(emptyList()); + + // when + List result = requisitionService.convertToOrder(list, user); + + // then + assertEquals(3, result.size()); + result.forEach(req -> assertEquals(RELEASED_WITHOUT_ORDER, req.getStatus())); + verify(orderFulfillmentService, never()).create(any(List.class)); + } + + @Test + public void shouldProcessStatusChangeForZeroPacksToShipRequisitions() { + // given + List list = setUpZeroPacksToShipRequisitions(1, APPROVED); + when(fulfillmentFacilitiesReferenceDataService.getFulfillmentFacilities( + user.getId(), convertToOrderRight.getId())).thenReturn(emptyList()); + + // when + requisitionService.convertToOrder(list, user); + + // then + verify(requisitionStatusProcessor).statusChange(any(Requisition.class), any(Locale.class)); + } + + @Test + public void shouldCreateOrderOnlyForRequisitionsWithNonZeroPacksToShip() { + // given: 2 non-zero + 2 zero + List nonZeroList = + setUpReleaseRequisitionsAsOrder(2, APPROVED); + List zeroList = + setUpZeroPacksToShipRequisitions(2, APPROVED); + List combined = new ArrayList<>(nonZeroList); + combined.addAll(zeroList); + + List facilities = nonZeroList.stream() + .map(r -> facilityReferenceDataService.findOne(r.getSupplyingDepotId())) + .collect(toList()); + when(fulfillmentFacilitiesReferenceDataService.getFulfillmentFacilities( + user.getId(), convertToOrderRight.getId())).thenReturn(facilities); + when(requisitionForConvertBuilder.getAvailableSupplyingDepots(any(UUID.class))) + .thenReturn(facilities); + + // when + List result = requisitionService.convertToOrder(combined, user); + + // then: all 4 returned + assertEquals(4, result.size()); + long releasedCount = result.stream() + .filter(r -> RELEASED.equals(r.getStatus())).count(); + long releasedWithoutOrderCount = result.stream() + .filter(r -> RELEASED_WITHOUT_ORDER.equals(r.getStatus())).count(); + assertEquals(2, releasedCount); + assertEquals(2, releasedWithoutOrderCount); + verify(orderFulfillmentService).create(any(List.class)); + } + } From c5160bfdf17f06755eebe1278ccb295b0c8f9d2b Mon Sep 17 00:00:00 2001 From: michaldev0 <125133223+michaldev0@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:12:46 +0200 Subject: [PATCH 2/5] Retrigger CI From af4a2cabc5ff28e773b2063c3a252186a4e784cf Mon Sep 17 00:00:00 2001 From: michaldev0 <125133223+michaldev0@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:24:45 +0200 Subject: [PATCH 3/5] OLMIS-4412: Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6daaa7795..59709f9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 8.6.0-SNAPSHOT (WIP) ================== +* [OLMIS-4412](https://openlmis.atlassian.net/browse/OLMIS-4412): Release requisitions with all zero quantities without creating an order. 8.5.1 / 2026-06-09 ================== From 40a32409a61c15fe5d4ca885a3985bf0ffd9fd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mr=C3=B3z?= Date: Mon, 22 Jun 2026 18:32:39 +0200 Subject: [PATCH 4/5] Retrigger CI From 084f562ca0f6eb0361b2dfabe5bf304cae851ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mr=C3=B3z?= Date: Mon, 22 Jun 2026 23:59:37 +0200 Subject: [PATCH 5/5] Retrigger CI