From f876c51f7ca70acb18393b2e4c660436a9416ffb Mon Sep 17 00:00:00 2001 From: Serhii Nosko Date: Mon, 15 Jan 2024 13:27:32 +0200 Subject: [PATCH 1/6] UIREC-292. Add missed module permission (#818) --- descriptors/ModuleDescriptor-template.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 78d6d7bd1..4ef676758 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -485,6 +485,8 @@ "inventory-storage.items.item.post", "inventory-storage.instance-types.collection.get", "inventory-storage.instance-statuses.collection.get", + "finance.funds.budget.item.get", + "finance.fiscal-years.item.get", "finance.order-transaction-summaries.item.get", "finance.order-transaction-summaries.item.post", "finance.order-transaction-summaries.item.put", From cbe8a8a31101ec43df6833d2ddc73d46205fb71d Mon Sep 17 00:00:00 2001 From: imerabishvili <144257054+imerabishvili@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:55:32 +0400 Subject: [PATCH 2/6] Modorders 970 (#817) [MODORDERS-970] - Do not overwrite Item fields when piece fields are null/empty --- ramls/acq-models | 2 +- .../org/folio/orders/utils/HelperUtils.java | 60 +++++++-------- .../service/inventory/InventoryManager.java | 31 +++++--- .../PieceUpdateFlowInventoryManager.java | 77 +++++++++---------- .../inventory/InventoryManagerTest.java | 73 ++++++++++++++++++ 5 files changed, 161 insertions(+), 82 deletions(-) diff --git a/ramls/acq-models b/ramls/acq-models index 72dd1b15a..4aab2ae48 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 72dd1b15ae5b846d61d9ad14bb780ea233e01e38 +Subproject commit 4aab2ae4808333aac379cae40828dacdf0ef986c diff --git a/src/main/java/org/folio/orders/utils/HelperUtils.java b/src/main/java/org/folio/orders/utils/HelperUtils.java index b1d5885a7..2f09d0027 100644 --- a/src/main/java/org/folio/orders/utils/HelperUtils.java +++ b/src/main/java/org/folio/orders/utils/HelperUtils.java @@ -155,7 +155,7 @@ public static Integer calculateTotalLocationQuantity(Location location) { */ public static int calculateTotalQuantity(CompositePoLine compPOL) { Cost cost = compPOL.getCost(); - int eQuantity = ObjectUtils.defaultIfNull(cost.getQuantityElectronic(), 0); + int eQuantity = ObjectUtils.defaultIfNull(cost.getQuantityElectronic(), 0); int physicalQuantity = ObjectUtils.defaultIfNull(cost.getQuantityPhysical(), 0); return eQuantity + physicalQuantity; } @@ -177,7 +177,7 @@ public static int calculateInventoryItemsQuantity(CompositePoLine compPOL) { /** * Calculates items quantity for specified locations. * - * @param compPOL composite PO Line + * @param compPOL composite PO Line * @param locations list of locations to calculate quantity for * @return quantity of items expected in the inventory for PO Line * @see #calculateInventoryItemsQuantity(CompositePoLine) @@ -297,27 +297,25 @@ public static MonetaryAmount calculateEncumbranceEffectiveAmount(MonetaryAmount } public static int getPhysicalLocationsQuantity(List locations) { - if (CollectionUtils.isNotEmpty(locations)) { - return locations.stream() - .map(Location::getQuantityPhysical) - .filter(Objects::nonNull) - .mapToInt(Integer::intValue) - .sum(); - } else { + if (CollectionUtils.isEmpty(locations)) { return 0; } + return locations.stream() + .map(Location::getQuantityPhysical) + .filter(Objects::nonNull) + .mapToInt(Integer::intValue) + .sum(); } public static int getElectronicLocationsQuantity(List locations) { - if (CollectionUtils.isNotEmpty(locations)) { - return locations.stream() - .map(Location::getQuantityElectronic) - .filter(Objects::nonNull) - .mapToInt(Integer::intValue) - .sum(); - } else { + if (CollectionUtils.isEmpty(locations)) { return 0; } + return locations.stream() + .map(Location::getQuantityElectronic) + .filter(Objects::nonNull) + .mapToInt(Integer::intValue) + .sum(); } /** @@ -375,8 +373,6 @@ public static CompositePurchaseOrder convertToCompositePurchaseOrder(JsonObject } public static boolean changeOrderStatus(PurchaseOrder purchaseOrder, List poLines) { - boolean isUpdateRequired = false; - if (toBeCancelled(purchaseOrder, poLines)) { purchaseOrder.setWorkflowStatus(PurchaseOrder.WorkflowStatus.CLOSED); purchaseOrder.setCloseReason(new CloseReason().withReason(REASON_CANCELLED)); @@ -384,14 +380,17 @@ public static boolean changeOrderStatus(PurchaseOrder purchaseOrder, List values, String fieldName, boolean strictMatch) { @@ -468,8 +467,7 @@ public static void verifyTitles(Map> lineIdTitles, Map> titles, Map poLineById) { - if (titles.keySet().stream().anyMatch(lineId -> titles.get(lineId).size() > 1 && - !poLineById.get(lineId).getIsPackage())) { + if (titles.keySet().stream().anyMatch(lineId -> titles.get(lineId).size() > 1 && !poLineById.get(lineId).getIsPackage())) { throw new HttpException(400, MULTIPLE_NONPACKAGE_TITLES); } } @@ -498,17 +496,17 @@ public static ConversionQuery getConversionQuery(Double exchangeRate, String fro } public static void makePoLinePending(CompositePoLine poLine) { - if (poLine.getPaymentStatus() == CompositePoLine.PaymentStatus.AWAITING_PAYMENT) { - poLine.setPaymentStatus(CompositePoLine.PaymentStatus.PENDING); - } - if (poLine.getReceiptStatus() == CompositePoLine.ReceiptStatus.AWAITING_RECEIPT) { - poLine.setReceiptStatus(CompositePoLine.ReceiptStatus.PENDING); - } + if (poLine.getPaymentStatus() == CompositePoLine.PaymentStatus.AWAITING_PAYMENT) { + poLine.setPaymentStatus(CompositePoLine.PaymentStatus.PENDING); + } + if (poLine.getReceiptStatus() == CompositePoLine.ReceiptStatus.AWAITING_RECEIPT) { + poLine.setReceiptStatus(CompositePoLine.ReceiptStatus.PENDING); + } } public static ConversionQuery buildConversionQuery(PoLine poLine, String systemCurrency) { Cost cost = poLine.getCost(); - if (cost.getExchangeRate() != null){ + if (cost.getExchangeRate() != null) { return ConversionQueryBuilder.of().setBaseCurrency(cost.getCurrency()) .setTermCurrency(systemCurrency) .set(RATE_KEY, cost.getExchangeRate()).build(); diff --git a/src/main/java/org/folio/service/inventory/InventoryManager.java b/src/main/java/org/folio/service/inventory/InventoryManager.java index 3bb87626a..71f211d9d 100644 --- a/src/main/java/org/folio/service/inventory/InventoryManager.java +++ b/src/main/java/org/folio/service/inventory/InventoryManager.java @@ -1268,15 +1268,28 @@ private Future> fetchHoldingsByFundIds(List holdingIds, }); } - private void updateItemWithPieceFields(Piece piece, JsonObject item) { - Optional.ofNullable(piece.getEnumeration()) - .ifPresentOrElse(enumeration -> item.put(ITEM_ENUMERATION, enumeration), () -> item.remove(ITEM_ENUMERATION)); - Optional.ofNullable(piece.getCopyNumber()) - .ifPresentOrElse(copyNumber -> item.put(COPY_NUMBER, copyNumber), () -> item.remove(COPY_NUMBER)); - Optional.ofNullable(piece.getChronology()) - .ifPresentOrElse(chronology -> item.put(ITEM_CHRONOLOGY, chronology), () -> item.remove(ITEM_CHRONOLOGY)); - Optional.ofNullable(piece.getDiscoverySuppress()) - .ifPresentOrElse(discSup -> item.put(ITEM_DISCOVERY_SUPPRESS, discSup), () -> item.remove(ITEM_DISCOVERY_SUPPRESS)); + void updateItemWithPieceFields(Piece piece, JsonObject item) { + if (StringUtils.isNotEmpty(piece.getEnumeration())) { + item.put(ITEM_ENUMERATION, piece.getEnumeration()); + } + if (StringUtils.isNotEmpty(piece.getCopyNumber())) { + item.put(COPY_NUMBER, piece.getCopyNumber()); + } + if (StringUtils.isNotEmpty(piece.getChronology())) { + item.put(ITEM_CHRONOLOGY, piece.getChronology()); + } + if (StringUtils.isNotEmpty(piece.getBarcode())) { + item.put(ITEM_BARCODE, piece.getBarcode()); + } + if (StringUtils.isNotEmpty(piece.getAccessionNumber())) { + item.put(ITEM_ACCESSION_NUMBER, piece.getAccessionNumber()); + } + if (StringUtils.isNotEmpty(piece.getCallNumber())) { + item.put(ITEM_LEVEL_CALL_NUMBER, piece.getCallNumber()); + } + if (piece.getDiscoverySuppress() != null) { + item.put(ITEM_DISCOVERY_SUPPRESS, piece.getDiscoverySuppress()); + } } public Future createShadowInstanceIfNeeded(String instanceId, RequestContext requestContext) { diff --git a/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowInventoryManager.java b/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowInventoryManager.java index fbd148c77..9f8b99fb0 100644 --- a/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowInventoryManager.java +++ b/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowInventoryManager.java @@ -3,7 +3,6 @@ import static org.folio.service.inventory.InventoryManager.ID; import static org.folio.service.inventory.InventoryManager.ITEM_HOLDINGS_RECORD_ID; import static org.folio.service.inventory.InventoryManager.ITEM_PURCHASE_ORDER_LINE_IDENTIFIER; -import static org.folio.service.inventory.InventoryManager.COPY_NUMBER; import java.util.Optional; @@ -45,9 +44,7 @@ public Future processInventory(PieceUpdateHolder holder, RequestContext re .compose(aVoid -> { if (Boolean.TRUE.equals(holder.getOriginPoLine().getIsPackage())) { return packagePoLineUpdateInventory(holder, requestContext); - } - else - { + } else { return nonPackagePoLineUpdateInventory(holder, requestContext); } }); @@ -93,15 +90,15 @@ private Future handleHolding(PieceUpdateHolder holder, RequestContext if (instanceId != null && DefaultPieceFlowsValidator.isCreateHoldingForPiecePossible(pieceToUpdate, poLineToSave)) { Location location = new Location().withLocationId(pieceToUpdate.getLocationId()); return inventoryManager.getOrCreateHoldingsRecord(instanceId, location, requestContext) - .map(holdingId -> { - Optional.ofNullable(holdingId).ifPresent(holdingIdP -> { - pieceToUpdate.setLocationId(null); - pieceToUpdate.setHoldingId(holdingId); - location.setLocationId(null); - location.setHoldingId(holdingId); - }); - return location; - }); + .map(holdingId -> { + Optional.ofNullable(holdingId).ifPresent(holdingIdP -> { + pieceToUpdate.setLocationId(null); + pieceToUpdate.setHoldingId(holdingId); + location.setLocationId(null); + location.setHoldingId(holdingId); + }); + return location; + }); } return Future.succeededFuture(new Location().withLocationId(pieceToUpdate.getLocationId())); } @@ -109,27 +106,26 @@ private Future handleHolding(PieceUpdateHolder holder, RequestContext private Future handleItem(PieceUpdateHolder holder, RequestContext requestContext) { CompositePoLine poLineToSave = holder.getPoLineToSave(); Piece pieceToUpdate = holder.getPieceToUpdate(); - if (DefaultPieceFlowsValidator.isCreateItemForPiecePossible(pieceToUpdate, poLineToSave)) { - return inventoryManager.getItemRecordById(pieceToUpdate.getItemId(), true, requestContext) - .compose(jsonItem -> { - if (holder.isCreateItem() && (jsonItem == null || jsonItem.isEmpty()) && pieceToUpdate.getHoldingId() != null) { + if (!DefaultPieceFlowsValidator.isCreateItemForPiecePossible(pieceToUpdate, poLineToSave)) { + return Future.succeededFuture(); + } + return inventoryManager.getItemRecordById(pieceToUpdate.getItemId(), true, requestContext) + .compose(jsonItem -> { + boolean jsonItemFound = jsonItem != null && !jsonItem.isEmpty(); + if (holder.isCreateItem() && !jsonItemFound && pieceToUpdate.getHoldingId() != null) { return pieceUpdateInventoryService.manualPieceFlowCreateItemRecord(pieceToUpdate, poLineToSave, requestContext); - } else if (jsonItem != null && !jsonItem.isEmpty()) { - return updateItemWithFields(jsonItem, poLineToSave, pieceToUpdate).compose( - aVoid -> inventoryManager.updateItem(jsonItem, requestContext).map(item -> jsonItem.getString(ID))); - } else { - return Future.succeededFuture(); } + if (jsonItemFound) { + return updateItemWithFields(jsonItem, poLineToSave, pieceToUpdate) + .compose(ignored -> inventoryManager.updateItem(jsonItem, requestContext).map(item -> jsonItem.getString(ID))); + } + return Future.succeededFuture(); }); - } - return Future.succeededFuture(); } private Future updateItemWithFields(JsonObject item, CompositePoLine compPOL, Piece piece) { Optional.ofNullable(piece.getHoldingId()) .ifPresent(pieceHoldingId -> item.put(ITEM_HOLDINGS_RECORD_ID, piece.getHoldingId())); - Optional.ofNullable(piece.getCopyNumber()) - .ifPresentOrElse(copyNumber -> item.put(COPY_NUMBER, copyNumber), () -> item.remove(COPY_NUMBER)); item.put(ITEM_PURCHASE_ORDER_LINE_IDENTIFIER, compPOL.getId()); return Future.succeededFuture(); } @@ -137,28 +133,27 @@ private Future updateItemWithFields(JsonObject item, CompositePoLine compP private Future nonPackageUpdateTitleWithInstance(PieceUpdateHolder holder, RequestContext requestContext) { CompositePoLine poLineToSave = holder.getPoLineToSave(); Piece pieceToUpdate = holder.getPieceToUpdate(); - if (poLineToSave.getInstanceId() == null && !PoLineCommonUtil.isInventoryUpdateNotRequired(poLineToSave)) { - return titlesService.getTitleById(pieceToUpdate.getTitleId(), requestContext) - .compose(title -> { - if (title.getInstanceId() == null) { - return createTitleInstance(title, requestContext); - } - return Future.succeededFuture(title.getInstanceId()); - }) - .map(instanceId -> poLineToSave.withInstanceId(instanceId).getInstanceId()); + if (poLineToSave.getInstanceId() != null || PoLineCommonUtil.isInventoryUpdateNotRequired(poLineToSave)) { + return Future.succeededFuture(poLineToSave.getInstanceId()); } - return Future.succeededFuture(poLineToSave.getInstanceId()); + return titlesService.getTitleById(pieceToUpdate.getTitleId(), requestContext) + .compose(title -> { + if (title.getInstanceId() == null) { + return createTitleInstance(title, requestContext); + } + return Future.succeededFuture(title.getInstanceId()); + }) + .map(instanceId -> poLineToSave.withInstanceId(instanceId).getInstanceId()); } private Future packageUpdateTitleWithInstance(Title title, RequestContext requestContext) { if (title.getInstanceId() != null) { return Future.succeededFuture(title); - } else { - return inventoryManager.getOrCreateInstanceRecord(title, requestContext) - .map(title::withInstanceId) - .compose(titleWithInstanceId -> titlesService.saveTitle(titleWithInstanceId, requestContext)) - .map(v -> title); } + return inventoryManager.getOrCreateInstanceRecord(title, requestContext) + .map(title::withInstanceId) + .compose(titleWithInstanceId -> titlesService.saveTitle(titleWithInstanceId, requestContext)) + .map(v -> title); } private Future<String> createTitleInstance(Title title, RequestContext requestContext) { diff --git a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java index 4f3f3e2c2..7cd07610a 100644 --- a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java +++ b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java @@ -29,16 +29,24 @@ import static org.folio.rest.impl.PurchaseOrderLinesApiTest.COMP_PO_LINES_MOCK_DATA_PATH; import static org.folio.rest.impl.PurchaseOrdersApiTest.X_OKAPI_TENANT; import static org.folio.rest.jaxrs.model.Eresource.CreateInventory.INSTANCE_HOLDING; +import static org.folio.service.inventory.InventoryManager.COPY_NUMBER; import static org.folio.service.inventory.InventoryManager.HOLDINGS_RECORDS; import static org.folio.service.inventory.InventoryManager.HOLDINGS_SOURCES; import static org.folio.service.inventory.InventoryManager.HOLDING_PERMANENT_LOCATION_ID; import static org.folio.service.inventory.InventoryManager.ID; import static org.folio.service.inventory.InventoryManager.ITEMS; +import static org.folio.service.inventory.InventoryManager.ITEM_ACCESSION_NUMBER; +import static org.folio.service.inventory.InventoryManager.ITEM_BARCODE; +import static org.folio.service.inventory.InventoryManager.ITEM_CHRONOLOGY; +import static org.folio.service.inventory.InventoryManager.ITEM_DISCOVERY_SUPPRESS; +import static org.folio.service.inventory.InventoryManager.ITEM_ENUMERATION; +import static org.folio.service.inventory.InventoryManager.ITEM_LEVEL_CALL_NUMBER; import static org.folio.service.inventory.InventoryManager.ITEM_PURCHASE_ORDER_LINE_IDENTIFIER; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -492,6 +500,71 @@ void testShouldBuildInstanceWithFieldFromTitles() { verify(title).getProductIds(); } + @Test + void testUpdateItemWithPieceFields() { + // given + Piece piece = new Piece(); + piece.setEnumeration("enumeration"); + piece.setCopyNumber("copy number"); + piece.setChronology("chronology"); + piece.setBarcode("barcode"); + piece.setAccessionNumber("accession number"); + piece.setCallNumber("call number"); + piece.setDiscoverySuppress(true); + + String oldValue = "old value"; + JsonObject item = new JsonObject(new HashMap<>(Map.of( + ITEM_ENUMERATION, oldValue, + COPY_NUMBER, oldValue, + ITEM_CHRONOLOGY, oldValue, + ITEM_BARCODE, oldValue, + ITEM_ACCESSION_NUMBER, oldValue, + ITEM_LEVEL_CALL_NUMBER, oldValue, + ITEM_DISCOVERY_SUPPRESS, false + ))); + + // when + inventoryManager.updateItemWithPieceFields(piece, item); + + // then + assertEquals(piece.getEnumeration(), item.getString(ITEM_ENUMERATION)); + assertEquals(piece.getCopyNumber(), item.getString(COPY_NUMBER)); + assertEquals(piece.getChronology(), item.getString(ITEM_CHRONOLOGY)); + assertEquals(piece.getBarcode(), item.getString(ITEM_BARCODE)); + assertEquals(piece.getAccessionNumber(), item.getString(ITEM_ACCESSION_NUMBER)); + assertEquals(piece.getCallNumber(), item.getString(ITEM_LEVEL_CALL_NUMBER)); + assertEquals(piece.getDiscoverySuppress(), item.getBoolean(ITEM_DISCOVERY_SUPPRESS)); + } + + @Test + void testUpdateItemWithPieceFields_notOverwrite() { + // given + Piece piece = new Piece(); + + String oldValue = "old value"; + JsonObject item = new JsonObject(new HashMap<>(Map.of( + ITEM_ENUMERATION, oldValue, + COPY_NUMBER, oldValue, + ITEM_CHRONOLOGY, oldValue, + ITEM_BARCODE, oldValue, + ITEM_ACCESSION_NUMBER, oldValue, + ITEM_LEVEL_CALL_NUMBER, oldValue, + ITEM_DISCOVERY_SUPPRESS, false + ))); + + // when + inventoryManager.updateItemWithPieceFields(piece, item); + + // then + assertEquals(oldValue, item.getString(ITEM_ENUMERATION)); + assertEquals(oldValue, item.getString(COPY_NUMBER)); + assertEquals(oldValue, item.getString(ITEM_CHRONOLOGY)); + assertEquals(oldValue, item.getString(ITEM_BARCODE)); + assertEquals(oldValue, item.getString(ITEM_ACCESSION_NUMBER)); + assertEquals(oldValue, item.getString(ITEM_LEVEL_CALL_NUMBER)); + assertFalse(item.getBoolean(ITEM_DISCOVERY_SUPPRESS)); + } + @Test void shouldCheckIfTheHoldingExistsWhenHoldingIdSpecifiedAndIfExistThenReturnHoldingIdFromLocation() throws IOException { String instanceId = UUID.randomUUID().toString(); From 8c6d0d932d8b654474240f7bd3691934de9685d7 Mon Sep 17 00:00:00 2001 From: Dmitriy-Butramyou <49509644+Dmitriy-Butramyou@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:50:38 +0300 Subject: [PATCH 3/6] [MODORDERS-886] - Updated Error for templateName (#820) * [MODORDERS-886] Updated Error for templateName * [MODORDERS-886] - Added test --- .../rest/core/exceptions/ErrorCodes.java | 3 ++- .../folio/rest/impl/OrderTemplatesAPI.java | 19 ++++++++++++++---- src/test/java/org/folio/TestConstants.java | 1 + .../java/org/folio/rest/impl/MockServer.java | 13 ++++++++++++ .../folio/rest/impl/OrderTemplateTest.java | 20 +++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java b/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java index 66e6ffb14..527424401 100644 --- a/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java +++ b/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java @@ -113,7 +113,8 @@ public enum ErrorCodes { INSTANCE_INVALID_PRODUCT_ID_ERROR("instanceInvalidProductIdError", "Instance connection could not be changed, the chosen instance contains an invalid Product ID."), FUND_LOCATION_RESTRICTION_VIOLATION("fundLocationRestrictionViolation", "One of the funds is restricted to be used for one of the locations."), ENCUMBRANCES_FOR_RE_ENCUMBER_NOT_FOUND("encumbrancesForReEncumberNotFound", "The encumbrances were correctly created during the rollover or have already been updated."), - CLAIMING_CONFIG_INVALID("claimingConfigInvalid", "Claiming interval should be set and greater than 0 if claiming is active"); + CLAIMING_CONFIG_INVALID("claimingConfigInvalid", "Claiming interval should be set and greater than 0 if claiming is active"), + TEMPLATE_NAME_ALREADY_EXISTS("templateNameNotUnique", "Template name already exists"); private final String code; diff --git a/src/main/java/org/folio/rest/impl/OrderTemplatesAPI.java b/src/main/java/org/folio/rest/impl/OrderTemplatesAPI.java index 6ac6dbe78..c15804912 100644 --- a/src/main/java/org/folio/rest/impl/OrderTemplatesAPI.java +++ b/src/main/java/org/folio/rest/impl/OrderTemplatesAPI.java @@ -3,6 +3,7 @@ import static io.vertx.core.Future.succeededFuture; import static org.folio.orders.utils.HelperUtils.handleErrorResponse; import static org.folio.rest.core.exceptions.ErrorCodes.MISMATCH_BETWEEN_ID_IN_PATH_AND_BODY; +import static org.folio.rest.core.exceptions.ErrorCodes.TEMPLATE_NAME_ALREADY_EXISTS; import java.util.Map; @@ -13,6 +14,7 @@ import org.apache.logging.log4j.Logger; import org.folio.helper.OrderTemplatesHelper; import org.folio.rest.annotations.Validate; +import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.jaxrs.model.OrderTemplate; import org.folio.rest.jaxrs.resource.OrdersOrderTemplates; @@ -25,6 +27,7 @@ public class OrderTemplatesAPI implements OrdersOrderTemplates { private static final Logger logger = LogManager.getLogger(); private static final String ORDER_TEMPLATE_LOCATION_PREFIX = "/orders/order-templates/%s"; + private static final String TEMPLATE_NAME_ALREADY_EXIST_ERROR = "lower(f_unaccent(jsonb ->> 'templateName'::text)) value already exists in table order_templates"; @Override @Validate @@ -40,7 +43,7 @@ public void postOrdersOrderTemplates(OrderTemplate entity, Map<String, String> o asyncResultHandler.handle(succeededFuture( orderTemplatesHelper.buildResponseWithLocation(String.format(ORDER_TEMPLATE_LOCATION_PREFIX, template.getId()), template))); }) - .onFailure(t -> handleErrorResponse(asyncResultHandler, orderTemplatesHelper, t)); + .onFailure(t -> handlePostPutErrorResponse(asyncResultHandler, t, orderTemplatesHelper)); } @Override @@ -69,8 +72,7 @@ public void putOrdersOrderTemplatesById(String id, OrderTemplate entity, Map<Str entity.setId(id); } - if (!entity.getId() - .equals(id)) { + if (!entity.getId().equals(id)) { helper.addProcessingError(MISMATCH_BETWEEN_ID_IN_PATH_AND_BODY.toError()); asyncResultHandler.handle(succeededFuture(helper.buildErrorResponse(422))); } else { @@ -79,7 +81,16 @@ public void putOrdersOrderTemplatesById(String id, OrderTemplate entity, Map<Str logger.info("Successfully updated order template with id={}", id); asyncResultHandler.handle(succeededFuture(helper.buildNoContentResponse())); }) - .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); + .onFailure(t -> handlePostPutErrorResponse(asyncResultHandler, t, helper)); + } + } + + private static void handlePostPutErrorResponse(Handler<AsyncResult<Response>> asyncResultHandler, Throwable t, OrderTemplatesHelper helper) { + if (StringUtils.isNotEmpty(t.getMessage()) && t.getMessage().contains(TEMPLATE_NAME_ALREADY_EXIST_ERROR)) { + handleErrorResponse(asyncResultHandler, helper, + new HttpException(422, TEMPLATE_NAME_ALREADY_EXISTS.toError())); + } else { + handleErrorResponse(asyncResultHandler, helper, t); } } diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java index 40d4fe68e..7877a34d3 100644 --- a/src/test/java/org/folio/TestConstants.java +++ b/src/test/java/org/folio/TestConstants.java @@ -82,4 +82,5 @@ private TestConstants() {} public static final String PIECE_PATH = BASE_MOCK_DATA_PATH + "pieces/"; public static final String TILES_PATH = BASE_MOCK_DATA_PATH + "titles/"; + public static final String ID_FOR_TEMPLATE_NAME_ALREADY_EXISTS = "cd0619fb-a628-4d90-be41-df8943e97768"; } diff --git a/src/test/java/org/folio/rest/impl/MockServer.java b/src/test/java/org/folio/rest/impl/MockServer.java index 0c208ba09..cf9b4d056 100644 --- a/src/test/java/org/folio/rest/impl/MockServer.java +++ b/src/test/java/org/folio/rest/impl/MockServer.java @@ -21,6 +21,7 @@ import static org.folio.TestConstants.ID_DOES_NOT_EXIST; import static org.folio.TestConstants.ID_FOR_INTERNAL_SERVER_ERROR; import static org.folio.TestConstants.ID_FOR_PIECES_INTERNAL_SERVER_ERROR; +import static org.folio.TestConstants.ID_FOR_TEMPLATE_NAME_ALREADY_EXISTS; import static org.folio.TestConstants.INACTIVE_ACCESS_PROVIDER_A; import static org.folio.TestConstants.INACTIVE_ACCESS_PROVIDER_B; import static org.folio.TestConstants.INSTANCE_TYPE_CONTAINS_CODE_AS_INSTANCE_STATUS_TENANT; @@ -2165,6 +2166,18 @@ private void handlePostGenericSubObj(RoutingContext ctx, String subObj) { case 500 -> respBody = INTERNAL_SERVER_ERROR.getReasonPhrase(); } + if (Objects.nonNull(body) && ID_FOR_TEMPLATE_NAME_ALREADY_EXISTS.equals(body.getString(ID))) { + Errors errors = new Errors(); + List<Error> errorList = new ArrayList<>(); + + errorList.add(new Error() + .withCode("422") + .withMessage("lower(f_unaccent(jsonb ->> 'templateName'::text)) value already exists in table order_templates: " + body.getString("templateName"))); + errors.withErrors(errorList); + + serverResponse(ctx, 422, APPLICATION_JSON, JsonObject.mapFrom(errors).encodePrettily()); + } + addServerRqRsData(HttpMethod.POST, subObj, body); serverResponse(ctx, status, contentType, respBody); } diff --git a/src/test/java/org/folio/rest/impl/OrderTemplateTest.java b/src/test/java/org/folio/rest/impl/OrderTemplateTest.java index cc1efd500..3ca0a010d 100644 --- a/src/test/java/org/folio/rest/impl/OrderTemplateTest.java +++ b/src/test/java/org/folio/rest/impl/OrderTemplateTest.java @@ -14,10 +14,12 @@ import static org.folio.TestConstants.BAD_QUERY; import static org.folio.TestConstants.EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10; import static org.folio.TestConstants.ID_DOES_NOT_EXIST; +import static org.folio.TestConstants.ID_FOR_TEMPLATE_NAME_ALREADY_EXISTS; import static org.folio.TestConstants.X_ECHO_STATUS; import static org.folio.TestUtils.getMockData; import static org.folio.orders.utils.ResourcePathResolver.ORDER_TEMPLATES; import static org.folio.rest.core.exceptions.ErrorCodes.MISMATCH_BETWEEN_ID_IN_PATH_AND_BODY; +import static org.folio.rest.core.exceptions.ErrorCodes.TEMPLATE_NAME_ALREADY_EXISTS; import static org.folio.rest.impl.MockServer.ORDER_TEMPLATES_COLLECTION; import static org.folio.rest.impl.MockServer.getQueryParams; import static org.folio.rest.impl.MockServer.getRqRsEntries; @@ -47,6 +49,8 @@ import org.folio.rest.jaxrs.model.Errors; import org.folio.rest.jaxrs.model.OrderTemplate; import org.folio.rest.jaxrs.model.OrderTemplateCollection; +import org.hamcrest.collection.IsCollectionWithSize; +import org.hamcrest.core.IsEqual; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -103,6 +107,22 @@ void testPostOrderTemplateSuccess() { assertThat(response.header(HttpHeaders.LOCATION), containsString(template.getId())); } + @Test + void testPostOrderTemplateAlreadyExists() { + logger.info("=== Test POST Order Template - failed case ==="); + OrderTemplate entity = new OrderTemplate() + .withId(ID_FOR_TEMPLATE_NAME_ALREADY_EXISTS) + .withTemplateName("Testing order template"); + String body = JsonObject.mapFrom(entity).encode(); + + Errors errors = verifyPostResponse(ORDER_TEMPLATES_ENDPOINT, body, prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), + APPLICATION_JSON, HttpStatus.HTTP_UNPROCESSABLE_ENTITY.toInt()).as(Errors.class); + + assertThat(errors.getErrors(), IsCollectionWithSize.hasSize(1)); + assertThat(errors.getErrors().get(0).getCode(), IsEqual.equalTo(TEMPLATE_NAME_ALREADY_EXISTS.getCode())); + assertThat(errors.getErrors().get(0).getMessage(), IsEqual.equalTo(TEMPLATE_NAME_ALREADY_EXISTS.getDescription())); + } + @Test void testGetOrderTemplateSuccess() { logger.info("=== Test GET Order Template - success case ==="); From b4bb8a402726c37be4e26c17b30ca09f10602883 Mon Sep 17 00:00:00 2001 From: Abdulkhakimov <89521577+Abdulkhakimov@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:03:18 +0500 Subject: [PATCH 4/6] [MODORDERS-989] - Placeholder - abiblity to move piece from Unrecivable into Expected (#819) --- src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java b/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java index d69eca54b..aa629cb54 100644 --- a/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java +++ b/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java @@ -191,7 +191,7 @@ private void addPieceIfValid(Piece piece, Map<String, List<Piece>> piecesByPoLin // request if (piecesByLineId.containsKey(poLineId) && piecesByLineId.get(poLineId).containsKey(pieceId)) { // Validate if the piece is not yet received - if (piece.getReceivingStatus() == ReceivingStatus.EXPECTED || isRevertToOnOrder(piece)) { + if (piece.getReceivingStatus() != ReceivingStatus.RECEIVED || isRevertToOnOrder(piece)) { piecesByPoLine.computeIfAbsent(poLineId, v -> new ArrayList<>()) .add(piece); } else { From 2073932b06726b86f3ff30c12f66922ee75d1039 Mon Sep 17 00:00:00 2001 From: imerabishvili <144257054+imerabishvili@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:18:55 +0400 Subject: [PATCH 5/6] [MODORDERS-983] - Receiving a piece on a closed/cancelled PO will reopen the PO (#821) --- .../helper/CheckinReceivePiecesHelper.java | 10 +- .../handlers/ReceiptStatusConsistency.java | 26 ++--- .../rest/impl/CheckinReceivingApiTest.java | 48 ++++++++ .../1196fcd9-7607-447d-ae85-6e91883d7e4f.json | 102 +++++++++++++++++ .../mockdata/lines/po_line_collection.json | 104 +++++++++++++++++- ...-06a95f03-eb00-4248-9f2e-2bd05957ff05.json | 13 +++ .../pieces/pieceRecordsCollection.json | 10 +- .../receiving/receive-physical-cancelled.json | 20 ++++ 8 files changed, 314 insertions(+), 19 deletions(-) create mode 100644 src/test/resources/mockdata/lines/1196fcd9-7607-447d-ae85-6e91883d7e4f.json create mode 100644 src/test/resources/mockdata/pieces/pieceRecords-06a95f03-eb00-4248-9f2e-2bd05957ff05.json create mode 100644 src/test/resources/mockdata/receiving/receive-physical-cancelled.json diff --git a/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java b/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java index aa629cb54..d3f81cfc2 100644 --- a/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java +++ b/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java @@ -21,6 +21,7 @@ import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_UPDATE_FAILED; import static org.folio.rest.core.exceptions.ErrorCodes.USER_HAS_NO_PERMISSIONS; import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.AWAITING_RECEIPT; +import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.CANCELLED; import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.FULLY_RECEIVED; import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.ONGOING; import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.PARTIALLY_RECEIVED; @@ -299,11 +300,12 @@ protected Future<Map<String, List<Piece>>> updateOrderAndPoLinesStatus(Map<Strin // Skip status update if PO line status is Ongoing List<Future<String>> futures = new ArrayList<>(); for (PoLine poLine : poLines) { - if (poLine.getReceiptStatus() != ONGOING) { - List<Piece> successfullyProcessedPieces = getSuccessfullyProcessedPieces(poLine.getId(), piecesGroupedByPoLine); - futures.add(calculatePoLineReceiptStatus(poLine, successfullyProcessedPieces, requestContext) - .compose(status -> purchaseOrderLineService.updatePoLineReceiptStatus(poLine, status, requestContext))); + if (poLine.getReceiptStatus() == CANCELLED || poLine.getReceiptStatus() == ONGOING) { + continue; } + List<Piece> successfullyProcessedPieces = getSuccessfullyProcessedPieces(poLine.getId(), piecesGroupedByPoLine); + futures.add(calculatePoLineReceiptStatus(poLine, successfullyProcessedPieces, requestContext) + .compose(status -> purchaseOrderLineService.updatePoLineReceiptStatus(poLine, status, requestContext))); } return collectResultsOnSuccess(futures).map(updatedPoLines -> { diff --git a/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java b/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java index a3430a2cb..5c840f610 100644 --- a/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java +++ b/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java @@ -67,12 +67,13 @@ public void handle(Message<JsonObject> message) { // 1. Get all pieces for poLineId getPieces(query, requestContext) .onSuccess(listOfPieces -> - // 2. Get PoLine for the poLineId which will be used to calculate PoLineReceiptStatus - purchaseOrderLineService.getOrderLineById(poLineIdUpdate, requestContext) - .map(poLine -> { - if (poLine.getReceiptStatus().equals(PoLine.ReceiptStatus.ONGOING)) { - promise.complete(); - } else { + // 2. Get PoLine for the poLineId which will be used to calculate PoLineReceiptStatus + purchaseOrderLineService.getOrderLineById(poLineIdUpdate, requestContext) + .map(poLine -> { + if (poLine.getReceiptStatus() == ReceiptStatus.CANCELLED || poLine.getReceiptStatus() == ReceiptStatus.ONGOING) { + promise.complete(); + return null; + } calculatePoLineReceiptStatus(poLine, listOfPieces) .compose(status -> purchaseOrderLineService.updatePoLineReceiptStatus(poLine, status, requestContext)) .map(updatedPoLineId -> { @@ -87,13 +88,12 @@ public void handle(Message<JsonObject> message) { logger.error("The error updating poLine by id {}", poLineIdUpdate, e); promise.fail(e); }); - } - return null; - }) - .onFailure(e -> { - logger.error("The error getting poLine by id {}", poLineIdUpdate, e); - promise.fail(e); - })) + return null; + }) + .onFailure(e -> { + logger.error("The error getting poLine by id {}", poLineIdUpdate, e); + promise.fail(e); + })) .onFailure(e -> { logger.error("The error happened getting all pieces by poLine {}", poLineIdUpdate, e); promise.fail(e); diff --git a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java index ece371129..76c5e5f61 100644 --- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java +++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java @@ -117,6 +117,7 @@ public class CheckinReceivingApiTest { private static final String ITEM_STATUS_NAME = "name"; private static final String ITEM_STATUS = "status"; private static final String COMPOSITE_POLINE_ONGOING_ID = "6e2b169a-ebeb-4c3c-a2f2-6233ff9c59ae"; + private static final String COMPOSITE_POLINE_CANCELED_ID = "1196fcd9-7607-447d-ae85-6e91883d7e4f"; private static boolean runningOnOwn; @@ -350,6 +351,53 @@ void testReceiveOngoingOrder() { verifyOrderStatusUpdateEvent(0); } + @Test + void testReceiveCanceledOrder() { + logger.info("=== Test POST Receive - Ongoing PO Lines"); + + CompositePoLine poLines = getMockAsJson(POLINES_COLLECTION).getJsonArray("poLines").getJsonObject(10).mapTo(CompositePoLine.class); + MockServer.addMockTitles(Collections.singletonList(poLines)); + + ReceivingCollection receivingRq = getMockAsJson(RECEIVING_RQ_MOCK_DATA_PATH + "receive-physical-cancelled.json").mapTo(ReceivingCollection.class); + receivingRq.getToBeReceived().get(0).setPoLineId(COMPOSITE_POLINE_CANCELED_ID); + + ReceivingResults results = verifyPostResponse(ORDERS_RECEIVING_ENDPOINT, JsonObject.mapFrom(receivingRq).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, 200).as(ReceivingResults.class); + + assertThat(results.getTotalRecords(), equalTo(receivingRq.getTotalRecords())); + + Map<String, Set<String>> pieceIdsByPol = verifyReceivingSuccessRs(results); + + List<JsonObject> pieceSearches = getPieceSearches(); + List<JsonObject> pieceUpdates = getPieceUpdates(); + List<JsonObject> polSearches = getPoLineSearches(); + List<JsonObject> polUpdates = getPoLineUpdates(); + + assertThat(pieceSearches, not(nullValue())); + assertThat(pieceUpdates, not(nullValue())); + + assertThat(polSearches, not(nullValue())); + + int expectedSearchRqQty = Math.floorDiv(receivingRq.getTotalRecords(), MAX_IDS_FOR_GET_RQ_15) + 1; + + // The piece searches should be made 1 time: 1st time to get all required piece records + assertThat(pieceSearches, hasSize(expectedSearchRqQty)); + assertThat(pieceUpdates, hasSize(receivingRq.getTotalRecords())); + assertThat(polSearches, hasSize(pieceIdsByPol.size())); + + // check no status updates were performed and POL remained canceled + assertThat(polUpdates, nullValue()); + polSearches.forEach(pol -> { + PoLine poLine = pol.mapTo(PoLineCollection.class).getPoLines().get(0); + assertThat(poLine.getCheckinItems(), is(false)); + assertThat(poLine.getReceiptStatus(), is(PoLine.ReceiptStatus.CANCELLED)); + assertThat(poLine.getReceiptDate(), is(notNullValue())); + }); + + // Verify no status updated for ongoing order + verifyOrderStatusUpdateEvent(0); + } + @Test void testPostCheckInLocationId() { diff --git a/src/test/resources/mockdata/lines/1196fcd9-7607-447d-ae85-6e91883d7e4f.json b/src/test/resources/mockdata/lines/1196fcd9-7607-447d-ae85-6e91883d7e4f.json new file mode 100644 index 000000000..a81a10ebf --- /dev/null +++ b/src/test/resources/mockdata/lines/1196fcd9-7607-447d-ae85-6e91883d7e4f.json @@ -0,0 +1,102 @@ +{ + "id": "1196fcd9-7607-447d-ae85-6e91883d7e4f", + "acquisitionMethod": "306489dd-0053-49ee-a068-c316444a8f55", + "alerts": [], + "cancellationRestriction": false, + "cancellationRestrictionNote": "ABCDEFGHIJKLMNOPQRSTUVW", + "claims": [], + "collection": false, + "contributors": [ + { + "contributor": "Ed Mashburn", + "contributorNameTypeId": "fbdd42a8-e47d-4694-b448-cc571d1b44c3" + } + ], + "cost": { + "currency": "USD", + "listUnitPrice": 24.99, + "quantityPhysical": 1 + }, + "description": "ABCDEFGH", + "details": { + "productIds": [ + { + "productId": "9780764354113", + "productIdType": "8261054f-be78-422d-bd51-4ed9f33c3422" + } + ], + "receivingNote": "ABCDEFGHIJKL", + "subscriptionFrom": "2018-10-09T00:00:00.000Z", + "subscriptionInterval": 824, + "subscriptionTo": "2020-10-09T00:00:00.000Z" + }, + "donor": "ABCDEFGHIJKLM", + "fundDistribution": [ + { + "code": "HIST", + "fundId": "fb7b70f1-b898-4924-a991-0e4b6312bb5f", + "encumbrance": "eb506834-6c70-4239-8d1a-6414a5b08ac3", + "distributionType": "percentage", + "value": 80.0 + } + ], + "locations": [ + { + "locationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "quantity": 1, + "quantityElectronic": 0, + "quantityPhysical": 1 + }, + { + "locationId": "53cf956f-c1df-410b-8bea-27f712cca7c0", + "quantity": 2, + "quantityElectronic": 0, + "quantityPhysical": 2 + } + ], + "orderFormat": "Physical Resource", + "paymentStatus": "Cancelled", + "physical": { + "createInventory": "Instance, Holding, Item", + "expectedReceiptDate": "2018-10-05T00:00:00.000Z", + "materialSupplier": "73d14bc5-d131-48c6-b380-f8e62f63c8b6", + "materialType": "2fa93835-ea37-479d-b133-1d2a2279fcd8", + "receiptDue": "2018-10-10T00:00:00.000Z", + "volumes": [ + "vol. 1" + ] + }, + "poLineDescription": "ABCDEFGHIJKLMNOPQRSTUVWXY", + "poLineNumber": "1EFC97C6B7-1", + "publicationDate": "2017", + "publisher": "Schiffer Publishing", + "purchaseOrderId": "95d29d04-34b1-4fe0-a15e-1cd129143692", + "receiptDate": "2018-10-09T00:00:00.000+0000", + "receiptStatus": "Cancelled", + "reportingCodes": [], + "requester": "Leo Bulero", + "rush": true, + "selector": "ABCD", + "tags": { + "tagList": [ + "important" + ] + }, + "titleOrPackage": "Kayak Fishing in the Northern Gulf Coast", + "vendorDetail": { + "instructions": "ABCDEFG", + "noteFromVendor": "ABCDEFGHIKJKLMNOP", + "referenceNumbers": [ + { + "refNumber": "123456-78", + "refNumberType": "Vendor title number", + "vendorDetailsSource": "OrderLine" + } + ], + "vendorAccount": "8910-25" + }, + "metadata": { + "createdByUserId": "28d10cfc-d137-11e8-a8d5-f2801f1b9fd1", + "createdDate": "2014-07-06T00:00:00.000" + } +} diff --git a/src/test/resources/mockdata/lines/po_line_collection.json b/src/test/resources/mockdata/lines/po_line_collection.json index f252b3cea..607ef1717 100644 --- a/src/test/resources/mockdata/lines/po_line_collection.json +++ b/src/test/resources/mockdata/lines/po_line_collection.json @@ -924,8 +924,110 @@ "createdByUserId": "28d10cfc-d137-11e8-a8d5-f2801f1b9fd1", "createdDate": "2014-07-06T00:00:00.000" } + }, + { + "id": "1196fcd9-7607-447d-ae85-6e91883d7e4f", + "acquisitionMethod": "306489dd-0053-49ee-a068-c316444a8f55", + "alerts": [], + "cancellationRestriction": false, + "cancellationRestrictionNote": "ABCDEFGHIJKLMNOPQRSTUVW", + "claims": [], + "collection": false, + "contributors": [ + { + "contributor": "Ed Mashburn", + "contributorNameTypeId": "fbdd42a8-e47d-4694-b448-cc571d1b44c3" + } + ], + "cost": { + "currency": "USD", + "listUnitPrice": 24.99, + "quantityPhysical": 1 + }, + "description": "ABCDEFGH", + "details": { + "productIds": [ + { + "productId": "9780764354113", + "productIdType": "8261054f-be78-422d-bd51-4ed9f33c3422" + } + ], + "receivingNote": "ABCDEFGHIJKL", + "subscriptionFrom": "2018-10-09T00:00:00.000Z", + "subscriptionInterval": 824, + "subscriptionTo": "2020-10-09T00:00:00.000Z" + }, + "donor": "ABCDEFGHIJKLM", + "fundDistribution": [ + { + "code": "HIST", + "fundId": "fb7b70f1-b898-4924-a991-0e4b6312bb5f", + "encumbrance": "eb506834-6c70-4239-8d1a-6414a5b08ac3", + "distributionType": "percentage", + "value": 80.0 + } + ], + "locations": [ + { + "locationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "quantity": 1, + "quantityElectronic": 0, + "quantityPhysical": 1 + }, + { + "locationId": "53cf956f-c1df-410b-8bea-27f712cca7c0", + "quantity": 2, + "quantityElectronic": 0, + "quantityPhysical": 2 + } + ], + "orderFormat": "Physical Resource", + "paymentStatus": "Ongoing", + "physical": { + "createInventory": "Instance, Holding, Item", + "expectedReceiptDate": "2018-10-05T00:00:00.000Z", + "materialSupplier": "73d14bc5-d131-48c6-b380-f8e62f63c8b6", + "materialType": "2fa93835-ea37-479d-b133-1d2a2279fcd8", + "receiptDue": "2018-10-10T00:00:00.000Z", + "volumes": [ + "vol. 1" + ] + }, + "poLineDescription": "ABCDEFGHIJKLMNOPQRSTUVWXY", + "poLineNumber": "1EFC97C6B7-1", + "publicationDate": "2017", + "publisher": "Schiffer Publishing", + "purchaseOrderId": "95d29d04-34b1-4fe0-a15e-1cd129143692", + "receiptDate": "2018-10-09T00:00:00.000+0000", + "receiptStatus": "Cancelled", + "reportingCodes": [], + "requester": "Leo Bulero", + "rush": true, + "selector": "ABCD", + "tags": { + "tagList": [ + "important" + ] + }, + "titleOrPackage": "Kayak Fishing in the Northern Gulf Coast", + "vendorDetail": { + "instructions": "ABCDEFG", + "noteFromVendor": "ABCDEFGHIKJKLMNOP", + "referenceNumbers": [ + { + "refNumber": "123456-78", + "refNumberType": "Vendor title number", + "vendorDetailsSource": "OrderLine" + } + ], + "vendorAccount": "8910-25" + }, + "metadata": { + "createdByUserId": "28d10cfc-d137-11e8-a8d5-f2801f1b9fd1", + "createdDate": "2014-07-06T00:00:00.000" + } } ], - "totalRecords": 9 + "totalRecords": 10 } diff --git a/src/test/resources/mockdata/pieces/pieceRecords-06a95f03-eb00-4248-9f2e-2bd05957ff05.json b/src/test/resources/mockdata/pieces/pieceRecords-06a95f03-eb00-4248-9f2e-2bd05957ff05.json new file mode 100644 index 000000000..f1909a663 --- /dev/null +++ b/src/test/resources/mockdata/pieces/pieceRecords-06a95f03-eb00-4248-9f2e-2bd05957ff05.json @@ -0,0 +1,13 @@ +{ + "pieces": [ + { + "id": "06a95f03-eb00-4248-9f2e-2bd05957ff05", + "poLineId": "1196fcd9-7607-447d-ae85-6e91883d7e4f", + "titleId": "9a665b22-9fe5-4c95-b4ee-837a5433c95d", + "receivingStatus": "Expected", + "receiptDate": "2018-10-09T00:00:00.000Z", + "receivedDate": "2019-03-05T08:06:58.473+0000" + } + ], + "totalRecords": 1 +} diff --git a/src/test/resources/mockdata/pieces/pieceRecordsCollection.json b/src/test/resources/mockdata/pieces/pieceRecordsCollection.json index 6722ca417..0464909ac 100644 --- a/src/test/resources/mockdata/pieces/pieceRecordsCollection.json +++ b/src/test/resources/mockdata/pieces/pieceRecordsCollection.json @@ -484,6 +484,14 @@ "receiptDate": "2018-10-09T00:00:00.000Z", "receivedDate": "2019-03-05T08:06:58.473+0000" }, + { + "id": "06a95f03-eb00-4248-9f2e-2bd05957ff05", + "poLineId": "1196fcd9-7607-447d-ae85-6e91883d7e4f", + "titleId": "9a665b22-9fe5-4c95-b4ee-837a5433c95d", + "receivingStatus": "Expected", + "receiptDate": "2018-10-09T00:00:00.000Z", + "receivedDate": "2019-03-05T08:06:58.473+0000" + }, { "id": "bf3c14fe-6eac-450a-90ca-391d0788a8a4", "itemId": "86481a22-633e-4b97-8061-0dc9fdaaeaca", @@ -494,5 +502,5 @@ "receivingStatus": "Received" } ], - "totalRecords": 60 + "totalRecords": 61 } diff --git a/src/test/resources/mockdata/receiving/receive-physical-cancelled.json b/src/test/resources/mockdata/receiving/receive-physical-cancelled.json new file mode 100644 index 000000000..1f280bf44 --- /dev/null +++ b/src/test/resources/mockdata/receiving/receive-physical-cancelled.json @@ -0,0 +1,20 @@ +{ + "toBeReceived": [ + { + "poLineId": "0dd8f1d2-ac2e-4155-a407-72071f6d5f4b", + "received": 1, + "receivedItems": [ + { + "barcode": 21111111122, + "callNumber": "PR 8923 W6 L36 1990 c.3", + "comment": "Very important note", + "caption": "Vol. 1", + "itemStatus": "In process", + "locationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "pieceId": "06a95f03-eb00-4248-9f2e-2bd05957ff05" + } + ] + } + ], + "totalRecords": 1 +} From d2508364657c8d7b61a476a514c956b6e2ec5052 Mon Sep 17 00:00:00 2001 From: Abdulkhakimov <89521577+Abdulkhakimov@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:54:16 +0500 Subject: [PATCH 6/6] [MODORDERS-989] - Implement batch endpoint to move multiple pieces to Expected status (#823) --- descriptors/ModuleDescriptor-template.json | 26 +++ ramls/acq-models | 2 +- ramls/expect.raml | 38 +++++ .../java/org/folio/helper/ExpectHelper.java | 151 ++++++++++++++++++ .../org/folio/rest/impl/ReceivingAPI.java | 14 +- src/test/java/org/folio/TestConstants.java | 1 + .../rest/impl/CheckinReceivingApiTest.java | 51 ++++++ 7 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 ramls/expect.raml create mode 100644 src/main/java/org/folio/helper/ExpectHelper.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4ef676758..d88b53873 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -436,6 +436,26 @@ "orders-storage.reporting-codes.item.get" ] }, + { + "methods": [ + "POST" + ], + "pathPattern": "/orders/expect", + "permissionsRequired": [ + "orders.expect.collection.post" + ], + "modulePermissions": [ + "orders-storage.pieces.collection.get", + "orders-storage.pieces.item.put", + "orders-storage.po-lines.collection.get", + "orders-storage.po-lines.item.put", + "orders-storage.purchase-orders.item.get", + "orders-storage.purchase-orders.item.put", + "orders-storage.titles.collection.get", + "acquisitions-units-storage.units.collection.get", + "acquisitions-units-storage.memberships.collection.get" + ] + }, { "methods": [ "GET" @@ -1277,6 +1297,11 @@ "displayName": "Orders - Check-in items", "description": "Check-in items spanning one or more po-lines in this order" }, + { + "permissionName": "orders.expect.collection.post", + "displayName": "Orders - Expect pieces", + "description": "Expect pieces spanning one or more po-lines in this order" + }, { "permissionName": "orders.receiving-history.collection.get", "displayName": "Orders - Receiving history", @@ -1697,6 +1722,7 @@ "orders.po-number.item.post", "orders.receiving.collection.post", "orders.check-in.collection.post", + "orders.expect.collection.post", "orders.receiving-history.collection.get", "orders.pieces.all", "orders.acquisitions-units-assignments.all", diff --git a/ramls/acq-models b/ramls/acq-models index 4aab2ae48..49e5e16ca 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 4aab2ae4808333aac379cae40828dacdf0ef986c +Subproject commit 49e5e16cafc41db6fc65cf644fd742843d3c5287 diff --git a/ramls/expect.raml b/ramls/expect.raml new file mode 100644 index 000000000..46e99379c --- /dev/null +++ b/ramls/expect.raml @@ -0,0 +1,38 @@ +#%RAML 1.0 +title: Receive +baseUri: https://github.com/folio-org/mod-orders +version: v1 +protocols: [ HTTP, HTTPS ] + +documentation: + - title: Orders Business Logic API + content: <b>API for transitioning pieces status from Unreceivable to Expected</b> + +types: + expect-collection: !include acq-models/mod-orders/schemas/expectCollection.json + receiving-results: !include acq-models/mod-orders/schemas/receivingResults.json + errors: !include raml-util/schemas/errors.schema + UUID: + type: string + pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ + +traits: + validate: !include raml-util/traits/validation.raml + +resourceTypes: + post-with-200: !include rtypes/post-json-200.raml + +/orders/expect: + displayName: Expect pieces + description: | + Expect pieces spanning one or more PO lines. The endpoint is used to: + - move a unreceivable piece back to "Expected" + type: + post-with-200: + requestSchema: expect-collection + responseSchema: receiving-results + requestExample: !include acq-models/mod-orders/examples/expectCollection.sample + responseExample: !include acq-models/mod-orders/examples/receivingResults.sample + is: [validate] + post: + description: Expect pieces spanning one or more PO lines diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java new file mode 100644 index 000000000..f4f97aeba --- /dev/null +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -0,0 +1,151 @@ +package org.folio.helper; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import one.util.streamex.StreamEx; +import org.apache.commons.lang3.StringUtils; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.jaxrs.model.ExpectCollection; +import org.folio.rest.jaxrs.model.ExpectPiece; +import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.ProcessingStatus; +import org.folio.rest.jaxrs.model.ReceivingResult; +import org.folio.rest.jaxrs.model.ReceivingResults; +import org.folio.rest.jaxrs.model.ToBeExpected; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; + +public class ExpectHelper extends CheckinReceivePiecesHelper<ExpectPiece> { + + /** + * Map with PO line id as a key and value is map with piece id as a key and + * {@link ExpectPiece} as a value + */ + private final Map<String, Map<String, ExpectPiece>> expectPieces; + + public ExpectHelper(ExpectCollection expectCollection, Map<String, String> okapiHeaders, Context ctx) { + super(okapiHeaders, ctx); + // Convert request to map representation + expectPieces = groupExpectPieceByPoLineId(expectCollection); + + // Logging quantity of the piece records to be expected + if (logger.isDebugEnabled()) { + int poLinesQty = expectPieces.size(); + int piecesQty = StreamEx.ofValues(expectPieces) + .mapToInt(Map::size) + .sum(); + logger.debug("{} piece record(s) are going to be expected for {} PO line(s)", piecesQty, poLinesQty); + } + } + + public Future<ReceivingResults> expectPieces(ExpectCollection expectCollection, RequestContext requestContext) { + return getPoLines(new ArrayList<>(expectPieces.keySet()), requestContext) + .compose(poLines -> removeForbiddenEntities(poLines, expectPieces, requestContext)) + .compose(vVoid -> processExpectPieces(expectCollection, requestContext)); + } + + private Future<ReceivingResults> processExpectPieces(ExpectCollection expectCollection, RequestContext requestContext) { + // 1. Get piece records from storage + return retrievePieceRecords(expectPieces, requestContext) + // 2. Update piece status to Expected + .map(this::updatePieceRecords) + // 3. Update received piece records in the storage + .compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext)) + // 4. Return results to the client + .map(piecesGroupedByPoLine -> prepareResponseBody(expectCollection, piecesGroupedByPoLine)); + } + + private Map<String, Map<String, ExpectPiece>> groupExpectPieceByPoLineId(ExpectCollection expectCollection) { + return StreamEx + .of(expectCollection.getToBeExpected()) + .distinct() + .groupingBy(ToBeExpected::getPoLineId, + mapping(ToBeExpected::getExpectPieces, + collectingAndThen(toList(), + lists -> StreamEx.of(lists) + .flatMap(List::stream) + .toMap(ExpectPiece::getId, expectPiece -> expectPiece)))); + } + + private ReceivingResults prepareResponseBody(ExpectCollection expectCollection, + Map<String, List<Piece>> piecesGroupedByPoLine) { + ReceivingResults results = new ReceivingResults(); + results.setTotalRecords(expectCollection.getTotalRecords()); + for (ToBeExpected toBeExpected : expectCollection.getToBeExpected()) { + String poLineId = toBeExpected.getPoLineId(); + ReceivingResult result = new ReceivingResult(); + results.getReceivingResults().add(result); + + // Get all processed piece records for PO Line + Map<String, Piece> processedPiecesForPoLine = StreamEx + .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) + .toMap(Piece::getId, piece -> piece); + + Map<String, Integer> resultCounts = new HashMap<>(); + resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0); + resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0); + for (ExpectPiece expectPiece : toBeExpected.getExpectPieces()) { + String pieceId = expectPiece.getId(); + + calculateProcessingErrors(poLineId, result, processedPiecesForPoLine, resultCounts, pieceId); + } + + result.withPoLineId(poLineId) + .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString())) + .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString())); + } + + return results; + } + + private Map<String, List<Piece>> updatePieceRecords(Map<String, List<Piece>> piecesGroupedByPoLine) { + StreamEx.ofValues(piecesGroupedByPoLine) + .flatMap(List::stream) + .forEach(this::updatePieceWithExpectInfo); + + return piecesGroupedByPoLine; + } + + private void updatePieceWithExpectInfo(Piece piece) { + ExpectPiece expectPiece = piecesByLineId.get(piece.getPoLineId()) + .get(piece.getId()); + + piece.setComment(expectPiece.getComment()); + piece.setReceivedDate(null); + piece.setReceivingStatus(Piece.ReceivingStatus.EXPECTED); + } + + @Override + protected boolean isRevertToOnOrder(Piece piece) { + return false; + } + + @Override + protected Future<Boolean> receiveInventoryItemAndUpdatePiece(JsonObject item, Piece piece, RequestContext requestContext) { + return Future.succeededFuture(false); + } + + @Override + protected Map<String, List<Piece>> updatePieceRecordsWithoutItems(Map<String, List<Piece>> piecesGroupedByPoLine) { + return Collections.emptyMap(); + } + + @Override + protected String getHoldingId(Piece piece) { + return StringUtils.EMPTY; + } + + @Override + protected String getLocationId(Piece piece) { + return StringUtils.EMPTY; + } +} diff --git a/src/main/java/org/folio/rest/impl/ReceivingAPI.java b/src/main/java/org/folio/rest/impl/ReceivingAPI.java index 24bada2f9..79a3bdd2e 100644 --- a/src/main/java/org/folio/rest/impl/ReceivingAPI.java +++ b/src/main/java/org/folio/rest/impl/ReceivingAPI.java @@ -10,12 +10,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.helper.CheckinHelper; +import org.folio.helper.ExpectHelper; import org.folio.helper.ReceivingHelper; import org.folio.rest.annotations.Validate; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.CheckinCollection; +import org.folio.rest.jaxrs.model.ExpectCollection; import org.folio.rest.jaxrs.model.ReceivingCollection; import org.folio.rest.jaxrs.resource.OrdersCheckIn; +import org.folio.rest.jaxrs.resource.OrdersExpect; import org.folio.rest.jaxrs.resource.OrdersReceive; import org.folio.rest.jaxrs.resource.OrdersReceivingHistory; @@ -24,7 +27,7 @@ import io.vertx.core.Handler; import io.vertx.core.json.JsonObject; -public class ReceivingAPI implements OrdersReceive, OrdersCheckIn, OrdersReceivingHistory { +public class ReceivingAPI implements OrdersReceive, OrdersCheckIn, OrdersExpect, OrdersReceivingHistory { private static final Logger logger = LogManager.getLogger(); @@ -50,6 +53,15 @@ public void postOrdersCheckIn(CheckinCollection entity, Map<String, String> okap .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); } + @Override + public void postOrdersExpect(ExpectCollection entity, Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) { + logger.info("Expect {} pieces", entity.getTotalRecords()); + ExpectHelper helper = new ExpectHelper(entity, okapiHeaders, vertxContext); + helper.expectPieces(entity, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(result -> asyncResultHandler.handle(succeededFuture(helper.buildOkResponse(result)))) + .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); + } + @Override @Validate public void getOrdersReceivingHistory(String totalRecords, int offset, int limit, String query, Map<String, String> okapiHeaders, diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java index 7877a34d3..25b64d563 100644 --- a/src/test/java/org/folio/TestConstants.java +++ b/src/test/java/org/folio/TestConstants.java @@ -16,6 +16,7 @@ private TestConstants() {} public static final String ORDERS_RECEIVING_ENDPOINT = "/orders/receive"; public static final String ORDERS_CHECKIN_ENDPOINT = "/orders/check-in"; + public static final String ORDERS_EXPECT_ENDPOINT = "/orders/expect"; public static final String PO_LINE_NUMBER_VALUE = "1"; diff --git a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java index 76c5e5f61..4c28fc338 100644 --- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java +++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java @@ -8,6 +8,7 @@ import static org.folio.TestConfig.isVerticleNotDeployed; import static org.folio.TestConstants.EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10; import static org.folio.TestConstants.ORDERS_CHECKIN_ENDPOINT; +import static org.folio.TestConstants.ORDERS_EXPECT_ENDPOINT; import static org.folio.TestConstants.ORDERS_RECEIVING_ENDPOINT; import static org.folio.TestUtils.getInstanceId; import static org.folio.TestUtils.getMinimalContentCompositePoLine; @@ -87,6 +88,8 @@ import org.folio.rest.jaxrs.model.Eresource; import org.folio.rest.jaxrs.model.Error; import org.folio.rest.jaxrs.model.Errors; +import org.folio.rest.jaxrs.model.ExpectCollection; +import org.folio.rest.jaxrs.model.ExpectPiece; import org.folio.rest.jaxrs.model.Physical; import org.folio.rest.jaxrs.model.Piece; import org.folio.rest.jaxrs.model.PoLine; @@ -98,6 +101,7 @@ import org.folio.rest.jaxrs.model.ReceivingResult; import org.folio.rest.jaxrs.model.ReceivingResults; import org.folio.rest.jaxrs.model.ToBeCheckedIn; +import org.folio.rest.jaxrs.model.ToBeExpected; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -836,6 +840,53 @@ void testPostReceivingPhysicalWithErrors() throws IOException { verifyOrderStatusUpdateEvent(1); } + @Test + void testMovePieceStatusFromUnreceivableToExpected() { + logger.info("=== Test POST Expect"); + + CompositePurchaseOrder order = getMinimalContentCompositePurchaseOrder(); + CompositePoLine poLine = getMinimalContentCompositePoLine(order.getId()); + poLine.setIsPackage(true); + poLine.setOrderFormat(CompositePoLine.OrderFormat.ELECTRONIC_RESOURCE); + poLine.setEresource(new Eresource().withCreateInventory(Eresource.CreateInventory.INSTANCE_HOLDING_ITEM)); + + Piece electronicPiece = getMinimalContentPiece(poLine.getId()).withReceivingStatus(Piece.ReceivingStatus.UNRECEIVABLE) + .withFormat(org.folio.rest.jaxrs.model.Piece.Format.ELECTRONIC) + .withId(UUID.randomUUID().toString()) + .withTitleId(UUID.randomUUID().toString()) + .withItemId(UUID.randomUUID().toString()); + + addMockEntry(PURCHASE_ORDER_STORAGE, order.withWorkflowStatus(CompositePurchaseOrder.WorkflowStatus.OPEN)); + addMockEntry(PO_LINES_STORAGE, poLine); + addMockEntry(PIECES_STORAGE, electronicPiece); + + List<ToBeExpected> toBeCheckedInList = new ArrayList<>(); + toBeCheckedInList.add(new ToBeExpected() + .withExpected(1) + .withPoLineId(poLine.getId()) + .withExpectPieces(Collections.singletonList(new ExpectPiece().withId(electronicPiece.getId()).withComment("test")))); + + ExpectCollection request = new ExpectCollection() + .withToBeExpected(toBeCheckedInList) + .withTotalRecords(1); + + Response response = verifyPostResponse(ORDERS_EXPECT_ENDPOINT, JsonObject.mapFrom(request).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, HttpStatus.HTTP_OK.toInt()); + assertThat(response.as(ReceivingResults.class).getReceivingResults().get(0).getProcessedSuccessfully(), is(1)); + + List<JsonObject> pieceUpdates = getPieceUpdates(); + + assertThat(pieceUpdates, not(nullValue())); + assertThat(pieceUpdates, hasSize(request.getTotalRecords())); + + pieceUpdates.forEach(pol -> { + Piece piece = pol.mapTo(Piece.class); + assertThat(piece.getId(), is(electronicPiece.getId())); + assertThat(piece.getReceivingStatus(), is(Piece.ReceivingStatus.EXPECTED)); + assertThat(piece.getComment(), is("test")); + }); + } + private void verifyProperQuantityOfHoldingsCreated(ReceivingCollection receivingRq) throws IOException { Set<String> expectedHoldings = new HashSet<>();