diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 4ef676758..7996457f3 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -436,6 +436,34 @@
"orders-storage.reporting-codes.item.get"
]
},
+ {
+ "methods": [
+ "POST"
+ ],
+ "pathPattern": "/orders/restore",
+ "permissionsRequired": [
+ "orders.restore.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",
+ "inventory.items.collection.get",
+ "inventory.items.item.put",
+ "inventory-storage.holdings.collection.get",
+ "inventory-storage.holdings.item.post",
+ "acquisitions-units-storage.units.collection.get",
+ "acquisitions-units-storage.memberships.collection.get",
+ "orders-storage.purchase-orders.collection.get",
+ "finance.encumbrances.item.put",
+ "finance.order-transaction-summaries.item.put",
+ "finance.transactions.collection.get"
+ ]
+ },
{
"methods": [
"GET"
@@ -1277,6 +1305,11 @@
"displayName": "Orders - Check-in items",
"description": "Check-in items spanning one or more po-lines in this order"
},
+ {
+ "permissionName": "orders.restore.collection.post",
+ "displayName": "Orders - Restore items",
+ "description": "Restore items spanning one or more po-lines in this order"
+ },
{
"permissionName": "orders.receiving-history.collection.get",
"displayName": "Orders - Receiving history",
@@ -1697,6 +1730,7 @@
"orders.po-number.item.post",
"orders.receiving.collection.post",
"orders.check-in.collection.post",
+ "orders.restore.collection.post",
"orders.receiving-history.collection.get",
"orders.pieces.all",
"orders.acquisitions-units-assignments.all",
diff --git a/ramls/restoration.raml b/ramls/restoration.raml
new file mode 100644
index 000000000..b4fd4f4d1
--- /dev/null
+++ b/ramls/restoration.raml
@@ -0,0 +1,38 @@
+#%RAML 1.0
+title: Restoration
+baseUri: https://github.com/folio-org/mod-orders
+version: v1
+protocols: [ HTTP, HTTPS ]
+
+documentation:
+ - title: Orders Business Logic API
+ content: API for transition pieces status from unreceivable to expected
+
+types:
+ receiving-collection: !include acq-models/mod-orders/schemas/receivingCollection.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/restore:
+ displayName: Restore items
+ description: |
+ Restore items spanning one or more PO lines. The endpoint is used to:
+ - move a received piece back to "Expected" in case "receivedItems" element's "itemStatus" is "On order"
+ type:
+ post-with-200:
+ requestSchema: receiving-collection
+ responseSchema: receiving-results
+ requestExample: !include acq-models/mod-orders/examples/receivingCollection.sample
+ responseExample: !include acq-models/mod-orders/examples/receivingResults.sample
+ is: [validate]
+ post:
+ description: Restore items spanning one or more PO lines
diff --git a/src/main/java/org/folio/helper/ReceivingHelper.java b/src/main/java/org/folio/helper/ReceivingHelper.java
index 8a07b206f..9052344ba 100644
--- a/src/main/java/org/folio/helper/ReceivingHelper.java
+++ b/src/main/java/org/folio/helper/ReceivingHelper.java
@@ -251,7 +251,7 @@ protected Map> updatePieceRecordsWithoutItems(Map okapiHeaders, Context ctx) {
+ super(receivingCollection, okapiHeaders, ctx);
+ }
+
+ public Future restorePiece(ReceivingCollection restorationCollection, RequestContext requestContext) {
+ return receiveItems(restorationCollection, requestContext);
+ }
+
+ @Override
+ protected boolean isRevertToOnOrder(Piece piece) {
+ return piece.getReceivingStatus() == Piece.ReceivingStatus.UNRECEIVABLE
+ && inventoryManager
+ .isOnOrderItemStatus(piecesByLineId.get(piece.getPoLineId()).get(piece.getId()));
+ }
+
+ @Override
+ protected void updatePieceWithReceivingInfo(Piece piece) {
+ super.updatePieceWithReceivingInfo(piece);
+
+ piece.setReceivedDate(null);
+ piece.setReceivingStatus(Piece.ReceivingStatus.EXPECTED);
+ }
+}
diff --git a/src/main/java/org/folio/rest/impl/ReceivingAPI.java b/src/main/java/org/folio/rest/impl/ReceivingAPI.java
index 24bada2f9..a8d3a1176 100644
--- a/src/main/java/org/folio/rest/impl/ReceivingAPI.java
+++ b/src/main/java/org/folio/rest/impl/ReceivingAPI.java
@@ -11,12 +11,14 @@
import org.apache.logging.log4j.Logger;
import org.folio.helper.CheckinHelper;
import org.folio.helper.ReceivingHelper;
+import org.folio.helper.RestoreHelper;
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.ReceivingCollection;
import org.folio.rest.jaxrs.resource.OrdersCheckIn;
import org.folio.rest.jaxrs.resource.OrdersReceive;
+import org.folio.rest.jaxrs.resource.OrdersRestore;
import org.folio.rest.jaxrs.resource.OrdersReceivingHistory;
import io.vertx.core.AsyncResult;
@@ -24,7 +26,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, OrdersReceivingHistory, OrdersRestore {
private static final Logger logger = LogManager.getLogger();
@@ -66,4 +68,13 @@ public void getOrdersReceivingHistory(String totalRecords, int offset, int limit
})
.onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t));
}
+
+ @Override
+ public void postOrdersRestore(ReceivingCollection entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) {
+ logger.info("Restoring {} items", entity.getTotalRecords());
+ RestoreHelper helper = new RestoreHelper(entity, okapiHeaders, vertxContext);
+ helper.restorePiece(entity, new RequestContext(vertxContext, okapiHeaders))
+ .onSuccess(result -> asyncResultHandler.handle(succeededFuture(helper.buildOkResponse(result))))
+ .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t));
+ }
}
diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java
index 40d4fe68e..c330aad6e 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_RESTORE_ENDPOINT = "/orders/restore";
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 ece371129..2a05ffec9 100644
--- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java
+++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java
@@ -9,6 +9,7 @@
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_RECEIVING_ENDPOINT;
+import static org.folio.TestConstants.ORDERS_RESTORE_ENDPOINT;
import static org.folio.TestUtils.getInstanceId;
import static org.folio.TestUtils.getMinimalContentCompositePoLine;
import static org.folio.TestUtils.getMinimalContentCompositePurchaseOrder;
@@ -111,6 +112,7 @@ public class CheckinReceivingApiTest {
private static final String RECEIVING_RQ_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "receiving/";
private static final String CHECKIN_RQ_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "checkIn/";
+ private static final String RESTORE_RQ_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "restore/";
private static final String ITEM_BARCODE = "barcode";
private static final String ITEM_LEVEL_CALL_NUMBER = "itemLevelCallNumber";
private static final String HOLDING_PERMANENT_LOCATION_ID = "permanentLocationId";
@@ -1025,6 +1027,50 @@ void testPostReceivingRevertElectronicResource() {
verifyOrderStatusUpdateEvent(1);
}
+ @Test
+ void testRestoreUnreceivablePiece() {
+ logger.info("=== Test POST Restore");
+
+ CompositePoLine poLines = getMockAsJson(POLINES_COLLECTION).getJsonArray("poLines").getJsonObject(9).mapTo(CompositePoLine.class);
+ MockServer.addMockTitles(Collections.singletonList(poLines));
+
+ ReceivingCollection receivingRq = getMockAsJson(RESTORE_RQ_MOCK_DATA_PATH + "receive-physical-ongoing.json").mapTo(ReceivingCollection.class);
+ receivingRq.getToBeReceived().get(0).setPoLineId(COMPOSITE_POLINE_ONGOING_ID);
+
+ ReceivingResults results = verifyPostResponse(ORDERS_RESTORE_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> pieceIdsByPol = verifyReceivingSuccessRs(results);
+
+ List pieceSearches = getPieceSearches();
+ List pieceUpdates = getPieceUpdates();
+ List polSearches = getPoLineSearches();
+ List 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(pieceUpdates, hasSize(1));
+
+ pieceUpdates.forEach(pieceUpdated -> {
+ Piece pieceMapped = pieceUpdated.mapTo(Piece.class);
+ assertThat(pieceMapped.getReceivedDate(), nullValue());
+ assertThat(pieceMapped.getReceivingStatus(), is(Piece.ReceivingStatus.EXPECTED));
+ });
+
+ // Verify no status updated for ongoing order
+ verifyOrderStatusUpdateEvent(0);
+ }
+
private void checkResultWithErrors(CheckinCollection request, int expectedNumOfErrors) {
ReceivingResult response = verifyPostResponse(ORDERS_CHECKIN_ENDPOINT, JsonObject.mapFrom(request).encode(),
diff --git a/src/test/resources/mockdata/restore/receive-physical-ongoing.json b/src/test/resources/mockdata/restore/receive-physical-ongoing.json
new file mode 100644
index 000000000..1141bc6b9
--- /dev/null
+++ b/src/test/resources/mockdata/restore/receive-physical-ongoing.json
@@ -0,0 +1,20 @@
+{
+ "toBeReceived": [
+ {
+ "poLineId": "0dd8f1d2-ac2e-4155-a407-72071f6d5f4a",
+ "received": 1,
+ "receivedItems": [
+ {
+ "barcode": 21111111122,
+ "callNumber": "PR 8923 W6 L36 1990 c.3",
+ "comment": "Very important note",
+ "caption": "Vol. 1",
+ "itemStatus": "On order",
+ "locationId": "fcd64ce1-6995-48f0-840e-89ffa2288371",
+ "pieceId": "05a95f03-eb00-4248-9f2e-2bd05957ff04"
+ }
+ ]
+ }
+ ],
+ "totalRecords": 1
+}