This is the seventh part of a tutorial series about how to build your own SAP Fiori Approve Purchase Order app.

The purpose of this tutorial is to show you step-by-step how to build your own SAP Fiori Approve Purchase Orders app and provides additional insights into why certain aspects have been developed as they are.

Please see the introductory post (Tutorial: Build Your Own SAP Fiori Approve Purchase Order App) for background information regarding this tutorial.

Previously posted chapters can be find here:

In this chapter, we set up the app using the SAP Web IDE and ran the app using mock data.

In this chapter, we adapted the list screen of the app.

In this chapter, we adapted the detail screen of the app.

In this chapter, we enabled approve and reject buttons.

In this sixth chapter, we set up a new model that stores the global app state.

In this seventh part, we’re going to encapsulate approve / reject service calls.

Encapsulating approve / reject service calls

Now, we‘ll implement the logic as part of the data model that invokes service calls. To do so, we’re going to create a new file called Approver.js in the model folder. The approver model provides services for sending approval / reject requests via OData to the backend.

Unlike the SubControllerFoApproval, there is no view / fragment associated with this class. It extends the base object class like SubControllerForApproval but since it encapsulates the service class without any associated view / fragment, it’s located in the model folder and does not have a controller in its name.

The approver class stores a reference to ListSelector to interact with the master list. Since swipe approvals may cause parallel calls, it tracks the number of open calls in the _iOpenCallsCount property and stores a map of purchase order IDs in _mRunningSwipes. The master list needs to be updated whenever a call is successfully finished. The _iOpenCallsCount property keeps track of whether this is required.

First, let’s create an empty stub for this class and extend it step-by-step. Since we need to update the list of purchase orders to be processed, the Approver requires the ListSelector. ListSelector is a convenience API for selecting / refreshing list items that is generated by the master-detail wizard in the controller folder.

File: model/Approver.js

sap.ui.define([“sap/ui/base/Object”,

“sap/ui/Device”

], function(Object, Device) {

“use strict”;

 

return Object.extend(“acme.purchaseorder.approve.model.Approver”, {

 

// This class provides the service of sending approve/reject requests to the

// backend. Moreover, it deals with concurrent handling and success dialogs.

// For this purpose, a single instance is created and attached to the

// application.

 

constructor: function(oListSelector) {

this._oListSelector = oListSelector;

this._iOpenCallsCount = 0;

this._mRunningSwipes = {};

this._bOneWaitingSuccess = false;

}

});

});

 

Next, we’ll implement the logic to trigger the backend processing. Since this is an asynchronous operation, a promise is used.

File: model/Approver.js

constructor: function(oListSelector) {

[…]

},

 

approve: function(bApprove, bFromSwipe, oView, aPOIds, sApprovalNote) {

return new Promise(function(fnResolve, fnReject) {

var sFunction = bApprove ? “/ApprovePurchaseOrder” : “/RejectPurchaseOrder”,

oModel = oView.getModel(),

oGlobalModel = oView.getModel(“globalProperties”),

oResourceBundle = oView.getModel(“i18n”).getResourceBundle(),

sSuccessMessage = “”,

sSupplier = “”;

if (bFromSwipe) {

// Note: Swipe approval is always single approval

this._mRunningSwipes[aPOIds[0]] = true;

oGlobalModel.setProperty(“/isSwipeRunning”, true);

} else {

oGlobalModel.setProperty(“/isBusyApproving”, true);

}

this._iOpenCallsCount++;

var fnOnError = function() {

this._callEnded(false, oGlobalModel);

fnReject();

}.bind(this);

var fnOnSuccess = function() {

this._callEnded(true, oGlobalModel);

// A success message is only sent when the last request has

// returned. Thus, when the user sends several requests via swipe,

// only one

// message toast is sent; this represents the request that came

// back as last.

if (this._iOpenCallsCount === 0) {

if (aPOIds.length === 1) {

sSupplier = oModel.getProperty(“/Z_C_Purchaseorder(‘” + aPOIds[0] + “‘)”).SupplierName;

sSuccessMessage = oResourceBundle.getText(bApprove ? “ymsg.approvalMessageToast” : “ymsg.rejectionMessageToast”, [sSupplier]);

} else {

sSuccessMessage = oResourceBundle.getText(bApprove ? “ymsg.massApprovalMessageToast” : “ymsg.massRejectionMessageToast”);

}

sap.ui.require([“sap/m/MessageToast”], function(MessageToast) {

MessageToast.show(sSuccessMessage);

});

}

fnResolve();

}.bind(this);

if (aPOIds.length === 1) {

oModel.callFunction(sFunction, {

method: “POST”,

urlParameters: {

POId: aPOIds[0],

Note: sApprovalNote

},

success: fnOnSuccess,

error: fnOnError

});

} else {

for (var i = 0; i < aPOIds.length; i++) {

oModel.callFunction(sFunction, {

method: “POST”,

urlParameters: {

POId: aPOIds[i],

Note: sApprovalNote

},

batchGroupId: “POMassApproval”,

changeSetId: i

});

}

oModel.submitChanges({

batchGroupId: “POMassApproval”,

success: fnOnSuccess,

error: fnOnError

});

}

}.bind(this));

}

Let’s discuss this in more detail. First, we’ll look at the signature. The promise object is used for approve and reject operations. Each of the operations has a dedicated function import in the OData service.
bApprove is used to indicate whether an approve or reject operation is triggered. The corresponding OData function imports are called /ApprovePurchaseOrder and /RejectPurchaseOrder.

sFunction = bApprove ? “/ApprovePurchaseOrder” : “/RejectPurchaseOrder”

The code above determines the function import to be used. Because functions require a list of purchase order IDs to be approved or rejected, the rest of this function is independent of the operation.

Since we need to call a service from the OData model and change the properties of the global model, both are requested for this function. As well as the list of purchase order IDs, the note the user entered in the popup dialog is also required.

Depending on whether the action is triggered on the detail screen or via swipe from the master list, we need to set different properties to adjust the screens. For example, if approval is triggered from detail, swipe on the master list should be locked:

if (bFromSwipe) {

// Note: Swipe approval is always single approval

this._mRunningSwipes[aPOIds[0]] = true;

oGlobalModel.setProperty(“/isSwipeRunning”, true);

} else {

oGlobalModel.setProperty(“/isBusyApproving”, true);

}

 

Swipe doesn’t block the list. Potentially, a user could swipe a different purchase order before the first service call returns. That’s why we need to count the number of pending calls and refresh the application by the time all services have returned – independent of whether they’re successful.

this._iOpenCallsCount++;

Then, we defined a function that will be called in case the service returns with an error and the state of the promise changes to rejected.

var fnOnError = function() {

this._callEnded(false, oGlobalModel);

fnReject();

}.bind(this);

The function _callEnded will be defined later. It basically does some housekeeping and cleanup. In a similar manner, we’ll define a function that will be called if the service call is successful, and the state of the promise changes to fulfilled.

var fnOnSuccess = function() {

this._callEnded(true, oGlobalModel);

if (this._iOpenCallsCount === 0) {

if (aPOIds.length === 1) {

sSupplier = oModel.getProperty(“/Z_C_Purchaseorder(‘” + aPOIds[0] + “‘)”).SupplierName;

sSuccessMessage = oResourceBundle.getText(bApprove ? “ymsg.approvalMessageToast” : “ymsg.rejectionMessageToast”, [sSupplier]);

} else {

sSuccessMessage = oResourceBundle.getText(bApprove ? “ymsg.massApprovalMessageToast” : “ymsg.massRejectionMessageToast”);

}

sap.ui.require([“sap/m/MessageToast”], function(MessageToast) {

MessageToast.show(sSuccessMessage);

});

}

fnResolve();

}.bind(this);

The function calls the function for housekeeping again. Once there are no calls pending (_iOpenCallsCound === 0), it’ll show a success message using a message toast. The message depends on the operation (approve or reject) and the number of processed purchase orders. In our single purchase order case, aPOIds will always be 1.

Finally, we can call the approve or reject function on the OData model. This calls a POST request to execute the business logic on the server and register the functions we defined above for the success and error use case.

oModel.callFunction(sFunction, {

method: “POST”,

urlParameters: {

POId: aPOIds[0],

Note: sApprovalNote

},

success: fnOnSuccess,

error: fnOnError

});

Since the promise object supports mass operations (aPOIds.length > 1), we can implement an optimized version using batch operations. A batch operation collects operations into a single request. This is more efficient than firing a single request for each purchase order.

for (var i = 0; i < aPOIds.length; i++) {

oModel.callFunction(sFunction, {

method: “POST”,

urlParameters: {

POId: aPOIds[i],

Note: sApprovalNote

},

batchGroupId: “POMassApproval”,

changeSetId: i

});

}

oModel.submitChanges({

batchGroupId: “POMassApproval”,

success: fnOnSuccess,

error: fnOnError

});

SubmitChanges is only necessary if the batch group is set to “deferred”. Setting a batch group to “deferred” provides the option to explicitly submit the request. For example, queue a number of batch requests into one big one.

It’s important to prevent double approvals. This could happen if an approval was started via a swipe operation, because it doesn’t block the app. We’ll therefore define a new function isSwipeApproving to check whether an approval is currently running that was started via a swipe operation.


File: model/Approver.js

approve: function(bApprove, bFromSwipe, oView, aPOIds, sApprovalNote) {

[…]

},

 

// Returns whether a swipe approval has been started for the

// specified PO since the last refresh

isSwipeApproving: function(sPOId) {

return !!this._mRunningSwipes[sPOId];

}

The double exclamation points “!!” ensure that either true or false is returned (otherwise it would be true or undefined)

Next, we’ll define the _callEnded function. It is called in the fnOnError and fnOnSuccess functions defined in the approve function above. The bSuccess parameter indicates whether it is called from fnOnSuccess (true) or fnOnError (false). It basically does some housekeeping. If there are no more approvals running, it changes the /isBusyApproving parameter from the global context. This removes the busy indicator from the detail screen. If at least one purchase order is successfully executed, it refreshes the master list.

File: model/Approver.js

 

isSwipeApproving: function(sPOId) {

[..]

},

 

// This method is called when a backend call for approve/reject has finished.

// bSuccess states whether the call was successful

// oGlobalModel is the global JSON model of the app

_callEnded: function(bSuccess, oGlobalModel) {

// Book-keeping:

this._iOpenCallsCount–;

this._bOneWaitingSuccess = bSuccess || this._bOneWaitingSuccess;

if (this._iOpenCallsCount === 0) { // When we are not waiting for another call

this._mRunningSwipes = {}; // start with a new round

oGlobalModel.setProperty(“/isSwipeRunning”, false);

oGlobalModel.setProperty(“/isBusyApproving”, false);

if (this._bOneWaitingSuccess) {

// At least one PO was approved/rejected successfully,

this._bOneWaitingSuccess = false;

// Note that the busy approving state is reset when

// the refresh is finished

}

}

}

 

Next, we need to define the text used in Approver.js

 


File: i18n/i18n.properties

#~~~ Approve and Reject Dialog ~~~~~~~~~~~~~~

#YMSG: MessageToast for successful approval

ymsg.approvalMessageToast=Purchase order for supplier {0} was approved

#YMSG: MessageToast for successful mass approval

ymsg.massApprovalMessageToast=Purchase orders were approved

#YMSG: MessageToast for successful rejection

ymsg.rejectionMessageToast=Purchase order for supplier {0} was rejected

#YMSG: MessageToast for successful mass rejection

ymsg.massRejectionMessageToast=Purchase orders were rejected

For each model, the BaseController class provides a getter for convenience. Although the approver is not a JSON or OData model, we can use the same mechanic to provide access to the approver to all view controllers.

In file BaseController.js, we’ll create a getter function:


File: controller/BaseController.js

getResourceBundle: function() {

[…]

},

 

/*

* Convenience method for getting the Approver

*/

getApprover: function() {

return this.getOwnerComponent().oApprover;

}

The getter function returns an approver instance from the component. We’ll create this now.


File: Component.js

sap.ui.define([

“sap/ui/core/UIComponent”,

“sap/ui/Device”,

“acme/purchaseorder/model/models”,

“acme/purchaseorder/controller/ListSelector”,

“acme/purchaseorder/controller/ErrorHandler”,

“acme/purchaseorder/model/Approver”

], function (UIComponent, Device, models, ListSelector, ErrorHandler, Approver ) {

“use strict”;

[…]

init : function () {

[…]

this._oErrorHandler = new ErrorHandler(this);

this.oApprover = new Approver(this.oListSelector);

UIComponent.prototype.init.apply(this, arguments);

[…]

}

Now, we can call the approve logic in the onConfirmAction event handler of our popup, replacing the temporary message toast:

File: controller/SubControllerForApproval.js

onConfirmAction: function() {

var sApprovalNote = this._oApprovalProperties.getProperty(“/approvalNote”),

aPOIds = this._aPurchaseOrders.map(function(mPO) {

return mPO.POId;

}),

oApprover = this._oParentView.getController().getOwnerComponent().oApprover,

oWhenAppovalIsDone = oApprover.approve(this._bApprove, false, this._oParentView, aPOIds, sApprovalNote);

oWhenAppovalIsDone.then(this._fnApproveActionFinished.bind(null, true), this._fnApproveFailed);

this._fnApproveActionFinished(true);

this._cleanup();

MessageToast.show(this._oResourceBundle.getText(“xbut.buttonOK”));/span>

},

 

And remove the message toast in the cancel event handler accordingly

File: controller/SubControllerForApproval.js

onCancelAction: function() {

this._fnApproveActionFinished(false);

this._cleanup();

MessageToast.show(this._oResourceBundle.getText(“xbut.buttonCancel”));

}

Finally, we also need to invoke the approve logic from the master list upon swipe. As previously mentioned, swipe means fast approval, so no popup and no rejection are possible.

File: controller/Master.controller.js

onSwipeApprove: function(oEvent){

var aPOIds = [getIdForItem(this._oList.getSwipedItem())];

this.getApprover.approve(true, true, this.getView(), aPOIds, “”);

this._oList.swipeOut();

MessageToast.show(this.getResourceBundle().getText(“xbut.approve”));

}

Since we’ll need this functionality also in the next chapter, let’s define this in a utility function in the master controller class.

File: controller/Master.controller.js

], function(BaseController, JSONModel, Filter, FilterOperator, GroupHeaderListItem, Device, formatter, grouper, GroupSortState,

MessageToast) {

“use strict”;

 

function getIdForItem(oListItem) {

return oListItem.getBindingContext().getProperty(“POId”);

}

 

return BaseController.extend(“acme.purchaseorder.controller.Master”, {

At this point, we don’t use the message toast anymore. You may remove the usage from master and detail controller now.
Let’s run the app now. You should be able to trigger an approval using swipe from the master list and approve / reject from the detail screen using the popup. But once the new approver logic is involved, you’ll see something like this:


A short look at the browser’s debugging tools console reveals the reason for this:

The mock server can intercept HTTP calls and provide fake output to the client without involving any backend system, but the behavior of special backend logic, such as that performed by function imports, isn’t automatically known to the mock server. We’ll cover this in the next chapter.

I hope the seventh part of the tutorial has sparked your interest in the chapters to come. Next time, we’ll mimic the backend logic.

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply