How to add custom tabs with fields from standard tables in SAP Portfolio and Project Management
This is a continuation of my how-to blog series:
- How to work with field conditions in SAP Portfolio and Project Management
- How to add transient fields in SAP Portfolio and Project Management
Although a bit late 😊, this step-by-step guide is meant to answer a relatively often question in SAP PPM consulting: https://answers.sap.com/questions/306245/custom-tab-in-ppm.html
It is often that companies will enhance SAP PPM to comply with a variety of use cases. This results in a multitude of new project types and lots of custom fields. In order to add some structure to the UIs and improve the usability, some of this fields can be grouped into separate tabs.
The following is a bottom-up approach to enhancing the Project Management application in SAP PPM with an additional tab. As in the previous blogs, we will continue to use the demo Airline (SCARR) data model delivered by SAP. For standard project type ‘Development Project’, we will add a new tab containing information about the Aircraft (like Characteristics) as seen in the following screenshot:
With the condition that all the values are persisted in the standard database table DPR_PROJECT.
Note: This example is built on SAP Portfolio and Project Management 6.1. I am not sure up to which previous version are these enhancement steps available.
1. Data Dictionary
1.1. (Optional) Let’s create new data elements for each attribute we want to persist:
Tip: Personally, I prefer creating data elements for each additional field in the ZDPR_TV_* namespace, even if they are already existing in the SAP system. This allows the maintenance of own labels and whether to log change documents:
1.2. Add the fields into the customer include CI_DPR_PROJECT. Don’t forget to add the prefix (ZZ or a customer defined namespace) in order avoid any conflicts with future updates from SAP.
Note: Each project element that can be enhanced this way, contains a corresponding CI_DPR_* structure. Example: DPR_PHASE, DPR_TASK, etc.
Tip: Although the new fields can be added directly into CI_DPR_PROJECT, in my experience it is a good practice to group together attributes that belong to a certain enhancement into a structure and include the structure instead. This will make things much easier on the long run for maintenance and other developers to figure out which fields belong to which add-on.
2. Creating the tab component
2.1. As described in SAP Note 123456, we will create a new Web Dynpro component for our new tab:
And implement standard interface DPR_CUST_EXT_INTF:
The WI_MAIN created initially can be removed, as WI_CUSTOMER_VIEW is added by the interface.
Note: This interface contains all the hook methods for handling the application events of SAP PPM. The purpose of each method is described in the SAP Note 123456 and in the demo implementation DPR_CUST_EXT_INTF_DEMO delivered with the standard solution.
2.2. Embed the view VI_MAIN into window WI_CUSTOMER_VIEW:
The result should be the following:
2.3. Create a DDIC structure with the fields to be shown in the UI:
Besides the persisted attributes (included in CI_DPR_PROJECT) we have the possibility to display transient attributes as well, like corresponding texts of selected values, etc. See blog post for more details.
Tip: The include structure created in step 1 really pays off here. Adding a Group alias to the include will also make it possible to address the fields as a whole and avoid any unnecessary MOVE-CORRESPONDING statements.
2.4. In the newly created ZZDPR_AIRCRAFT Web Dynpro component, defined the following nodes:
- VIEWDATA – is based on the DDIC structure ZDPR_TS_UI_AIRCRAFT_DATA and will store the content of the attributes to be displayed.
And include the attributes:
- UI – as recommended by SAP, will control the read-only property of the view.
Note: Adding a READONLY property in the context is an easy way to handle simple scenarios. Most often the whole tab will be shown/hidden based on the project type or any other criteria. For complex scenarios I would recommend using the attribute properties to handle field control. More info about this can be found here.
The COMPONENTCONTROLLER context should look like this:
2.5. Create a mapping between both nodes from the context of the component controller and the context of view VI_MAIN:
2.6. Create the view layout as follows:
Don’t forget to bind the read-only property of each input field to the READONLY property of the UI node.
Note: You can use the Web Dynpro wizard to generate the layout. Notice the long element IDs generated by Web Dynpro. Although they don’t cause any technical issue, it will make it easier for maintenance to rename them according to the PPM convention.
2.7. Implementing the retrieval and update of data:
In the Attributes tab of the COMPONENTCONTROLLER, define a new MV_PROJECT_GUID attribute:
In the Methods tab of the COMPONENTCONTROLLER:
2.7.1. Define a new method INITIALIZE with input parameter IS_INPUT of type DPR_TS_CUST_EXT_TABC_INPUT:
METHOD initialize. wd_this->mv_project_guid = is_input-guid. wd_this->update_data_from_be( ). ENDMETHOD.
2.7.2. Enable the context change log (more info here):
METHOD wddoinit. wd_context->get_context( )->enable_context_change_log( ). ENDMETHOD.
Note: The change log is used to reduce the number of calls to the backend.
2.7.3. Create a new method UPDATE_DATA_FROM_BE:
METHOD update_data_from_be. DATA: lo_node_viewdata TYPE REF TO if_wd_context_node, lo_node_ui TYPE REF TO if_wd_context_node, ls_viewdata TYPE wd_this->element_viewdata, lo_project TYPE REF TO cl_dpr_project_o, ls_project_int TYPE dpr_ts_project_int. lo_project ?= cl_dpr_api_services=>get_object_by_guid( wd_this->mv_project_guid ). lo_project->get_data_ext( IMPORTING es_project_int = ls_project_int ). ls_viewdata-zzaircraft_attr = ls_project_int-zzaircraft_attr. lo_node_viewdata = wd_context->get_child_node( wd_this->wdctx_viewdata ). lo_node_viewdata->set_static_attributes( ls_viewdata ). lo_node_ui = wd_context->get_child_node( wd_this->wdctx_ui ). lo_node_ui->set_attribute( name = 'READONLY' value = xsdbool( NOT lo_project->is_changeable( ) ) ). ENDMETHOD.
This method will fill the context nodes by retrieving the relevant project attributes from the backend object model and determining if the project can be changed.
Note: We need this in a separate method as it will be called from two different places.
2.7.4. Create a reusable method IS_DETAILVIEW:
METHOD is_detailview. DATA: lo_session_state TYPE REF TO cl_dpr_session_state, lv_detailview TYPE string. lo_session_state = cl_dpr_session_state=>get_instance( ). lv_detailview = lo_session_state->get_detailview( ). rv_yes = xsdbool( lv_detailview = cl_dpr_ui_services=>sc_tab_customer(3) OR ( lv_detailview GE 'EX0' AND lv_detailview LE 'EX9' ) ). ENDMETHOD.
2.7.5. Implement interface method ON_UPDATE:
METHOD on_update. " Make sure this tab is on focus. CHECK wd_this->is_detailview( ). wd_this->update_data_from_be( ). ENDMETHOD.
As this method is called by the Project Management application for every event, we want to restrict the backend calls to only when our new tab is selected in the UI.
2.7.6. Implement interface method ON_REPORT_CHANGES:
METHOD on_report_changes. DATA: lo_node_viewdata TYPE REF TO if_wd_context_node, ls_viewdata TYPE wd_this->element_viewdata, lo_project TYPE REF TO cl_dpr_project_o, ls_project_chg TYPE dpr_ts_project_chg. " Make sure this tab is on focus. CHECK wd_this->is_detailview( ). " ... and it contains changes. CHECK wd_context->get_context( )->get_context_change_log( ) IS NOT INITIAL. lo_node_viewdata = wd_context->get_child_node( wd_this->wdctx_viewdata ). lo_node_viewdata->get_static_attributes( IMPORTING static_attributes = ls_viewdata ). lo_project ?= cl_dpr_api_services=>get_object_by_guid( wd_this->mv_project_guid ). lo_project->get_data_ext( IMPORTING es_project_chg = ls_project_chg ). ls_project_chg-extended_attributes-zzaircraft_attr = ls_viewdata-zzaircraft_attr. TRY. lo_project->set_data_ext( ls_project_chg ). CATCH cx_dpr_object_update_error. " error handling should be implemented, " but it is not relevant for this example :) ENDTRY. ENDMETHOD.
This method will check if the new tab is selected in the UI and if any changes were made. If yes, the backend object will be updated accordingly.
2.7.7. Navigate to the WI_CUSTOMER_VIEW window and implement method HANDLEDEFAULT:
METHOD handledefault. wd_comp_controller->initialize( cust_input_values ). ENDMETHOD.
This method will be called every time the new tab is selected in the UI. We use it to initialize the component and set the MV_PROJECT_GUID attribute.
Alternatively, the current object GUID can also be determined using the CL_DPR_SESSION_STATE class.
Go to SAP Customizing Implementation Guide (Transaction code SPRO) and navigate to the following activity:
> SAP Portfolio and Project Management
> Project Management
> Global Enhancements to Project Elements
> Show Additional Tab Pages in Project Management
Add a new entry as follows:
Result should be:
3.1. (Optional) Show/Hide tab based on specific conditions
If the standard customizing in the previous step does not suffice, you can use BAdI DPR_EXTENSIONS to show/hide different tabs based on specific conditions.
Tip: Although this BAdI is filterable by the object type (Example: DPO for Operative Projects), I found it best to keep a single implementation and delegate to a corresponding method implementation.
Copy-paste the following demo implementation:
CLASS zcl_dpr_extensions DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES if_badi_interface. INTERFACES if_ex_dpr_extensions. PROTECTED SECTION. PRIVATE SECTION. METHODS get_extensions_dpo IMPORTING ir_project TYPE REF TO cl_dpr_project ir_extension_list TYPE REF TO cl_dpr_extension_list. ENDCLASS. CLASS zcl_dpr_extensions IMPLEMENTATION. METHOD if_ex_dpr_extensions~get_default_extension. RETURN. ENDMETHOD. METHOD if_ex_dpr_extensions~get_extensions. CASE flt_val. WHEN cl_dpr_co=>sc_ot_project. get_extensions_dpo( ir_project = ir_project ir_extension_list = cr_extension_list ). ENDCASE. ENDMETHOD. METHOD get_extensions_dpo. DATA: lv_title TYPE string. IF ir_project->get_project_type( ) = '000000000000001'. " If you use PPM in multiple languages, don't forget about i18n. lv_title = cl_wd_utilities=>get_otr_text_by_alias( 'ZDPR_DEMO_UI/AIRCRAFT_DATA' ). ir_extension_list->add_extension( VALUE dpr_ts_extension( name = 'ZZAIRCRAFT' title = lv_title controller_url = 'ZZDPR_AIRCRAFT' place_tab_after_tab_name = cl_dpr_ui_services=>sc_det_tab_additional extension_type = 'W' ) ). " You can also hide any standard tab. ir_extension_list->hide_view( iv_tab_name_wd = cl_dpr_ui_services=>sc_det_tab_portfoliodata ). ENDIF. ENDMETHOD. ENDCLASS.
Note: If you chose to control the tabs via the BAdI implementation, don’t forget to remove the static tab customizing created in step 3.
Opening the Project Management application with a project of type ‘Development Project’, the new Aircraft Data tab is now visible:
If we switch the project to edit mode or set the status to ‘Completed’, the fields in the Aircraft Data tab should be in display mode as well: