HCM Processes & Forms: A Concise Commentary on Comments
Hello again and where have you been??!?!! Oh wait….that should be the question for me! (haha) Sorry….been kinda busy as of late but given a bit of extra “free” time here lately (as the whole world seems to have), I thought I would blog once more. 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:
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,
To look up the notes by process reference number,
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….
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. I eventually settle on a way for FPM forms that I think even Matthew Billingham might approve of (and he’s tough!).
The Adobe Way
In the Adobe form layout for our “current comments” field, we have the data binding set as:
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)
The FPM Way
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. (this is where Matthew Billingham and other’s “best practice” suggestions come in.) 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.
We add the method GET_COMMENTS_FIELDS.
And we add our exporting parameters to send the notes back.
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).
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:
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.
It’s Not Good-Bye
I know it has been a while since I blogged about anything HCM P&F related. I am not sure how many people are still working with it in 2020, but I do hope it helps others. Funny it took this long to finally comment about “comments”, but part of me just thought it probably is not something that comes up as much as I think. I mean, how many people actually put much in the comments section of forms anyways? (haha) As always, I will keep blogging if you keep reading! Till next time…..