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!
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.
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,
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,
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.
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.
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,
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
“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.
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:
To test/show this, on the form I enter the notes:
Then when I click the “Check” button and switch to debugging in my service, I can see:
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