Skip to Content
Author's profile photo James Vernon Edenstrom

Tutorial: Build Your Own SAP Fiori Approve Purchase Order App – Part 11

This is the eleventh 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 chapter,  we encapsulated approve / reject service calls.

In this eighth chapter, we mimicked the backend logic.

In this ninth chapter, we refreshed the master and detail screen.

In this tenth chapter, we implemented code to block the screen for further input, for example, to prevent a user from approving the same purchase order twice, and we also created an extension point.

In this eleventh chapter, we’re going to implement the OData service used for the purchase approval app.

Implement the OData service
This tutorial keeps the backend implementation efforts as low as possible on the one hand, but tries to keep the coding as self-contained as possible. As a result, this coding shows the basic principles. Accept / reject is implemented in a very straight forward manner. For more complex business logic, please refer to the ABAP programming model for SAP Fiori.

Note.png

Reuse of OData Service

Please keep in mind that there is a dedicated OData service required for each SAP Fiori app. It is not allowed to reuse an OData service across several apps. However, reuse is allowed in the implementation. For example, you can encapsulate functionality to be reused across several apps in ABAP classes, BOPF Objects, CDS views, and so on.
Compatibility

Once an OData service is delivered, you need to ensure its compatibility. That is, you need to ensure that any change to a delivered OData service in the backend does not require an update of the Fiori app in the frontend or vice versa.

Basically, we’ll cover the following steps for our service:

  1. Connect the frontend to the backend.
  2. Create an empty OData service. This service will be extended in the steps that follow.
  3. Create CDS views for the purchase order and purchase order items and map them to the new OData service.
  4. Implement approval and reject functionality as part of our OData service.

Create an empty OData service
First, we want create an empty Gateway project to host our new OData service.

  1. Start the Gateway Service Builder via transaction SEGW on your ABAP server.
  2. Create a new project via Project –> Create.
  3. Enter the name of your project which is also used as name for our OData service. Since we don’t want to transport the service, click Local Object.
  4. Generate the project via Project –> Generate. A number of artifacts are generated.
    Depending on the namespace of the project, a number of popups will appear that need to be confirmed.

Create CDS views
Next, we’ll create the required CDS views for purchase orders and purchase order items. Both will select the data from the existing Enterprise Procurement Model (EPM).

Note.png

CDS Exposure

There are basically two options how to expose our two CDS views in the OData service: referenced and mapped data sources (RDS/MDS). We will use RDS because this is the most straight forward approach and the OData model is automatically generated from the CDS view definition. As side effect, the entity set names will be derived from the CDS view names. If you use other names than the ones in this tutorial, you either have to use the mapped data source approach which is not covered in this tutorial or adapt our frontend coding to your CDS view names.

  1. Launch the ABAP development tools
  2. Create a new project using File –> New –> ABAP Project –> select your backend system.

Note.png
In order to have access not only to the CDS views but also to generated ABAP classes, you should add the package with which you are working under Favorites in ABAP Development Tools. In the Project Explorer on the left hand side, right click Favorite Packages and add your package. In case you are working with $TMP as this tutorial does, select local objects of User and enter your user name.

CDS view for purchase order

Next, we’ll create a new DDL source. A DDL source represents an ABAP development object used to define an ABAP CDS entity.

1. To create a new DDL source, select File –> New –> Other. Expand the ABAP node and select DDL Source.

2. A wizard appears. Enter the package name that you used in the
Gateway Service Builder ($TMP). Enter the name of the CDS view (Z_C_PURCHASEORDER) and provide a description.

3. Finally, we’ll paste the following CDS DDL in our view:

CodeExample.png

@AbapCatalog.sqlViewName: ‘ZCPURCHASEORDER’

@AbapCatalog.compiler.compareFilter: true

@AccessControl.authorizationCheck: #CHECK

@EndUserText.label: ‘Purchase Order’

define view Z_C_Purchaseorder

as select from           SEPM_I_PurchaseOrder as PurchaseOrder

— the target currency for conversion of amounts is the employee’s company currency

left outer to one join SEPM_I_Employee      as CurrentUser on CurrentUser.SystemUser = $session.user

association [0..*] to Z_C_Purchaseorderitem as _PurchaseOrderItems on _PurchaseOrderItems.POId = $projection.POId

{

key PurchaseOrder.PurchaseOrder                                                                                                                      as POId,

 

@EndUserText.label: ‘Gross Amount’

@Semantics:   { amount.currencyCode: ‘CurrencyCode’ }

currency_conversion(

amount              => PurchaseOrder.GrossAmountInTransacCurrency,

source_currency     => PurchaseOrder.TransactionCurrency,

target_currency     => CurrentUser._Company.CompanyCurrency,

exchange_rate_date  => cast(left(cast(tstmp_current_utctimestamp() as abap.char(23)), 8) as abap.dats),

error_handling      => ‘SET_TO_NULL’ )                                                                                                         as GrossAmount,

 

@Semantics:   { currencyCode: true }

CurrentUser._Company.CompanyCurrency                                                                                                             as CurrencyCode,

 

count( distinct PurchaseOrder._Item.PurchaseOrderItem )                                                                                          as ItemCount,

 

PurchaseOrder._Supplier.CompanyName                                                                                                              as SupplierName,

 

@EndUserText.label: ‘Ordered By’

concat_with_space(PurchaseOrder._CreatedByUser.FirstName, PurchaseOrder._CreatedByUser.LastName, 1)                                              as OrderedByName,

 

— the cast statement makes the property map to the appropriate EDM type during OData exposure

@EndUserText.label: ‘Delivery Date’

cast(min(PurchaseOrder._Item._ScheduleLine.DeliveryDateTime) as timestampl preserving type )                                                     as DeliveryDateEarliest,

count( distinct PurchaseOrder._Item._ScheduleLine.DeliveryDateTime )                                                                             as LaterDelivDateExist,

 

@EndUserText.label: ‘Changed at’

PurchaseOrder.LastChangedDateTime                                                                                                                as ChangedAt,

 

@EndUserText.label: ‘Delivered To’

concat

(

concat_with_space(CurrentUser._Company._Address.StreetName, CurrentUser._Company._Address.HouseNumber, 1),

concat_with_space(‘,’,

concat(

concat_with_space(CurrentUser._Company._Address.PostalCode, CurrentUser._Company._Address.CityName, 1), concat_with_space(‘,’, CurrentUser._Company._Address.Country, 1)

), 1))                                                                                                                                       as DeliveryAddress,

 

/* Associations */

_PurchaseOrderItems

 

}

where

PurchaseOrder.PurchaseOrderOverallStatus = #D_PO_OA.’P’ — ‘P’ = ‘Awaiting Approval’

 

group by

PurchaseOrder.PurchaseOrder,

PurchaseOrder.LastChangedDateTime,

PurchaseOrder.GrossAmountInTransacCurrency,

PurchaseOrder.TransactionCurrency,

CurrentUser._Company.CompanyCurrency,

PurchaseOrder._Supplier.CompanyName,

PurchaseOrder._CreatedByUser.FirstName,

PurchaseOrder._CreatedByUser.LastName,

CurrentUser._Company._Address.StreetName,

CurrentUser._Company._Address.HouseNumber,

CurrentUser._Company._Address.PostalCode,

CurrentUser._Company._Address.CityName,

CurrentUser._Company._Address.Country

Do not generate the CDS view since it contains an association to a currently undefined purchase order item view.

There are a few things worth mentioning:

  • Each CDS view has a corresponding ABAP SQL view (@AbapCatalog.sqlViewName annotation). If these names are already taken, please use different ones. For each CDS view, provide a semantic name using the @EndUserText.label

CodeExample.png

@AbapCatalog.sqlViewName: ‘ZCPURCHASEORDER’

@AbapCatalog.compiler.compareFilter: true

@AccessControl.authorizationCheck: #CHECK

@EndUserText.label: ‘Purchase Order’

define view Z_C_Purchaseorder

  • The view selects its data from the underlying EPM view SEPM_I_PurchaseOrder. As part of the realized functionality, amounts are formatted to the user’s currency and the delivery address is taken from the address of the end user. This information is assumed to be stored in table SEPM_I_EMPLOYEE per session user. Therefore, a left outer join is used to combine the information in this view. Since this is usually not the case, a defaulting mechanism will be implemented later.

CodeExample.png

define view Z_C_Purchaseorder

as select from           SEPM_I_PurchaseOrder as PurchaseOrder

left outer to one join SEPM_I_Employee      as CurrentUser on Curren-tUser.SystemUser = $session.user

  • For currency conversion, a central conversion function is called. A label is defined using the label annotation. This semantic information is used to render the data on the UI. In our case, the label text will be reused and there is no need to specify a new label text.

CodeExample.png

@EndUserText.label: ‘Gross Amount’

@Semantics:   { amount.currencyCode: ‘CurrencyCode’ }

currency_conversion(

amount              => PurchaseOrder.GrossAmountInTransacCurrency,

source_currency     => PurchaseOrder.TransactionCurrency,

target_currency     => CurrentUser._Company.CompanyCurrency,

exchange_rate_date  => cast(left(cast(tstmp_current_utctimestamp() as abap.char(23)), 8) as abap.dats),

error_handling      => ‘SET_TO_NULL’ )                                                                                                         as GrossAmount,

  • Each purchase order item has its own deliver date. In the case of a single delivery date, the frontend should print this date. In the case of different delivery dates, the earliest one should be used and “and later” should be added. This formatting will be done on the frontend, but the backend needs to provide all information. The DeliveryDateEarliest field contains the earliest delivery date of all items. The LaterDelivDateExist field contains the number of different dates.

CodeExample.png

@EndUserText.label: ‘Delivery Date’

cast(min(PurchaseOrder._Item._ScheduleLine.DeliveryDateTime) as timestampl preserving type )                                                     as Deliv-eryDateEarliest,

count( distinct PurchaseOrder._Item._ScheduleLine.DeliveryDateTime )                                                                             as LaterDelivDateExist,

  • Finally, we’ll render the shipment address based on the company address of the current user. This is a very minimalistic implementation that just concatenates the different fields of the address into a single field DeliverAddress.CodeExample.png

@EndUserText.label: ‘Delivered To’

concat

(

concat_with_space(CurrentUser._Company._Address.StreetName, CurrentUser._Company._Address.HouseNumber, 1),

concat_with_space(‘,’,

concat(

concat_with_space(CurrentUser._Company._Address.PostalCode, CurrentUser._Company._Address.CityName, 1), concat_with_space(‘,’, CurrentUser._Company._Address.Country, 1)

), 1))

CDS view for purchase order items

In the same manner as the purchase order view, we’ll create a purchase order item view. As you can see, the view uses the same mechanism:

CodeExample.png

@AbapCatalog.sqlViewName: ‘ZCPOITEM’

@AbapCatalog.compiler.compareFilter: true

@AccessControl.authorizationCheck: #CHECK

@EndUserText.label: ‘Purchase Order Item’

define view Z_C_Purchaseorderitem

as select from           SEPM_I_PurchaseOrderItem as PurchaseOrderItem

— the target currency for conversion of amounts is the employee’s company currency

left outer to one join SEPM_I_Employee          as CurrentUser on CurrentUser.SystemUser = $session.user

{

key PurchaseOrderItem._PurchaseOrder.PurchaseOrder                                           as POId,

key PurchaseOrderItem.PurchaseOrderItem                                                      as POItemPos,

 

@EndUserText.label: ‘Gross Amount’

@Semantics:   { amount.currencyCode: ‘GrossAmountCurrency’ }

currency_conversion(

amount              => PurchaseOrderItem.GrossAmountInTransacCurrency,

source_currency     => PurchaseOrderItem.TransactionCurrency,

target_currency     => CurrentUser._Company.CompanyCurrency,

exchange_rate_date  => cast(left(cast(tstmp_current_utctimestamp() as abap.char(23)), 8) as abap.dats),

error_handling      => ‘SET_TO_NULL’

)                                                                                        as GrossAmount,

 

@Semantics:   { currencyCode: true }

CurrentUser._Company.CompanyCurrency                                                     as GrossAmountCurrency,

 

@EndUserText.label: ‘Quantity’

@Semantics:   { quantity.unitOfMeasure: ‘QuantityUnit’ }

PurchaseOrderItem._ScheduleLine.Quantity,

 

@Semantics:   { unitOfMeasure: true }

PurchaseOrderItem._ScheduleLine.QuantityUnit,

 

@EndUserText.label: ‘Delivery Time’

PurchaseOrderItem._ScheduleLine.DeliveryDateTime                                         as DeliveryDate,

 

@EndUserText.label: ‘Price’

@Semantics:   { amount.currencyCode: ‘PriceCurrency’ }

currency_conversion(

amount              => PurchaseOrderItem._Product.Price,

source_currency     => PurchaseOrderItem._Product.Currency,

target_currency     => CurrentUser._Company.CompanyCurrency,

exchange_rate_date  => cast(left(cast(tstmp_current_utctimestamp() as abap.char(23)), 8) as abap.dats),

error_handling      => ‘SET_TO_NULL’

)                                                                                        as Price,

 

@Semantics:   { currencyCode: true }

CurrentUser._Company.CompanyCurrency                                                     as PriceCurrency,

 

— try to get the product name in the current language, fall back to English

@EndUserText.label: ‘Product Name’

coalesce(

PurchaseOrderItem._Product._NameGroup._ShortText[ 1: Language = $session.system_language ].ShortText,

PurchaseOrderItem._Product._NameGroup._ShortText[1: Language = ‘E’].ShortText)         as Product

}

where

PurchaseOrderItem._PurchaseOrder.PurchaseOrderOverallStatus = #D_PO_OA.’P’ — ‘P’ = ‘Awaiting Approval’         Step 3: Implement Approval and Reject functionality

Activate and test CDS views

Now that both views are defined, we’ll activate them using ABAP Development Tools by clicking Activate.

Once done, we’ll test our views by choosing Run from the toolbar. Note though that since your user is probably not registered in view SEPM_I_EMPLOYEE, you won’t see data in the currency and address fields.

Expose CDS views in the OData service

Next, we’ll expose both CDS views as referenced data source in our OData service.

1. In SAP GUI, start the Gateway Service Builder using transaction SEGW.
2. Right click the Data Model node of your project node.
3. From the context menu select Reference –> Data Source.

4. Enter the name of the purchase order CDS view.

5. Also make sure to include the purchase order item view.

6. Check the consistency of your project and generate the runtime artifacts.
I hope the eleventh part of the tutorial has sparked your interest in the chapters to come. Next time, we’ll register current user as EPM user.

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Sujin Appukuttan
      Sujin Appukuttan

      Hi Jim,

      That was awesome. Thank you. Since SAP has introduced Annotations for OData Version 2.0, which approach do you think shall best fit the guidelines. The Fiori App using UI5 manual code (View.xml/Controller.js) or Fiori App using the CDS with Annotations along with BOPF (for transactional) ??

      Do you plan to blog about Fiori App using CDS with Transactional Annotations in future? Would be great to see that 🙂

      Author's profile photo James Vernon Edenstrom
      James Vernon Edenstrom
      Blog Post Author

      Glad you like the tutorial, Sujin ?
      As to your question, there are basically two options about how to build an SAP Fiori app: the one that is showcased in this tutorial uses app templates and JavaScript code, and, secondly, Fiori Elements/Smart Templates. The latter is purely annotation driven, and the Object Page template includes support for transactional annotations. Basically, it’s up to the required UX design as to what approach you choose. It also however depends on your system landscape and the patch level of your backend systems since core data services (CDS) require a high 7.40 support package or a NW 7.50 or higher system.

      The following two resources might be useful to you: