Building an Unmanaged RAP Application with Draft enable on EKKO&EKPO using BAPIs
Share

[[{“value”:”

Introduction

we will build a draft-enabled unmanaged RAP application for Purchase Order processing using the standard SAP tables EKKO and EKPO.

The application supports Purchase Order header and item processing using RAP. Since EKKO and EKPO are standard SAP application tables, the application does not directly perform database updates. Instead, the RAP Behavior Pool collects the requested changes and delegates the actual business processing to standard Purchase Order BAPIs.

The main BAPIs used in this implementation are:

  • BAPI_PO_CREATE1 – Create a Purchase Order
  • BAPI_PO_CHANGE – Update or mark Purchase Order data for deletion

Application Architecture

The application follows the following RAP architecture:

Standard Tables
     │
     ├── EKKO (Purchase Order Header)
     │
     └── EKPO (Purchase Order Item)
              │
              ▼
        Interface / Composite CDS Views
              │
              ▼
          Projection Views
              │
              ▼
       Metadata Extensions
              │
              ▼
       Behavior Definitions
              │
              ▼
          Behavior Pool
              │
              ▼
       Service Definition
              │
              ▼
       Service Binding
              │
              ▼
          Fiori Elements UI

 

Step 1: Create the CDS Data Model

We need two CDS entities:

  • Purchase Order Header
  • Purchase Order Item

The header entity becomes the root entity, while the item entity becomes a composition child.

The relationship is:

Purchase Order Header
        │
        │ Composition
        ▼
Purchase Order Items

This is important because Purchase Order items belong to a Purchase Order header.

Create the Header CDS Entity:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: ‘Interface view for ekko table’
@Metadata.ignorePropagatedAnnotations: true
@VDM.viewType: #COMPOSITE
define root view entity zi_b_ekko
as select from ekko
composition [1..*] of zi_b_ekpo as _items
{
key ebeln, //po number
bsart, //po type
aedat, //created on
ernam, //vcreated by
lifnr, //vendor AC
ekorg, //purchasing organ
ekgrp, //purchase grp
bukrs, //company code
waers, //currency code
@Semantics.systemDateTime.localInstanceLastChangedAt: true
lastchangedatetime, //timestamp
_items
}

Why Is EKKO the Root Entity?

The Purchase Order header controls the lifecycle of the business object.

A Purchase Order item cannot logically exist without its Purchase Order header.

Therefore:

EKKO
 │
 └── owns
       │
       ▼
      EKPO

The item entity is modeled as a composition child.


Composition Versus Association

Composition

Composition represents ownership.

Header
  │
  └── owns → Items

The child belongs to the parent.

Association

Association represents a relationship without ownership.

Entity A
   │
   └── references → Entity B

Both entities can exist independently.

For Purchase Orders, composition is appropriate because EKPO items belong to EKKO.

Create the Item CDS Entity:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: ‘Interface view for ekpo table’
@Metadata.ignorePropagatedAnnotations: true
@VDM.viewType: #COMPOSITE
define view entity zi_b_ekpo
as select from ekpo
association to parent zi_b_ekko as _header on $projection.ebeln = _header.ebeln
{
key ebeln, // po number
key ebelp, // po item
matnr, //material
txz01, // short text
werks, // plant
lgort, //storage loc
meins, // base unit

@Semantics.quantity.unitOfMeasure: ‘meins’
menge, //order quantity

matkl, // material grp

_header
}

The item entity uses:

association to parent

This tells RAP that the item belongs to the Purchase Order header.

Step 2: Create the Projection Views

The projection layer controls what is exposed to the service and UI.

The root projection view is:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: ‘projection for ekko’
@Metadata.ignorePropagatedAnnotations: true
@Metadata.allowExtensions: true
@VDM.viewType: #CONSUMPTION
define root view entity zc_b_ekko
provider contract transactional_query
as projection on zi_b_ekko
{
key ebeln,
bsart,
aedat,
ernam,
lifnr,
ekorg,
ekgrp,
bukrs,
waers,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
lastchangedatetime,
_items :redirected to composition child zc_b_ekPo
}

The item projection is:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: ‘projection for ekPo’
@Metadata.ignorePropagatedAnnotations: true
@Metadata.allowExtensions: true
@VDM.viewType: #CONSUMPTION
define view entity zc_b_ekPo as projection on zi_b_ekpo
{
key ebeln,
key ebelp,
matnr,
txz01,
werks,
lgort,
meins,

@Semantics.quantity.unitOfMeasure: ‘meins’
menge,
matkl,

_header :redirected to parent zc_b_ekko
}

Step 3: Add UI Metadata

Metadata extensions define how the fields are displayed in the Fiori Elements application.

The header metadata controls:

  • List Report columns
  • Object Page fields
  • Filter fields
  • Header and item facets

The item metadata controls how Purchase Order items are displayed.

The important relationship is created using:

@UI.facet

The item facet points to:

_items

As a result, the Purchase Order Object Page can display:

Purchase Order
│
├── Header Information
│
└── Item Information  

 Header Metadata Extension is:

@Metadata.layer: #CORE
annotate entity zc_b_ekko
with
{
@UI.facet: [
{
id: ‘Collection’,
label: ‘Purchase Order’,
type: #COLLECTION,
position: 10
},
{
id: ‘Header’,
label: ‘Purchase Order Header’,
type: #IDENTIFICATION_REFERENCE,
parentId: ‘Collection’,
position: 10
},
{
id: ‘Item’,
label: ‘Purchase Order Item’,
type: #LINEITEM_REFERENCE,
targetElement: ‘_items’,
position: 20
}
]
@UI.lineItem : [ { position: 10 }, { label: ‘Puchase Number’ } ]
@UI.identification: [{ position: 10 }]
ebeln;

@UI.lineItem : [ { position: 20 } ]
@UI.identification: [{ position: 20 }]
bsart;

@UI.lineItem : [ { position: 30 } ]
@UI.identification: [{ position: 30 }]
@UI.selectionField: [{ position: 10 }]
@search.defaultSearchElement: true
aedat;

@UI.lineItem : [ { position: 40 } ]
@UI.identification: [{ position: 40 }]
@UI.selectionField: [{ position: 20 }]
ernam;

@UI.lineItem : [ { position: 50 } ]
@UI.identification: [{ position: 50 }]
lifnr;

@UI.lineItem : [ { position: 60 } ]
@UI.identification: [{ position: 60 }]
ekorg;

@UI.lineItem : [ { position: 70 } ]
@UI.identification: [{ position: 70 }]
ekgrp;

@UI.lineItem : [ { position: 80 } ]
@UI.identification: [{ position: 80 }]
bukrs;

@UI.lineItem : [ { position: 90 } ]
@UI.identification: [{ position: 90 }]
waers;

@UI.lineItem:[{ position: 100 }]
@UI.identification: [{ position: 100 }]
lastchangedatetime;

}

Item MetaData Extension is:

@Metadata.layer: #CORE
annotate entity zc_b_ekPo
with
{
@UI.facet : [
{ id: ‘Item’,
label: ‘Item Data’,
position: 10,
type: #IDENTIFICATION_REFERENCE
}
]

@UI.lineItem: [ { position: 20 } ]
@UI.identification: [{ position: 20 }]
ebelp;

@UI.lineItem: [ { position: 30 } ]
@UI.identification: [{ position: 30 }]
matnr;

@UI.lineItem: [ { position: 40 } ]
@UI.identification: [{ position: 40 }]
txz01;

@UI.lineItem: [ { position: 50 } ]
@UI.identification: [{ position: 50 }]
werks;

@UI.lineItem: [ { position: 60 } ]
@UI.identification: [{ position: 60 }]
lgort;

@UI.lineItem: [ { position: 70 } ]
@UI.identification: [{ position: 70 }]
meins;

@UI.lineItem: [ { position: 80 } ]
@UI.identification: [{ position: 80 }]
menge;

@UI.lineItem: [ { position: 90 } ]
@UI.identification: [{ position: 90 }]
matkl;
}

Step 4: Create the Behavior Definition

The behavior definition defines the transactional capabilities of the RAP business object.

Since this application is implemented using an unmanaged RAP scenario, the behavior definition is connected to a Behavior Pool where the application logic is implemented manually.

The root entity zi_b_ekko manages the Purchase Order business object, while zi_b_ekpo acts as its composition child.

unmanaged implementation in class zbp_i_b_ekko unique;
strict ( 2 );
with draft;

define behavior for zi_b_ekko //alias <alias_name>
draft table zekko_b
late numbering
lock master
total etag lastchangedatetime
authorization master ( instance )
etag master lastchangedatetime
{
create;
update;
delete;
association _items { create; with draft;}
draft action Edit;
draft action Activate;
draft action Discard;
draft action Resume;

draft determine action Prepare;
field ( readonly ) ebeln;
mapping for ekko

{
ebeln = ebeln;
bsart = bsart;
aedat = aedat;
ernam = ernam;
lifnr = lifnr;
ekorg = ekorg;
ekgrp = ekgrp;
bukrs = bukrs;
waers = waers;
lastchangedatetime = lastchangedatetime;
}
}

define behavior for zi_b_ekpo //alias <alias_name>
draft table zekpo_b
late numbering
lock dependent by _header
authorization dependent by _header
//etag master <field_name>
{

update;
delete;
field ( readonly ) ebeln,ebelp;
association _header{ with draft; }
mapping for ekpo
{
ebeln = ebeln;
ebelp = ebelp;
matnr = matnr;
txz01 = txz01;
werks = werks;
lgort = lgort;
meins = meins;
menge = menge;
matkl = matkl;
}
}

Understanding the Behavior Pool

The Behavior Pool is the most important part of an unmanaged RAP application.

In a managed RAP application, RAP performs database operations automatically.

In an unmanaged RAP application, we implement the business logic ourselves.

However, the Behavior Handler should not immediately call the BAPI for every request.

Instead, the application follows this approach:

Create / Update / Delete Request
            │
            ▼
Behavior Handler
            │
            ▼
Store Requested Changes in Buffers
            │
            ▼
RAP Save Sequence
            │
            ▼
Saver Class
            │
            ├── adjust_numbers()
            │
            └── save()
                    │
                    ▼
                  BAPI

This separation is essential.

Behavior Pool Structure

The implementation contains two handler classes:

lhc_zi_b_ekko

and:

lhc_zi_b_ekpo

There is also one saver class:

lsc_zi_b_ekko

Their responsibilities are:

lhc_zi_b_ekko
│
├── Header Create
├── Header Update
├── Header Delete
├── Header Read
├── Header Lock
├── Read Items
└── Create Items by Association

lhc_zi_b_ekpo
│
├── Item Update
├── Item Delete
├── Item Read
└── Read Header Association

lsc_zi_b_ekko
│
├── adjust_numbers()
├── save()
├── cleanup()
└── cleanup_finalize()

 

 Understand the Temporary Buffers

The application uses internal tables as temporary buffers.

The most important buffers are:

gt_po_create
gt_po_update
gt_po_delete

Their purpose is:

User Operation
      │
      ▼
Behavior Handler
      │
      ▼
Internal Buffer
      │
      ▼
Saver Class
      │
      ▼
BAPI

For example:

User changes EKORG
        │
        ▼
update() method
        │
        ▼
gt_po_update
        │
        ▼
save()
        │
        ▼
BAPI_PO_CHANGE

This ensures that multiple user changes can be collected before the final save operation.

CLASS lhc_zi_b_ekko DEFINITION INHERITING FROM cl_abap_behavior_handler.
PUBLIC SECTION.
TYPES: BEGIN OF ts_item_buf,
ebelp TYPE ekpo-ebelp,
matnr TYPE ekpo-matnr,
txz01 TYPE ekpo-txz01,
werks TYPE ekpo-werks,
lgort TYPE ekpo-lgort,
meins TYPE ekpo-meins,
menge TYPE ekpo-menge,
matkl TYPE ekpo-matkl,
control TYPE zi_b_ekpo,
END OF ts_item_buf.

TYPES: BEGIN OF ts_header_buf,
pid TYPE abp_behv_pid,
ebeln TYPE ekko-ebeln,
bsart TYPE ekko-bsart,
aedat TYPE ekko-aedat,
ernam TYPE ekko-ernam,
lifnr TYPE ekko-lifnr,
ekorg TYPE ekko-ekorg,
ekgrp TYPE ekko-ekgrp,
bukrs TYPE ekko-bukrs,
waers TYPE ekko-waers,
control TYPE zi_b_ekko,
items TYPE STANDARD TABLE OF ts_item_buf WITH EMPTY KEY,
END OF ts_header_buf.

TYPES: BEGIN OF ts_del_buf,
ebeln TYPE ekko-ebeln,
is_draft TYPE abap_boolean,
END OF ts_del_buf.

CLASS-DATA:
gt_po_create TYPE STANDARD TABLE OF ts_header_buf WITH EMPTY KEY,
gt_po_update TYPE STANDARD TABLE OF ts_header_buf WITH EMPTY KEY,
gt_po_delete TYPE STANDARD TABLE OF ts_del_buf.


Header handler class — method signatures

PRIVATE SECTION.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR zi_b_ekko RESULT result.

METHODS create FOR MODIFY
IMPORTING entities FOR CREATE zi_b_ekko.

METHODS update FOR MODIFY
IMPORTING entities FOR UPDATE zi_b_ekko.

METHODS delete FOR MODIFY
IMPORTING keys FOR DELETE zi_b_ekko.

METHODS read FOR READ
IMPORTING keys FOR READ zi_b_ekko RESULT result.

METHODS lock FOR LOCK
IMPORTING keys FOR LOCK zi_b_ekko.

METHODS rba_Items FOR READ
IMPORTING keys_rba FOR READ zi_b_ekko_Items FULL result_requested RESULT result LINK association_links.

METHODS cba_Items FOR MODIFY
IMPORTING entities_cba FOR CREATE zi_b_ekko_Items.
ENDCLASS.

 

Behavior Handler: Header Create

The create method receives new Purchase Order data.

The important logic is:

METHOD create.
LOOP AT entities ASSIGNING FIELD-SYMBOL(<ls_entity>).
DATA(lv_pid) = cl_system_uuid=>create_uuid_x16_static( ).
APPEND VALUE #(
pid = lv_pid
ebeln = <ls_entity>-ebeln
bsart = <ls_entity>-bsart
aedat = <ls_entity>-aedat
ernam = <ls_entity>-ernam
lifnr = <ls_entity>-lifnr
ekorg = <ls_entity>-ekorg
ekgrp = <ls_entity>-ekgrp
bukrs = <ls_entity>-bukrs
waers = <ls_entity>-waers
control = CORRESPONDING #( <ls_entity>-%control )
) TO gt_po_create.
* Tell the framework: this %cid (from the client) corresponds to
” this %pid (our temporary id) for the rest of the save sequence.
mapped-zi_b_ekko = VALUE #( BASE mapped-zi_b_ekko (
%cid = <ls_entity>-%cid
%pid = lv_pid
) ).
ENDLOOP.
ENDMETHOD.

Why Do We Need %pid?

During creation, the final Purchase Order number does not yet exist.

The Purchase Order number is generated later by:

BAPI_PO_CREATE1

Therefore, RAP needs a temporary identifier.

That temporary identifier is:

%pid

The flow is:

Client creates Purchase Order
        │
        ▼
Client sends %cid
        │
        ▼
Behavior Pool generates %pid
        │
        ▼
Data stored in gt_po_create
        │
        ▼
BAPI creates Purchase Order
        │
        ▼
Final EBELN generated
        │
        ▼
%pid mapped to EBELN

Understanding %cid and %pid

%cid comes from the client request.

It identifies a newly created instance during the request.

%pid is used internally by RAP during late numbering.

The final key is not known yet.

Therefore:

Temporary Identity
      │
      ▼
     %pid
      │
      ▼
BAPI Creates PO
      │
      ▼
Final Key
      │
      ▼
    EBELN

Behavior Handler: Header Update

The update method does not immediately call:

BAPI_PO_CHANGE

Instead, it stores changed values in:

gt_po_update

The important concept is:

%control

The Update logic will be:

METHOD update.
” NOTE: no manual UPDATE zekko_b here. The framework persists draft
” field values to the draft table automatically for unmanaged BOs –
” writing to it ourselves caused the gateway exception seen earlier.
” We only need gt_po_update populated so save()/BAPI_PO_CHANGE has
” something to act on once Activate runs.
FIELD-SYMBOLS: <fs_key> LIKE LINE OF entities,
<fs_parent> TYPE ts_header_buf.

LOOP AT entities ASSIGNING <fs_key>.
“checks po already exists in the gt_po_update , if exists it assign po to <fs_parent>
ASSIGN gt_po_update[ ebeln = <fs_key>-ebeln ] TO <fs_parent>.
IF sy-subrc <> 0.
” it will add new po to gt_po_update with controls
APPEND VALUE #(
ebeln = <fs_key>-ebeln
control = CORRESPONDING #( <fs_key>-%control )
) TO gt_po_update ASSIGNING <fs_parent>.
ELSE.
“if already exists it will add the controls to <fs_parent> where already contains header
<fs_parent>-control = CORRESPONDING #( BASE ( <fs_parent>-control ) <fs_key>-%control ).
ENDIF.
“now it will check which field is changed based on the control details based on or off
“if_we done change on field <fs_key>-%control-lifnr it will contaion on then abap_behv=>mk-on
” both are equal then it will enter into if contion change the value
IF <fs_key>-%control-bsart = if_abap_behv=>mk-on.
<fs_parent>-bsart = <fs_key>-bsart.
ENDIF.
IF <fs_key>-%control-lifnr = if_abap_behv=>mk-on.
<fs_parent>-lifnr = <fs_key>-lifnr.
ENDIF.
IF <fs_key>-%control-ekorg = if_abap_behv=>mk-on.
<fs_parent>-ekorg = <fs_key>-ekorg.
ENDIF.
IF <fs_key>-%control-ekgrp = if_abap_behv=>mk-on.
<fs_parent>-ekgrp = <fs_key>-ekgrp.
ENDIF.
IF <fs_key>-%control-bukrs = if_abap_behv=>mk-on.
<fs_parent>-bukrs = <fs_key>-bukrs.
ENDIF.
IF <fs_key>-%control-waers = if_abap_behv=>mk-on.
<fs_parent>-waers = <fs_key>-waers.
ENDIF.
IF <fs_key>-%control-aedat = if_abap_behv=>mk-on.
<fs_parent>-aedat = <fs_key>-aedat.
ENDIF.
IF <fs_key>-%control-ernam = if_abap_behv=>mk-on.
<fs_parent>-ernam = <fs_key>-ernam.
ENDIF.

ENDLOOP.
ENDMETHOD.

This means:

Update the vendor only if RAP indicates that the vendor field was changed.

Why Is %control Important?

Suppose the Purchase Order contains:

Vendor       = 1000
Purchasing Org = 1000
Purchasing Group = 001

The user changes only:

Purchasing Group = 002

The RAP request contains control information indicating which field changed.

Conceptually:

LIFNR   → OFF
EKORG   → OFF
EKGRP   → ON

Therefore, only EKGRP is added as a change.

This is important because BAPI_PO_CHANGE also uses X structures to identify changed fields.

The flow is:

RAP %control
      │
      ▼
Internal Buffer
      │
      ▼
BAPI X Structure
      │
      ▼
Only Changed Fields Updated

Behavior Handler: Create by Association

The method:

cba_Items

means:

Create By Association

It is called when a user creates Purchase Order items under a Purchase Order header.

The flow is:

Purchase Order Header
        │
        ▼
Create Item
        │
        ▼
cba_Items()

The method first determines the correct header buffer.

For a new Purchase Order:

gt_po_create

For an existing Purchase Order:

gt_po_update

Then the item is stored inside the header buffer.


“method is used when you create new Purchase Order items under a Purchase Order header.
DATA: lv_max_ebelp TYPE ekpo-ebelp,
lv_ekpo_max TYPE ekpo-ebelp,
lv_zekpo_b_max TYPE ekpo-ebelp.
FIELD-SYMBOLS: <fs_cba> LIKE LINE OF entities_cba,
<fs_parent> TYPE ts_header_buf,
<fs_item> LIKE LINE OF <fs_cba>-%target,
<fs_existing> LIKE LINE OF <fs_parent>-items.

LOOP AT entities_cba ASSIGNING <fs_cba>.
“if we are creating the new PO it will work
ASSIGN gt_po_create[ pid = <fs_cba>-%pid ] TO <fs_parent>.

IF sy-subrc <> 0.
” if we are creating the po item for existing po
ASSIGN gt_po_update[ ebeln = <fs_cba>-ebeln ] TO <fs_parent>.
IF sy-subrc <> 0.
” if po already exists but not in gt_po_update then we add it
APPEND VALUE #( ebeln = <fs_cba>-ebeln ) TO gt_po_update ASSIGNING <fs_parent>.
ENDIF.
ENDIF.

CLEAR: lv_max_ebelp, lv_ekpo_max, lv_zekpo_b_max.
“Loop for existing items and get highest item number from Po
LOOP AT <fs_parent>-items ASSIGNING <fs_existing>.
IF <fs_existing>-ebelp > lv_max_ebelp.
lv_max_ebelp = <fs_existing>-ebelp.
ENDIF.
ENDLOOP.

IF <fs_cba>-ebeln IS NOT INITIAL.
SELECT MAX( ebelp )
FROM ekpo
WHERE ebeln = @<fs_cba>-ebeln
INTO @LV_ekpo_max.
IF lv_ekpo_max > lv_max_ebelp.
lv_max_ebelp = lv_ekpo_max.
ENDIF.

SELECT MAX( ebelp )
FROM zekpo_b
WHERE ebeln = @<fs_cba>-ebeln
INTO @LV_zekpo_b_max.
IF lv_zekpo_b_max > lv_max_ebelp.
lv_max_ebelp = lv_zekpo_b_max.
ENDIF.
ENDIF.

LOOP AT <fs_cba>-%target ASSIGNING <fs_item>.
lv_max_ebelp = lv_max_ebelp + 10.

APPEND VALUE #(
ebelp = lv_max_ebelp
matnr = <fs_item>-matnr
txz01 = <fs_item>-txz01
werks = <fs_item>-werks
lgort = <fs_item>-lgort
meins = <fs_item>-meins
menge = <fs_item>-menge
matkl = <fs_item>-matkl
control = CORRESPONDING #( <fs_item>-%control )
) TO <fs_parent>-items.
ENDLOOP.
ENDLOOP.
ENDMETHOD.

Item Number Generation

The implementation determines the maximum item number.

It checks:

Existing Buffered Items
        │
        ▼
EKPO
        │
        ▼
Draft Item Table

Then the new item number is generated:

lv_max_ebelp = lv_max_ebelp + 10.

For example:

00010
00020
00030

The next item becomes:

00040

The item is then added to the parent buffer.

The BAPI receives the item later during the save sequence.

Behavior Handler: Header Delete:

The delete logic handles draft instances and active Purchase Orders differently.

For a draft-only instance, the active Purchase Order does not yet exist in EKKO and EKPO. Therefore, the deletion logic removes the corresponding draft data.

For an active Purchase Order, the implementation does not directly modify EKKO or EKPO. Instead, the Purchase Order key is stored in gt_po_delete.

During the RAP save sequence, the Saver Class processes the buffered deletion request using BAPI_PO_CHANGE.

The flow is:

Delete Request
      │
      ├── Draft Instance
      │       │
      │       └── Remove Draft Data
      │
      └── Active Purchase Order
              │
              ▼
         gt_po_delete
              │
              ▼
            save()
              │
              ▼
        BAPI_PO_CHANGE

METHOD delete.
LOOP AT keys ASSIGNING FIELD-SYMBOL(<fs_key>).

” 1. Handle draft discarding / draft-only deletions
IF <fs_key>-%is_draft = if_abap_behv=>mk-on.
” Delete header draft record
DELETE FROM zekko_b WHERE ebeln = @<fs_key>-ebeln.
” Cascade delete: remove all corresponding item draft records
DELETE FROM zekpo_b WHERE ebeln = @<fs_key>-ebeln.

” 2. Handle active production document deletions
ELSE.
” Buffer the active key to process via the BAPI in the save phase later
APPEND VALUE #( ebeln = <fs_key>-ebeln
is_draft = abap_false ) TO lhc_zi_b_ekko=>gt_po_delete.
ENDIF.
CLEAR: failed, reported.
ENDLOOP.
ENDMETHOD.

%is_draft is a RAP-supplied flag indicating whether the instance being deleted currently only exists as a draft (never activated) or is a real, active EKKO record. The two branches handle very different situations:

  1. Draft-only deletion — if the record was never activated, nothing has ever touched EKKO/EKPO, so there’s nothing to “undo” via a BAPI. It’s safe to simply DELETE the rows straight out of the draft tables (zekko_b for the header, zekpo_b for its items — a manual cascade delete, since draft tables don’t get automatic foreign-key cascading). This can and does happen immediately, right here in the handler method, rather than being deferred to the save sequence.
  2. Active document deletion — if the PO is already a real, saved document, it can’t be safely deleted right here, because RAP handler methods aren’t allowed to make final database changes outside the controlled save sequence (this is fundamental to how RAP guarantees transactional consistency, and lets the whole request still be rolled back if something later fails). So instead, the key is simply appended to the static gt_po_delete buffer, to be picked up and actually processed via BAPI_PO_CHANGE inside the saver class’s save() method.

CLEAR: failed, reported. resets the standard RAP response structures at the end of each iteration — defensive housekeeping to make sure no stale failure/message data leaks between keys being processed in the same loop.

read():

METHOD read.
LOOP AT keys INTO DATA(ls_key).

” Fetch header record
SELECT SINGLE ebeln, bsart, aedat, ernam, lifnr, ekorg, ekgrp, bukrs, waers
FROM ekko
WHERE ebeln = @LS_key-ebeln
INTO @DATA(ls_hdr).

IF sy-subrc = 0.
APPEND VALUE #( %tky = ls_key-%tky
ebeln = ls_hdr-ebeln
bsart = ls_hdr-bsart
aedat = ls_hdr-aedat
ernam = ls_hdr-ernam
lifnr = ls_hdr-lifnr
ekorg = ls_hdr-ekorg
ekgrp = ls_hdr-ekgrp
bukrs = ls_hdr-bukrs
waers = ls_hdr-waers ) TO result.
ENDIF.

ENDLOOP.
ENDMETHOD.

%tky — this is RAP’s generic table key structure, and it’s essential here: it’s what tells the framework exactly which requested instance this returned row corresponds to. Every row appended to result must carry the %tky from its originating key, or RAP won’t be able to match the read result back to the correct request.

rba_Items() — read items by association:

METHOD rba_Items.
LOOP AT keys_rba INTO DATA(ls_key).

” Fetch item records for this header
SELECT ebeln, ebelp, matnr, txz01, werks, lgort, meins, menge, matkl
FROM ekpo
WHERE ebeln = @LS_key-ebeln
INTO TABLE @DATA(lt_items).

LOOP AT lt_items INTO DATA(ls_item).
APPEND VALUE #( %tky = VALUE #( ebeln = ls_item-ebeln
ebelp = ls_item-ebelp )
ebeln = ls_item-ebeln
ebelp = ls_item-ebelp
matnr = ls_item-matnr
txz01 = ls_item-txz01
werks = ls_item-werks
lgort = ls_item-lgort
meins = ls_item-meins
menge = ls_item-menge
matkl = ls_item-matkl ) TO result.

” association_links tells RAP which item belongs to which header
APPEND VALUE #( source-ebeln = ls_key-ebeln
target-ebeln = ls_item-ebeln
target-ebelp = ls_item-ebelp ) TO association_links.
ENDLOOP.

ENDLOOP.
ENDMETHOD.

This method runs whenever the framework needs to resolve the _items association for one or more headers — for example, when a Fiori Elements object page loads and needs to display a PO’s line items, or when the UI expands a header row in a list.

Item handler class (lhc_zi_b_ekpo)

CLASS lhc_zi_b_ekpo DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS update FOR MODIFY
IMPORTING entities FOR UPDATE zi_b_ekpo.
METHODS delete FOR MODIFY
IMPORTING keys FOR DELETE zi_b_ekpo.
METHODS read FOR READ
IMPORTING keys FOR READ zi_b_ekpo RESULT result.
METHODS rba_Header FOR READ
IMPORTING keys_rba FOR READ zi_b_ekpo_Header FULL result_requested RESULT result LINK association_links.
ENDCLASS.

Behavior Handler: Item Update

The item update method follows the same buffering approach.

The method:

  1. Finds the parent Purchase Order buffer.
  2. Finds the item inside the buffer.
  3. Creates a buffer entry if necessary.
  4. Stores only changed values.
  5. Stores %control information.

METHOD update.
” NOTE: no manual UPDATE zekpo_b – same reasoning as the header update().
FIELD-SYMBOLS: <fs_item> LIKE LINE OF entities,
<fs_parent> TYPE lhc_zi_b_ekko=>ts_header_buf,
<fs_line> TYPE lhc_zi_b_ekko=>ts_item_buf.

LOOP AT entities ASSIGNING <fs_item>.

ASSIGN lhc_zi_b_ekko=>gt_po_update[ ebeln = <fs_item>-ebeln ] TO <fs_parent>.
IF sy-subrc <> 0.
APPEND VALUE #( ebeln = <fs_item>-ebeln ) TO lhc_zi_b_ekko=>gt_po_update ASSIGNING <fs_parent>.
ENDIF.

ASSIGN <fs_parent>-items[ ebelp = <fs_item>-ebelp ] TO <fs_line>.
IF sy-subrc <> 0.
APPEND VALUE #( ebelp = <fs_item>-ebelp ) TO <fs_parent>-items ASSIGNING <fs_line>.
ENDIF.

IF <fs_item>-%control-matnr = if_abap_behv=>mk-on.
<fs_line>-matnr = <fs_item>-matnr.
ENDIF.
IF <fs_item>-%control-txz01 = if_abap_behv=>mk-on.
<fs_line>-txz01 = <fs_item>-txz01.
ENDIF.
IF <fs_item>-%control-werks = if_abap_behv=>mk-on.
<fs_line>-werks = <fs_item>-werks.
ENDIF.
IF <fs_item>-%control-lgort = if_abap_behv=>mk-on.
<fs_line>-lgort = <fs_item>-lgort.
ENDIF.
IF <fs_item>-%control-meins = if_abap_behv=>mk-on.
<fs_line>-meins = <fs_item>-meins.
ENDIF.
IF <fs_item>-%control-menge = if_abap_behv=>mk-on.
<fs_line>-menge = <fs_item>-menge.
ENDIF.
IF <fs_item>-%control-matkl = if_abap_behv=>mk-on.
<fs_line>-matkl = <fs_item>-matkl.
ENDIF.
<fs_line>-control = CORRESPONDING #( BASE ( <fs_line>-control ) <fs_item>-%control ).

ENDLOOP.
ENDMETHOD.

 Method Delete:

METHOD delete.
LOOP AT keys ASSIGNING FIELD-SYMBOL(<fs_key>).
IF <fs_key>-%is_draft = if_abap_behv=>mk-on.
” Delete only this specific draft item row
DELETE FROM zekpo_b WHERE ebeln = @<fs_key>-ebeln
AND ebelp = @<fs_key>-ebelp.
ELSE.
” Active item deletions would be buffered here for BAPI handling,
” if single-item (rather than whole-PO) deletion were supported
ENDIF.
ENDLOOP.
CLEAR: failed, reported.
ENDMETHOD.

Saver class (lsc_ZI_B_EKKO) — where the BAPIs actually get called:

CLASS lsc_ZI_B_EKKO DEFINITION INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS finalize REDEFINITION.
METHODS check_before_save REDEFINITION.
METHODS adjust_numbers REDEFINITION.
METHODS save REDEFINITION.
METHODS cleanup REDEFINITION.
METHODS cleanup_finalize REDEFINITION.
ENDCLASS.

adjust_numbers — this is exactly where late numbering gets resolved, and it’s the method that first calls out to the real world:

METHOD adjust_numbers.
DATA: ls_bapi_header TYPE bapimepoheader,
ls_bapi_headerx TYPE bapimepoheaderx,
lt_bapi_item TYPE STANDARD TABLE OF bapimepoitem,
lt_bapi_itemx TYPE STANDARD TABLE OF bapimepoitemx,
lt_return TYPE STANDARD TABLE OF bapiret2,
lv_new_ebeln TYPE ekko-ebeln.

LOOP AT lhc_zi_b_ekko=>gt_po_create ASSIGNING FIELD-SYMBOL(<fs_create>).
CLEAR: ls_bapi_header, ls_bapi_headerx, lt_bapi_item, lt_bapi_itemx, lt_return.

ls_bapi_header-doc_type = <fs_create>-bsart.
ls_bapi_headerx-doc_type = ‘X’.
ls_bapi_header-comp_code = <fs_create>-bukrs.
ls_bapi_headerx-comp_code = ‘X’.
ls_bapi_header-vendor = <fs_create>-lifnr.
ls_bapi_headerx-vendor = ‘X’.
ls_bapi_header-purch_org = <fs_create>-ekorg.
ls_bapi_headerx-purch_org = ‘X’.
ls_bapi_header-pur_group = <fs_create>-ekgrp.
ls_bapi_headerx-pur_group = ‘X’.

LOOP AT <fs_create>-items ASSIGNING FIELD-SYMBOL(<fs_item_c>).
APPEND VALUE #(
po_item = <fs_item_c>-ebelp
material = <fs_item_c>-matnr
plant = <fs_item_c>-werks
* store_loc = <fs_item_c>-lgort ” Fixed Typo: stge_loc -> store_loc
quantity = <fs_item_c>-menge
po_unit = <fs_item_c>-meins
) TO lt_bapi_item.

APPEND VALUE #(
po_item = <fs_item_c>-ebelp
po_itemx = ‘X’
material = ‘X’
plant = ‘X’
* store_loc = ‘X’ ” Fixed Typo: stge_loc -> store_loc
quantity = ‘X’
po_unit = ‘X’
) TO lt_bapi_itemx.
ENDLOOP.

CALL FUNCTION ‘BAPI_PO_CREATE1’
EXPORTING
poheader = ls_bapi_header
poheaderx = ls_bapi_headerx
IMPORTING
EXPPURCHASEORDER = lv_new_ebeln ” Fixed Typo: exppurchaseorder -> exp_purchaseorder
TABLES
return = lt_return
poitem = lt_bapi_item
poitemx = lt_bapi_itemx.

“if errors or abort things happen it will update status and reason to TO reported-zi_b_ekko

IF line_exists( lt_return[ type = ‘E’ ] ) OR line_exists( lt_return[ type = ‘A’ ] ).
APPEND VALUE #( %pid = <fs_create>-pid
%fail-cause = if_abap_behv=>cause-unspecific
) TO failed-zi_b_ekko.

LOOP AT lt_return INTO DATA(ls_ret) WHERE type = ‘E’ OR type = ‘A’.
APPEND VALUE #(
%pid = <fs_create>-pid
%msg = new_message( id = ls_ret-id
number = ls_ret-number
severity = if_abap_behv_message=>severity-error
v1 = ls_ret-message_v1
v2 = ls_ret-message_v2
v3 = ls_ret-message_v3
v4 = ls_ret-message_v4 )
) TO reported-zi_b_ekko.
ENDLOOP.
“it will add beln to the po based on %pid
ELSE.
<fs_create>-ebeln = lv_new_ebeln.

mapped-zi_b_ekko = VALUE #( BASE mapped-zi_b_ekko (
%pid = <fs_create>-pid
ebeln = lv_new_ebeln
) ).
ENDIF.
ENDLOOP.
ENDMETHOD.

BAPI_PO_CREATE1 is then called, and exppurchaseorder returns the real, freshly-assigned PO number. From here, two paths:

  • Failure — if BAPIRET2 contains any E (error) or A (abort) type messages, the instance (identified by %pid, since it still has no real key) is appended to failed-zi_b_ekko, and each BAPI message is translated into a proper RAP message via new_message(...) and appended to reported-zi_b_ekko — this is what surfaces the BAPI’s actual error text back to the Fiori user.
  • Success — the buffer’s own ebeln field is updated with the real number, and — critically — mapped-zi_b_ekko is populated, mapping the %pid to the real ebeln. This is the other half of the mapping set up back in create() .RAP now has the full chain %cid (client’s screen row) → %pid (our temporary ID) → ebeln (final real key), letting the Fiori UI seamlessly swap the temporary row for the real, saved PO once the response comes back.

save — handles everything that already has a real key: active-document deletions and updates.

METHOD save.
DATA: ls_bapi_header TYPE bapimepoheader,
ls_bapi_headerx TYPE bapimepoheaderx,
lt_bapi_item TYPE STANDARD TABLE OF bapimepoitem,
lt_bapi_itemx TYPE STANDARD TABLE OF bapimepoitemx,
lt_return TYPE STANDARD TABLE OF bapiret2,
ls_ret_u TYPE bapiret2,
lv_padded_item TYPE ebelp.
FIELD-SYMBOLS: <fs_update> LIKE LINE OF lhc_zi_b_ekko=>gt_po_update,
<fs_item_u> LIKE LINE OF <fs_update>-items.

” ======================================================================
” NEW LOGIC: Handle Active Production Document Deletions
” ======================================================================
IF lhc_zi_b_ekko=>gt_po_delete IS NOT INITIAL.
LOOP AT lhc_zi_b_ekko=>gt_po_delete INTO DATA(ls_delete) WHERE is_draft = abap_false.
CLEAR: lt_bapi_item, lt_bapi_itemx, lt_return.

” Fetch all active items for this PO from the standard table (EKPO)
SELECT ebelp
FROM ekpo
WHERE ebeln = @LS_delete-ebeln
INTO TABLE @DATA(lt_active_items).

IF sy-subrc = 0.
LOOP AT lt_active_items INTO DATA(ls_item).
” Mark Item for Deletion
APPEND VALUE #( po_item = ls_item-ebelp
delete_ind = ‘X’ ) TO lt_bapi_item.

” Flag the Deletion field change
APPEND VALUE #( po_item = ls_item-ebelp
po_itemx = ‘X’
delete_ind = ‘X’ ) TO lt_bapi_itemx.
ENDLOOP.

” Call BAPI to delete the items
CALL FUNCTION ‘BAPI_PO_CHANGE’
EXPORTING
purchaseorder = ls_delete-ebeln
TABLES
return = lt_return
poitem = lt_bapi_item
poitemx = lt_bapi_itemx.

” Error Handling for Deletion
IF line_exists( lt_return[ type = ‘E’ ] ) OR line_exists( lt_return[ type = ‘A’ ] ).
APPEND VALUE #( ebeln = ls_delete-ebeln
%fail-cause = if_abap_behv=>cause-unspecific
) TO failed-zi_b_ekko.

LOOP AT lt_return INTO ls_ret_u WHERE type = ‘E’ OR type = ‘A’.
APPEND VALUE #(
ebeln = ls_delete-ebeln
%msg = new_message( id = ls_ret_u-id
number = ls_ret_u-number
severity = if_abap_behv_message=>severity-error
v1 = ls_ret_u-message_v1
v2 = ls_ret_u-message_v2
v3 = ls_ret_u-message_v3
v4 = ls_ret_u-message_v4 )
) TO reported-zi_b_ekko.
ENDLOOP.
ENDIF.
ENDIF.
ENDLOOP.
ENDIF.

” ======================================================================
” EXISTING LOGIC: Handle Active Production Document Updates
” ======================================================================
LOOP AT lhc_zi_b_ekko=>gt_po_update ASSIGNING <fs_update>.
CLEAR: ls_bapi_header, ls_bapi_headerx, lt_bapi_item, lt_bapi_itemx, lt_return.

IF <fs_update>-control-ekgrp = if_abap_behv=>mk-on.
ls_bapi_header-pur_group = <fs_update>-ekgrp.
ls_bapi_headerx-pur_group = ‘X’.
ENDIF.
IF <fs_update>-control-lifnr = if_abap_behv=>mk-on.
ls_bapi_header-vendor = <fs_update>-lifnr.
ls_bapi_headerx-vendor = ‘X’.
ENDIF.
IF <fs_update>-control-ekorg = if_abap_behv=>mk-on.
ls_bapi_header-purch_org = <fs_update>-ekorg.
ls_bapi_headerx-purch_org = ‘X’.
ENDIF.
IF <fs_update>-control-waers = if_abap_behv=>mk-on.
ls_bapi_header-currency = <fs_update>-waers.
ls_bapi_headerx-currency = ‘X’.
ENDIF.

LOOP AT <fs_update>-items ASSIGNING <fs_item_u>.
CLEAR lv_padded_item.
lv_padded_item = <fs_item_u>-ebelp.

APPEND VALUE #(
po_item = lv_padded_item
material = <fs_item_u>-matnr
plant = <fs_item_u>-werks
stge_loc = <fs_item_u>-lgort
quantity = <fs_item_u>-menge
po_unit = <fs_item_u>-meins
) TO lt_bapi_item.

APPEND VALUE #(
po_item = lv_padded_item
po_itemx = ‘X’
material = COND #( WHEN <fs_item_u>-control-matnr = if_abap_behv=>mk-on THEN ‘X’ ELSE ” )
plant = COND #( WHEN <fs_item_u>-control-werks = if_abap_behv=>mk-on THEN ‘X’ ELSE ” )
stge_loc = COND #( WHEN <fs_item_u>-control-lgort = if_abap_behv=>mk-on THEN ‘X’ ELSE ” )
quantity = COND #( WHEN <fs_item_u>-control-menge = if_abap_behv=>mk-on THEN ‘X’ ELSE ” )
po_unit = COND #( WHEN <fs_item_u>-control-meins = if_abap_behv=>mk-on THEN ‘X’ ELSE ” )
) TO lt_bapi_itemx.
ENDLOOP.

CALL FUNCTION ‘BAPI_PO_CHANGE’
EXPORTING
purchaseorder = <fs_update>-ebeln
poheader = ls_bapi_header
poheaderx = ls_bapi_headerx
TABLES
return = lt_return
poitem = lt_bapi_item
poitemx = lt_bapi_itemx.

IF line_exists( lt_return[ type = ‘E’ ] ) OR line_exists( lt_return[ type = ‘A’ ] ).
APPEND VALUE #( ebeln = <fs_update>-ebeln
%fail-cause = if_abap_behv=>cause-unspecific
) TO failed-zi_b_ekko.

LOOP AT lt_return INTO ls_ret_u WHERE type = ‘E’ OR type = ‘A’.
APPEND VALUE #(
ebeln = <fs_update>-ebeln
%msg = new_message( id = ls_ret_u-id
number = ls_ret_u-number
severity = if_abap_behv_message=>severity-error
v1 = ls_ret_u-message_v1
v2 = ls_ret_u-message_v2
v3 = ls_ret_u-message_v3
v4 = ls_ret_u-message_v4 )
) TO reported-zi_b_ekko.
ENDLOOP.
ENDIF.
ENDLOOP.

ENDMETHOD.

STEP 5: Projection Behavior Definition

The projection behavior exposes the operations implemented by the underlying business object.

projection;
strict ( 2 );
use draft;

define behavior for zc_b_ekko //alias <alias_name>
{
use create;
use update;
use delete;

use association _items { create; with draft; }
use action Edit;
use action Activate;
use action Discard;
use action Resume;
use action Prepare;
}

define behavior for zc_b_ekPo //alias <alias_name>
{

use update;
use delete;

use association _header{ with draft; }
}

The projection layer does not implement the logic again.

It simply exposes the capabilities implemented in the interface behavior.

STEP 6: Create the Service Definition

@EndUserText.label: ‘Service binding for ekko,ekpo’
define service Zsb_ekko {
expose zc_b_ekko as PurchaseHeader;
expose zc_b_ekPo as PurchaseItem;
}

STEP 7: Create and Publish the Service Binding:

Create a Service Binding for the service definition.

After activation and publishing, the RAP application can be consumed by a Fiori Elements application or other OData consumers.

bhuvaneswarg_0-1788772391618.png

STEP 8: Testing the Application

After the Service Binding is activated and published, click Preview to open the generated Fiori Elements application.

The application initially displays the Purchase Order List Report.

After selecting the required filter values and choosing Go, the matching Purchase Order records are displayed.

When a Purchase Order is selected, the Object Page opens and displays the Purchase Order header information together with the associated item data.

To modify an existing Purchase Order, choose Edit, make the required changes, and then choose Save.

To create a new Purchase Order, choose Create, enter the required header and item information, and save the draft. During activation, the RAP save sequence processes the request and the Purchase Order is created using BAPI_PO_CREATE1.

bhuvaneswarg_1-1788430647845.png

you will see the Data when we click on Go.

when we select any record it will be displayed in the detailed manner with the item data.

bhuvaneswarg_2-1788430733202.png

If we want to make any changes on record data you can click on edit and make the changes and click on save.

when we click on create we will get this page:

bhuvaneswarg_3-1788430860910.png

Give the details and click on create the record will be created.

Conclusion:

we created a draft-enabled unmanaged RAP application for Purchase Order processing using EKKO and EKPO.

The most important concept in this implementation is understanding the separation between the Behavior Handler and the Saver Class.

The Behavior Handler is responsible for:

  • Receiving RAP requests
  • Reading data
  • Collecting create requests
  • Collecting update requests
  • Collecting delete requests
  • Handling associations
  • Managing temporary buffers

The Saver Class is responsible for:

  • Late numbering
  • Calling BAPI_PO_CREATE1
  • Calling BAPI_PO_CHANGE
  • Mapping temporary RAP identifiers to final Purchase Order numbers
  • Processing BAPI errors
  • Clearing transactional buffers

The complete processing flow can therefore be summarized as:

Fiori UI
   │
   ▼
RAP Request
   │
   ▼
Behavior Handler
   │
   ▼
Temporary Buffers
   │
   ▼
RAP Save Sequence
   │
   ├── adjust_numbers()
   │       └── BAPI_PO_CREATE1
   │
   └── save()
           └── BAPI_PO_CHANGE
   │
   ▼
EKKO / EKPO

This pattern is particularly useful when building RAP applications on top of existing SAP business objects where standard BAPIs must be used instead of directly modifying database tables.

“}]] 

  Read More Technology Blog Posts by Members articles 

#abap

By ali

Leave a Reply