Skip to Content
Author's profile photo James Wood

Navigating the BOPF: Part 3 – Working with the BOPF API

In my previous blog post, we explored the anatomy of business objects within the BOPF. There, we were able to observe the various entities that make up a BO: nodes/attributes, actions, associations, determinations, validations, and queries. Now that you have a feel for what these entities are, we’re ready to begin taking a look at the API that is used to manipulate these entities. To guide us through this demonstration, we’ll explore the construction of a simple ABAP report program used to perform CRUD operations on a sample BOPF BO shipped by default by SAP: /BOBF/DEMO_CUSTOMER. You can download the complete example program source code here.

Note: The code bundle described above has been enhanced as of 9/18/2013. The code was reworked to factor out a BOPF utilities class of sorts and also demonstrate how to traverse over to dependent objects (DOs).

BOPF API Overview

Before we begin coding with the BOPF API, let’s first take a look at its basic structure. The UML class diagram below highlights some of the main classes that make up the BOPF API. At the end of the day, there are three main objects that we’ll be working with to perform most of the operations within the BOPF:

  • /BOBF/IF_TRA_TRANSACTION_MGR
    • This object reference provides a transaction manager which can be used to manage transactional changes. Such transactions could contain a single step (e.g. update node X) or be strung out across multiple steps (add a node, call an action, and so on).
  • /BOBF/IF_TRA_SERVICE_MANAGER
    • The service manager object reference provides us with the methods we need to lookup BO nodes, update BO nodes, trigger validations, perform actions, and so on.
  • /BOBF/IF_FRW_CONFIGURATION
    • This object reference provides us with metadata for a particular BO. We’ll explore the utility of having access to this metadata coming up shortly.

BOPFClassDiagram.png

In the upcoming sections, I’ll show you how these various API classes collaborate in typical BOPF use cases. Along the way, we’ll encounter other useful classes that can be used to perform specific tasks. You can find a complete class listing within package /BOBF/MAIN.

Note: As you’ll soon see, the BOPF API is extremely generic in nature. While this provides tremendous flexibility, it also adds a certain amount of tedium to common tasks. Thus, in many applications, you may find that SAP has elected to wrap the API up in another API that is more convenient to work with. For example, in the SAP EHSM solution, SAP defines an “Easy Node Access” API which simplfies the way that developers traverse BO nodes, perform updates, and so on.

Case Study: Building a Simple Report Program to Manipulate Customer Objects

To demonstrate the BOPF API, we’ll build a custom report program which performs basic CRUD operations on a sample BO provided by SAP: /BOBF/DEMO_CUSTOMER. The figure below shows the makeup of this BO in Transaction /BOBF/CONF_UI.

DEMO_CUSTOMER.png

Our sample program provides a basic UI as shown below. Here, users have the option of creating, changing, and displaying a particular customer using its ID number. Sort of a simplified Transaction XK01-XK03 if you will.

BOPFDemoProgram.png

Getting Started

To drive the application functionality, we’ll create a local test driver class called LCL_DEMO. As you can see in the code excerpt below, this test driver class loads the core BOPF API objects at setup whenever the CONSTRUCTOR method is invoked. Here, the factory classes illustrated in the UML class diagram shown in the previous section are used to load the various object references.

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PRIVATE SECTION.
    DATA mo_txn_mngr TYPE REF TO /bobf/if_tra_transaction_mgr.
    DATA mo_svc_mngr TYPE REF TO /bobf/if_tra_service_manager.
    DATA mo_bo_conf  TYPE REF TO /bobf/if_frw_configuration.

    METHODS:
      constructor RAISING /bobf/cx_frw.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD constructor.
    "Obtain a reference to the BOPF transaction manager:
    me->mo_txn_mngr =
      /bobf/cl_tra_trans_mgr_factory=>get_transaction_manager( ).


    "Obtain a reference to the BOPF service manager:
    me->mo_svc_mngr =
      /bobf/cl_tra_serv_mgr_factory=>get_service_manager(
        /bobf/if_demo_customer_c=>sc_bo_key ).

    "Access the metadata for the /BOBF/DEMO_CUSTOMER BO:
    me->mo_bo_conf =
      /bobf/cl_frw_factory=>get_configuration(
        /bobf/if_demo_customer_c=>sc_bo_key ).
  ENDMETHOD.                 " METHOD constructor
ENDCLASS.

For the most part, this should seem fairly straightforward. However, you might be wondering where I came up with the IV_BO_KEY parameter in the GET_SERVICE_MANAGER() and GET_CONFIGURATION() factory method calls. This value is provided to us via the BO’s constants interface (/BOBF/IF_DEMO_CUSTOMER_C in this case) which can be found within the BO configuration in Transaction /BOBF/CONF_UI (see below). This auto-generated constants interface provides us with a convenient mechanism for addressing a BO’s key, its defined nodes, associations, queries, and so on. We’ll end up using this interface quite a bit during the course of our development.

ConstantsInterface.png

ConstantsInterface2.png

Creating New Customers

Once we have the basic framework in place, we are ready to commence with the development of the various CRUD operations that our application will support. To get things started, we’ll take a look at the creation of a new customer instance. For the most part, this involves little more than a call to the MODIFY() method of the /BOBF/IF_TRA_SERVICE_MANAGER object reference. Of course, as you can see in the code excerpt below, there is a fair amount of setup that we must do before we can call this method.

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      create_customer IMPORTING iv_customer_id
                           TYPE /bobf/demo_customer_id.
  ...
ENDCLASS.

CLASS lcl_demo IMPLEMENTATION.
  METHOD create_customer.
    "Method-Local Data Declarations:
    DATA lo_driver   TYPE REF TO lcl_demo.
    DATA lt_mod      TYPE /bobf/t_frw_modification.
    DATA lo_change   TYPE REF TO /bobf/if_tra_change.
    DATA lo_message  TYPE REF TO /bobf/if_frw_message.
    DATA lv_rejected TYPE boole_d.
    DATA lx_bopf_ex  TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg  TYPE string.


    DATA lr_s_root     TYPE REF TO /bobf/s_demo_customer_hdr_k.
    DATA lr_s_txt      TYPE REF TO /bobf/s_demo_short_text_k.
    DATA lr_s_txt_hdr  TYPE REF TO /bobf/s_demo_longtext_hdr_k.
    DATA lr_s_txt_cont TYPE REF TO /bobf/s_demo_longtext_item_k.


    FIELD-SYMBOLS:
      <ls_mod> LIKE LINE OF lt_mod.


    "Use the BOPF API to create a new customer record:
    TRY.
      "Instantiate the driver class:
      CREATE OBJECT lo_driver.


      "Build the ROOT node:
      CREATE DATA lr_s_root.
      lr_s_root->key = /bobf/cl_frw_factory=>get_new_key( ).
      lr_s_root->customer_id    = iv_customer_id.
      lr_s_root->sales_org      = 'AMER'.
      lr_s_root->cust_curr      = 'USD'.
      lr_s_root->address_contry = 'US'.
      lr_s_root->address        = '1234 Any Street'.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-key         = lr_s_root->key.
      <ls_mod>-data        = lr_s_root.


      "Build the ROOT_TEXT node:
      CREATE DATA lr_s_txt.
      lr_s_txt->key      = /bobf/cl_frw_factory=>get_new_key( ).
      lr_s_txt->text     = 'Sample Customer Record'.
      lr_s_txt->language = sy-langu.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        = /bobf/if_demo_customer_c=>sc_node-root_text.
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-source_node = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-association =

        /bobf/if_demo_customer_c=>sc_association-root-root_text.
      <ls_mod>-source_key  = lr_s_root->key.
      <ls_mod>-key         = lr_s_txt->key.
      <ls_mod>-data        = lr_s_txt.


      "Build the ROOT_LONG_TEXT node:
      "If you look at the node type for this node, you'll notice that
      "it's a "Delegated Node". In other words, it is defined in terms
      "of the /BOBF/DEMO_TEXT_COLLECTION business object. The following
      "code accounts for this indirection.
      CREATE DATA lr_s_txt_hdr.
      lr_s_txt_hdr->key = /bobf/cl_frw_factory=>get_new_key( ).


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node            = /bobf/if_demo_customer_c=>sc_node-root_long_text.
      <ls_mod>-change_mode     = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-source_node     = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-association     =

        /bobf/if_demo_customer_c=>sc_association-root-root_long_text.
      <ls_mod>-source_key      = lr_s_root->key.
      <ls_mod>-key             = lr_s_txt_hdr->key.
      <ls_mod>-data            = lr_s_txt_hdr.


      "Create the CONTENT node:
      CREATE DATA lr_s_txt_cont.
      lr_s_txt_cont->key          = /bobf/cl_frw_factory=>get_new_key( ).
      lr_s_txt_cont->language     = sy-langu.
      lr_s_txt_cont->text_type    = 'MEMO'.
      lr_s_txt_cont->text_content = 'Demo customer created via BOPF API.'.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        =

        lo_driver->mo_bo_conf->query_node(

          iv_proxy_node_name = 'ROOT_LONG_TXT.CONTENT' ).
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-source_node = /bobf/if_demo_customer_c=>sc_node-root_long_text.
      <ls_mod>-source_key  = lr_s_txt_hdr->key.
      <ls_mod>-key         = lr_s_txt_cont->key.
      <ls_mod>-data        = lr_s_txt_cont.

      <ls_mod>-association =
        lo_driver->mo_bo_conf->query_assoc(
          iv_node_key   = /bobf/if_demo_customer_c=>sc_node-root_long_text
          iv_assoc_name = 'CONTENT' ).


      "Create the customer record:
      CALL METHOD lo_driver->mo_svc_mngr->modify
        EXPORTING
          it_modification = lt_mod
        IMPORTING
          eo_change       = lo_change
          eo_message      = lo_message.


      "Check for errors:
      IF lo_message IS BOUND.
        IF lo_message->check( ) EQ abap_true.
          lo_driver->display_messages( lo_message ).
          RETURN.
        ENDIF.
      ENDIF.


      "Apply the transactional changes:
      CALL METHOD lo_driver->mo_txn_mngr->save
        IMPORTING
          eo_message  = lo_message
          ev_rejected = lv_rejected.


      IF lv_rejected EQ abap_true.
        lo_driver->display_messages( lo_message ).
        RETURN.
      ENDIF.


      "If we get to here, then the operation was successful:
      WRITE: / 'Customer', iv_customer_id, 'created successfully.'.
    CATCH /bobf/cx_frw INTO lx_bopf_ex.
      lv_err_msg = lx_bopf_ex->get_text( ).
      WRITE: / lv_err_msg.
    ENDTRY.
  ENDMETHOD.                 " METHOD create_customer
ENDCLASS.

As you can see in the code excerpt above, the majority of the code is devoted to building a table which is passed in the IT_MODIFICATION parameter of the MODIFY() method. Here, a separate record is created for each node row that is being modified (or inserted in this case). This record contains information such as the node object key (NODE), the edit mode (CHANGE_MODE), the row key (KEY) which is an auto-generated GUID, association/parent key information, and of course, the actual data (DATA). If you’ve ever worked with ALE IDocs, then this will probably feel vaguely familiar.

Looking more closely at the population of the node row data, you can see that we’re working with data references which are created dynamically using the CREATE DATA statement. This indirection is necessary since the BOPF API is generic in nature. You can find the structure definitions for each node by double-clicking on the node in Transaction /BOBF/CONF_UI and looking at the Combined Structure field (see below).

CombinedStructure.png

Once the modification table is filled out, we can call the MODIFY() method to insert the record(s). Assuming all is successful, we can then commit the transaction by calling the SAVE() method on the /BOBF/IF_TRA_TRANSACTION_MANAGER instance. Should any errors occur, we can display the error messages using methods of the /BOBF/IF_FRW_MESSAGE object reference which is returned from both methods. This is evidenced by the simple utility method DISPLAY_MESSAGES() shown below. That’s pretty much all there is to it.

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PRIVATE SECTION.
    METHODS:
      display_messages IMPORTING io_message
                            TYPE REF TO /bobf/if_frw_message.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD display_messages.
    "Method-Local Data Declarations:
    DATA lt_messages TYPE /bobf/t_frw_message_k.
    DATA lv_msg_text TYPE string.
    FIELD-SYMBOLS <ls_message> LIKE LINE OF lt_messages.


    "Sanity check:
    CHECK io_message IS BOUND.


    "Output each of the messages in the collection:
    io_message->get_messages( IMPORTING et_message = lt_messages ).
    LOOP AT lt_messages ASSIGNING <ls_message>.
      lv_msg_text = <ls_message>-message->get_text( ).
      WRITE: / lv_msg_text.
    ENDLOOP.
  ENDMETHOD.                 " METHOD display_messages
ENDCLASS.

Performing Customer Queries

If you look closely at the customer creation code illustrated in the previous section, you can see that each node row is keyed by an auto-generated GUID of type /BOBF/CONF_KEY (see below). Since most users don’t happen to have 32-character hex strings memorized, we typically have to resort to queries if we want to find particular BO instances. For example, in our customer demo program, we want to provide a way for users to lookup customers using their customer ID value. Of course, we could have just as easily defined an alternative query selection to pull the customer records.

BOPFNodeKey.png

As we learned in the previous blog post, most BOs come with one or more queries which allow us to search for BOs according to various node criteria. In the case of the /BOBF/DEMO_CUSTOMER business object, we want to use the SELECT_BY_ATTRIBUTES query attached to the ROOT node (see below). This allows us to lookup customers by their ID value.

BOQuery.png

The code excerpt below shows how we defined our query in a method called GET_CUSTOMER_FOR_ID(). As you can see, the query is executed by calling the aptly named QUERY() method of the /BOBF/IF_TRA_SERVICE_MANAGER instance. The query parameters are provided in the form of an internal table of type /BOBF/T_FRW_QUERY_SELPARAM. This table type has a similar look and feel to a range table or SELECT-OPTION. The results of the query are returned in a table of type /BOBF/T_FRW_KEY. This table contains the keys of the node rows that matched the query parameters. In our sample case, there should be only one match, so we simply return the first key in the list.

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PRIVATE SECTION.
    METHODS:
      get_customer_for_id IMPORTING iv_customer_id
                               TYPE /bobf/demo_customer_id
                          RETURNING VALUE(rv_customer_key)
                               TYPE /bobf/conf_key
                            RAISING /bobf/cx_frw.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD get_customer_for_id.
    "Method-Local Data Declarations:
    DATA lo_driver        TYPE REF TO lcl_demo.
    DATA lt_parameters    TYPE /bobf/t_frw_query_selparam.
    DATA lt_customer_keys TYPE /bobf/t_frw_key.
    DATA lx_bopf_ex       TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg       TYPE string.

    FIELD-SYMBOLS <ls_parameter> LIKE LINE OF lt_parameters.
    FIELD-SYMBOLS <ls_customer_key> LIKE LINE OF lt_customer_keys.


    "Instantiate the test driver class:
    CREATE OBJECT lo_driver.


    "Though we could conceivably lookup the customer using an SQL query,
    "the preferred method of selection is a BOPF query:
    APPEND INITIAL LINE TO lt_parameters ASSIGNING <ls_parameter>.
    <ls_parameter>-attribute_name =

      /bobf/if_demo_customer_c=>sc_query_attribute-root-select_by_attributes-customer_id.
    <ls_parameter>-sign           = 'I'.
    <ls_parameter>-option         = 'EQ'.
    <ls_parameter>-low            = iv_customer_id.


    CALL METHOD lo_driver->mo_svc_mngr->query
      EXPORTING
        iv_query_key            =

          /bobf/if_demo_customer_c=>sc_query-root-select_by_attributes
        it_selection_parameters = lt_parameters
      IMPORTING
        et_key                  = lt_customer_keys.


    "Return the matching customer's KEY value:
    READ TABLE lt_customer_keys INDEX 1 ASSIGNING <ls_customer_key>.
    IF sy-subrc EQ 0.
      rv_customer_key = <ls_customer_key>-key.
    ENDIF.
  ENDMETHOD.                 " METHOD get_customer_for_id
ENDCLASS.

Displaying Customer Records

With the query logic now in place, we now know which customer record to lookup. The question is, how do we retrieve it? For this task, we must use the

RETRIEVE() and RETRIEVE_BY_ASSOCIATION() methods provided by the /BOBF/IF_TRA_SERVICE_MANAGER instance. As simple as this sounds, the devil is in the details. Here, in addition to constructing the calls to the RETRIEVE*() methods, we must also dynamically define the result tables which will be used to store the results.

As you can see in the code excerpt below, we begin our search by accessing the customer ROOT node using the RETRIEVE() method. Here, the heavy lifting is performed by the GET_NODE_ROW() and GET_NODE_TABLE() helper methods. Looking at the implementation of the GET_NODE_TABLE() method, you can see how we’re using the /BOBF/IF_FRW_CONFIGURATION object reference to lookup the node’s metadata. This metadata provides us with the information we need to construct an internal table to house the results returned from the RETRIEVE() method. The GET_NODE_ROW() method then dynamically retrieves the record located at the index defined by the IV_INDEX parameter.

Within the DISPLAY_CUSTOMER() method, we get our hands on the results by performing a cast on the returned structure reference. From here, we can access the row attributes as per usual.

After the root node has been retrieved, we can traverse to the child nodes of the /BOBF/DEMO_CUSTOMER object using the RETRIEVE_BY_ASSOCIATION() method. Here, the process is basically the same. The primary difference is in the way we lookup the association metadata which is used to build the call to RETRIEVE_BY_ASSOCIATION(). Once again, we perform a cast on the returned structure reference and display the sub-node attributes from there.

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      display_customer IMPORTING iv_customer_id
                            TYPE /bobf/demo_customer_id.


  PRIVATE SECTION.

    METHODS:
      get_node_table IMPORTING iv_key TYPE /bobf/conf_key
                               iv_node_key TYPE /bobf/obm_node_key
                               iv_edit_mode TYPE /bobf/conf_edit_mode

                                         DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                     RETURNING VALUE(rr_data) TYPE REF TO data
                       RAISING /bobf/cx_frw,


      get_node_row IMPORTING iv_key TYPE /bobf/conf_key
                             iv_node_key TYPE /bobf/obm_node_key
                             iv_edit_mode TYPE /bobf/conf_edit_mode

                               DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                             iv_index TYPE i DEFAULT 1
                   RETURNING VALUE(rr_data) TYPE REF TO data
                     RAISING /bobf/cx_frw,


      get_node_table_by_assoc IMPORTING iv_key TYPE /bobf/conf_key
                                        iv_node_key TYPE /bobf/obm_node_key
                                        iv_assoc_key TYPE /bobf/obm_assoc_key
                                        iv_edit_mode TYPE /bobf/conf_edit_mode

                                          DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                              RETURNING VALUE(rr_data) TYPE REF TO data
                                RAISING /bobf/cx_frw,


      get_node_row_by_assoc IMPORTING iv_key TYPE /bobf/conf_key
                                      iv_node_key TYPE /bobf/obm_node_key
                                      iv_assoc_key TYPE /bobf/obm_assoc_key
                                      iv_edit_mode TYPE /bobf/conf_edit_mode

                                        DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                                      iv_index TYPE i DEFAULT 1
                            RETURNING VALUE(rr_data) TYPE REF TO data
                              RAISING /bobf/cx_frw.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD display_customer.
    "Method-Local Data Declarations:
    DATA lo_driver       TYPE REF TO lcl_demo.
    DATA lv_customer_key TYPE /bobf/conf_key.
    DATA lx_bopf_ex      TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg      TYPE string.

    DATA lr_s_root TYPE REF TO /bobf/s_demo_customer_hdr_k.
    DATA lr_s_text TYPE REF TO /bobf/s_demo_short_text_k.


    "Try to display the selected customer:
    TRY.
      "Instantiate the test driver class:
      CREATE OBJECT lo_driver.


      "Lookup the customer's key attribute using a query:
      lv_customer_key = lo_driver->get_customer_for_id( iv_customer_id ).


      "Display the header-level details for the customer:
      lr_s_root ?=
        lo_driver->get_node_row(

                  iv_key = lv_customer_key
                  iv_node_key = /bobf/if_demo_customer_c=>sc_node-root
                  iv_index = 1 ).


      WRITE: / 'Display Customer', lr_s_root->customer_id.
      ULINE.
      WRITE: / 'Sales Organization:', lr_s_root->sales_org.
      WRITE: / 'Address:', lr_s_root->address.
      SKIP.


      "Traverse to the ROOT_TEXT node to display the customer short text:
      lr_s_text ?=
        lo_driver->get_node_row_by_assoc(

          iv_key = lv_customer_key
          iv_node_key = /bobf/if_demo_customer_c=>sc_node-root
          iv_assoc_key = /bobf/if_demo_customer_c=>sc_association-root-root_text
          iv_index = 1 ).
      WRITE: / 'Short Text:', lr_s_text->text.
    CATCH /bobf/cx_frw INTO lx_bopf_ex.
      lv_err_msg = lx_bopf_ex->get_text( ).
      WRITE: / lv_err_msg.
    ENDTRY.
  ENDMETHOD.                 " METHOD display_customer


  METHOD get_node_table.
    "Method-Local Data Declarations:
    DATA lt_key       TYPE /bobf/t_frw_key.
    DATA ls_node_conf TYPE /bobf/s_confro_node.
    DATA lo_change    TYPE REF TO /bobf/if_tra_change.

    DATA lo_message   TYPE REF TO /bobf/if_frw_message.


    FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.
    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.


    "Lookup the node's configuration:
    CALL METHOD mo_bo_conf->get_node
      EXPORTING
        iv_node_key = iv_node_key
      IMPORTING
        es_node     = ls_node_conf.


    "Use the node configuration metadata to create the result table:
    CREATE DATA rr_data TYPE (ls_node_conf-data_table_type).
    ASSIGN rr_data->* TO <lt_data>.


    "Retrieve the target node:
    APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.
    <ls_key>-key = iv_key.


    CALL METHOD mo_svc_mngr->retrieve
      EXPORTING
        iv_node_key = iv_node_key
        it_key      = lt_key
      IMPORTING
        eo_message  = lo_message
        eo_change   = lo_change
        et_data     = <lt_data>.


    "Check the results:
    IF lo_message IS BOUND.
      IF lo_message->check( ) EQ abap_true.
        display_messages( lo_message ).
        RAISE EXCEPTION TYPE /bobf/cx_dac.
      ENDIF.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_table


  METHOD get_node_row.
    "Method-Local Data Declarations:
    DATA lr_t_data TYPE REF TO data.


    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.
    FIELD-SYMBOLS <ls_row> TYPE ANY.


    "Lookup the node data:
    lr_t_data =
      get_node_table( iv_key       = iv_key
                      iv_node_key  = iv_node_key
                      iv_edit_mode = iv_edit_mode ).


    IF lr_t_data IS NOT BOUND.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.


    "Try to pull the record at the specified index:
    ASSIGN lr_t_data->* TO <lt_data>.
    READ TABLE <lt_data> INDEX iv_index ASSIGNING <ls_row>.
    IF sy-subrc EQ 0.
      GET REFERENCE OF <ls_row> INTO rr_data.
    ELSE.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_row


  METHOD get_node_table_by_assoc.
    "Method-Local Data Declarations:
    DATA lt_key         TYPE /bobf/t_frw_key.
    DATA ls_node_conf   TYPE /bobf/s_confro_node.
    DATA ls_association TYPE /bobf/s_confro_assoc.
    DATA lo_change      TYPE REF TO /bobf/if_tra_change.
    DATA lo_message     TYPE REF TO /bobf/if_frw_message.


    FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.
    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.


    "Lookup the association metadata to find out more
    "information about the target sub-node:
    CALL METHOD mo_bo_conf->get_assoc
      EXPORTING
        iv_assoc_key = iv_assoc_key
        iv_node_key  = iv_node_key
      IMPORTING
        es_assoc     = ls_association.


    IF ls_association-target_node IS NOT BOUND.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.


    "Use the node configuration metadata to create the result table:

    ls_node_conf = ls_association-target_node->*.


    CREATE DATA rr_data TYPE (ls_node_conf-data_table_type).
    ASSIGN rr_data->* TO <lt_data>.


    "Retrieve the target node:
    APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.
    <ls_key>-key = iv_key.


    CALL METHOD mo_svc_mngr->retrieve_by_association
      EXPORTING
        iv_node_key    = iv_node_key
        it_key         = lt_key
        iv_association = iv_assoc_key
        iv_fill_data   = abap_true
      IMPORTING
        eo_message     = lo_message
        eo_change      = lo_change
        et_data        = <lt_data>.


    "Check the results:
    IF lo_message IS BOUND.
      IF lo_message->check( ) EQ abap_true.
        display_messages( lo_message ).
        RAISE EXCEPTION TYPE /bobf/cx_dac.
      ENDIF.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_table_by_assoc


  METHOD get_node_row_by_assoc.
    "Method-Local Data Declarations:
    DATA lr_t_data TYPE REF TO data.


    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.
    FIELD-SYMBOLS <ls_row> TYPE ANY.


    "Lookup the node data:
    lr_t_data =
      get_node_table_by_assoc( iv_key       = iv_key
                               iv_node_key  = iv_node_key
                               iv_assoc_key = iv_assoc_key
                               iv_edit_mode = iv_edit_mode ).


    IF lr_t_data IS NOT BOUND.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.


    "Try to pull the record at the specified index:
    ASSIGN lr_t_data->* TO <lt_data>.
    READ TABLE <lt_data> INDEX iv_index ASSIGNING <ls_row>.
    IF sy-subrc EQ 0.
      GET REFERENCE OF <ls_row> INTO rr_data.
    ELSE.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_row_by_assoc
ENDCLASS.


Note: In this simple example, we didn’t bother to drill down to display the contents of the ROOT_LONG_TEXT node. However, if we had wanted to do so, we would have needed to create a separate service manager instance for the /BOBF/DEMO_TEXT_COLLECTION business object since the data within that node is defined by that delegated BO as opposed to the /BOBF/DEMO_CUSTOMER BO. Otherwise, the process is the same.

Modifying Customer Records

The process of modifying a customer record essentially combines logic from the display and create functions. The basic process is as follows:

  1. First, we perform a query to find the target customer record.
  2. Next, we use the RETRIEVE*() methods to retrieve the node rows we wish to modify. Using the returned structure references, we can modify the target attributes using simple assignment statements.
  3. Finally, we collect the node row changes into the modification table that is fed into MODIFY() method provided by the /BOBF/IF_TRA_SERVICE_MANAGER instance.

The code excerpt below shows how the changes are carried out. Here, we’re simply updating the address string on the customer. Of course, we could have performed wholesale changes if we had wanted to.

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      change_customer IMPORTING iv_customer_id
                           TYPE /bobf/demo_customer_id.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD change_customer.
    "Method-Local Data Declarations:
    DATA lo_driver       TYPE REF TO lcl_demo.
    DATA lv_customer_key TYPE /bobf/conf_key.
    DATA lt_mod          TYPE /bobf/t_frw_modification.
    DATA lo_change       TYPE REF TO /bobf/if_tra_change.
    DATA lo_message      TYPE REF TO /bobf/if_frw_message.
    DATA lv_rejected     TYPE boole_d.
    DATA lx_bopf_ex      TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg      TYPE string.


    FIELD-SYMBOLS:
      <ls_mod> LIKE LINE OF lt_mod.


    DATA lr_s_root TYPE REF TO /bobf/s_demo_customer_hdr_k.


    "Try to change the address on the selected customer:
    TRY.
      "Instantiate the test driver class:
      CREATE OBJECT lo_driver.


      "Access the customer ROOT node:
      lv_customer_key = lo_driver->get_customer_for_id( iv_customer_id ).


      lr_s_root ?=
        lo_driver->get_node_row( iv_key = lv_customer_key
                                 iv_node_key = /bobf/if_demo_customer_c=>sc_node-root
                                 iv_edit_mode = /bobf/if_conf_c=>sc_edit_exclusive
                                 iv_index = 1 ).


      "Change the address string on the customer:
      lr_s_root->address = '1234 Boardwalk Ave.'.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_update.
      <ls_mod>-key         = lr_s_root->key.
      <ls_mod>-data        = lr_s_root.


      "Update the customer record:
      CALL METHOD lo_driver->mo_svc_mngr->modify
        EXPORTING
          it_modification = lt_mod
        IMPORTING
          eo_change       = lo_change
          eo_message      = lo_message.


      "Check for errors:
      IF lo_message IS BOUND.
        IF lo_message->check( ) EQ abap_true.
          lo_driver->display_messages( lo_message ).
          RETURN.
        ENDIF.
      ENDIF.


      "Apply the transactional changes:
      CALL METHOD lo_driver->mo_txn_mngr->save
        IMPORTING
          eo_message  = lo_message
          ev_rejected = lv_rejected.


      IF lv_rejected EQ abap_true.
        lo_driver->display_messages( lo_message ).
        RETURN.
      ENDIF.


      "If we get to here, then the operation was successful:
      WRITE: / 'Customer', iv_customer_id, 'updated successfully.'.
    CATCH /bobf/cx_frw INTO lx_bopf_ex.
      lv_err_msg = lx_bopf_ex->get_text( ).
      WRITE: / lv_err_msg.
    ENDTRY.
  ENDMETHOD.                 " METHOD change_customer
ENDCLASS.

Next Steps

I often find that the best way to learn a technology framework is to see how it plays out via code. At this level, we can clearly visualize the relationships between the various entities and see how they perform at runtime. Hopefully after reading this post, you should have a better understanding of how all the BOPF pieces fit together. In my next blog post, we’ll expand upon what we’ve learned and consider some more advanced features of the BOPF API.

Assigned Tags

      67 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Aliaksandr Shchurko
      Aliaksandr Shchurko

      Thanks.

      Author's profile photo Former Member
      Former Member

      Very much helpful , thanks !

      Author's profile photo Former Member
      Former Member

      Very helpful. Could you please provide me the code to read the long text as well.

       

      Thanks

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      The complete code bundle can be downloaded from here:

      http://www.bowdark.com/downloads/BOPFDemoProgram.zip

       

      Thanks.

      Author's profile photo Former Member
      Former Member

      James,

       

      I have downloaded the full program. Neither the full program nor the blog post have the logic to read the long text from node ROOT_LONG_TEXT. Could you please share that logic if you have so.

       

      Thanks

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Ah, I missed the long text part. Please find an updated version of the code at the download link above. I made some wholesale changes to the code so that it's cleaner and more organized. Alas, the original demo was rather primitive. Let me know if you have any further questions. Thanks.

      Author's profile photo Former Member
      Former Member

      James,

      Thank you so much for the complete code.

      Author's profile photo Former Member
      Former Member

      Hi James,

       

      This blog seems very helpful, so I'd like to start at the beginning of the series.  Unfortunately the link in your first paragraph is broken.  Any idea where I can find it?

       

      Thanks,

      Eric

      Author's profile photo Former Member
      Former Member
      Author's profile photo Former Member
      Former Member

      Hi James

      Great blogs, starting to give me some understanding of BOPF.

       

      I have a question though. I have a requirement for my client for a monster "super query" against the EHHSS_INCIDENT business object which basically would allow them to query all Incidents by any combination of any selection criteria from any nodes!

       

      I have already told them I think this is out of the question.  However I want to at least be able to give them something.  My main challenge is that it seems that Queries are attached to nodes, but what I really want is a cross-node query.

       

      For example there is one node called "Persons involved"  (table EHHSSD_INC_PINV) and another node called "Person Role" (table EHHSSD_INC_PROLE).  This would allow me to query the persons involved in an Incident (selecting by John Smith for example) or the roles of people involved in an incident (eg Witness).   But what it does not allow me to do is to query the Incidents where John Smith is a Witness.  To do that I have to use a foreign key relationship in a GUID on EHHSSD_INC_PROLE to point to the DB_KEY on EHHSSD_INC_PINV.

       

      So my main question is: Is it possible to do cross-node queries?  If so how?

       

      I thought about creating a database view as a join of the two tables, but then I don't know how to hook this database view onto the BO processing framework and how to attach queries to it. Or is the way of having a transient structure which is a join of the two tables & somehow hook this into database retrieval & queries.

       

      Would really appreciate some guidance on this.

      Thanks

      Andy

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Andrew,

       

      To answer your question: yes. For your specific use case(s), I think the approach would be to create custom queries. So, for example, if you want to create a query in which you pull all incidents where John Smith is a witness, I think you'd simply want to create a custom query against the ROOT node. Here, you'd have a data structure to capture the selection criteria (e.g. person name and role) and a custom class to implement the query logic using a SQL JOIN. A decent example of this would be the SELECT_BY_OSHA_CRIT query defined against the PERSON_INVOLVED node. Hope this helps.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Thanks James, good to know it is possible.  I'll be giving it a try over the next few days.

      Author's profile photo Zhenbo Wang
      Zhenbo Wang

      Great article, thanks. I am just beginning to like BOPF.

      Author's profile photo Former Member
      Former Member

      Hi James,

       

      Thank you for publishing such an informative blog series to understand the BOPF.

       

      I am facing one error in the program. Can you guide please?

       

      I am trying to create a chemical using BO EHFND_CHEMICAL using program. Referring the sample code listed in your blog.

      I have passed ROOT node and BI_CHEMICAL nodes. But I am getting error 'Mandatory node ROLE is missing'.

      When I tried to add ROLE node , in the code its giving errors like 'Mandatory node ROLE is missing' OR can not create, source object does not exist'.

       

      I went through the node structure for the BO. In it we have Root - > Revision -> Role.

      So my query is how to pass information for ROLE node? We need to add REVISION node also?

       

      If you can provide the code will be very helpful.

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Shubhada,

       

      Yes, you'll need to create REVISION node, too.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Thanks James.

       

      I have added REVISION node as below but still getting error 'Mandetory node ROLE missing'.

       

      *"Build the Revision node:

            CREATE DATA lr_s_revision.

            lr_s_revision->key      = /bobf/cl_frw_factory=>get_new_key( ).

            lr_s_revision->root_KEY = lr_s_root->key.

       

            APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

            <ls_mod>-node        = IF_EHFND_CHM_C=>sc_node-revision.

            <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.

            <ls_mod>-source_node = IF_EHFND_CHM_C=>sc_node-root.

            <ls_mod>-association =

       

              IF_EHFND_CHM_C=>sc_association-root-revision.

            <ls_mod>-source_key  = lr_s_root->key.

            <ls_mod>-key         = lr_s_revision->key.

            <ls_mod>-data        = lr_s_revision.

       

       

      *        "Build the ROLE node:

              CREATE DATA lr_s_role.

              lr_s_role->key      = /bobf/cl_frw_factory=>get_new_key( ).

              lr_s_role->PARENT_KEY = lr_s_revision->key.

              lr_s_role->ROOT_KEY = lr_s_root->key.

              lr_s_role->chemical_role    = '1'.

       

              APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

              <ls_mod>-node        = if_ehfnd_chm_c=>sc_node-role.

              <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.

              <ls_mod>-source_node = if_ehfnd_chm_c=>sc_node-revision.

              <ls_mod>-association =

       

                if_ehfnd_chm_c=>sc_association-revision-role.

              <ls_mod>-source_key  = lr_s_revision->key.

              <ls_mod>-root_key  = lr_s_root->key.

              <ls_mod>-key         = lr_s_role->key.

              <ls_mod>-data        = lr_s_role.

       

      Can you guide me please?

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Shubhada,

       

      What version of EHSM are you on? I'm looking at an EHSM 3.0 system with SP 4 installed and I don't see a REVISION or ROLE node available in EHFND_CHEMICAL. These types of nodes are not uncommon to master data objects, so it wouldn't surprise me that they were added in a later release of the software. However, as I can't see the nodes myself, it's hard to speculate what the error condition might be. At a glance, your code above looks to be correct...

       

      One thing I might suggest is to look closely at the contents of EO_MESSAGE after you attempt to modify and/or save the BO. Here, I'd recommend scanning through the MT_MESSAGE table to find the error message in question and see if the NODE_KEY/VAL_KEY fields are populated. This might give you more of a clue about where the error condition is emanating from. Hope this helps.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Hi James,

       

      Thank you for the reply.

       

      I am using SAP EHS Management Extension 4.0, release 400.

       

      I will try to look at the MT_MESSAGE as suggested by you.

       

      Thank you.

      Author's profile photo Former Member
      Former Member

      Very good in detail information for techies.

       

      Thank you very much and well done.

       

      Regards,

      Surender reddy

      Author's profile photo Bob Varghese
      Bob Varghese

      Hi James,

       

      The above blog regarding BOPF is really good.

      Thanks for sharing your insight in BOPF.

       

      Regards,

      Bob.

      Author's profile photo Paul Hardy
      Paul Hardy

      Mr.James,

       

      Here is a very technical question about the mechanism whereby a number (like a customer number) gets turned into a GUID type key.

       

      When I debug the SERVICE_MANAGER->QUERY method I see that a fuly dynamic SQL statement is being built up and then the database table queried using the customer number.

       

      As there is no index on custmer number I would expect a full table scan to occur, and the performance to be dreadful. Yet this does not seem to be the case - performance is OK and the ST05 trace did not say "full table scan" but some Oracle gobbledegook I had not seen before.

       

      Is there some black magic at work here to get around the fact you are selecting on a field where there is no index?

       

      Cheersy Cheers

       

      Paul

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Paul,

       

      So am I correct in assuming that you're testing with the /BOBF/DEMO_CUSTOMER business object demonstrated in this blog post? If so, I'm seeing that SAP has in fact created an index on the CUSTOMER_ID field in the table behind the ROOT node (/BOBF/DM_CST_HDR). Are you seeing something different on your end?

       

      In general, I would say that there's nothing real special going on with these node attribute queries. When you get past all of the dynamic code, the SQL queries executed by the BOPF runtime are basically OpenSQL as per usual.

       

      Anyway, I hope this helps. If I'm off base with my analysis here, let me know a little more details about what you're testing with and I'll dig a little deeper.

       

      Thanks,

       

      James

      Author's profile photo Paul Hardy
      Paul Hardy

      As a test I had created my own object with a "number" field which is what the human in front of the computer would use to search for the object. An invoice number let us say.

       

      As the primary key was the GUID and I deliberately did not put an index on the "number" then I expected the SQL trace to say "full table scan".

       

      I actually got something like "ROW STOPKEY" which I think means the database looks at every sinlge record in the table until it finds a match and then stops, which is in effect a full table scan.

       

      I was just wondering if there was anything magic happening here, but it seems not.

       

      This does throw into question the entire wisdom of having a database table with a GUID as the primary key - if you have an object where people are always going to search by number - invoices are a great example - then isn't having two indexes - the primary key and the index on the number - just a doubling up of resources?

       

      I know in HANA world this is not going to matter, but realistically most people are not going to be there any time soon.

       

      Cheersy Cheers

       

      Paul

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Paul,

       

      OK, I'm with you now. You make a good point here on the wisdom of using GUIDs vs. semantic keys. In standalone environments, I frequently find this approach to be painful to work with (CRM comes to mind). In the dynamic world of the BOPF though, I think that the choice to use GUIDs actually makes a lot of sense. Being able to traverse from node to node by joining on PARENT.DB_KEY = CHILD.PARENT_KEY makes it very easy to build generic BO query frameworks where the performance is actually quite good. The primary overhead is when you hit the header table which would normally require an index on the semantic key. I suppose anytime you build a framework like this, there's going to be some overhead, but in my mind, what they have here is pretty manageable. Anyway, my two cents.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Hi James

       

      I am new to BOPF, and trying to set up a condition in TM using BOPF. Standard SAP provide a BO /SCMTMS/SUPPLIER which is a Master data Object. I can read a Carrier in run time using this. There is another BO /SCMTMS/TOR, where I can read the data from a freight order for example Customer (in ConsigneeID field).

       

      So when in SAP TM, I select a carrier to be assigned to a Freight Order, I can read Carrier separately using /SCMTMS/SUPPLIER and Customer from the FO separately under /SCMTMS/TOR. Once the carrier is assigned and FO is saved, I can read the carrier under /SCMTMS/TOR as well under TSP_ID but before that TSP_ID is blank.

       

      My requirement is to read the "carrier to be assigned" under  /SCMTMS/TOR before FO is saved, so that I can check a condition between customer and carrier before saving it. In other words I want to read the Partner value of  /SCMTMS/SUPPLIER (Master data Object) in /SCMTMS/TOR in Business Process Object. Is this feasible? How to achieve this. Looking forward for your response. Thanks for the Help.

       

      Regards

      Pankaj

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Pankaj,

       

      I haven't worked with TM before, nor do I have access to a TM system, so I can only speculate on some of this. Some questions:

       

      1. Am I correct in assuming that the /SCMTMS/SUPPLIER BO is linked to the /SCMTMS/TOR BO via a cross-business object (XBO) association?
      2. Is TSP_ID a (transient) attribute defined somewhere underneath the /SCMTMS/TOR BO node hierarchy?

       

      If my assumptions above are correct, I expect that you should be able to back track from the XBO association class to figure out how the two BOs are physically linked. If the carrier's being identified before the save event occurs, I'd expect that you'd be able to get your hands on the foreign key to the carrier somewhere inside the /SCMTMS/TOR BO. From here, you may need to enhance/create a determination to preemptively fill the TSP_ID field using a SQL query based on the selected carrier key.

       

      Again, I'm sort of flying blind here, so let me know if I'm off base or need further clarification. Hope this helps.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Hi James,

       

      Thanks for your reply..

      For the ease of understanding you can consider Freight order as a Sales Order and Carrier as a TSP (transport service provider partner which is not yet enter in the SO) and consignee as a ship to party.. Suppose you have saved the sales order with out the partner.. So you can see the Order number in VBAK table, you can see the Ship to party also in VBPA. But Since the carrier partner TSP is not yet assigned in the SO, it will be not be there in VBPA, although it exists as a master data in LFA1 table.

       

      Now Consider LFA1 as /SCMTMS/SUPPLIER BO which is just a master data, and VBAK/PA as /SCMTMS/TOR BO which is the transaction data. When I pass the SO number in VBPA table, I can read ship to party, and when I pass the carrier number in LFA1, I can read the Carrier number from there.

       

      Similarly I am using a data access determination using /SCMTMS/SUPPLIER (~LFA1) in a condition Cond1 and dad using /SCMTMS/TOR (~VBPA)  in a condition Con2, when I pass the Carrier in cond1, I can read the carrier there and when I pass the freight order number in cond2, I can read ship to (consignee) in cond2, but since they are read in two different condition, I am not able to do some logical operations on them..

       

      So I want to read both (Carrier to be assigned) and the Consignee under one condition. To do so I am trying to create a dad for /SCMTMS/SUPPLIER, and dad for /SCMTMS/TOR under same condition. Technically its not possible to so I am trying to read the  /SCMTMS/SUPPLIER in  /SCMTMS/TOR using some association and data crawler.

       

      The TSP_ID field stores the carrier for a freight order and is directly under the ROOT node of TOR BO. But it can be read only once the Carrier is entered and freight order is saved with it.
      /SCMTMS/SUPPLIER is a master data BO, and stores the Carrier under ROOT node in Partner field.

       

      I tried to find an association in Trxn BOPF for /SCMTMS/TOR and I could find an association named BO_TSP_ROOT, But I am not sure if it links with /SCMTMS/SUPPLIER or not, don't know how to check it.

       

      I am looking for your help to find out more insight about association, and how to see what how two BO nodes associate.. and In case there in no association, is there any mechanism to read the Master data from a Master data node into the Business process Object in run time?

       

      Sorry for such a lengthy post, I appreciate your help and patiently helping me out here.

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Pankaj,

       

      This all makes logical sense. One question though: in your description above you mention that the carrier is not yet assigned to the freight order. Assuming that's the case, I'm curious to understand when the condition you're building is supposed to fire? Am I correct in assuming that you want this to start kicking in at the point when the carrier's assigned but before the freight order's saved?

       

      Anyway, can you send some screenshots in BOPF of the /SCMTMS/TOR BO? Looking specifically for screenshots with the expanded node hierarchy, association definitions, etc. That would help point you in the right direction I think.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      1.JPG

       

      2.JPG

       

       

      3.JPG

       

       

      Hi James

       

      Actually its related to one Incompatibility setting, where when I select a Carrier and Freight Order, System checks the Carrier and Consignee (assigned in FO), and based on the condition result either allow or gives error. Standard SAP has given two separate condition for Carrier and FO, hence I have some limitation. I am trying to club both under one condition. My guess was that BO_TSP_ROOT can be one association, but somehow its not working as its not the Master data BO..

       

      Thanks

      Pankaj

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Can you also please send me a screenshot of the BO_TSP_ROOT association (highlighted above) when you double-click on it? In that definition, you should get a sense for how these two BOs are related. From here, perhaps we can backtrack and see if we can artificially build a linkage to satisfy your condition.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Hi James

      The association is already there in the picture, If you click open it. Its bigger image hence not visible comment box..

       

      Regards

      Pankaj

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Yes, but what we need is the details around the association. For instance, is there an association class defined? On the Association Binding tab, what do the attribute bindings look like? To get where you want to go, you'll need to figure out how to hoist the carrier ID up to a point where you can access it in your condition. So, you may have to create another determination to achieve this which utilizes similar logic to the association definition.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Hi James

       

      Thanks once again for helping me. I am adding some screenshots around association.

       

      The association class is /BOBF/CL_LIB_C_CROSS_BO

       

      Please let me know If you are looking for anything else.

       

      Regards

      Pankaj

      4.JPG

      5.JPG

      7.JPG

       

      6.JPG

      8.JPG

       

      9.JPG

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Pankaj,

       

      Given the way this association is defined, I'm thinking that you may have to get clever with this. I'm thinking something along the lines of the following:

       

      1. Implement some enhancement logic to intercept the carrier assignment event and store the selected carrier ID in a shared memory object (which internally uses a hash table to associate the carrier ID with the corresponding FO).
      2. Create a transient attribute on the root node of the freight order BO to expose the carrier ID.
      3. Create a custom determination to populate the transient attribute with the carrier ID value from shared memory.

       

      I think this should allow you to access the carrier ID from your condition record before the FO is saved. What do you think?

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Hello Pankaj,

       

      I am a SAP Technical guy new to TMS...We have a bunch of inbound EDI coming into TMS...can you tell me if TMS has IDOC's built in it like ECC to handle these Inbound EDI or else how are they posted in TMS?

      Any help would be appreciated

       

      Thanks
      Ram

      Author's profile photo Former Member
      Former Member

      Hi James

      I am not very strong in technical area specially in BOPF as I mostly work in functional side only, but If I translate what I understood in my language, its a 3 step process

       

      1.- I need to read the Carrier data in a custom Table (at the time of carrier creation event, the carrier will be stored in /scmtms/supplier as well as in this custom table)

      2. - Maintain a custom defined field on BO node of Freight order for carrier.

      and

      3. - then read from there at the run time while saving the FO using the custom determination..

      Please let me know if this is correct understanding.. So could you please help with the steps to do it. I will try and see if this works. Also I can use this idea in other problems as well.

      I also have one basic query.. Once I find the association for a BO node, how can I find out what other BO nodes its associated to..

       

      Thanks

       

      Pankaj

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Pankaj,

       

      My thought with point #1 was to capture the event when the carrier is associated with the freight order (but before the FO is saved). Based on what you commented earlier (see below), I gathered that this was the gap you were struggling with: figuring out how to read the carrier ID in a condition before the FO is saved. The logic described above was intended to provide you with a separate field which makes it easy to link up the FO and carrier data from within the FO business object. Am I off base here?

       

      ...Once the carrier is assigned and FO is saved, I can read the carrier under /SCMTMS/TOR as well under TSP_ID but before that TSP_ID is blank...


      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      No James, you are absolutely right.. I think I misunderstood pt. 1.. So how to proceed on this?

       

      Thanks and regards

      Pankaj

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Pankaj,

       

      The next step is to start writing code. Perhaps there's a developer on your project you can work with to take this concept to realization. Best of luck with the project.

       

      Thanks,

       

      James

      Author's profile photo Former Member
      Former Member

      Sure James.. I will check with the development team here..
      Thanks a lot for your valuable suggestions on this topic.. I will keep you updated if this works.

       

      Thanks and Regards

      Pankaj

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Shakeel,

       

      The issue with your code is in the way you're creating the PERSON_INVOLVED record. For both records, you're mapping SY-UNAME to the PERSON_INVOLVED.ID field. To create separate records, you need to map different IDs for each distinct person. Here, you have three different types to choose from:

       

      Employee Types (A + {pernr})

      Business Partner Types (B + {bupa})

      User Types (D + {user ID})

       

      Without knowing a ton about your use case, it would seem that if all you have to go on is contact information, you probably will have to either use that information to look up one of the three person types listed above or create an ad hoc business partner on the fly. Regardless of the path you take, the resultant ID is what you would plug into the ID field. Do that, and I think you're on the right track.

       

      Thanks,

       

      James

      Author's profile photo Md Shakeel Ahmed
      Md Shakeel Ahmed

      Hi James,

       

      Thanks for quick replay.

       

      I have tried by your above inputs and know two Involved person are getting created.

       

      But how to create the REPORTING PERSON under the same INVOLVED_PERSON node. I.e mean who is creating the incident.

       

      I am trying to create the three INVOLVED PERSONS

       

      1.Reporting Person

      2.Injured Person

      3. Witness Person

       

      Thanks a lot.

       

      Regards,

      Shakeel.

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      The reporting person would be created just like all the others. In this case though, you'd probably want to use the CL_EHFND_PARTY_PROXY class' CONVERT_USER_NAME_TO_PARTY_KEY() method to convert SY-UNAME into an EHSM party key. Then, plug that key into the ID field and assign the reporting person role in the PERSON_ROLE node. Thanks.

      Author's profile photo Md Shakeel Ahmed
      Md Shakeel Ahmed

      Hi James,

       

      Thanks for reply.

       

      I have followed the above steps and resolved my issue.

       

      I have  another question to ask about the delegated node like 'NEAR_MISS_DESC' and 'INJURY_ILLNESS_BP_DESC'  in EHHSS_INCIDENT.

       

       

      I wrote this code to create the injury description but it is not updating.

       

       

             CREATE DATA lr_s_inj_info.

             lr_s_inj_info->key = /bobf/cl_frw_factory=>get_new_key( ).

             lr_s_inj_info->oc_inc_type = 'EHHSS_OIT_ACC_ON_WAY'.

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node            = if_ehhss_inc_c=>sc_node-person_inj_info.

             <ls_mod>-change_mode     = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node     = if_ehhss_inc_c=>sc_node-person_involved.

             <ls_mod>-association     = if_ehhss_inc_c=>sc_association-person_involved-person_inj_info.

             <ls_mod>-root_key        = lr_s_root->key.

             <ls_mod>-source_key      = lr_s_per_inv->key.

             <ls_mod>-key             = lr_s_inj_info->key.

             <ls_mod>-data            = lr_s_inj_info.

       

             CREATE DATA lr_s_injury_illness.

             lr_s_injury_illness->key = /bobf/cl_frw_factory=>get_new_key( ).

             lr_s_injury_illness->inj_ill = 'EHHSS_ILLC_INJ'.

      *      lr_s_INJURY_ILLNESS->TYPE = 'EHHSS_OIT_ACC_ON_WAY'.

             lr_s_injury_illness->type_desc = desc.   " Description

             lr_s_injury_illness->type_desc = 'DESCR'.

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node            = if_ehhss_inc_c=>sc_node-injury_illness.

             <ls_mod>-change_mode     = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node     = if_ehhss_inc_c=>sc_node-person_inj_info.

             <ls_mod>-association     = if_ehhss_inc_c=>sc_association-person_inj_info-injury_illness.

             <ls_mod>-root_key        = lr_s_root->key.

             <ls_mod>-source_key      = lr_s_inj_info->key.

             <ls_mod>-key             = lr_s_injury_illness->key.

             <ls_mod>-data            = lr_s_injury_illness.

       

             CREATE DATA lr_s_root_txt.

             lr_s_root_txt->key = /bobf/cl_frw_factory=>get_new_key( ).

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node            = if_ehhss_inc_c=>sc_node-INJURY_ILLNESS_DESC.

             <ls_mod>-change_mode     = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node     = if_ehhss_inc_c=>sc_node-INJURY_ILLNESS.

             <ls_mod>-association     = if_ehhss_inc_c=>sc_association-INJURY_ILLNESS-injury_illness_desc.

             <ls_mod>-root_key        = lr_s_root->key.

             <ls_mod>-source_key      = lr_s_INJURY_ILLNESS->key.

             <ls_mod>-key             = lr_s_root_txt->key.

             <ls_mod>-data            = lr_s_root_txt.

       

             create data lr_s_INJURY_ILLNESS_DESC.

             lr_s_INJURY_ILLNESS_DESC->key = /bobf/cl_frw_factory=>get_new_key( ).

             lr_s_INJURY_ILLNESS_DESC->TEXT_EXISTS_IND = 'X'.

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node            = if_ehhss_inc_c=>sc_node-INJURY_ILLNESS_DESC.

             <ls_mod>-change_mode     = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node     = if_ehhss_inc_c=>sc_node-INJURY_ILLNESS.

             <ls_mod>-association     = if_ehhss_inc_c=>sc_association-INJURY_ILLNESS-INJURY_ILLNESS_DESC.

             <ls_mod>-root_key        = lr_s_root->key.

             <ls_mod>-source_key      = lr_s_INJ_INFO->key.

             <ls_mod>-key             = lr_s_INJURY_ILLNESS_DESC->key.

             <ls_mod>-data            = lr_s_INJURY_ILLNESS_DESC.

       

             "Create the TEXT node:

             CREATE DATA lr_s_text.

             lr_s_text->key          = /bobf/cl_frw_factory=>get_new_key( ).

             lr_s_text->TEXT_TYPE    = 'DESCR'.

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node        = lo_driver->mo_bo_conf->query_node( iv_proxy_node_name = 'INJURY_ILLNESS_DESC.TEXT' ).

             <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node = if_ehhss_inc_c=>sc_node-injury_illness_desc.

             <ls_mod>-source_key  = lr_s_INJURY_ILLNESS_DESC->key.

             <ls_mod>-root_key    = lr_s_root->key.

             <ls_mod>-key         = lr_s_text->key.

             <ls_mod>-data        = lr_s_text.

       

              <ls_mod>-association =

               lo_driver->mo_bo_conf->query_assoc(

                 iv_node_key   = if_ehhss_inc_c=>sc_node-injury_illness_desc

                 iv_assoc_name = 'TEXT' ).

       

             "Create the TEXT_CONTENT node:

             CREATE DATA lr_s_txt_cont.

             lr_s_txt_cont->key          = /bobf/cl_frw_factory=>get_new_key( ).

             lr_s_txt_cont->TEXT         = 'Text for Injury / Illness Description'.

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node        = lo_driver->mo_bo_conf->query_node( iv_proxy_node_name = 'INJURY_ILLNESS_DESC.TEXT_CONTENT' ).

             <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node = if_ehhss_inc_c=>sc_node-injury_illness_desc.

             <ls_mod>-source_key  = lr_s_INJ_INFO->key.     "lr_s_text->key.

             <ls_mod>-root_key    = lr_s_root->key.

             <ls_mod>-key         = lr_s_txt_cont->key.

             <ls_mod>-data        = lr_s_txt_cont.

       

             <ls_mod>-association =

               lo_driver->mo_bo_conf->query_assoc(

                 iv_node_key   = if_ehhss_inc_c=>sc_node-injury_illness_desc

                 iv_assoc_name = 'TEXT_CONTENT' ).

       

      Thanks,

      Shakeel

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      For near misses, you can fill in the description field directly in the NEAR_MISS node using the DESC_TEXT field. Behind the scenes, BOPF determinations will copy the text into the subordinate node automagically.

       

      For injury illness, the field is BP_DESC_TEXT in the INJURY_ILLNESS node.

       

      Hope this helps.

       

      Thanks,

       

      James

      Author's profile photo Md Shakeel Ahmed
      Md Shakeel Ahmed

      Hi James,

       

      Thank You.

       

      Now texts are getting created.

       

      Thanks & Regards,

      Shakeel

      Author's profile photo Md Shakeel Ahmed
      Md Shakeel Ahmed

      Hi James,

       

      I am trying to attach the documents like images, videos, .doc and .xls file to sap but it is uploading.

       

      Here is my code.

       

      ********Attach file to Node ATT_DOCUMENT

           data: lv_filesize TYPE sdok_fsize.

       

             CREATE DATA lr_s_att_document.

             describe field content length lv_filesize in byte mode.

             lr_s_att_document->key      = /bobf/cl_frw_factory=>get_new_key( ).

             lr_s_att_document->key_ref  = lr_s_root->key.

             lr_s_att_document->FILE_SIZE  = lv_filesize  .

             lr_s_att_document->FORM_NAME  = 'INC_INFO_WITNESS'.

             lr_s_att_document->MIME_CODE  = '/SAP/PUBLIC/BOBF'.

             lr_s_att_document->FILE_NAME  = 'EHS_Image_file'.

             lr_s_att_document->CONTENT    = 'C:\Users\Desktop\mobo_logo.png'.

       

             APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.

             <ls_mod>-node        = if_ehhss_inc_c=>sc_node-ATT_DOCUMENT.

             <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.

             <ls_mod>-source_node = if_ehhss_inc_c=>sc_node-root.

             <ls_mod>-association = if_ehhss_inc_c=>sc_association-root-ATT_DOCUMENT.

             <ls_mod>-source_key  = lr_s_root->key.

             <ls_mod>-key         = lr_s_att_document->key.

             <ls_mod>-data        = lr_s_att_document.


       

      data: lt_att_doc_key TYPE /bobf/t_frw_key,

                 IS_ATTACHMENT type /BOBF/S_ATF_A_CREATE_FILE.

       

            "Create the attachment record:

             CALL METHOD lo_driver->mo_svc_mngr->DO_ACTION(

             exporting

               iv_act_key      =   if_ehhss_inc_c=>sc_action-att_document-upload_document

               it_key          =   lt_att_doc_key ).

      *        is_parameters   =   is_attachment ).

       

      Please help me.

       

      Thanks & Regards,

      Shakeel Ahmed.

      Author's profile photo James Wood
      James Wood
      Blog Post Author

      Hi Shakeel,

       

      Send me an e-mail and I can send you some sample code to look at.

       

      Thanks,

       

      James

       

      <email address removed by moderator, it is already shared in SCN profile>

      Author's profile photo Former Member
      Former Member

      Hi James, Can you please share the sample code for Attachment folder? Thanks, Kp

      Author's profile photo Sanjeev Gowda
      Sanjeev Gowda

      Hi James,

      Thanks for the wonderful blog and explanation . I am having issue with attaching document. Can you please share the sample code Thanks ...

      Author's profile photo Sanjeev Gowda
      Sanjeev Gowda

      Hi, Were you able to resolve the issue of attaching the document . If so , can u please share the steps or code snippet to resolve the issue . thanks for your help

      Author's profile photo Former Member
      Former Member

      James, This blog very helpful for me to understand BOPF. Thank you.

      Author's profile photo Former Member
      Former Member

      Thank you very much for this blog, god knows how long would I have to crawl through tons of generic code hadn't I found this here!

      A commendable effort!

      Author's profile photo Vishwanath Gupta
      Vishwanath Gupta

      James,

      Thanks a ton for detailed blogs on BOPF.

      I was searching for some information around how buffer works in BOPF frame work like control of load of buffer, update to the buffer and to the DB from buffer etc..

      Can you suggest any source of information on this topic.

      Thanks in advance,
      Vishwa.

      Author's profile photo Sudarshan David
      Sudarshan David

      Hi James Wood ,

      Great Blog, Thank you so much for it.

       

      After reading the blog part 3 at the end, one question raised in me => I am eager to know what the point creating customer master via BOPF API, we can also use BAPI's for creating.

       

      And If its just an example, then does we have to create screens and call the lcl_demo, for creating Customer ID.

       

      Please let me know, I am curious about it.

       

      Thanks in Advance,

      Sudarshan D

      Author's profile photo David Lawn
      David Lawn

      Problem

      Hi James Wood and thank you.

      I have a problem trying to write to the DOCUMENT standard node of /BOBF/ATTACHMENT_FOLDER  delegated object under /SRMSMC/MO_BUPA~ROOT_ATTACHMENT_FOLDER (“RAF”).

      You write: “However, if we had wanted to do so, we would have needed to create a separate service manager instance for the /BOBF/DEMO_TEXT_COLLECTION business object since the data within that node is defined by that delegated BO as opposed to the /BOBF/DEMO_CUSTOMER BO. ”

      So I understand I need a new instance of service manager to be able to work with /BOBF/ATTACHMENT_FOLDER.

      I find

      • I can read existing DOCUMENT entries using the same service manager instance I use for everything else (query MO_BUPA, retrieve_by_association to get to RAF, modify, …) but I cannot write any new DOCUMENTs to my newly-created MO_BUPA~RAF node. No error messages but no documents. (I do do get_content_key_mapping to get association and node.)
      • when I try to create a second service manager using

      me->mo_svc_mgr_do =
         /bobf/cl_tra_serv_mgr_factory=>get_service_manager(
             /bobf/if_attachment_folder_c=>sc_bo_key ).

      I get an error coming from /BOBF/CL_TRA_SERV_MGR_FACTORY saying

      IF ls_confro_object-objcat = /bobf/if_conf_c=>sc_objcat_do.
      ” It is not allowed to access a delegated object directly via a service manager
      ” Only the host object may access its dependend objects.
      set_application_error( ).

      So can you suggest the way? I seek to create new DOCUMENTs under my newly created RAF.

      With best wishes

      Author's profile photo Jacky D
      Jacky D

      Hi Devid.

      i ran into the same question, did you got any solution?

      Author's profile photo jugal singh
      jugal singh

      Hi James,

       

      query option is not working for me.I don't see sc_query attribute

      not able to pass /bobf/if_demo_customer_c=>sc_query-root-select_by_attributes. and it's dumping here.

      Please suggest how to achieve this.

      Thanks in Advance...

      Jugal

      Author's profile photo Jerry Zhang
      Jerry Zhang

      Why in this demo do you use local class? If this class is create private, how can I try all the methods in it? Thank you!

      Author's profile photo Shivakrishna Eshakoyla
      Shivakrishna Eshakoyla

      Hi Zhang,

      Declare CREATE_CUSTOMER as public static method and access the method using class name. Since CREATE_CUSTOMER method has self object created (lo_driver), this will trigger constructor (private) and rest of the objects are instantiated (Transaction manager, Service manager and configuration manager).

      regards,

      Shiva Krishna E

      Author's profile photo Ismail ElSayed
      Ismail ElSayed

      Can you upload the program again ? because the exist link is not working

      Author's profile photo Ka Ro
      Ka Ro

      I have test to create new instance in bopf test environment (BOBT) for BO:/BOBF/DEMO_CUSTOMER, and saved it with error dispayed for input data. and after this , when i run bobt by BO:/SCMTMS/TOR [by alternative key :tor_id] and input FO to search data, then there is a short dump happened.

      is there anyone know how to solve the error , thank u .

       

       

       

       

       

      Author's profile photo IRINEU AVANCO
      IRINEU AVANCO

      Hello James  ..

      I am trying to download the source code of this article,  as you mentioned , bu the link is not working.

      Could you please kindly provide us with an alternative link to get access to this source code ?

       

      Thank you !

      Author's profile photo Sudhanshu Sharma
      Sudhanshu Sharma

      Hi,

      Its been long time this article was posted. If possible please provide new link(s) to download to source code.

      Thanks

      Author's profile photo Praveen Kumar Nelaballi
      Praveen Kumar Nelaballi

      Hi,

      I'm also facing the same issue.

      Author's profile photo Izabela Kotus
      Izabela Kotus

      Hi,

      in addition, could you provide guidelines/examples of unit test classes for enabling access/modifications of BOPF objects?

      Thanks

      Author's profile photo Seokyeon Hong
      Seokyeon Hong

      Hi.

      I am curious about the performance of RETRIEVE_METHOD. This prevents you from retrieving only the columns you need other than using IT_PARAMETER.

      Which is better if I want to read ROOT node, RETRIEVE_METHOD or Open SQL (with only 20 columns)?

       

      Thanks