Friday, May 15, 2009

ABAP Report: Upload file in an internal table

There is always a requirement of creating a program that uploads a file in an internal table for some reason or the other. I end up copying some old program and then modifying it to reflect the new structure. I thought it’s easier to make generic program that does the upload based on the structure definition using RTTS. The program needs the name of the structure, whether there is a header or not and field separator. Here is the template:

*&---------------------------------------------------------------------*
*& Report  ZUPLOAD
*&
*&---------------------------------------------------------------------*
*&
*&
*&---------------------------------------------------------------------*
REPORT  zupload.
*----------------------------------------------------------------------*
*       CLASS lcl_report DEFINITION
*----------------------------------------------------------------------*
*
*----------------------------------------------------------------------*
CLASS lcl_report DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS:
      open_file RETURNING value(r_file) TYPE string.
    METHODS:
      read_file IMPORTING i_file TYPE string,
      convert_file2struc IMPORTING i_header TYPE boolean
                                   i_separator TYPE c.
  PRIVATE SECTION.
    CONSTANTS:
      struc_name TYPE string VALUE 'ZST_TEST01'. " Put yours in here
    DATA:
      t_filecontent TYPE string_table,
      d_struc_content TYPE REF TO data.
ENDCLASS.                    "lcl_report DEFINITION
DATA:
  o_report TYPE REF TO lcl_report.
SELECTION-SCREEN BEGIN OF BLOCK file WITH FRAME TITLE text-001.
PARAMETERS:
  p_file TYPE string.
SELECTION-SCREEN END OF BLOCK file.
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_file.
  p_file = lcl_report=>open_file( ).
START-OF-SELECTION.
  CREATE OBJECT o_report.
  o_report->read_file( i_file = p_file ).
END-OF-SELECTION.
* The file will be loaded in the data object d_struc_content
  o_report->convert_file2struc( i_header    = ''
                                i_separator = '' )."cl_abap_char_utilities=>horizontal_tab ).
*----------------------------------------------------------------------*
*       CLASS lcl_report IMPLEMENTATION
*----------------------------------------------------------------------*
*
*----------------------------------------------------------------------*
CLASS lcl_report IMPLEMENTATION.
  METHOD open_file.
    DATA:
      l_t_file_table TYPE TABLE OF file_table,
      l_s_file_table TYPE file_table,
      l_rc TYPE i.
    CALL METHOD cl_gui_frontend_services=>file_open_dialog
      CHANGING
        file_table              = l_t_file_table
        rc                      = l_rc
      EXCEPTIONS
        file_open_dialog_failed = 1
        cntl_error              = 2
        error_no_gui            = 3
        not_supported_by_gui    = 4
        OTHERS                  = 5.
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
                 WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    ENDIF.
    READ TABLE l_t_file_table INTO l_s_file_table INDEX 1.
    r_file = l_s_file_table-filename.
  ENDMETHOD.                    "open_file
  METHOD read_file.
    CALL METHOD cl_gui_frontend_services=>gui_upload
      EXPORTING
        filename                = i_file
*        has_field_separator     = 'X'
      CHANGING
        data_tab                = t_filecontent
      EXCEPTIONS
        file_open_error         = 1
        file_read_error         = 2
        no_batch                = 3
        gui_refuse_filetransfer = 4
        invalid_type            = 5
        no_authority            = 6
        unknown_error           = 7
        bad_data_format         = 8
        header_not_allowed      = 9
        separator_not_allowed   = 10
        header_too_long         = 11
        unknown_dp_error        = 12
        access_denied           = 13
        dp_out_of_memory        = 14
        disk_full               = 15
        dp_timeout              = 16
        not_supported_by_gui    = 17
        error_no_gui            = 18
        OTHERS                  = 19.
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
                 WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    ENDIF.
  ENDMETHOD.                    "read_file
  METHOD convert_file2struc.
    DATA:
      l_struc TYPE REF TO data,
      l_o_datadescr TYPE REF TO cl_abap_datadescr,
      l_o_tabledescr TYPE REF TO cl_abap_tabledescr,
      l_t_fields TYPE string_table,
      l_s_fields TYPE string.
    FIELD-SYMBOLS:
      <l_filecontent> TYPE ANY,
      <l_field> TYPE ANY,
      <l_row> TYPE ANY,
      <l_struc_content> TYPE table.
* Create the structure
    l_o_datadescr ?= cl_abap_datadescr=>describe_by_name( struc_name ).
    CREATE DATA l_struc TYPE HANDLE l_o_datadescr.
    ASSIGN l_struc->* TO <l_row>.
* Create the table
    TRY.
        CALL METHOD cl_abap_tabledescr=>create
          EXPORTING
            p_line_type = l_o_datadescr
          RECEIVING
            p_result    = l_o_tabledescr.
      CATCH cx_sy_table_creation .
    ENDTRY.
    CREATE DATA d_struc_content TYPE HANDLE l_o_tabledescr.
    ASSIGN d_struc_content->* TO <l_struc_content>.
    IF i_header = abap_true.
      DELETE t_filecontent INDEX 1.
    ENDIF.
    LOOP AT t_filecontent ASSIGNING <l_filecontent>.
      IF i_separator IS INITIAL. " File is same as structure
        <l_row> = <l_filecontent>.
      ELSE.
*---split based on the separator
        SPLIT <l_filecontent> AT i_separator INTO TABLE l_t_fields.
        LOOP AT l_t_fields INTO l_s_fields.
          ASSIGN COMPONENT sy-tabix OF STRUCTURE <l_row> TO <l_field>.
          <l_field> = l_s_fields.
        ENDLOOP.
      ENDIF.
      APPEND <l_row> TO <l_struc_content>.
    ENDLOOP.
  ENDMETHOD.                    "convert_file2struc
ENDCLASS.                    "lcl_report IMPLEMENTATION

Tuesday, May 12, 2009

ABAP Web Dynpro: Let’s make a simple report

Intro

If you are trying to start development in Web Dynpro a word of advice, stay clear of SAP documentation or courses or books. They are just too complex for the uninitiated. A visit to them is not a bad idea if you know what you are looking for. But to start off it is better to look at some samples or basic tutorials. Since I have gone through this road I thought of writing a small intro with a simple example.

As you might have heard or know already SAP has made great strides in developing its platform for the web. First came the ITS server which was a transformation engine of sorts that converted the screens of SAP into HTML. Then the other technology introduced was BSP which was based on the model of JSP and ASP which involved writing a mix of HTML and ABAP . Now with ABAP (not talking about the JAVA one, that’s another story) Web Dynpro we have a full blown ABAP environment that create a pukka web application.

First let me clear one thing that might be confusing. What we in layman terms call Web Dynpro application is in fact in SAP lingo a Web Dynpro Component. For SAP, Web Dynpro application is similar to an ABAP transaction that refers to a report and a screen. The following image referred from help.sap.com tells more about it.

The Web Dynpro application is merely a link to a view of a Web Dynpro.

The Web Dynpro component contains any number of windows and views. It will have at least one window and one view for an Web Dynpro application to work. There is also the concept of controllers talked about which basically are the collection of attributes, events and methods that will influence the global component or view or window. Each of these have their own controller.

Practical

Enough of theory, let’s see how it translates to practical application.We will build a ABAP like report where we will display the address of customers.

image Web Dynpro Component

image The output

Component reuse

So as can be seen above, we have a selection screen and the output. Let’s see how to build the selection screen first, this is going to be used all the time in reporting. I haven’t built the selection screen myself but used one of SAP’s component. There is one great advantage of the component model of the Web Dynpro and that is reusability. SAP has a Web Dynpro Component WDR_SELECT_OPTIONS which provides the functionality of selection screen.

When you create a Web Dynpro it will create a Window and the global controller. To include an existing Web Dynpro component go to component definition screen and add WDR_SELECT_OPTIONS. Now that the Web Dynpro has been added we can add its components in the our own different components, in our case view component.

image

Create a view, give it a name e.g. V_RC_CUSTOMERS. Go to its properties tab and click on the create controller button. Select WDR_SELECT_OPTIONS/INTERFACECONTROLLER. This now can be initialised and used in this view. For the layout we have the following:

image

VC_SEARCH element is of type ViewContainerUIElement. The ViewContainerUIElement UI element is an area within a view that contains another view. We will embed the view WND_SELECTION_SCREEN of Web Dynpro WDR_SELECT_OPTIONS here. This can be done dynamically or statically. We are doing statically here. Go to the Window of our Component and drag and drop the view V_RC_CUSTOMERS into the Window W_RC_REP_CUSTOMERS. You will have something like this:

image

Initialise Component

VC_SEARCH is the place holder of the embedded view. Right click on VC_SEARCH and select embed view. In the window that pops up do an F4 on view to be embedded. Select WND_SELECTION_SCREEN. Having done this, when the Window calls the View V_RC_CUSTOMERS then the embedded View will also be called. There is just one more step before that happens. The component needs to be initialised first. We go back to the View and go to the method WDDOINIT. This is initialisation method of the View V_RC_CUSTOMERS. Create a local method INIT_SELECT_OPTIONS and add a call to this in WDDOINIT. Go the method init_select_options( ). SAP has some wizards that do very basic jobs. We will use that here to initiated the component that has been added. Click on the wizard. image

Select Instantiate Used Components

image

This will write a bunch of code that does the initiation. Now using the wizard we want to call the method of select_option controller that does the initialising of the selection screen and also returns a assisting class of the type IF_WD_SELECT_OPTIONS.

image

This again will write a bunch of code. Here is the final code listing:

method INIT_SELECT_OPTIONS .
* Call wizard to activate component usage of Select Option
  DATA: l_ref_cmp_usage TYPE REF TO if_wd_component_usage.
  l_ref_cmp_usage =   wd_this->wd_cpuse_select_options( ).
  IF l_ref_cmp_usage->has_active_component( ) IS INITIAL.
    l_ref_cmp_usage->create_component( ).
  ENDIF.
* Call wizard to make a call to init_selection_screen
  wd_this->o_wdr_so_component =   wd_this->wd_cpifc_select_options( ).
  wd_this->o_so_helper_class = wd_this->o_wdr_so_component->init_selection_screen( ).
  CALL METHOD wd_this->o_so_helper_class->set_global_options
    EXPORTING
      i_display_btn_cancel  = abap_false
      i_display_btn_check   = abap_false
      i_display_btn_reset   = abap_true
      i_display_btn_execute = abap_false.
endmethod.
We have a attributes defined in the view that will refer to the helper class of select_option view and the controller itself.

image


Add fields in selection

Now all the initialisation has been done, time to add some fields in the selection_option view. Again this will be done in the method WDDOINIT. For this we have created a local method called ADD_SELECTION_FIELDS. Using the helper class of select_option component we can create ranges for a given data type and then attach that range table in the view. Here is the code that will do that:
METHOD add_selection_fields .
* Add ranges to selection screen
  DATA:
    lt_range_table TYPE REF TO data.
* create a range table for customer
  lt_range_table = wd_this->o_so_helper_class->create_range_table( i_typename = 'KUNNR' ).
  wd_this->o_so_helper_class->add_selection_field( i_id         = 'KUNNR'
                                                   i_obligatory = abap_true
                                                   it_result    = lt_range_table ).
  CALL METHOD wd_this->o_so_helper_class->add_horizontal_divider
    EXPORTING
      i_id           = 'LINE'
*    I_WITHIN_BLOCK = MC_ID_MAIN_BLOCK
      .
* create a range table for Postal code
  lt_range_table = wd_this->o_so_helper_class->create_range_table( i_typename = 'PSTLZ' ).
  wd_this->o_so_helper_class->add_selection_field( i_id        = 'PSTLZ'
                                                   it_result   = lt_range_table ).
* create a range table for Region
  lt_range_table = wd_this->o_so_helper_class->create_range_table( i_typename = 'REGIO' ).
  wd_this->o_so_helper_class->add_selection_field( i_id        = 'REGIO'
                                                   it_result   = lt_range_table ).
* create a range table for email
  lt_range_table = wd_this->o_so_helper_class->create_range_table( i_typename = 'AD_SMTPADR' ).
  wd_this->o_so_helper_class->add_selection_field( i_id        = 'SMTP_ADDR'
                                                   it_result   = lt_range_table ).
ENDMETHOD.

Finally the WDDOINIT will look like this:
METHOD wddoinit .
  init_select_options( ).
  add_selection_fields( ).
ENDMETHOD.

Search Event

This is a web report that means the user has to press a submit button to send the selection options to get some result back. The submit is done by adding a button on the view layout that triggers an event in the form of a method.

image

As defined above we have SEARCH_CENTRES as the action handler and it will correspond to a method called ONACTIONSEARCH_CENTRES. In the event we will do 2 things. 1. Select the customer and show it on the screen. We have 2 methods for this:
METHOD onactionsearch_centres .
  select_centres( ).
  add_to_view( ).
ENDMETHOD.

Get ranges and select

We have to get the user entered selection options from the screen before we run the SQL on our tables. Again using the helper class of the select_option component we will call a method that returns the range tables filled with user entered data. Using these range table we can simple do select on our SAP tables. The code for SELECT_CENTRES is as:
METHOD select_centres .
  DATA:
  l_t_range TYPE REF TO data.
  FIELD-SYMBOLS:
    <l_fs_r_kunnr> TYPE ANY TABLE,
    <l_fs_r_pstlz> TYPE ANY TABLE,
    <l_fs_r_regio> TYPE ANY TABLE,
    <l_fs_r_smtp_addr> TYPE ANY TABLE,
    <l_fs_r_kvgr2> TYPE ANY TABLE,
    <l_fs_r_kvgr1> TYPE ANY TABLE,
    <l_fs_centre> TYPE zst_wd_centre_add.
  CALL METHOD wd_this->o_so_helper_class->get_range_table_of_sel_field
    EXPORTING
      i_id           = 'KUNNR'
    RECEIVING
      rt_range_table = l_t_range.
  ASSIGN l_t_range->* TO <l_fs_r_kunnr>.
  CALL METHOD wd_this->o_so_helper_class->get_range_table_of_sel_field
    EXPORTING
      i_id           = 'PSTLZ'
    RECEIVING
      rt_range_table = l_t_range.
  ASSIGN l_t_range->* TO <l_fs_r_pstlz>.
  CALL METHOD wd_this->o_so_helper_class->get_range_table_of_sel_field
    EXPORTING
      i_id           = 'REGIO'
    RECEIVING
      rt_range_table = l_t_range.
  ASSIGN l_t_range->* TO <l_fs_r_regio>.
  CALL METHOD wd_this->o_so_helper_class->get_range_table_of_sel_field
    EXPORTING
      i_id           = 'SMTP_ADDR'
    RECEIVING
      rt_range_table = l_t_range.
  ASSIGN l_t_range->* TO <l_fs_r_smtp_addr>.
  SELECT kna1~kunnr adrnr INTO TABLE wd_this->t_centres
    FROM kna1
    JOIN knvv ON knvv~kunnr = kna1~kunnr
    AND          knvv~vkorg = '5000'
    AND          knvv~vtweg = '05'
    AND          knvv~spart = '50'
    JOIN adr6 ON adr6~addrnumber = kna1~adrnr
    AND          adr6~persnumber = space
    JOIN knvp ON knvp~kunnr = kna1~kunnr
    AND          knvp~kunn2 <> kna1~kunnr
    AND          knvp~parvw = 'RE' " Bill to party
    WHERE kna1~kunnr IN <l_fs_r_kunnr>
    AND   pstlz IN <l_fs_r_pstlz>
    AND   regio IN <l_fs_r_regio>
    AND   smtp_addr IN <l_fs_r_smtp_addr>.
  LOOP AT wd_this->t_centres ASSIGNING <l_fs_centre>.
    CALL FUNCTION 'ADDRESS_INTO_PRINTFORM'
      EXPORTING
*       ADRSWA_IN                            =
*       ADDRESS_1                            =
*       ADDRESS_2                            =
*       ADDRESS_3                            =
       address_type                         = '1'
       address_number                       = <l_fs_centre>-adrnr
*       ADDRESS_HANDLE                       = ' '
*       PERSON_NUMBER                        = ' '
*       PERSON_HANDLE                        = ' '
*       SENDER_COUNTRY                       = ' '
*       RECEIVER_LANGUAGE                    = ' '
*       NUMBER_OF_LINES                      = 10
*       STREET_HAS_PRIORITY                  = ' '
*       LINE_PRIORITY                        = ' '
*       COUNTRY_NAME_IN_RECEIVER_LANGU       = ' '
*       LANGUAGE_FOR_COUNTRY_NAME            = ' '
*       NO_UPPER_CASE_FOR_CITY               = ' '
*       IV_NATION                            = ' '
*       IV_NATION_SPACE                      = ' '
*       IV_PERSON_ABOVE_ORGANIZATION         = ' '
*       IS_BUPA_TIME_DEPENDENCY              = ' '
     IMPORTING
*       ADRSWA_OUT                           =
       address_printform                    = <l_fs_centre>-address
*       ADDRESS_SHORT_FORM                   =
*       ADDRESS_SHORT_FORM_S                 =
*       ADDRESS_DATA_CARRIER                 =
*       ADDRESS_DATA_CARRIER_0               =
*       NUMBER_OF_USED_LINES                 =
*       NAME_IS_EMPTY                        =
*       ADDRESS_NOT_FOUND                    =
*       ADDRESS_PRINTFORM_TABLE              =
*       ADDRESS_SHORT_FORM_WO_NAME           =
*       EV_NATION                            =
              .
  ENDLOOP.
ENDMETHOD.
We will store the selected data in the attribute of the view.

Show

Now that we have the data in an internal table, we have to display it. Note that I am not using Context here which is another way of storing data within the components of a Web Dynpro.

To display the data we are going to do a bit of dynamic programming as the number of lines is variable. But to see what we want to achieve I first made UI elements in the layout so that I knew which ones to use and where to place them. Having known which elements to use, we just program it dynamically and attaching them to the view. The design is worked out as follows:

image

Create a group and within the group create transparent container for each customer. Each customer container will have one container containing the customer number and another container containing the address lines.

Any UI element that needs to be accessed or created requires a handle on the view first. We use the method WDDOMODIFYVIEW to get the reference for the view object. Code is:
METHOD wddomodifyview .
  IF first_time = abap_true.
    wd_this->o_view = view.
  ENDIF.
ENDMETHOD.
And finally code to actual creation of UI elements:
METHOD add_to_view .
  DATA:
    l_o_container TYPE REF TO cl_wd_uielement_container,
    l_o_group     TYPE REF TO cl_wd_group,
    l_o_caption   TYPE REF TO cl_wd_caption,
    l_o_t_cont_top   TYPE REF TO cl_wd_transparent_container,
    l_o_t_cont   TYPE REF TO cl_wd_transparent_container,
    l_o_ft_view   TYPE REF TO cl_wd_formatted_text_view,
    l_str_kunnr TYPE string,
    l_str_line TYPE string,
    l_cell_backgroup TYPE wdui_cell_bg_design VALUE '00', " Transparent
    l_rc TYPE i.
  FIELD-SYMBOLS:
    <l_fs_centres> TYPE zst_wd_centre_add,
    <l_fs_line> TYPE lines.
*  CHECK wd_this->t_centres IS NOT INITIAL.
* Get access to the route
  l_o_container ?= wd_this->o_view->get_element( 'ROOTUIELEMENTCONTAINER' ).
* Remove any previous existance
  l_o_container->remove_child( id = 'GRP_CENTRES' ).
* Create a new group
  l_o_group = cl_wd_group=>new_group( id = 'GRP_CENTRES'
                                      design = '03' ). "  secondarybox
  l_o_caption = cl_wd_caption=>new_caption( text = 'Centre Addresses' ).
  l_o_group->set_header( the_header = l_o_caption ).
  cl_wd_flow_data=>new_flow_data( element = l_o_group ).
  cl_wd_grid_layout=>new_grid_layout( cell_padding = 5
                                      cell_spacing = 5
                                      col_count    = 1
                                      container    = l_o_group ).
  LOOP AT wd_this->t_centres ASSIGNING <l_fs_centres>.
* Create a new transparent container to contain centre and address
    l_o_t_cont_top = cl_wd_transparent_container=>new_transparent_container( ).
* For each new centre toggle background colour
    IF l_cell_backgroup = '00'.
      l_cell_backgroup = '01'.                              " Fill1
    ELSE.
      l_cell_backgroup = '00'. " Transparent
    ENDIF.
    cl_wd_grid_data=>new_grid_data( cell_background_design = l_cell_backgroup
                                    element                = l_o_t_cont_top ).
    cl_wd_grid_layout=>new_grid_layout( cell_padding = 5
                                        cell_spacing = 5
                                        col_count    = 2
                                        container    = l_o_t_cont_top ).
* add Center code to a new T container
    l_o_t_cont = cl_wd_transparent_container=>new_transparent_container( ).
    cl_wd_grid_data=>new_grid_data( element = l_o_t_cont
                                    v_align = '01' ). " Top
    cl_wd_flow_layout=>new_flow_layout( container = l_o_t_cont ).
    l_str_kunnr = <l_fs_centres>-kunnr.
    l_o_ft_view = cl_wd_formatted_text_view=>new_formatted_text_view( text = l_str_kunnr ).
    cl_wd_flow_data=>new_flow_data( element = l_o_ft_view ).
    l_o_t_cont->add_child( the_child = l_o_ft_view ).
    l_o_t_cont_top->add_child( the_child = l_o_t_cont ).
* Address lines
    l_o_t_cont = cl_wd_transparent_container=>new_transparent_container( ).
    cl_wd_grid_data=>new_grid_data( element = l_o_t_cont
                                    v_align = '00' ). " Baseline
    cl_wd_grid_layout=>new_grid_layout( container = l_o_t_cont ).
    CLEAR l_rc.
    WHILE l_rc IS INITIAL.
      ASSIGN COMPONENT sy-index OF STRUCTURE <l_fs_centres>-address TO <l_fs_line>.
      l_rc = sy-subrc.
      IF l_rc IS NOT INITIAL.
        EXIT.
      ENDIF.
      CHECK <l_fs_line> IS NOT INITIAL.
      l_str_line = <l_fs_line>.
      l_o_ft_view = cl_wd_formatted_text_view=>new_formatted_text_view( text = l_str_line ).
      cl_wd_grid_data=>new_grid_data( element = l_o_ft_view ).
      l_o_t_cont->add_child( the_child = l_o_ft_view ).
    ENDWHILE.
    l_o_t_cont_top->add_child( the_child = l_o_t_cont ).
    l_o_group->add_child( the_child = l_o_t_cont_top ).
  ENDLOOP.
* If nothing is found display message
  IF sy-subrc IS NOT INITIAL.
    l_o_ft_view = cl_wd_formatted_text_view=>new_formatted_text_view( text = 'No results' ).
    cl_wd_grid_data=>new_grid_data( element = l_o_ft_view ).
    l_o_group->add_child( the_child = l_o_ft_view ).
  ENDIF.
  l_o_container->add_child( the_child = l_o_group ).
ENDMETHOD.
Once all this is done just create an application that links to the view and component and test it out!