Quantcast
Channel: ABAP Development
Viewing all 948 articles
Browse latest View live

Step by step guide to enhance/update Vendor Master and generate idocs - Part2

$
0
0

The last section (part1) covered the steps required to enhance vendor master( table/screens ). In this section I will demonstrate the procedure to update the custom fields of vendor master and generate change pointers for these fields ( to eventually generate IDOCS for changes to field values).

 

Unfortunately since there is no standard BAPI to update the vendor master record, the only other options available to update custom fields of vendor master are to either 1) Perform a BDC on XK02 transaction to update custom fields on subscreen with these fields. 2) Enhance vendor API class VMD_EI_API to process and update custom fields.

 

The former (BDC) may not be feasible in many scenarios, specially if the custom screen fields are read only( like in my case) fields or these fields are not added to the subscreen in the first place.

 

Given this shortcoming of BDC I will only cover ( and recommend ) the second option for vendor master update ( vendor API class).

 

Step1: Enhance structure of importing parameter to pass values for custom fields.

  • Enhance structure VMDS_EI_EXTERN by appending fields to include structure CI_CUSTOM. The screenshot below shows vendor safety structure (which was also appended to LFA1) is added as a component to CI_CUSTOM. This ensures any additional fields added to this structure for vendor master (general data) enhancement will automatically update importing parameter structure.

     VMDS_EI_EXTERN_enhancement.png

  • Enhancing VMDS_EI_EXTEN enables passing of values for custom vendor master fields using interface of method VMD_EI_API=>MAINTAIN( ). This method will be called to update vendor master data.

     VMD_EI_API_MAINTAIN.png

Step2: Implement enhancement to read the values passed for custom fields and assign them to LFA1 structure which is already being populated by the standard code. The screenshot below shows implementation of explicit enhancement section EHP603_EXTERN_TO_ECC_UPDATE_02 of enhancement spot ES_VMD_EI_API_MAP_STRUCTURE.

     ZMM_VSCS_ENTERN_TO_ECC_UPDATE.png

    • Although the above shown enhancement only updates custom the central data (general data ) of vendor master, the enhancement spot offers other enhancement sections which can be used in a similar fashion to update other sections of the vendor master ( purchase org, company, banking etc ).
    • As per sap note 1989074 always replace an standard sap implementation instead of changing it.

     This completes the enhancements required in the vendor master API class for updating vendor master.

Step3: The next step involves calling the MAINTAIN method of VMD_EI_API class to update vendor master.  In my scenario I am reading vendor master details from an XML file, transforming it and updating Vendor master, however The code snippet only shows the call to update vendor master, rest is beyond scope of this article.

 

 

 

METHOD update_vendor_master.



  DATA: ls_vmds_ei_main TYPE vmds_ei_main,

        ls_message      TYPE cvis_message,

        lt_return       TYPE bapiret2_t,

        ls_return       LIKE LINE OF lt_return.

  DATA: lt_vmds_ei_extern TYPE vmds_ei_extern_t,

        ls_vmds_ei_extern LIKE LINE OF lt_vmds_ei_extern.

  DATA: lv_success TYPE sap_bool,

        ls_lfa1_new TYPE lfa1.

  DATA: ls_update_err TYPE LINE OF zmmtt_lifnr_update_failed_list.

** Populate the method input parameters.

  ls_vmds_ei_extern-header-object_instance = iv_lifnr.

  ls_vmds_ei_extern-header-object_task     = 'U'"<- indicator to update

  ls_vmds_ei_extern-zzvend_safety_compl    = is_safety_score. "<- input parameter to this method, originally read from XML



  APPEND ls_vmds_ei_extern TO lt_vmds_ei_extern.

  ls_vmds_ei_main-vendors = lt_vmds_ei_extern.



  vmd_ei_api=>maintain(

    EXPORTING

      is_master_data = ls_vmds_ei_main    " Vendor Total Data

    IMPORTING

      es_error       ls_message   )." Error Indicator and System Messages



  IF ls_message-is_error IS INITIAL.

    COMMIT WORK AND WAIT.     "<-- Explicit commit required to finalize changes in database

    IF sy-subrc EQ 0.

      lv_success = abap_true.

    ENDIF.

  ELSE.

    lv_success = abap_false.

  ENDIF.



  IF lv_success EQ abap_true.

    ls_lfa1_new = is_lfa1_old.

    ls_lfa1_new-zzvscs_company_name     = is_safety_score-zzvscs_company_name.

    ls_lfa1_new-zzvscs_isn_code         = is_safety_score-zzvscs_isn_code.

    ls_lfa1_new-zzvscs_naics_code       = is_safety_score-zzvscs_naics_code.

    ls_lfa1_new-zzvscs_trir_3_years     = is_safety_score-zzvscs_trir_3_years.

    ls_lfa1_new-zzvscs_fatality_3_years = is_safety_score-zzvscs_fatality_3_years.

    ls_lfa1_new-zzvscs_scq_v_score      = is_safety_score-zzvscs_scq_v_score.

    ls_lfa1_new-zzvscs_emr_3_years      = is_safety_score-zzvscs_emr_3_years.

    ls_lfa1_new-zzvscs_ravs_score       = is_safety_score-zzvscs_ravs_score.

    ls_lfa1_new-zzvscs_citation_info    = is_safety_score-zzvscs_citation_info.

    ls_lfa1_new-zzvscs_dashboard_score  = is_safety_score-zzvscs_dashboard_score.

    ls_lfa1_new-zzvscs_hes_showstpr     = is_safety_score-zzvscs_hes_showstpr.

    ls_lfa1_new-zzvscs_eval_rept        = is_safety_score-zzvscs_eval_rept.

    ls_lfa1_new-zzvscs_score_accept_hdr = is_safety_score-zzvscs_score_accept_hdr.

    ls_lfa1_new-zzvscs_changed_by       = is_safety_score-zzvscs_changed_by.

    ls_lfa1_new-zzvscs_changed_at       = is_safety_score-zzvscs_changed_at.

    ls_lfa1_new-zzvscs_changed_on       = is_safety_score-zzvscs_changed_on.



    rv_success = me->perform_update_change_pointers( iv_lifnr = iv_lifnr is_lfa1_old = is_lfa1_old is_lfa1_new = ls_lfa1_new ).
     " perform_update_change_pointers -> covered in next step


  ELSE.
     " custom error handling logic based on requirements.
    ls_update_err-lifnr = iv_lifnr.

    ls_update_err-name  = is_lfa1_old-name1.

    ls_update_err-changed_on = sy-datum.

    ls_update_err-text       = text-e06.



    lt_return = ls_message-messages.

    READ TABLE lt_return INTO ls_return WITH KEY type = 'E'.

    IF sy-subrc EQ 0.

      REPLACE FIRST OCCURRENCE OF '&' IN ls_update_err-text WITH ls_return-message.

    ENDIF.

    APPEND ls_update_err TO me->mt_lfa1_failed.

    CLEAR ls_update_err.

  ENDIF.

ENDMETHOD.

 

Step3: If the vendor master update is successful in the previous method, the next step is creation of change documents for the custom fields, which will eventually create the change pointers ( for IDOC creation).

  • To ensure change documents are created for custom fields, your code needs to keep a copy of vendor master before and after the change ( is_lfa1_old and ls_lfa1_new in the above code ).

 

 

METHOD perform_update_change_pointers.

  DATA:

        l_lfb1      TYPE lfb1,

        l_lfm1      TYPE lfm1,

        l_ylfa1     TYPE lfa1,

        l_ylfb1     TYPE lfb1,

        l_ylfm1     TYPE lfm1,

        lt_xlfas    TYPE STANDARD TABLE OF flfas,

        lt_xlfb5    TYPE STANDARD TABLE OF flfb5,

        lt_xlfbk    TYPE STANDARD TABLE OF flfbk,

        lt_xlfza    TYPE STANDARD TABLE OF flfza,

        lt_ylfas    TYPE STANDARD TABLE OF flfas,

        lt_ylfb5    TYPE STANDARD TABLE OF flfb5,

        lt_ylfbk    TYPE STANDARD TABLE OF flfbk,

        lt_ylfza    TYPE STANDARD TABLE OF flfza,

        lt_xknvk    TYPE STANDARD TABLE OF fknvk,

        lt_xlfat    TYPE STANDARD TABLE OF flfat,

        lt_xlfbw    TYPE STANDARD TABLE OF flfbw,

        lt_xlfei    TYPE STANDARD TABLE OF flfei,

        lt_xlflr    TYPE STANDARD TABLE OF flflr,

        lt_xlfm2    TYPE STANDARD TABLE OF flfm2,

        lt_xwyt1    TYPE STANDARD TABLE OF fwyt1,

        lt_xwyt1t   TYPE STANDARD TABLE OF fwyt1t,

        lt_xwyt3    TYPE STANDARD TABLE OF fwyt3.

  DATA: lv_objectid TYPE cdobjectv,

        ls_update_err TYPE LINE OF ZMMTT_LIFNR_UPDATE_FAILED_LIST.



  lv_objectid = iv_lifnr.

  GET TIME.         "<- Gets the latest date and time.

  CALL FUNCTION 'KRED_WRITE_DOCUMENT' IN UPDATE TASK

    EXPORTING

      objectid                = lv_objectid

      tcode                   = me->gc_tcode_change

      utime                   = sy-uzeit

      udate                   = sy-datum

      username                = sy-uname

      planned_change_number   = ' '

      object_change_indicator = me->gc_update

      planned_or_real_changes = ' '

      no_change_pointers      = ' '

      upd_knvk                = space

      n_lfa1                  = is_lfa1_new

      o_ylfa1                 = is_lfa1_old

      upd_lfa1                = abap_true

      upd_lfas                = ' '

      upd_lfat                = ' '

      n_lfb1                  = l_lfb1

      o_ylfb1                 = l_lfb1

      upd_lfb1                = ' '

      upd_lfb5                = ' '

      upd_lfbk                = ' '

      upd_lfbw                = ' '

      upd_lfei                = ' '

      upd_lflr                = ' '

      n_lfm1                  = l_lfm1

      o_ylfm1                 = l_lfm1

      upd_lfm1                = ' '

      upd_lfm2                = ' '

      upd_lfza                = ' '

      upd_wyt1                = ' '

      upd_wyt1t               = ' '

      upd_wyt3                = ' '

    TABLES

      xknvk                   = lt_xknvk

      yknvk                   = lt_xknvk

      xlfas                   = lt_xlfas

      ylfas                   = lt_xlfas

      xlfat                   = lt_xlfat

      ylfat                   = lt_xlfat

      xlfb5                   = lt_xlfb5

      ylfb5                   = lt_xlfb5

      xlfbk                   = lt_xlfbk

      ylfbk                   = lt_ylfbk

      xlfbw                   = lt_xlfbw

      ylfbw                   = lt_xlfbw

      xlfei                   = lt_xlfei

      ylfei                   = lt_xlfei

      xlflr                   = lt_xlflr

      ylflr                   = lt_xlflr

      xlfm2                   = lt_xlfm2

      ylfm2                   = lt_xlfm2

      xlfza                   = lt_xlfza

      ylfza                   = lt_xlfza

      xwyt1                   = lt_xwyt1

      ywyt1                   = lt_xwyt1

      xwyt1t                  = lt_xwyt1t

      ywyt1t                  = lt_xwyt1t

      xwyt3                   = lt_xwyt3

      ywyt3                   = lt_xwyt3.



  if sy-subrc eq 0.

    COMMIT WORK AND WAIT. "<-- Explicit commit required to finalize changes in database

    if sy-subrc eq 0.

      rv_success = abap_true.

    endif.

  ENDIF.
     "Custom error handing logic
  if rv_success ne abap_true.

    ls_update_err-lifnr = iv_lifnr.

    ls_update_err-name  = is_lfa1_new-name1.

    ls_update_err-changed_on = SY-DATUM.

    ls_update_err-text       = text-e05.

    APPEND ls_update_err to me->mt_lfa1_failed.

    CLEAR ls_update_err.

  ENDIF.

ENDMETHOD.

 

 

 

This completes vendor master update. In the next part will cover creation of CREAMAS idocs for vendor master changes.


Device-Independent Datamatrix in Smartforms, Proof of Concept

$
0
0

Lately SAP note 2001392 has been released that allow printing of Datamatrix for some device types. There are also fonts and other 3rd party tools that allows you to print a datamatrix.

 

However most of these solutions have some specific requirements, as a workaround to this I created a proof of concept that allows for printing a datamatrix with minimal requriements.

 

The solution uses 2 tricks:

 

Only 10x10 datamatrices have been implemented, allowing it to contain up to 3 characters.

 

The code can be downloaded from https://github.com/larshp/Datamatrix

 

To test, run the report

1.png

 

and it will show the smartform as print preview

2.png

 

The smartform consists of a 10x10 template where the background colour of each cell is changed from ABAP,

3.png

 

 

This technique can be extended to QR, aztec and other 2d barcode symbologies.

 

Note: Do not regard this as best practice, however in some situations it might be beneficial to use this approach.

version comparison failed with error message 37501

$
0
0

Version management between active version and inactive version failes with error message 37501.

37501.PNG

The problem can be resolved by SAP Note 1993101 and SAP Note 2045124.

"Program RSABADAB is outdated" pops up when using F4 help in SE38/SA38

$
0
0

When try to use F4 help function in SE38/SA38, "Program RSABADAB is outdated" pops up, for example:

sa38.PNG


The problem can be resolved by implementing SAP Note 2100218.

ABAP Trapdoors: R-F-Conversion on a STRING

$
0
0

Welcome back to another ABAP Trapdoors article. It’s been a while – I’ve posted my last article in 2011. In the meantime, I’ve collected a number of interesting topics. If you’re interested in the older articles, you can find the links at the end of this article.

 

A few weeks ago, I had to code some seemingly simple task: From a SAP Business Workflow running in system ABC, a sub-workflow with several steps had to be started in another system, or even a number of other systems. Since a BPM engine was not available, I decided to use simple RFC-enabled function modules to raise workflow events in the target system. The sub-workflows can then be started via simple object type linkage entries. While this approach works quite well for my simple scenario, I ran into an altogether unexpected issue that took me quite a while to figure out.

 

There are two function modules to raise the workflow events: SAP_WAPI_CREATE_EVENT and SAP_WAPI_CREATE_EVENT_EXTENDED. In my case, I used the extended function module because I was working with class-based events. So the call basically was

 

  CALL FUNCTION 'SAP_WAPI_CREATE_EVENT_EXTENDED' DESTINATION l_rfcdest    EXPORTING      catid             = 'CL'      typeid            = 'ZCL_MY_CLASS'      instid            = l_instid      event             = 'MY_EVENT'    ... 

To my surprise, it did not work - the system kept telling me that the event M does not exist. After spending a considerable time debugging and scratching my head, I finally identified the issue. Since it can be tricky to reproduce this particular issue, here is a very simple function module to demonstrate the actual problem:

 

FUNCTION ztest_rfc_echo.
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*"  IMPORTING
*"     VALUE(I_INPUT_VALUE) TYPE  STRING
*"  EXPORTING
*"     VALUE(E_OUTPUT_VALUE) TYPE  STRING
*"----------------------------------------------------------------------  e_output_value = i_input_value.

ENDFUNCTION.

(If you want to try this for yourself, make sure that the function module is RFC-enabled.)

This is no more than a simple value assignment – Text goes in, text comes out, right? Let’s see. Here is a demo program to check it out:

 

REPORT ztest_rfc_conversion.

DATA: g_value TYPE string.

START-OF-SELECTION.

  CALL FUNCTION 'ZTEST_RFC_ECHO'    EXPORTING      i_input_value  = 'This is a C literal'    IMPORTING      e_output_value = g_value.  WRITE: / '1:', g_value.  CALL FUNCTION 'ZTEST_RFC_ECHO'    EXPORTING      i_input_value  = `This is a STRING literal`    IMPORTING      e_output_value = g_value.  WRITE: / '2:', g_value.  CALL FUNCTION 'ZTEST_RFC_ECHO' DESTINATION 'NONE'    EXPORTING      i_input_value  = 'This is a C literal'    IMPORTING      e_output_value = g_value.  WRITE: / '3:', g_value.  CALL FUNCTION 'ZTEST_RFC_ECHO' DESTINATION 'NONE'    EXPORTING      i_input_value  = `This is a STRING literal`    IMPORTING      e_output_value = g_value.  WRITE: / '4:', g_value.

 

In this program, the function module is called twice within the same session and twice starting a new session, using both a character literal and a string literal (note the difference between 'X' and `X`!). And the output is:

 

output.png

 

As you can easily see, the character literal is chopped off after the first character, but only if the function module is called via RFC. The same thing happened in my original program since the parameter EVENT of the function module SAP_WAPI_CREATE_EVENT_EXTENDED is of type STRING.

 

I considered this a bug, especially since neither SLIN nor the code inspector warned about this issue. As a good SAP citizen, I created a SAPnet ticket. After a lengthy discussion, I was told

There is no "implicit conversion" in RFC, therefore application need to adjust(or choose) a proper data types for calling/receiving RFMs.

In the end, this problem is very similar to the one explained by Jerry Wang in his blog a few weeks ago – another trapdoor in the development environment you constantly have to keep in mind when doing RFC programming if you want to avoid being reduced to a single character with a lot of trouble…

 

Older ABAP Trapdoors articles

Controlled multi-threading in ABAP

$
0
0

You may have been in a situation where you have to process multiple objects. When you do this sequentially, it'll take some time before that processing finishes. One solution could be to schedule your program in background, and just let it run there.

But what if you don't have the luxury to schedule your report in background, or what if the sheer amount of objects is so large that it would take a day to process all of them, with the risk of overrunning your nightly timeframe and impacting the daily work.

 

Multi Threading

It would be better if you could actually just launch multiple processing blocks at the same time. Each block could then process a single object and when it finishes off, release the slot so the next block can be launched.

That could mean that you could have multiple objects updated at the same time. Imagine 10 objects being processed at once rather than just one object sequentially. You could reduce your runtime to only 10% of the original report.

 

It's actually not that hard. If you create a remote enabled function module, containing the processing logic for one object, with the necessary parameters, you can simply launch them in a new task. That creates a new process (you can monitor it in transaction SM50) which will end as soon as your object is processed.

 

Newtask.png

 

Here's a piece of pseudo-code to realise this principle.

data: lt_object type whatever. "big *** table full of objects to update

 

 

 

while lt_object[] is not initial.

     loop at lt_object assigning <line>.

 

          call function ZUPDATE starting new task

               exporting <line>

               exceptions other = 9      

 

          if sy-subrc = 0.

               delete lt_object.

          endif.

     endloop.

endwhile.


Something like that.

Notice how there's a loop in a loop, to make sure that we keep trying until every object has been launched to a process. Once an object has been successfully launched, remove it from the list.

 

Queue clog

But there's a catch with that approach. As long as the processing of an individual object doesn't take up too much time, and you have enough DIAlog processes available, things will work fine. As soon as a process ends, it's freed up to take on a new task.

 

But what if your processes are called upon faster than they finish off? That means that within a blink of an eye, all your processes will be taken up, and new tasks will be queued. That also means that no-one can still work on your system, because all dialog processes are being hogged by your program.

queue clog.png

* notice how the queue is still launching processes, even after your main report has already ended.

 

You do not want that to happen.

 

First time that happened to me was on my very first assignment, where I had to migrate 200K Maintenance Notifications. I brought the development system to its knees on multiple occasions.

The solution back then was double the amount of Dialog processes. One notification process finished fast enough before the main report could schedule 19 new tasks, so the system never got overloaded.

 

Controlled Threading

So what you want, is to control the number of threads that can be launched at any given time. You want to be able to say that only 5 processes may be used, leaving 5 more for any other operations. (That means you could even launch these mass programs during the day!)

But how do you do that?

 

Well, you'll have to receive the result of each task, so you can keep a counter of active threads and prevent more from being spawned as long as you don't want them to.

 

caller:

data: lt_object type whatever. "big *** table full of objects to update

 

 

 

while lt_object[] is not initial.

     loop at lt_object assigning <line>.

          call function ZUPDATE starting new task

               calling receive on end of task

               exporting <line>

               exceptions other = 9      

      

          if sy-subrc = 0.

               delete lt_object.

               add 1 to me->processes

          endif.

     endloop.

endwhile.

receiver

RECEIVE RESULTS FROM FUNCTION 'ZUPDATE'.

substract 1 from me->processes

 

This still just launches all processes as fat as possible with no throtling. It just keeps the counter, but we still have to do something with that counter.

And here's the trick. There's a wait statement you can use to check if the number of used processes is less than whatever you specify.

But this number is not updated after a receive, unless you logical unit of work is updated. And that is only done after a commit, or a wait statement.

But wait, we already have a wait statement, won't that update it?

Why yes, it will, but than it's updated after you waited, which is pretty daft, cause then you're still not quite sure whether it worked.

 

so here's a trick to get around that.

caller:

data: lt_object type whatever. "big *** table full of objects to update

 

 

while lt_object[] is not initial.

     loop at lt_object assigning <line>.

 

 

          while me->processes <= 5.

               wait until me->processes < 5.

          endwhile.

 

 

          call function ZUPDATE starting new task

               calling receive on end of task

               exporting <line>

               exceptions other = 9      

 

 

          if sy-subrc = 0.

               delete lt_object.

               add 1 to me->processes

          endif.

     endloop.

endwhile.

 

That'll keep the number of threads under control and still allow you to achieve massive performance improvements on mass processes!

 

Alternatives

Thanks to Robin Vleeschhouwer for pointing out the Destination groups. Starting your RFC in a specific destination group, your system administrators can control the number of processes in that group. The downside is that it's not as flexible as using a parameter on your mass-processing report, and you have to run everything past your sysadmins.

 

Another sweet addition came from Shai Sinai under the form of bgRFC. I have to admit that I actually never even heard of that, so there's not much I can say at this point in time. Except, skimming through the doco, it looks like something pretty nifty.

Create Simple Real-Time Cockpit in Fiori Style (but with SAPGUI)

$
0
0

Scenario: Your Boss says you are getting your HANA Server as a christmas present, under the condition you provide a real-time cockpit for some KPIs from your production ERP by tonight.

 

Mission Impossible?

 

i guess you don't have a preinstalled HANA-Live with the KPI Framework(and also no Hana yet), Fiori, UI5 Framework, ADT on Eclipse or Hana Studio and maybe your javascript skills are not yet up to date

 

but you still can trust on good old ABAP (Screens)

 

you only use Old-Fashioned SE80 and after a few clicks, you (auto-refreshing) Dasboard looks like a fiori launchpad with 'smart' tiles, where the numbers in the box represent the actual kpi, the screen is auto-refreshed and also works with webgui. a double click on the kpi is calling the transaction with the details

dashboard1.gif

(as i use SAPGUI 7.40 with BlueCrystal Design, you have the fiori-style icons)

 

 

so what's the task?

 

create a report with a screen:

REPORT Z_MY_DASHBOARD.
DATA : ob_timer  TYPE REF TO cl_gui_timer .

data: lv_error_01 type i, lv_warning_01 type i.
data: lv_error_02 type i, lv_warning_02 type i.
data: frame01(30).
data: frame02(30).
data: number01 type i.
data: number02 type i.
data: icon01(50).
data: icon02(50).
data: it_session_list type  SSI_SESSION_LIST.
data: it_worker_list type SSI_WORKER_LIST.
data: lo_server_info type ref to cl_server_info.
create object lo_server_info.


class lcl_event_handler definition.

public section.

class-methods: on_finished for event finished of cl_gui_timer.

endclass.                    "lcl_event_handler DEFINITION

class lcl_event_handler implementation.

method on_finished.

perform get_data.
ob_timer
->run( ) .
*cause PAI
call method cl_gui_cfw=>set_new_ok_code
EXPORTING
new_code
= 'REFRESH'.

endmethod.                    "on_finished

endclass.                    "lcl_event_handler IMPLEMENTATION


start-of-selection.
*   Initialise object CL_GUI_TIMER.
data: event_handler type ref to lcl_event_handler.
CREATE OBJECT ob_timer .
create object event_handler .
*   Bind event handling method to object of CL_GUI_TIMER
*    SET HANDLER timer_event FOR ob_timer .
set handler  event_handler->on_finished for ob_timer.

*   Set interval for timer
ob_timer
->interval = 10.

*   Call method RUN of CL_GUI_TIMER.
ob_timer
->run( ) .


perform get_data.

call screen 0100.

*----------------------------------------------------------------------*
***INCLUDE Z_MY_DASHBOARD_STATUS_0100O01.
*----------------------------------------------------------------------*
*&---------------------------------------------------------------------*
*&      Module  STATUS_0100  OUTPUT
*&---------------------------------------------------------------------*
*       text
*----------------------------------------------------------------------*
MODULE STATUS_0100 OUTPUT.
SET PF-STATUS 'MAIN'.
*  SET TITLEBAR 'xxx'.
ENDMODULE.                 " STATUS_0100  OUTPUT


MODULE USER_COMMAND_0100 INPUT.

data: lv_field(50).

case sy-ucomm.
when 'EXIT' or 'CANC' or 'BACK'.
leave program.
when 'SELE'.
* Double Click for Navigation/Drill Down

GET CURSOR FIELD lv_field.
if lv_field cp '*01'.
call transaction 'VF04'.
elseif lv_field cp '*02'.
call transaction 'SM04'.
endif.

endcase.
ENDMODULE.                 " USER_COMMAND_0100  INPUT

form get_data.


FRAME01 = 'Amount Billing Due List'.
FRAME02
= 'Logged in Sessions'.

* KPI 1:
select sum( netwr ) from vkdfs
*    connection dbco-con_name "for sidecar/hana live
into number01. "where fkdat le sy-datum.


* KPI 2:

CALL METHOD lo_SERVER_INFO->GET_SESSION_LIST
*  EXPORTING
*    WITH_APPLICATION_INFO = 1
*    TENANT                = ''
RECEIVING
SESSION_LIST         
= it_session_list
.
describe table it_session_list lines number02.

* For SM50/Processes:

*CALL METHOD lo_SERVER_INFO->GET_WORKER_LIST


perform get_icon using number01 lv_warning_01 lv_error_01 icon01.
perform get_icon using number02 lv_warning_02 lv_error_02 icon02.
endform.
 
form get_icon using number warning error icon.

if number > error.
data: lv_icon(50).
CALL FUNCTION 'ICON_CREATE'
EXPORTING
name  
= 'ICON_RED_LIGHT'
*      text   = 'Refresh'
*      info   = 'Refresh Monitor Status'
IMPORTING
RESULT
= lv_icon
EXCEPTIONS
OTHERS = 0.
else.
if number > warning.
CALL FUNCTION 'ICON_CREATE'
EXPORTING
name  
= 'ICON_YELLOW_LIGHT'
*      text   = 'Refresh'
*      info   = 'Refresh Monitor Status'
IMPORTING
RESULT
= lv_icon
EXCEPTIONS
OTHERS = 0.
else.

CALL FUNCTION 'ICON_CREATE'
EXPORTING
name  
= 'ICON_GREEN_LIGHT'
*      text   = 'Refresh'
*      info   = 'Refresh Monitor Status'
IMPORTING
RESULT
= lv_icon
EXCEPTIONS
OTHERS = 0.
endif.
endif.
ICON = lv_icon.

endform.

 

 

 

Screen:

dashboard2.gif

 

dashboard3.gif

PROCESS BEFORE OUTPUT.
MODULE STATUS_0100.

PROCESS AFTER
INPUT.
MODULE USER_COMMAND_0100.



thats it -> welcome to simple smart business



of course its all hardcoded, but maybe your boss gives you some time till tomorrow to add a control table (/NSE11)

similar to

framenr
, "Frame Number 01, 02 etc
text type text50, "Frame Description
db_con
type dbcon-con_name, "Database connection for Hana Live/Sidecar
sql_statement
type string, "SQL Statement with Placeholders
class type char50, "or Name of an ABAP Class that retrieves the KPI as a Returning Parameter called RESULT
warning
type int4, "show Yellow Icon if value higher 
error
type int4,   "show red light if value higher
symbol type char50,"ICON Name

last_value type char 50, "(if youre not having hana you can use a periodic job to update kpi here)

christmas wish list: which kpis your boss likes to have?


-> definitely look at the new OPEN SQL Syntax for realtime-database-calculations on the fly (also for Non-Hana DB):  http://help.sap.com/abapdocu_740/en/index.htm?file=ABENNEWS-740_SP05-OPEN_SQL.htm



after that you should get your christmas hana present and start doing the real thing!



Step by step guide to enhance/update Vendor Master and generate idocs - Part3

$
0
0
The previous two blogs of this series have covered vendor master table and screen enhancement (part1) and the vendor master update api class (part2) used to update vendor master. This final part will cover the steps required to enhance CREMAS idoc and generate idocs for changes to custom fields added to vendor master structure (LFA1 for this scenario).
STEP1: Create a new IDOC segment for the custom fields that need to be distributed as part of the vendor master idoc - transaction WE31. Release the new segment after saving. This step ensures the new segment is active and valid for use by the idoc extension.
New Z segment.png
Zsegment release.png
STEP2: Create a new IDOC extension to include the new segment created in step1 - transaction WE30. Select CREMAS04 as the reference basic type and append the Z segment at the same /sub level of one of the header segments (E1LFA1M, E1LFA1H, E1FLA1A - other segments can be used for purchase org or company code depending on requirement ). Release the IDOC extension after saving
ZCREMAS_extension_creation.png
ZCREMAS extension.png
Release ZCREMAS04.png
STEP3: Use transaction WE82 to assign extension created in previous step to combination of basic type (CREMAS04/05) and message type. Enter the Release version based on your current sap release.
Assign extension to message type.png
STEP4: Update the message type defined in partner profile used in your scenario to define the extension created in step2, transaction - WE20.
Change partner profile for ZESCREMAS.png
STEP5: To enable change document creation for custom fields appended to vendor master table(s) follow the steps outlined below:
      • Activate change pointers globally - transaction BD61
      • Activate change pointers for message type - transaction  BD50
      • Assign table fields which are relevant for change pointer creation for message type. - transaction BD52
For more details regarding change pointers for IDOCS, check the sap help link.
Activate change pointer globally.png
Activate change pointers for ZESCREMAS04.png
BD52 change pointers.png
STEP6: Implement badi VENDOR_ADD_DATA_BI which is called during execution of function module MASTER_IDOC_CREMAS_SMD. This function module creates the idocs for vendor master related change pointers.
The code below is implemented in BADi method FILL_ALE_SEGMENTS_OWN_DATA. It gets the vendor master header details from E1LFA1M segment and read the custom field values from LFA1 table. Finally a segment record ( with custom segment data) is appended to IDOC data.
METHOD if_ex_vendor_add_data_bi~fill_ale_segments_own_data.

  CONSTANTS: c_newseg_name TYPE edilsegtyp VALUE 'ZE1LFA1A'.

  DATA: idoc_data   TYPE LINE OF edidd_tt,

        wa_e1lfa1m  TYPE e1lfa1m,

        wa_ze1lfa1a TYPE ze1lfa1a.

  DATA: wa_lfa1     TYPE lfa1.

  DATA: lr_vendor   TYPE REF TO zcl_vendor_base.

  FIELD-SYMBOLS: <fs_idoc_data> TYPE LINE OF edidd_tt.



  "Only for message type ZESCREMAS and segment name = E1LFA1M

  IF i_message_type = 'ZESCREMAS' AND i_segment_name = 'E1LFA1M'.

    LOOP AT t_idoc_data ASSIGNING <fs_idoc_data> WHERE segnam = 'E1LFA1M'.

      MOVE <fs_idoc_data>-sdata TO wa_e1lfa1m.

      EXIT.

    ENDLOOP.



*** get Vendor compliance safe score information for this vendor.

*** assign value to idoc_data-SDATA and append to changing T_IDOC_DATA

    CREATE OBJECT lr_vendor

      EXPORTING

        iv_vendor = wa_e1lfa1m-lifnr.



    wa_lfa1 = lr_vendor->get_vendor_master( ).



*** ASSIGN the values from LFA1 (wa_lfa1) to structure of segment ZE1LFA1A ( wa_lfa1fa1a ) ****

    wa_ze1lfa1a-CUSTOM_FIELDS     = wa_lfa1-zzvscs_CUSTOM_FIELDS.



*** set the idoc extention name in control record.

    e_cimtype = 'ZCREMAS04'.



*** populate segment name in the data record, copy data contents into

*** it, and append the data record to existing data records

    idoc_data-mandt    = sy-mandt.

    idoc_data-hlevel   = '01'.

    MOVE c_newseg_name TO idoc_data-segnam.

    MOVE wa_ze1lfa1a   TO idoc_data-sdata.

    APPEND idoc_data   TO t_idoc_data.

  ENDIF.

ENDMETHOD.
Testing IDOC generation for custom fields.

IDOC generation can be tested by one of the methods noted below

  • Using transaction BD14. - Provide vendor number and IDOC message type
  • Using transaction BD21 - Provide the message type for IDOC generation.

Status of generated IDOC can be checked using transaction WE02 - provide the basic type CREMAS04/05.

BD14.png

BD21.png

 

Troubleshooting

Below are some pointers to try if IDOCS are not generated/generated without custom segment during testing.

  1. Ensure change pointers are being generated. this can be verified by checking entries of table BDCP2. following tips could help if change pointer generation is the problem
    • Ensure the fields for which change pointers need to be generated have data elements with "change document" field selected ( as noted in part 1).
    • Ensure fields for which change pointers are needed are specified  in transaction BD52.
    • In case BADI- BDCP_BEFORE_WRITE is implemented, ensure logic is not preventing change pointer creation.
  2. IDOC is generated but no custom segment. Following tips might come in handy.
    • Ensure IDOC reduction is not active ( most unlikely, since this is a new segment ) using transaction SALE.
    • Check if some sort of segment filter is  active ,using transaction SALE.
    • Check transaction BD64 to validate if any filters are setup for your segment.

SALE.png


Top of page in SALV

$
0
0

Day 1

 

I was given a report to develop which would have a parameter as input on selection screen and display an ALV Grid report as output with required details. The purpose of the report was to download ALV output into excel and send it to vendor.

 

I wanted to make this report more user friendly and hence planned to develop this report using OOPS ALV with splitters & containers. Left pane showing the list of recently created data and the right pane showing details of the selected record.

 

Day 2 & 3

 

I figured out all the required database tables, created structures, internal tables and completed the business logic.

 

Created screens, used splitter container and build 2 ALV Grid (30% left and 70% right) using SALV class. Wrote handlers for click and double click. Everything was working fine at this instance.

 

Day 4

 

Since report was ready, I started adding top of page. Here came the hurdle. The report was not showing the top of page as I had used containers . I had looked in SDN and many had suggested to create a container on top and populate it. But when I did that and exported the report to excel, the heading(TOP-OF-PAGE) was missing .

 

I commented the parameter  r_container  in cl_salv_table=>factory, the report was showing properly. But this time, my hitlist(Left container) was missing.

 

Approach

 

I found the program and screen number which SALV was using to display the report. I created a docking container instead of splitter and placed the docking container on that screen. Sample code below.

 

Sample

CREATE OBJECT go_dockcontainer

     EXPORTING

        repid = 'SAPLSLVC_FULLSCREEN' " This is the program which uses SALV

        dynnr = '0500' "Screen number

*     NO_FLUSH     = 'X'

*     DOCK_AT      = DOCK_AT_LEFT

       extension    = 300

*     HEIGHT       = 42

     EXCEPTIONS

       create_error = 1

       OTHERS       = 2.

 

Then I placed the hit list ALV on this docking container.

 

Sample

TRY.

       cl_salv_table=>factory(

         EXPORTING

           r_container = go_dockcontainer

         IMPORTING

           r_salv_table = go_hit_list_salv

         CHANGING

           t_table      = gt_sflight ).

     CATCH cx_salv_msg.                                  "#EC NO_HANDLER

   ENDTRY.

 

The rest of the logic remained the same.  I also found that I had to call  set_top_of_list_print along with  set_top_of_list in order to get exported to excel.

 

Another hurdle

 

Now, everything looked fine but I faced another issue. SALV was not refreshing. It was expecting an event in order to get refreshed.

 

I had created a dummy button and assigned to PF status. Triggered this OK_CODE via below code during event handling of hit list.

 

Sample

  cl_gui_cfw=>set_new_ok_code(

     EXPORTING

       new_code = 'DUMMY'    " New OK_CODE

   ).

 

I had refreshed the grid in user command of the second ALV.

 

 

Note:

 

- Every time something is selected on the hit list, an event is triggered. So if data is high, it might create a performance issue.

 

 

Comments and suggestions are most welcomed

Searching for SAP correction Notes from transaction ST22

$
0
0

You may have heard about ANST (Automated Notes Search Tool) and PANKS (Performance Assistant Notes and KBA Search).


Both ANST and PANKS make it possible to find SAP Notes and KBAs directly from your system, based on the context:

 

  1. Valid for your system and Support Packages level.
  2. In the case of ANST, relevant for the replicated process.
  3. In the case of PANKS, relevant for the message class and number and the context from which the ABAP message has been prompted.

 

 

 

 

 

 

However, you may not know there is another tool that searches for SAP Notes directly from transaction ST22. Our SAP colleague, Dr. Peter John developed this helpful feature some years ago, but still SAP users or “dump victims” are not very acquainted with it. I hope after publishing this article more SAP users will benefit from it.

 

 

How “ST22 SAP correction Notes search” works:

 

 

It is very simple:
Those dumps belonging to the categories “ABAP programming error” and “Screen programming error” come along with a “Note Search” button:

 

correction1.jpg

 

 

After clicking on the SAP Correction Notes icon, you will receive a pop up screen with the notes, if found.

 

 

 

correction2.jpg

 

 

In the example above, SAP correction notes will be searched that:

 

  1. Contain the following search terms: DATA_OFFSET_TOO_LARGE RPULCP00.
  2. Are valid for your system and Support Packages level.

 

 

 

 

Warning: The “SAP correction Notes” icon does not show up when the dump occurs but only displaying the same dump from transaction ST22.

 

 

You will find more information about this new functionality in the following KBA:

 

 

2102812 - Search for correction notes in transaction ST22

 

 

The following video demo is also available:

 

 

A huge thanks to Carlos Martinez Escribano for putting together this information!

{z}restapi - lightweight REST API for SAP WebAS

$
0
0

Introduction

 

The {z}restapi is an alternative solution to simplify the development of REST APIs on top of the Internet Communication Framework (ICF).

 

By separating what changes from what stays the same, it helps you to concentrate your effort on the business logic instead of spending your time developing repetitive code.

 

 

Features

 

  • Resource routing - resources are automatically identified by the request URL and HTTP method. The simple implementation of one (or all) of the 4 interfaces (CRUD methods) used by the {z}restapi is enough to make the resource available in your API.

 

  • Automatic value assignment - the values of form and querystring parameters sent in the HTTP request can be automatically assigned to the fields of the importing structure which have the same name (not case-sensitive). The dictionary structure to be used is easily defined by simply implementing one interface method.

 

  • Automatic conversion to JSON - the response data can be a dictionary structure, table or even a complex type. By default the response data is converted to the JSON format but you can easily add your own data conversion in the corresponding method of the {z}restapi handler.

 

 

Installation

 

 

  1. Install SAPLink - If you already have SAPLink installed in your system you can skip this step. We strongly recommend that you install also the SAPLink plugins.Click here to view the SAPLink wiki page.

  2. Install zJSON - If you already have zJSON installed in your system you can skip this step. Download the latest version of zJSON on GitHub and import the nugg file using the ZSAPLINK program. Don't forget to activate the imported objects respecting the dependencies (dictionary objects first and classes second, in this case).

  3. Install {z}restapi - Download the latest version of {z}restapi on GitHub or ISinitial.org and import the nugg file NUGG_ZRESTAPI-x.x.x.nugg using the ZSAPLINK program. Don't forget to activate the imported objects respecting the dependencies (dictionary objects first, interfaces and classes second and finally the reports).



Usage

 

The first thing that you have to do is to create a new service in the ICF tree.

 

Go to the SICF transaction and create it under the SAP node like shown in figure 1.

 

http://isinitial.org/img/ICF.jpg

Figure 1 - Create the ICF service

 

 

On the Handler list tab inform the class ZCL_REST_HANDLER.

 

Optionally, for your first tests, you can go to the Logon Data tab and inform Client, User, Language and Password. This way you will not be prompted to inform your credentials.

 

Save, go back to the previous screen, right click the service and activate it.

 

http://www.isinitial.org/img/ICF2.jpg

Figure 2 - Activate the ICF service

 

 

 

Hello World example

 

 

Step 1 - Create the Resource

 

Go to transaction SE24 and create a new class

 

ZCL_REST_RESOURCE_HELLO_WORLD

 

The text that goes after the "ZCL_REST_RESOURCE_", in this case "HELLO_WORLD", defines the resource name.

 

Go to the Interfaces tab and inform the ZIF_REST_RESOURCE_READ interface as shown in the figure 3.


http://www.isinitial.org/img/HelloWorld1.jpg

Figure 3 - Implementing the interface



 

Step 2 - Implement the Interface READ method

 

Go to the Methods tab and implement the method ZIF_REST_RESOURCE_READ~READ with the code below.

 

METHOD zif_rest_resource_read~read.  DATA: ls_response TYPE zst_rest_response_basic.  ls_response-success = 'true'.  ls_response-msg = 'Hello World'.  ls_response-code = 200.  e_response = ls_response.
ENDMETHOD.

 

Step 3 - Test your service

 

Copy the URL below and paste it into your browser's address bar, replace the "host" and "port" with the hostname and port of your server and test your service.

 

 

host:port/sap/zrestapi/myApp/hello_world

 

 

If everything is OK you will receive the JSON response below.

 

{  success: "true",  code: 200,  msg: "Hello World"
}


Retrieving parameters

 

Let's extend our Hello World example to say Hello to everyone!

 

The form and/or querystring parameters sent in the HTTP request can be retrieved in two ways:

 

  1. They can be retrieved automatically by simply implementing the method GET_REQUEST_TYPE of the corresponding interface. All you have to do is to return the name of the dictionary structure to be used in the automatic value assignment. The values of the parameters are passed to the fields of the structure that have the same name. This assignment is not case-sensitive.

  2. Reading the internal table I_T_NVP that contains the Name/Value pairs of all form and querystring parameters of the HTTP request. This internal table is passed as an import pararameter to the CREATE / READ / UPDATE / DELETE methods of the corresponding interfaces.

 

Let's change our Hello World example to retrieve a "NAME" parameter.

 

First we have to implement the GET_REQUEST_TYPE method to return the dictionary structure name. Create a structure on the ABAP Dictionary (SE11) with name ZST_HELLO_REQUEST. For our example this structure need to have only one field called "NAME" and type STRING.


Now let's modify the READ method to receive the parameter and use it to compose the return message.


METHOD zif_rest_resource_read~read.  DATA: ls_request  TYPE zst_hello_request,             ls_response TYPE zst_rest_response_basic.  ls_request = i_request.  ls_response-success = 'true'.  CONCATENATE 'Hello' ls_request-name    INTO ls_response-msg    SEPARATED BY space.  ls_response-code  = 200.  e_response = ls_response.
ENDMETHOD.


Now, let's test our Hello World passing the "NAME" parameter.

 

/hello_world?name=Everyone!

 

If everything is Ok you will receive the JSON response below.

 

{  success: "true",  code: 200,  msg: "Hello Everyone!"
}

 

Well, that's all for now . Over the next weeks I'll be publishing other blog posts here talking about the usage of the {z}restapi inside BSP pages and about token based authentication for stateless services using the custom user authentication mechanism that is delivered along with the {z}restapi.



Download


www.isinitial.org


christianjianelli/zrestapi · GitHub


/NSE16H, /NTAANA, or Hana Studio, which way to go? Instant Database Analytics

$
0
0

Remember Transaction /NTAANA? Do you already use the new SE16H Transaction?

 

Transaction TAANA is a very useful transaction to analyze number of documents per year or per Document Type etc for Data Archiving Projects, Carve-Out analysis etc.

TAANA is able to compute the statistics in Batch so even in tables with millions of rows, we are able to analyse the results in a comfortable way.

i played around with this on hana, and of course i don't need batch analytics, the results come in dialog in seconds.

 

see also note for some limitations: http://search.sap.com/notes?id=0001879808

 

taana.gif

select 'create ad hoc variant button'

 

taana2.gif

 

choose statistic fields like organisation values (salesorg, co area) or order type etc

 

taana3.gif

non-hana: choose in the background for large tables

hana: online!

 

taana4.gif

 

analysis results: number of documents etc

 

in Netweaver 7.40, we can do the same using SE16H: (works also for Classic Databases)

see note http://search.sap.com/notes?id=0001636416

 

advantage here: we can also define JOINS!

 

se16h.jpg

just select group or total and you will get the distinct/total values instead of the detail lines

 

join definition:

 

se16h_join.gif

 

se16h_result.gif

 

in realtime

 

 

hana studio:

get a quick impression by using data preview/distinct values:

hana_studio.gif

 

or use analysis for more details (select x/y axis):

hana_studio2.gif

 

in my version of web ide there was no preview feature, i think this is coming in Hana SPS9:

hana_web_ide.gif

CL_SALV_TABLE->FACTORY method, no negative symbols on ALV export.

$
0
0

Fellow ABAP Developers,

 

I would like to raise a possible issue or misunderstanding that I have come across recently when using the CL_SALV_TABLE->FACTORY method for generating ALV reports. It had recently come to my attention that when exporting a report to spreadsheet from ALV, that the negation symbols would be missing in a spreadsheet and output when the ALV originated from the CL_SALV_TABLE->FACTORY method. A SCN/Google search revealed only two previous posts on the matter which will be referenced below. The posts suggested solutions from anonymous users which resolve the issue but fail to adequately explain the behavior. I want to raise this again for discussion not only for clarity of the problem/misunderstanding but to make a point in the chance there are other developers out there who used this method but are not aware of the potential issue.

 

PROBLEM

When using method CL_SALV_TABLE->FACTORY to generate ALV reports, negative signs or symbols do not come across for domains which do not have a “Sign” attribute for them flagged. This means inherently that these domains are not meant to have negations signs or any other symbols for that matter attached to them. However, even prior to CL_SALV... the use of these corresponding data types never seemed to be an issue for output to ALV when negation is done directly on the field. In fact, ALV always displays the correct value on screen (shows negative symbol) when using the REUSE FMs and CL_SALV_TABLE->FACTORY. The problem comes when the fields are exported to spreadsheet and possibly other methods (raw text) from ALV. In the case where this is exported to a spreadsheet from ALV, and using the CL_SALV_TABLE->FACTORY method to generate the ALV, the negative signs are missing. This behavior with the signs and spreadsheet export did not seem to be an issue when using the REUSE FMs.

 

In my experience, the spreadsheet export function is used very frequently in relation to ALV which is why I see this to be very problematic as one may view the data correctly on screen through ALV (negative sign shown) but when exported, the data is now invalidated because the sign is now missing.

 

SOLUTION

As mentioned earlier, a simple internet search revealed two discussions on this:

 

1. http://scn.sap.com/thread/2069432

2. http://scn.sap.com/thread/1042384

 

Anonymous users suggested the following two solutions:

 

1. Do not use a data type of a domain that does not have their sign attribute flagged.

2. Call method on the columns experiencing this issue SET_SIGN( 'X' ).

ex. lo_column->set_sign( 'X').


The problem or misunderstanding with solution 1 is that this may not always be clear on what types to use for the ALV output, assuming the developer is aware of this potentiality and corruption of exported data.

 

The problem or misunderstanding with solution 2 is that the column type CL_SALV_COLUMN should have this attribute “SIGN” set to true ( 'X' ) by default. This means that this is being overridden somewhere, but alas, could find no documentation which mentions this in the class tree or reasoning for this.

 

POINTS OF DISCUSSION/REVIEW

  1. Bring this potential problem to the attention of other developers who may have used this method incorrectly and have not checked the ALV export data which may contain negative signs.
  • This problem misunderstanding could be easy for a ABAP developer to miss especially if they are migrating from the REUSE FMs.
  • It may also be harder to catch the issue as the spreadsheet export might not be checked as the ALV on screen outputshows the correct symbol.

 

  1. Why, in more depth, does this behavior occur?
  • Data is displayed correctly in ALV on screen but not in the export.
  • This did not appear to be an issue with RESUE FMs.
  • Why and where does the SIGN attribute get set to FALSE for column type CL_SALV_COLUMN when using the FACTORY method?

 

  1. Was this behavior intended for CL_SALV_TABLE ALV use and the developer must explicitly set the SIGN attribute where this behavior for effected columns may occur?
  • Would it be better if the class default for attribute SIGN be left TRUE for CL_SALV_COLUMN?
    • What advantage is there for a user to explicitly define this if the problem/understanding can be avoided altogether if this remains TRUE?
      • Perhaps other issues arise when this is set to TRUE?

     4. Are there any other similar caveats such as this one that a developer migrating to CL_SALV from the REUSE FMs should be aware of?

SAP2SAP Runtime Remote Data Typing

$
0
0

Whenever structures or internal tables with many fields are involved in an sap2sap integration i usually carry out it using remote runtime data-typing, as i've shown in my two blogs:


 

This approach avoids data dictionary redundancy across systems and saves also a lot of time in data dictionary creation if involved structures contain dozens or hundreds of fields. Recently, thanks to the work of Hans (and previously also thanks to my colleague Fabrizio), i realized that the solution was limited to simple structures; simple in the sense that weren't managed fields defined as:

 

  1. structure include
  2. nested structures
  3. nested internal tables

 

For example (considering all above cases) a structure defined as:

 

Untitled.png

 

Although a simple structure is the most common case i decided to put my hands on code once again, starting from Hans's work, to handle all these cases. At first glance i thought it was a really hard work but i was wrong; in fact, once reviewed my old code, it has been very easy. I did a few simple fixes to my zcl_dyn_remote_type_builder=>get_components method to achieve the goal. Now the method is able to handle following internal data types:

 

  1. 'h' internal table (cl_abap_elemdescr=>typekind_table)
  2. 'u' structure (cl_abap_elemdescr=>typekind_struct1)
  3. 'v' deep structure (cl_abap_elemdescr=>typekind_struct2)

 

and create recursively the related abap components by means of usuals zcl_dyn_remote_type_builder=>create_struct_type and zcl_dyn_remote_type_builder=>create_table_type methods. It was necessary to call ddif_fieldinfo_get function module specifying the "all_types flag". It was also necessary to handle the returned internal table "lines_descr" for nested internal table data typing. At last i used the lfield field returned in dfies_tab to distinguish between fields added as structure include (.include field in picture) and fields of a structure included (the struc1 field in picture). Below a snippet of the new code:

 

* build elements

  call function 'DDIF_FIELDINFO_GET' destination i_rfcdest

    exporting

      tabname        = i_struct

      all_types      = 'X'

    importing

      x030l_wa       = ls_x030l

      lines_descr    = lt_lines

    tables

      dfies_tab      = lt_fields

    exceptions

      not_found      = 1

      internal_error = 2

      others         = 3.


  [...]

 

   loop at lt_fields into ls_dfies where not lfieldname cs '-'.

 

   [...]

 

*   build elements

    case ls_dfies-inttype.


*     build table types

      when cl_abap_elemdescr=>typekind_table.

     

        read table lt_lines into ls_line with key typename = ls_dfies-rollname.

        read table ls_line-fields into ls_lfield with key tabname = ls_line-typename.

        lo_table = zcl_dyn_remote_type_builder=>create_table_type

                               ( i_rfcdest = i_rfcdest

                                 i_struct  = ls_lfield-rollname ).

        ls_comp-type = lo_table.


*     build structure types

      when cl_abap_elemdescr=>typekind_struct1 or                      

           cl_abap_elemdescr=>typekind_struct2.

     

        lo_struct = zcl_dyn_remote_type_builder=>create_struct_type

                                  ( i_rfcdest = i_rfcdest

                                    i_struct  = ls_dfies-rollname ).

        ls_comp-type = lo_struct.


*     build element types (also for fields in included structures)

      when others.

      lo_elem = zcl_dyn_remote_type_builder=>get_elemdescr

                                      ( i_inttype  = ls_dfies-inttype

                                        i_intlen   = lv_intlen

                                        i_decimals = lv_decimals ).

      ls_comp-type = lo_elem.

 

    endcase.

 

    append ls_comp to result.

   

  [...]

 

  endloop.


My saplink nugg is available there.

Once you tired of ALV GRID

$
0
0

Today is the day you may learn how to show table data!

 

No, not again! Not again one of those REUSE_ALV_GRID or CL_GUI_ALV_GRID nor CL_SALV_TABLE. Not a bit!

Imagine you need to provide users with great layout he or she familiar with.

Furthermore you want to let users edit functionality.

 

Please feel free to use one of those techniques. But CL_SALV_TABLE fals out since you need edit function.

You shouldn't expect each user to be happy with SAP standard interface. But almost all users are pretty familiar with MS Office.

 

Let's imagine if we can create an Excel workbook with data users want to see. It would be a great deal because almost everyone in the modern world is familiar with its interface:

http://social.technet.microsoft.com/wiki/cfs-file.ashx/__key/communityserver-wikis-components-files/00-00-00-00-05/2158.ExcelResults.png

This nice-looking interface has additional strengths. At least one: it may manipulate with data that is not shown on the screen.

Some of data may be lost if someone do copy-paste operation with a huge amount of data working with ALV GRID.

 

It's surely solved with MS Excel.

Pros and cons:

ALV Grid:

+ Already exists

+ Easy to use

+ Can be used in backgroud jobs

+ Pretty (? Sure it is pretty if you're a SAP professional)

- Not transparent behavior for users and developers in case of editable Grid

- Not as flexible as Excel

- Takes time to get used to it

 

MS Excel

+ Pretty

+ Works with great amount of data

+ Users are familiar with it

+ Can have Charts, Logo and so on

- Needs some magic to implement it (it is discussed below)

- Needs MS Office to be installed onto user's PC (being honest: it's almost a standard to have MS Office on each work Win PC)

- Holds some extra space of user's screen (shown below)

 

Why we don't try to create this MS Excel integration in our report?

 

What we need: a template XLSX file. This will hold styles, charts and logos (if realy need it).

Perhaps you know it already XLSX file is actualy a zip archive with several XML files within it.

We need only two:

.\xl\sharedStrings.xml

Без имени-2.jpg

and

.\xl\worksheets\sheet1.xml

Без имени-3.jpg

 

A quick info: the first is to store all unique texts of the workbook cells and the second is to keep the first worksheet.

All we need is to add new strings into the first one and to put a table into the second.

I've created a simple tool to add new texts into a XLSX file and its use looks like:

Без имени-4.jpg

You may find it at my posts if you realy need it.

And then I've created a XSL-transformation to fill the worksheet:

 

Please forgive me for placing it here:

<xsl:transform version="1.0"

   xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

   xmlns:sap="http://www.sap.com/sapxsl"

>

<xsl:strip-space elements="*"/>

<xsl:template match="/">

<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac"

xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">

   <dimension ref="A1:J2"/>

   <sheetViews>

     <sheetView tabSelected="1" workbookViewId="0"/>

   </sheetViews>

   <sheetFormatPr defaultRowHeight="14.4" x14ac:dyDescent="0.3"/>

   <cols>

     <col min="1" max="1" width="9" bestFit="1" customWidth="1"/>

     <col min="2" max="2" width="28" bestFit="1" customWidth="1"/>

     <col min="3" max="4" width="10.109375" bestFit="1" customWidth="1"/>

     <col min="5" max="5" width="20" bestFit="1" customWidth="1"/>

     <col min="6" max="6" width="7.21875" bestFit="1" customWidth="1"/>

     <col min="7" max="7" width="8.5546875" bestFit="1" customWidth="1"/>

     <col min="8" max="8" width="12.109375" bestFit="1" customWidth="1"/>

     <col min="9" max="9" width="8.6640625" bestFit="1" customWidth="1"/>

     <col min="10" max="10" width="9.21875" bestFit="1" customWidth="1"/>

   </cols>

   <sheetData>

     <row r="1" spans="1:10" ht="30.6" x14ac:dyDescent="0.3">

       <c r="A1" s="1" t="s">

         <v>0</v>

       </c>

       <c r="B1" s="1" t="s">

         <v>1</v>

       </c>

       <c r="C1" s="1" t="s">

         <v>2</v>

       </c>

       <c r="D1" s="1" t="s">

         <v>3</v>

       </c>

       <c r="E1" s="1" t="s">

           <v>9</v>

       </c>

       <c r="F1" s="1" t="s">

         <v>4</v>

       </c>

       <c r="G1" s="1" t="s">

         <v>5</v>

       </c>

       <c r="H1" s="1" t="s">

         <v>6</v>

       </c>

       <c r="I1" s="1" t="s">

         <v>7</v>

       </c>

       <c r="J1" s="1" t="s">

         <v>8</v>

       </c>

     </row>

     <xsl:for-each select="//ITEMS/*">

     <row spans="1:10" x14ac:dyDescent="0.3">

       <xsl:attribute name="r">

          <xsl:value-of select="INDX"/>

       </xsl:attribute>

       <c s="2" t="s"><v><xsl:value-of select="F1"/></v>

       </c>

       <c s="2" t="s"><v><xsl:value-of select="F2"/></v>

       </c>

       <c s="2" t="s"><v><xsl:value-of select="F3"/></v>

       </c>

       <c s="2" t="s"><v><xsl:value-of select="F4"/></v>

       </c>

       <c s="2" t="s"><v><xsl:value-of select="F10"/></v>

       </c>

       <c s="3"><v><xsl:value-of select="F5"/></v>

       </c>

       <c s="3"><v><xsl:value-of select="F6"/></v>

       </c>

       <c s="3"><v><xsl:value-of select="F7"/></v>

       </c>

       <c s="3"><v><xsl:value-of select="F8"/></v>

       </c>

       <c s="3"><v><xsl:value-of select="F9"/></v>

       </c>

     </row></xsl:for-each>

   </sheetData>

   <sheetProtection password="DE25" sheet="1" formatCells="0" formatColumns="0" formatRows="0" insertColumns="0" insertRows="0" insertHyperlinks="0" deleteColumns="0" deleteRows="0" sort="0" autoFilter="0" pivotTables="0"/>

   <pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>

   <pageSetup paperSize="9" orientation="portrait" horizontalDpi="0" verticalDpi="0" r:id="rId1"/>

</worksheet>

</xsl:template>

</xsl:transform>

 

Here are thre points to be discussed:

1. Text values go with <c t="s"> tag (note attribute t value). And I pass here indexes from my tool.

2. Number values go without attribute t for tag 'c'.

3. Protection for the sheet is set on.

 

The last point will forbid any unexpected changes. In my example there is a difference between <c s="2"> style and <c s="3">.  The main purpose is to portect uneditable cells and let the rest be changeable.

 

At last we are to show the XLSX.

Let we have:

1. A binary string with zip content of an XLSX file

2. A screen with a container

 

And here is my example to show the MS Excel file:

     DATA: lv_string TYPE char1024.

*       container   TYPE REF TO cl_gui_container,

*       doi_proxy   TYPE REF TO i_oi_document_proxy.

*       l_control   TYPE REF TO i_oi_container_control

*       control     TYPE REF TO i_oi_ole_container_control

*       xlsx_string TYPE xstring " holds binary of the XLSX file

 

     CHECK container IS NOT BOUND.

 

     CREATE OBJECT container TYPE cl_gui_custom_container

       EXPORTING

         container_name = 'CONTAINER'.                       "#EC NOTEXT

 

     c_oi_container_control_creator=>get_container_control(

       IMPORTING

         control = l_control ).

 

     control ?= l_control.

 

     CALL METHOD control->init_control

       EXPORTING

         r3_application_name      = 'Demo'                   "#EC NOTEXT

         inplace_enabled          = abap_true

         inplace_scroll_documents = abap_true

         parent                   = container

         register_on_close_event  = abap_true

         register_on_custom_event = abap_true.

 

     CALL METHOD control->get_document_proxy

       EXPORTING

         document_type      = 'Excel.Sheet'                  "#EC NOTEXT

         register_container = abap_true

       IMPORTING

         document_proxy     = doi_proxy.

 

 

     DATA: lt_table TYPE enh_version_management_hex_tb,

           lv_size  TYPE i.

 

     CALL FUNCTION 'ENH_XSTRING_TO_TAB'

       EXPORTING

         im_xstring = xlsx_string

       IMPORTING

         ex_data    = lt_table

         ex_leng    = lv_size.

 

     doi_proxy->open_document_from_table(

       document_table = lt_table

       document_size  = lv_size

       open_inplace   = abap_true ).

 

And the final screenshot to encourage:

Без имени-5.jpg

Hope this may help or inspirit you!


Best Practice updating IT759 Compensation Process Records in SAP Enterprise Compensation Management

$
0
0

I was recently helping one of my clients regarding a reporting issue with SAP Enterprise Compensation Management and data not being displayed correctly on one of their custom reports. This report was a copy of standard report PECM_SUMMARIZE_CHNGS - Summarize Comp. Planning Changes with some customer specific enhancements and additions. As the title of the report gives away, the report is to summarize the changes to the compensation data for a particular compensation plan during a particular compensation review. This client is on SAP ECC 6.0 EHP5, and has been live with ECM compensation planning for about 2 years (Merit and Bonus).

 

I tried to understand what was going on and found out from the client that they ran a custom program to mass update existing IT759 records recently and that was the start of the issue. The issue was that none of the most recent changes were not picked up by the report, and in many cases some of the associates who were changed in mass were not picked up by the report at all.

 

So I looked into the logic of the custom version of the summarize change report and saw that it reads based upon a history table T71ADM_EE_HIST which captures the changes of the IT759 record during the compensation planning process. I next noticed that none of the changes that were made via the custom program were captured into this program. So I debugged the program used to make the changes and found the following:

 

IF p_test EQ ' '.

UPDATE pa0759 ls_pa0759.

 

The program was written to update the database table PA0759 directly! This is never recommended, and I immediately let the client know that we should change this. There are many issues with updating database tables directly, including the lack of a data consistently check with updating the values directly and the fact that it does not update the corresponding fields such as Changed On, & changed By unless you specifically write the logic.

 

My next thought was that we should update the program using Function Module HR_INFOTYPE_OPERATION, as is the standard practice for updating infotypes. I was thinking that there was logic built by SAP that would then automatically update the history table.

 

 

img2.png

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

After making the change and testing, unfortunately the history table remained unchanged.

Next, I decided to look into the SAP standard code and see how they update the history table, which I confirmed is update whenever you use any standard SAP route to create/update IT759. The standard routes for updating this table are the following

  1. Create & Update via MSS when manager changes data
  2. Standard program PECM_CREATE_COMP_PRO - Create Compensation Process Records
  3. Standard program PECM_ADJUST_0759 - Adjust Compensation Process Records
  4. Update master data directly via PA30

I debugged the standard SAP code and found that SAP created a message handler method PROCESS_IT_CHANGE (CL_HRECM00_EE_HIST)  implemented in class ‘CL_HRECM00_EE_HIST’ to modify the history table.  Whenever there is any update to an IT759, this method will compare the old and new records then modify the record in history table

img3.png

 

img4.png

 

After debugging and finding this, I called this METHOD in the custom program so that whenever we run the custom program to update IT759, it will also update the history table!

ABAP Modern Code Conventions

$
0
0

Introduction

Coming from a computer science background and having learnt ABAP on the job in a SAP consultancy, I always wondered why there are so restrictive naming-conventions and why every customer has their own custom ones, each apparently trying to trump the other with even more detailed naming-conventions for every little technical possibility... To me it seems to be a SAP-speciality that has vanished everywhere else (or so it seems ... hello cobol legacy applications, hello hungarian notation)


Having read this interesting Blog post: Nomen est omen a while ago and having to tackle a big custom code-base ourselves (with our own inevitable hardcore-detailed naming conventions and our own fair share of technical debt ...) we discussed what to focus on in the future.


Goals: Simple, short & concise code conventions that ease development and make the code easier to read.

After all, you as a developer are spending about 70% of your time reading & analysing existing code. You want that code to be short and readable. And you don't want to constantly lookup naming conventions, do you?

 

How much code do you fully understand from top to bottom? Can you analyse & memorize only 100 lines of unbroken, unknown code and estimate the implication of new changes? I don't. I feel big relief if that code is broken up into as many form-routines (functions, methods, whatever) as possible: We dearly want Seperation of Concerns.


Decisions

  • No global variables (except where required... Hello, dynpro-binding): Local variables are the norm
  • No technical duplication of information already explicitly & neatly specified in the type-system (we love statically typed languages and despise dynamic languages, dont' we?). Instead focus on semantics, readability and meaningful names.
  • Keep it short: Form-Routines, Function-Modules and Class-Methods are limited to 70 lines of code/LOC.
    From all we've heard this should automagically lead to better (not good) maintainability

 

Some Rules derived from these decisions:

  • Since every variable is local there is no need for "My prefix hereby tells you that I am ...*fanfare*... local!".
    So no more L_ prefix. If you see a variable like "flight_time", it is local. Spare prefixes for special occasions, to emphasize.
  • Global variables are a special case, use the G_ prefix to discriminate them as the despicable things they are.
    Your brain/eye instantly catches these special prefixes, they no longer disappear between all those other L_ ...
  • Class-attributes are like local variables, they are class-local, they are not global.
    As such they don't have any prefix either but you may choose to use "me->attribute" to clearly indicate attribute-access.
  • Use meaningful constants like co_open_in_external_window instead of 'E'
    It does not matter whether these constants are defined globally or locally, just use a "CO_" prefix to specifiy them as being a constant.
    If you have an interface that only contains constants (an ABAP design pattern to create nice container for constants), you may omit the "CO_" prefix altogether. And yes, you may argue this constant-prefix too
  • If you define types it does not matter whether the type-definition is local-only or globally, just use "TY_" for types.
  • The most controversial: The variable shall have a meaningful name, no abbreviations and shall not contain technical information that is already specified in the type-system. Focus on the semantics!
    So no more "ls_sflight" and "lt_sflight" but "sflight_entry" and e.g. "sflight_tab_delete" (emphasize on "delete" to describe what entries are contained in the table and why).
    You may argue "So you traded lt_ for the _tab suffix and actually gained nothing, well done.".
    In some way or the other you have to declare a table as being a multitude of things, it does not matter if you use the typical s-plural suffix for "sflightS" or use a "_tab" suffix, the important thing is to focus on its meaning and reserve prefixes to emphasize on important things (hello ugly globals...).
    Besides, ABAP itself is already shouting in your face whether that variable is a table/structure:
    READ TABLE x, LOOP AT x, x-comp = 'foobar' etc.    you really don't need anything more 90% of the time...
  • Use mathematical notation instead of abbreviations that make the code harder to read: <= is way more intuitive than LE.
    Just use normal math-operators that you've already learnt in school. I often hear that NE and LE are perfectly reasonable and understandable but this argument always comes from guys with decades of experience. I think the more simple way (not easy) is always to be preferred, there is no point to distinguish yourself from lesser experienced by using voodoo'ish notation...

 

TL/DR: This leads to the following inevitable naming-convention-matrix:

PrefixComment
LocalsNONE
GlobalsG_
Field-Symbols<NONE>Really ugly: global Field-Symbols: <G_XXX>
AtttributesNONEUse "me->attribute" to distinguish from locals if necessary
ConstantsCO_No distinction local vs. global (Omit prefix in constant-interfaces)
TypesTY_No distinction local vs. global
Form UsingI_Concentrate: Using = Importing/Input, no need to distinguish
Form ChangingC_
FM ImportingI_Try by reference
FM ExportingE_Try by reference
FM ChangingC_Try to avoid
FM TableT_Avoid!
Method ImportingI_Try by reference
Method ReturningR_
Method ExportingE_Try to avoid
Method ChangingC_Try to avoid
Select-OptionsS_
ParametersP_

 

This table is hopefully concise enough, we actually printed it on 1 DinA5, instead of the former 2 DinA4 pages.

 

Before starting the eagerly awaited flame war, please consider that these conventions follow those described in the book Official ABAP Programming Guidelines. of Horst Keller, Wolf Hagen Thümmel - by SAP PRESS (p. 208ff). Even if the internal SAP conventions really hurt my eyes and every SAP development team seems to use their own even more cumbersome conventions, Thank you Horst Keller for these guidelines.


No conclusion yet, we just started with these conventions and still figure out how to transform the existing code-base in a pragmatic approach...

 

Tools

Code-Inspector Test based on CL_CI_TEST_SCAN to limit the allowed Lines of code (Will release that soon...)

We are using smartDevelop but still need to figure out a good transformation-strategy.

 

Thank you for your attention, any comments appreciated.

Happy 6E65772079656172!

$
0
0

cl_demo_output=>new( )->write_html(

  cl_abap_codepage=>convert_from(

    CONV xstring(

      `3C68313E486170707920` &&

      `4E657720596561723C2F` &&

      `68313E3C62723E2E2E2E` &&

      `20616E64206C6F747320` &&

      `6F662066756E20776974` &&

      `68203C423E414241503C` &&

      `2F623E20696E203C623E` &&

      `323031353C2F623E21` ) ) )->display( ).

AngularJS Single-Page Application + {z}restapi and token authentication

$
0
0

Building a Single-Page Application (SPA) with AngularJS  and Bootstrap CSS to consume a REST API based on {z}restapi and token authentication

 

In this blog we are going to develop a Single-Page Application (SPA) with AngularJS and Boostrap CSS to store contacts on the SAP WebAS ABAP using a REST API based on {z}restapi and its token based authentication mechanism. The Single-Page Application will allow us to make use of the 4 CRUD methods, Create, Read, Update and Delete, provided by the REST API.



Pre-requisites

 

In order to develop and test the application it is necessary to have:

 

On the Server side

 

  • A SAP WebAS ABAP with the {z}restapi installed ({z}restapi download and installation instructions on GitHub)

 

On the Client side

 

 

Note: you can deploy and run the Single-Page Application (SPA) as a BSP application on your SAP WebAS ABAP or locally using the XAMPP Apache Server. Here we are going to use the XAMPP Apache Server to consume the API from the outside (other domain) in order to explore the Cross Origin Resource Sharing - CORS.

 

 

Downloads

 

You can download below the .nugg files to import the ABAP objects into your system and the zip file with the Single-Page Application.

 

Nugg Files

 

 

Zip File

 

 

GitHub Repository

 

 

 

Dictionary objects

 

If you want to create the dictionary objects manually please find below the details of each object.

 

TABLE


  • ZTB_CONTACTS
    • MANDT         type MANDT
    • EMAIL           type CHAR30
    • FIRSTNAME type CHAR30
    • LASTNAME   type CHAR30
    • PHONE         type CHAR30


TABLE TYPE

 

  • ZTT_CONTACTS line type ZTB_CONTACTS

 

STRUCTURE

 

  • ZST_CONTACTS
    • .INCLUDE ZST_REST_RESPONSE_BASIC
      • SUCCESS  type STRING
      • MSG       type STRING 
      • CODE     type i
    • CONTACTS    type ZTT_CONTACTS

 

 

Implementing the Contacts REST API

 

Let's create the resource class ZCL_REST_RESOURCE_CONTACTS.


Go to SE24 and create the class ZCL_REST_RESOURCE_CONTACTS, final with public instantiation as shown in figure 01.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_01.JPG

    Figure 01 - Class Properties

 

On the interfaces Tab inform the four interfaces provided by {z}restapi

 

  • ZIF_REST_RESOURCE_CREATE    REST API - Resource Create Method
  • ZIF_REST_RESOURCE_READ        REST API - Resource Read Method
  • ZIF_REST_RESOURCE_UPDATE    REST API - Resource Update Method
  • ZIF_REST_RESOURCE_DELETE     REST API - Resource Delete Method

 

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_02.JPG

   Figure 02 - Interfaces Tab

 

Now let's implement the inherited methods. Go to the Methods tab.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_03.JPG

    Figure 03 - Methods Tab

 

First of all let's implement our GET_REQUEST_TYPE and GET_RESPONSE_TYPE methods.

 

The GET_REQUEST_TYPE of all interfaces will have the same request type, what means that all CRUD methods expect to receive the fields of our contacts table as parameters.

 

GET_REQUEST_TYPE method implementation

 

METHOD ZIF_REST_RESOURCE_CREATE~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

 

METHOD ZIF_REST_RESOURCE_READ~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

 

METHOD ZIF_REST_RESOURCE_READ~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

 

METHOD ZIF_REST_RESOURCE_READ~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

We will see later (testing the API) that when we call the methods passing parameters with the same name of the contact's table fields the values are automatically passed to the importing structure I_REQUEST, which in our case will have the type ZTB_CONTACTS.

 

The only CRUD method that needs to have the GET_RESPONSE_TYPE method implemented is the READ method, where we will return the contact's data. All other methods don't need to be implemented.


GET_RESPONSE_TYPE method implementation


METHOD ZIF_REST_RESOURCE_READ~GET_RESPONSE_TYPE.

   r_response_type = 'ZST_CONTACTS'.

ENDMETHOD.


We will see later that the {z}restapi has a fall back mechanism that uses the structure ZST_REST_RESPONSE_BASIC when a custom response type is not defined.


Now let's implement the CREATE, READ, UPDATE and DELETE methods.


CREATE method implementation


METHOD zif_rest_resource_create~create.   DATA: ls_contact  TYPE ztb_contacts,         ls_response TYPE zst_rest_response_basic.   ls_contact = i_request.   IF NOT ls_contact IS INITIAL.     INSERT ztb_contacts FROM ls_contact.     IF sy-subrc = 0.       ls_response-success = 'true'.       ls_response-code    = 200.       ls_response-msg     = 'Contact created successfully!'.     ELSE.       ls_response-success = 'false'.       ls_response-code    = 409.       ls_response-msg     = 'Contact already exists!'.     ENDIF.   ELSE.     ls_response-success = 'false'.     ls_response-code    = 403.     ls_response-msg     = 'Contact has no information!'.   ENDIF.   e_response = ls_response.
ENDMETHOD.


 

READ method implementation


METHOD ZIF_REST_RESOURCE_READ~READ.   DATA: ls_request  TYPE ztb_contacts,         ls_response TYPE zst_contacts.   ls_request = i_request.   IF ls_request-email IS INITIAL.     SELECT * FROM ztb_contacts       INTO TABLE ls_response-contacts.   ELSE.     SELECT * FROM ztb_contacts       INTO TABLE ls_response-contacts       WHERE email = ls_request-email.   ENDIF.   e_response = ls_response.
ENDMETHOD.

 

UPDATE method implementation


METHOD zif_rest_resource_update~update.   DATA: ls_contact  TYPE ztb_contacts,         ls_response TYPE zst_rest_response_basic.   ls_contact = i_request.   UPDATE ztb_contacts FROM ls_contact.   IF sy-subrc = 0.     ls_response-success = 'true'.     ls_response-code    = 200.     ls_response-msg     = 'Contact updated successfully!'.   ELSE.     ls_response-success = 'false'.     ls_response-code    = 409.     ls_response-msg     = 'Contact not found!'.   ENDIF.   e_response = ls_response.
ENDMETHOD.


DELETE method implementation


METHOD ZIF_REST_RESOURCE_DELETE~DELETE.   DATA: ls_request  TYPE ztb_contacts,          ls_response TYPE zst_rest_response_basic.   ls_request = i_request.   IF NOT ls_request-email IS INITIAL.     DELETE FROM ztb_contacts       WHERE email = ls_request-email.     IF sy-subrc = 0.       ls_response-success = 'true'.       ls_response-code = 200.       ls_response-msg = 'Contact deleted successfully!'.     ELSE.       ls_response-success = 'false'.       ls_response-code = 409.       ls_response-msg = 'Contact not found!'.     ENDIF.   ENDIF.   e_response = ls_response.
ENDMETHOD.

 

Let's test our Contacts REST Resource using the POSTMAN Google Chrome extension.

 

Assuming that {z}restapi is installed and its ICF service is created under /sap  (see figure 4) the URL of our Contacts service is:

 

http://server:port/sap/zrestapi/myApp/contacts

 

where myApp is the name of our application (that is required by {z}restapi) and contacts is what identifies our contacts resource, i.e., everything that goes after "ZCL_REST_RESOURCE_". As you have already noticed, together they match the resource class name.

 

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_04.JPG

    Figure 4 - {z}restapi ICF service

 

Here we are not going to cover all test cases, only the most basic of the positive test cases.

 

Testing the CREATE method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_05.JPG

     Figure 5 - testing the create method

 

 

Testing the READ method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_06.JPG

     Figure 6 - testing the read method

 

 

Testing the UPDATE method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_07.JPG

     Figure 7 - testing the update method

 

 

Testing the DELETE method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_08.JPG

    Figure 8 - testing the delete method

 

 

Developing the Contacts AngularJS Web Application

 

Now that our API is ready let's start to develop our frontend application.

 

You can download the zip file with the complete web application from this link.

 

INDEX.HTML page

 

The index.html is a very basic html page where we are going to reference the CSS and javascript files used by the application and do the basic setup of the AngularJS application. Below is a extract of the index.html source code.

 

 

<!DOCTYPE html><html lang="en"><head>  <title>SCN Blog 8 - AngularJS Contacts App with {z}restapi and token authentication</title>  <meta charset="utf-8">  <!-- Mobile Specific Metas -->    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">    <!-- Libs CSS -->    <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">    <link href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">    <!-- Custom CSS -->    <link href="app.css" rel="stylesheet"></head><body ng-app="myApp">  <!-- Placeholder for the views -->    <div class="container" ng-view=""></div>  <!-- Start Js Files -->    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.6/angular.min.js" type="text/javascript"></script>    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.6/angular-route.min.js" type="text/javascript"></script>    <script src="hmac-sha1.js" type="text/javascript"></script>    <script src="app.js" type="text/javascript"></script></body></html>

On the body tag we are informing the attribute ng-app="myApp" which defines our application and the <div class="container" ng-view=""></div> which is the placeholder for the views of the Single-Page Application.

 

Our Single-Page Application will be composed by 2 views:

 

  • main.html
  • contact.html

 

 

 

MAIN.HTML (view)

 

The main view will have a form to add new contacts and a List to display all contacts.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_09.JPG

    Figure 9 - main view

 

<!-- Overlay to display the loading indicator --><div id="overlay" ng-show="$parent.data.loading"><i id="ajax-loader" class="fa fa-3x fa-spinner fa-spin"></i></div><h3>Add Contact</h3><div class="row" ng-hide="data.showPaneAddContact">  <div class="col-xs-12">  <a ng-click="data.showPaneAddContact=!data.showPaneAddContact"><i class="fa fa-2x fa-plus-square"></i></a>  </div></div><div class="row" ng-show="data.showPaneAddContact">  <div class="col-xs-12">  <a ng-click="data.showPaneAddContact=!data.showPaneAddContact"><i class="fa fa-2x fa-minus-square"></i></a>  </div></div><form ng-show="data.showPaneAddContact" name="contactForm" novalidate class="css-form" role="form" ng-submit="addContact(contactForm)">  <h5 ng-show="$parent.data.message" class="text-center">{{$parent.data.message}}</h5>  <fieldset>  <div class="row">  <div class="form-group col-sm-12 col-sm-3">  <label for="email">Email address</label>  <input type="email" class="form-control" id="email" placeholder="Enter e-mail" ng-model="data.contact.email" ng-maxlength="30" required>  </div>  <div class="form-group col-sm-12 col-sm-3">  <label for="firstname">First Name</label>  <input type="text" class="form-control" id="firstname" placeholder="Enter first name" ng-model="data.contact.firstname" ng-maxlength="30" required>  </div>  <div class="form-group col-sm-12 col-sm-3">  <label for="lastname">Last Name</label>  <input type="text" class="form-control" id="lastname" placeholder="Enter last name" ng-model="data.contact.lastname" ng-maxlength="30" required>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="phone">Phone</label>  <input type="tel" class="form-control" id="phone" placeholder="Enter phone" ng-model="data.contact.phone" ng-pattern="/^[-+.() ,0-9]+$/" ng-maxlength="30" required>  </div>  </div>  <button type="submit" class="btn btn-primary">Add Contact</button>  <button type="button" class="btn btn-default" ng-click="resetForm(contactForm)">Reset</button>  <button type="button" class="btn btn-default" ng-click="data.showPaneAddContact=!data.showPaneAddContact">Hide</button>  </fieldset></form><h3>Contact List</h3><div class="list-group">  <a href="#/" class="list-group-item" ng-show="isEmpty()">No Contacts</a>    <a href="#/contact/{{contact.email}}" class="list-group-item repeated-item" ng-repeat="contact in data.contacts | orderBy:'+firstname'">        <p><span class="glyphicon glyphicon-user"></span> {{contact.firstname}} {{contact.lastname}}</p>    </a></div>



CONTACT.HTML (view)

 

The contact view will have a form to allow us to update or delete the contact and also call or send e-mail to the contact.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_10.JPG

    Figure 10 - contact view

 

 

<!-- Overlay to display the loading indicator --><div id="overlay" ng-show="$parent.data.loading"><i id="ajax-loader" class="fa fa-3x fa-spinner fa-spin"></i></div><h3>Contact Details</h3><h5 ng-show="$parent.data.message" class="text-center sample-show-hide">{{$parent.data.message}}</h5><form name="contactForm" novalidate class="css-form" role="form">  <fieldset>  <div class="row">  <div class="form-group col-sm-12 col-md-3">  <label for="email">Email address</label>  <input type="email" class="form-control" id="email" ng-model="data.contact.email" readonly>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="firstname">First Name</label>  <input type="text" class="form-control" id="firstname" placeholder="Enter first name" ng-model="data.contact.firstname" required>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="lastname">Last Name</label>  <input type="text" class="form-control" id="lastname" placeholder="Enter last name" ng-model="data.contact.lastname" required>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="phone">Phone</label>  <input type="tel" class="form-control" id="phone" placeholder="Enter phone" ng-model="data.contact.phone" required>  </div>  </div>  <div class="row">  <div class="col-xs-12">  <a href="tel:{{data.contact.phone}}"><i class="fa fa-3x fa-phone-square green"></i></a>  <a href="mailto:{{data.contact.email}}"><i class="fa fa-3x fa-envelope-square green"></i></a>  </div>  </div>  <br/>  <div class="row">  <div class="col-xs-12">  <button type="submit" class="btn btn-primary" ng-click="updateContact(contactForm)">Update</button>  <button type="button" class="btn btn-danger" ng-click="deleteContact()">Delete</button>  <button type="button" class="btn btn-default" ng-click="back()">Back</button>  </div>  </div>  </fieldset></form>

 

 

APP.CSS

 

We need some custom styles to set the border color of our input boxes to red when the field gets the invalid state. We also need some css to style the overlay container that shows a spin icon while the ajax calls is running (waiting the server response).

 

 

.glyphicon-user {  margin-top: 10px;  margin-right: 5px;
}
.css-form input.ng-invalid.ng-touched {    border-color: #FA787E;
}
#overlay{    position: absolute;    top: 0;    left: 0;    width: 100%;    height: 100%;    min-height: 100%;    min-width: 100%;    z-index: 10;    text-align: center;    background-color: rgba(0,0,0,0.5); /*dim the background*/
}
#ajax-loader{    margin-top: 25%;
}
.green{    color: #5cb85c;
}
.green:hover {    color: #449d44;
}

 

 

APP.JS

 

Although it is not a good practice, I decided to keep all javascript code in one file just to keep things as simple as possible. This way I believe that it is easier to understand how the pieces (modules, services, controllers and views) work together to form the application. The drawback here is that having all code in one file makes it a little bit big and can seem to be challenging to understand. But believe me, it will not be that hard.


Before diving into the code let's take a look at the parts or pieces that form the application.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_11.png

   Figure 11 - App Overview


All application code is encapsulated inside the myApp module. The myApp module has a config where we define the routes and bind Controllers to the Views. It has also two Services, Token Service and Contacts Service and the Controllers Main and Contact.

 


myApp Module


If you have ever tried to use AngularJS to build web applications in the SAP WebAS ABAP you may probably know that the functions $http.post and $http.put provided by the $http service does not behave like jQuery.ajax(). AngularJS transmits data using Content-Type: application/json and the SAP WebAS ABAP is not able to unserialize it. So it is necessary to transform the http request to transmit data using Content-Type: x-www-form-urlencoded. Thanks to Ezekiel Victor we don't need to write the code to do this. The solution is very well explained bt Ezekiel in his blog.


Make AngularJS $http service behave like jQuery.ajax()

http://victorblog.com/2012/12/20/make-angularjs-http-service-behave-like-jquery-ajax/


 

angular.module('myApp', ['ngRoute'], function($httpProvider) {    // Use x-www-form-urlencoded Content-Type    $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';    $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';    /**     * Converts an object to x-www-form-urlencoded serialization.     * @param {Object} obj     * @return {String}     */    var param = function(obj) {        var query = '',            name,            value,            fullSubName,            subName,            subValue,            innerObj,            i;        for (name in obj) {            if (obj.hasOwnProperty(name)) {                value = obj[name];                if (value instanceof Array) {                    for (i = 0; i < value.length; i = i + 1) {                        subValue = value[i];                        fullSubName = name + '[' + i + ']';                        innerObj = {};                        innerObj[fullSubName] = subValue;                        query += param(innerObj) + '&';                    }                } else if (value instanceof Object) {                    for (subName in value) {                        if (value.hasOwnProperty(subName)) {                            subValue = value[subName];                            fullSubName = name + '[' + subName + ']';                            innerObj = {};                            innerObj[fullSubName] = subValue;                            query += param(innerObj) + '&';                        }                    }                } else if (value !== undefined && value !== null) {                    query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';                }            }        }        return query.length ? query.substr(0, query.length - 1) : query;    };    // Override $http service's default transformRequest    $httpProvider.defaults.transformRequest = [        function(data) {            return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;        }    ];
});

 

 

 

myApp Config - Defining app routes


The application have only two routes. The "/" points to the main view and "/contact:email/" that points to the contact view.  


/**
* Defines myApp module configuration
*/
angular.module('myApp').config(    /* Defines myApp routes and views */    function($routeProvider) {        $routeProvider.when('/', {            controller: 'MainController',            templateUrl: 'main.html'        });        $routeProvider.when('/contact/:email/', {            controller: 'ContactController',            templateUrl: 'contact.html'        });        $routeProvider.otherwise({            redirectTo: '/'        });    }
);





Token Service


The Token Service is used by the Contacts Service to create authentication tokens for each http request sent to the server. It concatenates all parameters, the private key and a timestamp separated by pipes "|" and creates a hash (SHA1). It also returns the auth_token_s2s that contains the string to sign that is the name (and order) of the parameters that the server must use to calculate the hash and verify the token. The auth_token_uid contains the User Id. Notice that the Private Key is not sent along with the http request. The server already knows it. The askForPkey method prompts the user to inform it so the service can use it to create the authentication tokens.



/**
*  Creates the token service, responsible for generating the authentication tokens
*/
angular.module('myApp').service('TokenService', function($rootScope) {    // Stores user's private key used to create the token in the getToken method.    this.pkey = '';    var self = this;    this.askForPkey = function() {        self.pkey = window.prompt("Please inform your private key", "12345");    };    this.setPkey = function(sPkey) {        self.pkey = sPkey;    };    this.getToken = function(params) {        var timestamp = Date.now();        var auth_token_con = '';        var auth_token_s2s = '';        angular.forEach(params, function(value, key) {            auth_token_con = auth_token_con + value + '|';            auth_token_s2s = auth_token_s2s + key + '|';        });        if (self.pkey === '') {            self.askForPkey();        }        auth_token_con = auth_token_con + timestamp + '|' + self.pkey;        auth_token_s2s = auth_token_s2s + 'timestamp';        var token = CryptoJS.SHA1(auth_token_con);        var auth = {            token: token.toString(),            token_s2s: auth_token_s2s,            token_uid: 'IU_TEST',            timestamp: timestamp        };        return auth;    };
});




Contacts Service


The contacts service is the heart of the application. All communication with the server is handled by this service at it is also responsible for storing the contacts data displayed by the application views.


It has basically the implementation of the 4 CRUD methods that allow the application to Create, Read, Update and Delete contacts on the server side through the Contacts REST API. It makes use of the token service to create the authentication token to sign every single http request sent to the server.


The source code is not short enough to be presented in-line here so please click here to see it.




Main Controller


The main controller make use of the Contacts Service to read all contacts (to display them on the contacts list) and to Add new contacts. It is important to highlight here that the $scope.data.contacts points to the ContactsService.contacts, so whenever the ContactsService updates its contacts data the view is automatically updated thanks to AngularJS data-binding.



/**
*  Defines the main controller (for the view main.html)
*/
angular.module('myApp').controller('MainController',    function($scope, $rootScope, ContactsService) {        var rootScope = $rootScope;        $scope.data = {};        if ($rootScope.data === undefined) {            $rootScope.data = {};            $rootScope.data.message = null;            $rootScope.data.pkey = null;        }        $rootScope.data.loading = false;        ContactsService.resetSelectedContact();        $scope.data.contact = ContactsService.contact;        $scope.data.contacts = ContactsService.contacts;        $scope.data.showPaneAddContact = false;        ContactsService.getContacts();        /**         *  Checks whether the contacts array is empty or not         */        $scope.isEmpty = function(){            if($scope.data.contacts.length > 0){                return false;            }else{                return true;            }        };        /**         *  Adds a new contact         */        $scope.addContact = function(form){            if(form.$valid === true) {                  var contact = {                    email: $scope.data.contact.email,                    firstname: $scope.data.contact.firstname,                    lastname: $scope.data.contact.lastname,                    phone: $scope.data.contact.phone                };                ContactsService.addContact(contact);                form.$setUntouched();            }else{                $rootScope.data.message = "All fields are required!";            }        };        $scope.resetForm = function(form){            ContactsService.resetSelectedContact();            form.$setUntouched();        };        /**         *  Clears displayed messages after 3 seconds         */        $scope.resetMessage = function() {            window.setTimeout(function() {                rootScope.$apply(function() {                    rootScope.data.message = null;                });            }, 3000);          };        /**         *  Watches changes of the variable "$rootScope.data.message" in order to clear         *  displayed messages after 3 seconds         */        $rootScope.$watch(function(scope) { return scope.data.message },            function(newValue, oldValue) {                if (newValue !== oldValue && newValue !== "") {                    $scope.resetMessage();                };            }        );    }
);




Contact Controller


The Contact Controller also make use of the ContactService to read the selected contact's data, update the selected contact or delete it. Just like the main controller, the $scope.data.contact points to the ContactsService.contact.


 

/**
*  Defines the contact controller (for the view contact.html)
*/
angular.module('myApp').controller('ContactController',    function($scope, $routeParams, $location, $rootScope, ContactsService) {        var rootScope = $rootScope;        $rootScope.data = {};        $rootScope.data.message = null;        $rootScope.data.isFinished = true;        $scope.data = {};        $scope.data.isFinished = $rootScope.data.isFinished;        $scope.data.contact = ContactsService.contact;        ContactsService.getContact($routeParams.email);        /**         *  Executes the updateContact method of ContactsService to update the selected contact.         *  No information needs to be passed to identify the selected contact because the service knows who it is.         */        $scope.updateContact = function(form){            if(form.$valid === true) {                ContactsService.updateContact();            }else{                $rootScope.data.message = "All fields are required!";            }        };        /**         *  Executes the deleteContact method of ContactsService to delete the selected contact.         *  No information needs to be passed to identify the selected contact because the service knows who it is.         */        $scope.deleteContact = function(){            ContactsService.deleteContact();        };        /**         *  Navigates back to the main view         */        $scope.back = function(){            ContactsService.resetSelectedContact();            $location.url("/");        };        /**         *  Clears displayed messages after 3 seconds         */        $scope.resetMessage = function() {            window.setTimeout(function() {                rootScope.$apply(function() {                    rootScope.data.message = null;                });            }, 3000);          };        /**         *  Watches changes of the variable "$rootScope.data.isFinished" in order to trigger         *  the navigation back to the main view when a contact is deleted         */        $rootScope.$watch(function(scope) { return scope.data.isFinished },            function(newValue, oldValue) {                if (newValue === true && oldValue === false) {                    $scope.back();                };            }        );        /**         *  Watches changes of the variable "$rootScope.data.message" in order to clear         *  displayed messages after 3 seconds         */        $rootScope.$watch(function(scope) { return scope.data.message },            function(newValue, oldValue) {                if (newValue !== oldValue && newValue !== "") {                    $scope.resetMessage();                };            }        );    }
);



Testing the application locally with XAMPP



http://www.jianelli.com.br/scnblog8/figures/scnblog8_12.png

   Figure 12 - Starting with no contacts


http://www.jianelli.com.br/scnblog8/figures/scnblog8_13.JPG

   Figure 13 - Adding a new contact


http://www.jianelli.com.br/scnblog8/figures/scnblog8_14.JPG

   Figure 14 - Contact Added


http://www.jianelli.com.br/scnblog8/figures/scnblog8_15.JPG

http://www.jianelli.com.br/scnblog8/figures/scnblog8_16.JPG

   Figure 15 and 16- Updating the contact


 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_17.JPG

   Figure 17 - Contact updated!



http://www.jianelli.com.br/scnblog8/figures/scnblog8_18.JPG

   Figure 18 - Contact list updated


http://www.jianelli.com.br/scnblog8/figures/scnblog8_19.JPG

   Figure 19 - Contact list empty again after deleting the contact

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_20.JPG

   Figure 20 - No contacts


http://www.jianelli.com.br/scnblog8/figures/scnblog8_21.JPG

   Figure 21 - Authentication token sent in the request header



You can see a live demo at


SCN Blog 8 - AngularJS Contacts App with {z}restapi and token authentication


but this demo uses a PHP backend to simulate the SAP WebAS responses.


All code is available on GitHub


christianjianelli/scnblog8 · GitHub




Fix missing "Spreadsheet" option while right-clicking on ALV generated with REUSE_ALV_GRID_DISPLAY

$
0
0

Today I came across a situation where the "Spreadsheet" option was missing while right-clicking on an ALV generated using REUSE_ALV_GRID_DISPLAY.

(See below)

 

2015-01-05_110428.jpg

 

I went through couple of discussions on this topic but most answered the missing "Export" option in Application bar (List> Export> Spreadsheet)

 

Observed that in my case, I could view the "Spreadsheet" option via from Application bar.

2015-01-05_111256.jpg

 

The Internal table declaration is as below-

* TYPES-----------------------------------------------------------------

  types :   begin of ty_output,

              kbeln      type kbeln,

              begru      type begru,

              bstat      type bstat,

              end of ty_output.

**--Internal Table----------------------------------------------------

Data: it_outtab type standard table of ty_output.

 

..

..

Logic for fetching data

..

* Display the data in grid

    call function 'REUSE_ALV_GRID_DISPLAY'

      exporting

        i_callback_program = sy-repid

        is_layout          = rec_layout

        it_fieldcat        = li_fldcatlog[]

        i_default          = 'X'          

        i_save             = 'A'          

        is_variant         = g_v_variant  

      tables

        t_outtab           = li_output.

While debugging, found that the internal table 'li_output' had a deep structure 'BSTAT'.

2015-01-05_113753.jpg

The types declaration was updated as below-

 

The Internal table declaration is as below-

* TYPES-----------------------------------------------------------------

  types :   begin of ty_output,

              kbeln      type kbeln,

              begru      type begru,

              bstat      type wrfbstat, "<<<<<-

              end of ty_output.

**--Internal Table----------------------------------------------------

Data: it_outtab type standard table of ty_output.

 

Post the code change, the 'Spreadsheet" Option appeared.

2015-01-05_114302.jpg

Documentation on REUSE_ALV_GRID_DISPLAY didn't mention about not using "Deep structures", though.

 

 

Hope this will be helpful for others!

Viewing all 948 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>