Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
Showing results for 
Search instead for 
Did you mean: 
Active Participant
What a trip this case has been. The ABAP RESTful Programming model isn't exactly new, and yet in my reality it hasn't been used much. And suddenly I saw myself facing the need for an unmanaged scenario.

First off, this is what you will find here: every single step, from CDS data definitions, to behavior definitions with implementation, and finally to an OData service which can query, read, filter, and even expand/navigate to child entries (NavigationProperty as it is called in the metadata document of OData).

Please note: I do not claim to be an expert but thought this blog post and guide could be of help to anyone needing to solve a similar scenario. I very happily receive feedback and tips on things that maybe could have been done better.

Let's start.

The Case

The company is using CATS today for time reporting. Additionally, there is an integration to JIRA, whereas a JIRA case creates a service order in SAP, which can be used for time reporting. The integration moves the times reported in JIRA into CATS and onto the corresponding service order.

The company is now moving to S/4HANA 2022 and needs a better solution for subcontractors. Subcontractors today get access to JIRA which is not desirable. Instead, we will offer them a simple web app for time reporting. Because of this, we need an OData service for Service Orders which can do the following:

  • Create a new service order (replacing the old integration in JIRA)

  • Update service orders (mostly for new status, closing the order when the JIRA case is resolved etc.)

  • Read/query service orders for a certain user ("my orders I'm allowed to report time on"): of course we only want the correct type of orders

    • For a future, possible web app for employees, we also want to include Project Networks in this query

    • In this query read, we also need to include all hours that have so far been reported on an order (or network) as a sum

  • Read all time reporting entries of a an order/network

  • Create, update or delete a certain time entry


Trying to use as much SAP Standard as possible, we decided to use the OData service of the SAP Standard app "My Timesheets (Version 3)" (F3074) for anything related to creating, editing or deleting time entries. Unfortunately there is no Standard app handling service orders, and even less so, one that could handle both service orders and PS networks "as one object for time reporting", as this case demands. That's why we come to the unmanaged scenario: we need to let JIRA create and update service orders via BAPI_ALM_ORDER_MAINTAIN. Legacy code for this exists and will be reused; a Z-function module which eventually calls the mentioned BAPI (I will not go into detail here, how that function works). However, right of the bat I was a little worried; yes, I want to control create and update, but read and query I want to work as usual and when I started this, I did not know how this is solved. We'll get to that :)...

CDS Data Definitions

We need two CDS; one for the time reporting service order or PS network and one for time entries, which have been booked against them. So we also immediately see the need for a parent-child relationship.

Time Reporting Object CDS (Parent)

(Please note: I will not include every single line of code here but the most important ones)
define root view entity ZI_TimeReportingObject
as select from I_LogisticsOrder as TRO
composition [0..*] of ZP_TimeReportingObjectEntries as _TimeEntries
association [0..*] to I_WBSElement as _WBSElement
on $projection.WBSElementID = _WBSElement.WBSElementInternalID
association [0..*] to I_CostCenter as _CostCenter
on $projection.ControllingArea = _CostCenter.ControllingArea
and $projection.ResponsibleCostCenter = _CostCenter.CostCenter
key TRO.OrderID,


// Admin
@Semantics.systemDate.createdAt: true
// ETC.....

// Assignments
TRO.ObjectInternalID as ObjectNumber,
@Consumption.valueHelpDefinition: [ { entity: {
name: 'I_WBSElementBasicDataStdVH', element: 'WBSElementInternalID'
} } ]
@ObjectModel.foreignKey.association: '_WBSElement'
TRO.WBSElementInternalID_2 as WBSElementID,
when TRO.OrderCategory = '20' then 'X'
else ''
end as boole_d
) as IsNetwork,
// Responsible
@Consumption.valueHelpDefinition: [ { entity: { name: 'I_CostCenterStdVH', element: 'CostCenter' } } ]
@ObjectModel.foreignKey.association: '_CostCenter'
_WBSElement.ResponsiblePerson as WBSResponsiblePerson,

// Check status of order/network to see if it's allowed to report times
@ObjectModel.virtualElement: true
@ObjectModel.virtualElementCalculatedBy: 'ZCL_HCM_VIRT_ELEM'
@EndUserText.label: 'Time Reporting Allowed'
cast('' as zallowed_e ) as IsAllowedForReporting,

// Total hours reported
@ObjectModel.virtualElement: true
@ObjectModel.virtualElementCalculatedBy: 'ZCL_HCM_VIRT_ELEM'
@EndUserText.label: 'Total Reported Hours'
cast(0 as abap.dec(6,2) ) as TotalReportHours,

// FI/CO
@ObjectModel.foreignKey.association: '_ControllingArea'

// Associations
TRO.OrderType = 'SM01'
or TRO.OrderType = 'SM02'
or TRO.OrderType = 'PS02'
and TRO.IsMarkedForDeletion = '';

Few things I want to especially point out:

  • I use "define root view entity" instead of just "define view" which is the more modern version! Use the newest!

  • I use standard CDS I_LogisticsOrder to once again make use of what has already been done for me, instead of creating my own with join over AUFK and AFKO etc.

  • Parent-child relationship starts here with the composition [0..*] of command, telling that the associated CDS isn't just a regular association, but is the child of the current CDS

  • The rest is quiet regular CDS, but a few additional remarks:

    • I made a simple boolean field to distinguish between orders and networks (IsNetwork)

    • I have two virtual elements to add some extra ABAP logic:

      • Reading the status of the order/network to discern if the order/network is allowed to receive any time reporting at all

      • Calculating the sum of all reported hours

    • Lastly, in my where-clause I restrict the orders/network OrderTypes, which are the ones we need for time reporting in CATS (and of course, we are not interested in deletion-marked objects)

Time Entries (on Time Reporting Objects) (Child)

(Please note: I will not include every single line of code here but the most important ones)
define view entity ZP_TimeReportingObjectEntries 
as select from catsdb
association to parent ZI_TimeReportingObject as _TRObject
on $projection.TRObjectID = _TRObject.OrderID
key counter,
raufnr as ReceiverOrderID,
rnplnr as NetworkID,
when raufnr = '' then rnplnr
else raufnr
end as TRObjectID,

// reporting info
skostl as CostCenter,
workdate as WorkDate,
lstar as ActivityType,
awart as AttAbsType,
ltxa1 as ShortDesciption,

// status
status as Status,

// time
@DefaultAggregation: #SUM
@Semantics.quantity.unitOfMeasure: 'ReportUnit'
catshours as ReportHours,
meinh as ReportUnit,

// Associations

Very simple CDS, here a few remarks:

  • I called the view ZP_* for private view because this child view is not meant to be used on its own

  • Once again, "define view entity" (not root this time) instead of "define view"

  • Parent-child relationship on child entity: with command association to parent

  • I created my own field TRObjectID which receives the order number or network id of the time reporting entry, to have a single field as ID to the parent (I was worried this could cause an issue since the "parent key" isn't part of the "child key" but it works like this, which was nice to find out)

  • Rest is regular CDS

CDS Data Projections

Next, because we are doing RAP, we of course need projections.

Time Reporting Object Projection (Parent)

define root view entity ZC_TimeReportingObject
provider contract transactional_query
as projection on ZI_TimeReportingObject
key OrderID,
// all other fields...

/* Associations */
_TimeEntries : redirected to composition child ZC_TimeReportingObEntries,

I left out the listing of fields but most important in this one:

  • We also need to use the same on the projection: "define root view entity"

  • The line "provider contract transactional_query": I am honestly not sure why it is needed - maybe someone can comment below and explain - but I added this line because ADT gave me a warning or an error when it wasn't there

  • Lastly, in order for OData to add a navigation property for the parent-child, we have to use the "redirect to composition child" command (entering the CDS data definition projection view name), similar as we defined the composition in our data definition

Time Entries (on Time Reporting Object) Projection (Child)

define view entity ZC_TimeReportingObEntries 
as projection on ZP_TimeReportingObjectEntries
key counter,
// all fields...
/* Associations */
_TRObject : redirected to parent ZC_TimeReportingObject

Even simpler as the other one, with just two remarks:

  • Using "define view entity", same as data definition

  • Adding navigation property in OData to our parent via "redirected to parent" command (giving the CDS data definition projection view name)

So far not so hard. Let's talk about behavior next.

CDS Behavior Definitions

Now comes the fun part. The behavior definitions themselves aren't all that complicated, but remember we have an unmanaged scenario and need to handle the BOs lifecycle methods. Let's get started.

First of, I only have one behavior definition for the complete "composition tree", from root/parent to child. This one behavior definition handles both, my parent and my child:
unmanaged implementation in class zbp_i_timereportingobject unique;
strict ( 2 );

define behavior for ZI_TimeReportingObject alias TRObject
etag master LastChangeDate

field(mandatory) OrderID, OrderCategory, OrderType, OrderDescription;
field(readonly) IsAllowedForReporting, TotalReportHours;

association _TimeEntries { }

define behavior for ZP_TimeReportingObjectEntries alias TREntry
//implementation in class zbp_c_timereportingobjectentries unique
field ( readonly ) TRObjectID, counter;

association _TRObject { }

The class name zbp_i_timereportingobject was suggested by ADT and I didn't change it. But basically it stands for z-behavior pool and then the name of the CDS data definition, without the z-namespace. I didn't choose a different class for the child entry (it's commented away, but possible) so everything is one class for me.

You may noticed that I'm not using the "persistent table" statement and no mapping to said table. The reason for that is simply that one table is not enough. Service orders are spread over several tables. Secondly, as said before, we want to create and update via a BAPI.
The time entries come only from CATSDB, so there I could have done the table mapping. But, as explained before, we will use the SAP Standard OData service for time entries, and all I want is to READ; no modify at all.

I created the class via right-click in ADT and quick fix, after activating this behavior definition and it ending in an error, that a class with that name does not exist. I especially mention this because if you do it in this way, ADT will pre-create the whole shell of your behavior pool, including all methods needed. So highly recommended. Let's look at the class next.

Behavior Definition Implementation (Behavior Pool Class)

Global Class (generated by ADT)
class zbp_i_timereportingobject definition public abstract final 
for behavior of zi_timereportingobject.

class zbp_i_timereportingobject implementation.

The CDS name here refers to the Behavior Definition name above.

We have 2 local class types, because we decided to handle parent and child in the same behavior definition. Let's start with the parent, alias TRObject (from behavior definition, highly recommended to use):
class lhc_TRObject definition inheriting from cl_abap_behavior_handler.
private section.
methods create for modify
importing entities for create TRObject.

methods update for modify
importing entities for update TRObject.

methods read for read
importing keys for read TRObject result result.

methods rba_TimeEntries for read
importing keys_rba for read TRObject\_TimeEntries full result_requested result result link association_links.


Depending on what is defined in the unmanaged behavior definition, we need the corresponding methods. In our case we have:

  • create, update, read (quite self-explanatory)

  • rba_TimeEntries for read: rba stands for "ready by association": this is the navigation between child and parent, which, in an unmanaged scenario, we have to implement manually. The name _TimeEntries is the name of the association in the behavior definition

This is the class shell, now let's look at the implementation.
class lhc_TRObject implementation.

method create.
data ls_msg type symsg.

loop at entities assigning field-symbol(<ls_trobject>).
clear ls_msg.
call function 'Z_LEGACY_FUNCTION'
is_data_in = <ls_trobject>
es_return = ls_msg.
if ls_msg is initial.
mapped-TRObject = value #( base mapped-TRObject
( %cid = <ls_trobject>-%cid
OrderID = <ls_trobject>-OrderID
) ).
append value #( %cid = <ls_trobject>-%cid
OrderID = <ls_trobject>-OrderID )
to failed-TRObject.

append value #( %msg = new_message( id = ls_msg-msgid
number = ls_msg-msgno
v1 = ls_msg-msgv1
v2 = ls_msg-msgv2
v3 = ls_msg-msgv3
v4 = ls_msg-msgv4
severity = if_abap_behv_message=>severity-error )
%key-OrderID = <ls_trobject>-OrderID
%cid = <ls_trobject>-%cid
%create = 'X'
OrderID = <ls_trobject>-OrderID )
to reported-TRObject.


method update.
data ls_msg type symsg.

loop at entities assigning field-symbol(<ls_trobject>).
clear ls_msg.
call function 'Z_LEGACY_FUNCTION'
is_data_in = <ls_trobject>
es_return = ls_msg.
if ls_msg is not initial.
append value #( %cid = <ls_trobject>-%cid_ref
OrderID = <ls_trobject>-OrderID )
to failed-TRObject.

append value #( %msg = new_message( id = ls_msg-msgid
number = ls_msg-msgno
v1 = ls_msg-msgv1
v2 = ls_msg-msgv2
v3 = ls_msg-msgv3
v4 = ls_msg-msgv4
severity = if_abap_behv_message=>severity-error )
%key-OrderID = <ls_trobject>-OrderID
%cid = <ls_trobject>-%cid_ref
%update = 'X'
OrderID = <ls_trobject>-OrderID )
to reported-TRObject.


method read.

select * from ZC_TimeReportingObject
for all entries in @keys
where OrderID = @keys-OrderID
into corresponding fields of table @result.


method rba_TimeEntries.
data: lt_time_entries type table of ZP_TimeReportingObjectEntries,
ls_result like line of result.

loop at keys_rba assigning field-symbol(<ls_key>).
" get time entries
select * from ZP_TimeReportingObjectEntries
into table @lt_time_entries
where TRObjectID = @<ls_key>-OrderID.
if sy-subrc = 0.
loop at lt_time_entries assigning field-symbol(<time_entry>).
value #(
source-%tky = <ls_key>-%tky
target-%tky = value #(
counter = <time_entry>-counter
) )
into table association_links.

if result_requested = abap_true.
"ls_time_entry = CORRESPONDING #( <time_entry> MAPPING TO ENTITY ).
move-corresponding <time_entry> to ls_result.
insert ls_result into table result.


sort association_links by source ascending.
delete adjacent duplicates from association_links comparing all fields.

sort result by %tky ascending.
delete adjacent duplicates from result comparing all fields.



I'm not going into absolute detail here, but basically we use a combination of ABAP and EML:

  • Both create and update methods simply loop over the imported table entities and then call the legacy function. The function will return an error message if anything went wrong, in which case we add the entity that failed into the failed object (append value #.... to failed-TRObject). We also send back the error message into the reported object (append value #.... to reported-TRObject). If everything goes well, only on create, the newly created Order is added into the mapped object (mapped-TRObject = value #(...)). Honestly, I copied this from a guide that I followed and I'm not sure why this step is needed. My own interpretation is that the order id would be freshly generated, and to return this new order id, we do this step, and that's why it's only in create and not update. But maybe someone can explain this better in the comments.

  • In the read method, I simply and directly read from the CDS view. I was not sure if this would work, because again, I wanted all read and query operations to work as if it was managed, so I wasn't sure what to do here. This seems to work though, but once again, if someone can add some elaboration here, whether my code is fine or should be changed, that would be great!

  • Lastly we have the implementation of the "read by association" where once again I read directly from the corresponding CDS view and fill both the association_links and result returning parameters. When I test my OData service this works, but if anyone has any comments here, go ahead!

Next, because we have "modifying" methods and we have an unmanaged scenario, a behavior pool for saving is added:
class lsc_ZC_TIMEREPORTINGOBJECT definition 
inheriting from cl_abap_behavior_saver.
protected section.

methods finalize redefinition.

methods check_before_save redefinition.

methods save redefinition.

methods cleanup redefinition.

methods cleanup_finalize redefinition.


Since in our case all the logic is already handled in the legacy function. I didn't need to implement anything additional here, so basically no implementation.

Lastly we have the implementation on the child entity. We only have read and "read by association". I will simply paste the code here, but not further explain it, as it's already been covered with the parent and is done in the exact same way (direct read from CDS):
class lhc_TREntry definition inheriting from cl_abap_behavior_handler.
private section.

methods read for read
importing keys for read TREntry result result.

methods rba_TRObject for read
importing keys_rba for read TREntry\_TRObject full result_requested result result link association_links.



CDS Behavior Projection

Next comes the projection of the behavior. Because I only have 1 behavior definition, I also only have 1 projection:
strict ( 2 );

define behavior for ZC_TimeReportingObject alias TRObject
use create;
use update;
use association _TimeEntries { }

define behavior for ZC_TimeReportingObEntries alias TREntry
use association _TRObject { }

The names after "define behavior" are the names of the CDS data definition projection!

Be sure to include the associations here, or the parent-child relationship won't be transferred to the OData service.

OData Service

Last but not least we create the OData service. One thing that bugs me when working with RAP, are the loads of steps to take before we can even test our code. The CDS itself can be tested well enough with the Data Preview, but it already fails for example Virtual Elements. I wish that SAP could add a way to test things earlier. Simulate a service, as if there were shadow projections (always managed in the case of testing of course) that simply include everything. Just a little side note from me.

Service Definition

define service ZUI_TimeReportingObject {
expose ZC_TimeReportingObject;
expose ZC_TimeReportingObEntries;

Simply exposing the CDS projections. I chose ZUI_* in the name to create an OData service for Fiori.

Service Binding

I created service binding ZUI_V4_TIMEREPORTINGOBJECT with binding type OData V4 - UI. Since this is an OData V4, I published the Service via transaction /IWFND/V4_ADMIN (see note 3101976).

Result Example

Once my OData service was ready, I of couse ran my tests and for example this OData call works as intended:

"@odata.context" : "$metadata#ZC_TimeReportingObject(_TimeEntries())/$entity",
"@odata.metadataEtag" : "W/\"20230302113047\"",
"OrderID" : "4000500",
"OrderCategory" : "30",
"OrderType" : "SM01",
"OrderDescription" : "Regular service order test",
"IsMarkedForDeletion" : false,
"ProjectID" : "0",
"WBSElementID" : "0",
"IsNetwork" : false,
"ResponsibleCostCenter" : "17101321",
"WBSResponsiblePerson" : "0",
"ResponsibleUser" : "FSM_TECH1",
"IsAllowedForReporting" : true,
"TotalReportHours" : 8.00,
"ControllingArea" : "A000",
"__EntityControl" : {
"Updatable" : true
"SAP__Messages" : [

"_TimeEntries" : [
"counter" : "000000000642",
"ReceiverOrderID" : "4000500",
"NetworkID" : "",
"TRObjectID" : "000004000500",
"CostCenter" : "17101902",
"PersonnelID" : "59",
"WorkDate" : "2023-02-21",
"ActivityType" : "T002",
"AttAbsType" : "0800",
"ShortDesciption" : "",
"Status" : "20",
"ReportHours" : 8.00,
"ReportUnit" : "H"

(this is only test data of course)

Final Words

I hope this helps anyone needing to tackle unmanaged scenarios with RAP. Personally I wish there were a bit better documentation resources (such as the SAPUI5 SDK, which is an excellent example of prefect documentation). And no, SAP Help isn't it. It's fairly terrible with only ever giving half the information needed and hardly any example. I had to search far and wide to able to solve this, while, looking at it now, it doesn't look that complex. But if you don't know, nor find a good place to look for answers, you will be stuck in Google search mess.

One such source that I found that isn't from SAP I would like to link here: they have an amazing guide that almost got me all the way through: BO Behavior in RAP.

Have a good one and let me know if you wonder anything or have any tips yourself!
Labels in this area