Friday 17 April 2020

HCM Processes & Forms: A Concise Commentary on Comments

This time, I thought I would finally get around to covering one topic that has come up probably the most of all over the years with HCM Processes and Forms….how do we access the comments (aka. notes) that are put into the form?!?!?!? Well, there are multiple ways depending on what exactly you need to do, and I hope to cover those here. Sooo come along and let’s all get reacquainted!

Notes? Comments? What???


First off, let’s lay some groundwork here. What we are talking about are the “current” and “previous” notes fields that are available for each form and auto-magically created for us at runtime by the HCM P&F framework itself. These are also often times called “user comments”. These “notes” are not actually stored in any infotype field or anywhere other than in Case Management as part of our process data. The technical field names are actually:

◉ HRASR_CURRENT_NOTE
◉ HRASR_PREVIOUS_NOTES

The framework handles everything for us….creating the field, reading and updating these fields and storing them in Case Management as part of the process object data. For some reason though, in SAP’s apparent infinite wisdom, they never “expose” these to us in any easy way to use for our own needs (for example, validating that a user has entered comments). Let’s look at a few scenarios that come up and how to handle them.

Reading Notes After the Fact


Consider that a process initiator has submitted a form, and we have a process reference number assigned. We might need to read the notes in a custom workflow task, display the notes in some custom report, or anything else.

For reading the notes using the process reference number, we can get our “instance” of the process and then easily traverse through the form scenarios and scenario steps using the GUIDs to locate the notes.

Here are a few example methods of a custom class I made,

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

To look up the notes by process reference number,

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

method get_notes_from_refnum.

  data: t_t5asrprocesses         type t5asrprocesses.
  data: t_t5asrscenarios         type table of t5asrscenarios.
  data: w_t5asrscenarios         type t5asrscenarios.
  data: v_activity               type hrasr00_process_modelling-activity.
  data: message_list             type ref to cl_hrbas_message_list.
  data: ref_pobj_runtime         type ref to if_hrasr00_process_runtime.
  data: is_authorized            type boole_d.
  data: is_ok                    type boole_d.
  data: lt_notes                 type hrasr00_note_tab.
  data: ls_notes                 type hrasr00_note.
  data: no_of_notes.
  data: process_object_guid      type  scmg_case_guid.

  clear notes.  refresh notes.

  select single * into t_t5asrprocesses from t5asrprocesses
     where reference_number = refnum.

  if sy-subrc eq 0.
    select * from t5asrscenarios into table t_t5asrscenarios
       where parent_process = t_t5asrprocesses-case_guid.

    v_activity = 'R'.
    create object message_list.

    sort t_t5asrscenarios descending.

    loop at t_t5asrscenarios into w_t5asrscenarios.
*     Get instance of POBJ_RUNTIME
      call method cl_hrasr00_process_runtime=>get_instance
        exporting
          scenario_guid         = w_t5asrscenarios-case_guid
          activity              = v_activity
          message_handler       = message_list
          no_auth_check         = 'X'
        importing
          instance_pobj_runtime = ref_pobj_runtime
          pobj_guid_out         = process_object_guid
          is_authorized         = is_authorized
          is_ok                 = is_ok.
    endloop.

*   Get the Notes from the Last Saved Scenario since it will have all notes
    sort t_t5asrscenarios descending.
    loop at t_t5asrscenarios into w_t5asrscenarios.
      refresh lt_notes.
      clear: lt_notes, ls_notes.

*     Get notes for the scenario
      call method ref_pobj_runtime->get_notes_of_scenario
        exporting
          scenario_guid   = w_t5asrscenarios-case_guid
          message_handler = message_list
        importing
          notes           = lt_notes
          is_ok           = is_ok.

      describe table lt_notes lines no_of_notes.
      if no_of_notes > 0.
        sort lt_notes by changed_timestamp ascending.
        notes[] = lt_notes[].
        exit.
      endif.
    endloop.
  endif.

endmethod. 

And as an extra special bonus for you all paying attention, we can use the same method above and then filter by the user (can you guess the additional import parameter to the method? haha) ,

if user cs 'US'.
    move user+2(12) to l_created_by.
  else.
    move user+0(12) to l_created_by.
  endif.

  l_name = ZCL_HRASR_WF_UTILITY=>GET_NAME_FOR_USERID( CONV SYSID( l_created_by ) ).

  lt_notes = get_notes_from_refnum( refnum = refnum ).

  describe table lt_notes lines g_lin.
  if g_lin gt 0.
    sort lt_notes by changed_timestamp descending.

    "The top record SHOULD be from our user otherwise comments have been made since their comment
    "so it likely won't be relevant now.
    READ TABLE lt_notes INTO ls_notes INDEX 1.
    IF sy-subrc = 0. "should be
      IF ls_notes-created_by = l_created_by.
          clear ls_info.

          write ls_notes-created_date to l_date mm/dd/yyyy.
          write ls_notes-created_time to l_time using edit mask '__:__:__'.

          concatenate 'Created by' l_name '(' ls_notes-created_by ') on' l_date 'at' l_time into ls_info-str separated by space.
          append ls_info to notes.

          call function 'SWA_STRING_SPLIT'
            exporting
              input_string                 = ls_notes-content
              max_component_length         = 72
            tables
              string_components            = ls_info_tab
            exceptions
              max_component_length_invalid = 1
              others                       = 2.
          if sy-subrc = 0.
            loop at ls_info_tab assigning <current_comment_line>.
              append <current_comment_line> to notes.
            endloop.
          endif.
      ENDIF.
    ENDIF.

Writing Notes After the Fact


Similar to reading the notes using the process reference number, we can also traverse down using GUIDs to write to the notes with just a small bit of extra work.

In this example code, we are passing in the process reference number for which we want to add notes on the last step. In this particular usage, I had created a process for “Position Create” which actually creates multiple positions at once. As the positions are created, I would capture their position ID and concatenate it into a variable called “resultstring”. Along with the process reference number, I would pass this “resultstring” into a method (called in a custom background workflow task) that would then append itself to the most current note (ie. the last note from the last processor who approved the creation of positions).

DATA: ref_process         TYPE t5asrprocesses,
        ref_scenario        TYPE t5asrscenarios,
        process_object_guid TYPE scmg_case_guid..
  DATA: v_activity        TYPE hrasr00_process_modelling-activity,
        message_list      TYPE REF TO cl_hrbas_message_list,
        message_handler   TYPE REF TO IF_HRBAS_MESSAGE_HANDLER,
        error_messages    TYPE hrbas_message_tab..
  DATA: ref_pobj_runtime  TYPE REF TO if_hrasr00_process_runtime,
        is_authorized     TYPE boole_d,
        is_ok             TYPE boole_d.

  DATA: lt_steps          TYPE HRASR00STEPS_TAB,
        wa_step           TYPE ASR_GUID.

  DATA: lt_notes          TYPE HRASR00_NOTE_TAB,
        ls_note           TYPE hrasr00_note,
        l_new_content     TYPE string,
        no_of_notes       TYPE i.

  CONSTANTS: c_fld_lead_object TYPE string VALUE 'LEAD_OBJECT_ID',
             c_fld_position_id TYPE string VALUE 'POS_OBJECT_ID',
             c_completed       TYPE asr_process_status VALUE 'COMPLETED',
             c_archived        TYPE asr_process_status VALUE 'ARCHIVED'.


* Look up by process number....Is this a valid process?
  SELECT SINGLE * INTO ref_process FROM t5asrprocesses WHERE reference_number = proc_ref_no
                                                               AND status <> c_completed
                                                               AND status <> c_archived.

  IF sy-subrc <> 0.
    procstate = IF_HRASR00_UI_CONSTANTS=>C_PROC_STATUS_ERROR. "ERROR
    EXIT. "no data
  ENDIF.

* Get form scenario for process by GUID
  SELECT SINGLE * from t5asrscenarios INTO ref_scenario WHERE parent_process = ref_process-case_guid.

  v_activity = 'R'.
  create object message_list.

* Get instance of POBJ_RUNTIME for scenario
  call method cl_hrasr00_process_runtime=>get_instance
    exporting
      scenario_guid         = ref_scenario-case_guid
      activity              = v_activity
      message_handler       = message_handler
      no_auth_check         = 'X'
    importing
      instance_pobj_runtime = ref_pobj_runtime
      pobj_guid_out         = process_object_guid
      is_authorized         = is_authorized
      is_ok                 = is_ok.

  IF is_ok = abap_false.
    procstate = IF_HRASR00_UI_CONSTANTS=>C_PROC_STATUS_ERROR. "ERROR
    EXIT. "no data
  ENDIF.

* Get all steps
  call method ref_pobj_runtime->IF_HRASR00_POBJ_FACTORY~GET_ALL_STEPS
     EXPORTING
      message_handler = message_handler
      scenario_guid   = ref_scenario-case_guid
    IMPORTING
      steps           = lt_steps
      is_ok           = is_ok.
  IF is_ok = abap_false.
    procstate = IF_HRASR00_UI_CONSTANTS=>C_PROC_STATUS_ERROR. "ERROR
    EXIT. "no data
  ENDIF.

* This is simply the easiest way to have wa_step loaded up with the LAST step keeping in mind
* we don't know how many steps it went through because it may have been returned or such one
* or several times before final processing.
  LOOP AT lt_steps INTO wa_step.
  ENDLOOP.

* Get reference to step
  CALL METHOD cl_hrasr00_process_runtime=>get_instance
    EXPORTING
      step_guid             = wa_step
      message_handler       = message_list
    IMPORTING
      instance_pobj_runtime = ref_pobj_runtime
      pobj_guid_out         = process_object_guid
      is_authorized         = is_authorized
      is_ok                 = is_ok.

  refresh lt_notes.
  clear: lt_notes, ls_note.

* get notes of current step
  CALL METHOD ref_pobj_runtime->IF_HRASR00_POBJ_NOTE~GET_NOTES_OF_STEP
    EXPORTING
      step_guid = wa_step
      message_handler = message_handler
    IMPORTING
      notes           = lt_notes
      is_ok           = is_ok.

  IF is_ok = abap_false.
    procstate = IF_HRASR00_UI_CONSTANTS=>C_PROC_STATUS_ERROR. "ERROR
    EXIT. "no data
  ENDIF.

* auto-create a note using our "string of positions created"
  IF resultstring IS NOT INITIAL.
    "text-04 = The following positions were created with process reference numbers noted as well:
    CONCATENATE text-004 cl_abap_char_utilities=>newline INTO l_new_content.
    "loop resultstring and do same thing
    LOOP AT resultstring ASSIGNING FIELD-SYMBOL(<pos>).
      CONCATENATE l_new_content <pos> cl_abap_char_utilities=>newline INTO l_new_content.
    ENDLOOP.
  ENDIF.

  READ TABLE lt_notes INTO ls_note WITH KEY category = '0001'.
  IF sy-subrc EQ 0. "update existing note
    CONCATENATE ls_note-content cl_abap_char_utilities=>newline cl_abap_char_utilities=>newline l_new_content INTO l_new_content.
    ls_note-content = l_new_content.
    ls_note-changed_by = sy-uname.
    ls_note-changed_date = sy-datum.
    ls_note-changed_time = sy-uzeit.
    GET TIME STAMP FIELD ls_note-created_timestamp.
    MODIFY lt_notes FROM ls_note INDEX sy-tabix.
  ELSE. "create new note
    "note header
    ls_note-category = '0001'.  "public
    ls_note-object = 'HR_ASR_PRC'.
    ls_note-name = wa_step. "GUID of step
    ls_note-lang = 'E'.
    ls_note-created_by = sy-uname.
    ls_note-created_date = sy-datum.
    ls_note-created_time = sy-uzeit.
    GET TIME STAMP FIELD ls_note-created_timestamp.
    ls_note-changed_by = sy-uname.
    ls_note-changed_date = sy-datum.
    ls_note-changed_time = sy-uzeit.
    GET TIME STAMP FIELD ls_note-created_timestamp.
    "actual note content
    ls_note-content = l_new_content.

    APPEND ls_note TO lt_notes.
  ENDIF.

* Write notes back to form scenario
  CALL METHOD ref_pobj_runtime->IF_HRASR00_POBJ_NOTE~ASSIGN_NOTES_2_CURRENT_STEP
    EXPORTING
      NOTES           = lt_notes
      MESSAGE_HANDLER = message_list
    IMPORTING
      IS_OK           = is_ok.

* set return
  IF is_ok = abap_false.
    CALL METHOD message_list->get_error_list IMPORTING messages = error_messages.

    procstate = IF_HRASR00_UI_CONSTANTS=>C_PROC_STATUS_ERROR. "ERROR
  ELSE.
    procstate = IF_HRASR00_UI_CONSTANTS=>C_PROC_STATUS_PROCESSED. "PROCESSED
  ENDIF.

Check/Read/Validate Notes at Run Time


This is by far the request that comes up the most. For example, we want to validate that the process initiator (the person starting the whole thing) has entered comments. If we add “PROCESS_REFERENCE_NUMBER” to our form fields, the framework will fill it’s value for us because it is a reserved field name (ie. it always is created by the framework). We can check the value in our custom generic services as well as display it on the form. Common sense would then tell us to just explicitly add the notes fields into our form fields, and then viola….same kind of thing would happen…….WRONG! This is what happens….

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

But hey, we learned how to just read it through the process object like above, right? Well, bad thing there is that we do not yet have an actual process (nothing in tables or Case Management) which also means we do not have a process reference number assigned (so the above examples are useless here!). There is a nice interface, IF_HRASR00_POBJ_NOTE, that has very nice methods to read and write to notes (we used it above!), but again, we do not have a process object at this time.

Over the years, I had to achieve this in a few ways. For Adobe forms oddly enough, the “workaround” was easy. For FPM forms? Not so much. I have done it many different ways over the years and most were to “ugly” or “gross” to ever share….yes, I’d be embarrassed of the amount of “kludge” involved. (haha) I waited on SAP to make it easier….but if I kept waiting, I would end up the actual meme of the “still waiting” skeleton. 

The Adobe Way

“It ain’t pretty, but it does work.”….that describes my first car but also this “solution”. By taking advantage of Javascript on the client side for Adobe Interactive Forms and a “flag” field in our process configuration, we can achieve our need.

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

In the Adobe form layout for our “current comments” field, we have the data binding set as:

$record.HRASR_CURRENT_NOTE.DATA[*].FIELD

Then on the events for the field for “exit”, we have the Javascript,

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

This will set our “flag” field (hidden on the form layout).

Over in our custom generic service, we can then simply check our “flag” field to validate if the user did enter comments or not. (not my code! haha)

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

The FPM Way

“This is where things get weird and ugly.”….that describes my experiences with online dating but also the way this “solution” ended up being a thing. For FPM forms, we don’t have the luxury (and headaches?) of client side Javascript. Something “ABAP-y” had to be found. Funny enough, SAP gave us the “spectacular” class CL_HRASR00_PROCESS_EXECUTE which really opened up the world of HCM P&F…..this is what allows us to pretty much launch any process (or workflow work item for a process) from anywhere and any frontend that can communicate with however we expose the class (ex. through web services or through Gateway services or whatever you like). That class does all kinds of things. It even lets us read and add attachments! But it does nothing for notes. WHAT?!??! However, we can fix that.

The notes fields are kept in a private class attribute table MT_SPECIAL_DATA. Being private, we have no way to “get at it”. How to access private attributes of classes? Hmmmm. There is the “hack” or “cheater” way of doing this by making a custom subclass with CL_HRASR00_PROCESS_EXECUTE as the super class and then create a public method in our sublcass that expose the “parent’s” private information….parents in general don’t tend to like when their kids do that. There is a better way…..simply take advantage of the enhancement framework.

(*WARNING: Keep in mind that often if a developer…even SAP….has chosen to keep things private/protected in a class, there is usually a very good reason for that. With that said, take caution when you choose to change that. In our case “read” is fairly safe. I would not do this for “write” unless absolutely necessary and after in depth research of risk.)

The first step is to enhance the standard interface IF_HRASR00_PROCESS_EXECUTE to create our new custom method.

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

We add the method GET_COMMENTS_FIELDS.

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

And we add our exporting parameters to send the notes back.

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

With the interface method defined, we have to go implement it in classes that implement the interface. Thankfully, a quick “where used” will tell you that it is only in one class. We then enhance the standard class CL_HRASR00_PROCESS_EXECUTE which implements this interface to add the “meat” of our method. The code here is loosely based on the code in the classes private method ADD_COMMENT_FIELDS (we just “reverse” the logic)

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

Now, back over in my own custom generic service, if I want to check/read/validate that comments are entered, I can check in my own custom code:

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

To test/show this, on the form I enter the notes:

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

Then when I click the “Check” button and switch to debugging in my service, I can see:

SAP ABAP Tutorial and Materials, SAP ABAP Study Materials, SAP ABAP Certifications, SAP ABAP Prep

I can now easily see that I can read the notes and then do whatever I want them such as validating they exist, scanning for specific terms, etc.

No comments:

Post a Comment