RAP Draft pour scenarios de workflow : Processus d

Catégorie
RAP
Publié
Auteur
Johannes

Les workflows d’approbation sont un element central des applications d’entreprise. Les demandes de conges, les validations de commandes ou les notes de frais passent par plusieurs etapes d’approbation avant d’etre finalement approuvees. Le RESTful ABAP Programming Model (RAP) offre avec le Draft Handling la base ideale pour de tels workflows : les demandes peuvent etre sauvegardees comme brouillon, soumises et traitees par differents approbateurs.

Dans cet article, nous implementons un workflow complet de demande de conges avec approbation multi-niveaux. Vous apprendrez a definir des Actions basees sur le statut, a utiliser le Feature Control pour un controle UI contextuel et a modeliser l’ensemble du processus d’approbation dans RAP.

Apercu du workflow

Notre demande de conges passe par les phases suivantes :

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DRAFT │────▶│ SUBMITTED │────▶│ APPROVED │────▶│ ACTIVE │
│ (Brouillon)│ │ (Soumis) │ │(Approuve N1)│ │ (Final) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DISCARDED │ │ REJECTED │ │ REJECTED │
│ (Rejete) │ │ (Refuse N1) │ │ (Refuse N2) │
└─────────────┘ └─────────────┘ └─────────────┘
StatutDescriptionActions suivantes
DRAFTL’employe cree/edite la demandeSubmit, Discard
SUBMITTEDSoumis, en attente du responsableApprove, Reject (Niveau 1)
APPROVEDApprouve par le responsable, en attente RHApprove, Reject (Niveau 2)
ACTIVEApprouve finalement, actif dans le systeme-
REJECTEDRefuse, retourne au demandeurResubmit, Discard

Modele de donnees

Table de base de donnees pour les demandes de conges

@EndUserText.label : 'Demande de conges"
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
define table zleave_request {
key client : abap.clnt not null;
key request_uuid : sysuuid_x16 not null;
request_id : abap.numc(10);
employee_id : abap.numc(8);
employee_name : abap.char(80);
leave_type : abap.char(2); // VA=Vacances, SP=Special, SI=Maladie
start_date : abap.dats;
end_date : abap.dats;
days_requested : abap.int4;
reason : abap.string(500);
// Gestion des statuts
workflow_status : abap.char(2); // DR/SU/A1/AC/RJ
current_approver : abap.char(12);
approval_level : abap.int1; // 1=Manager, 2=RH
// Historique d'approbation
submitted_at : timestampl;
submitted_by : abap.char(12);
approved_l1_at : timestampl;
approved_l1_by : abap.char(12);
approved_l2_at : timestampl;
approved_l2_by : abap.char(12);
rejected_at : timestampl;
rejected_by : abap.char(12);
rejection_reason : abap.string(500);
// Champs administratifs
created_by : abap.char(12);
created_at : timestampl;
last_changed_by : abap.char(12);
last_changed_at : timestampl;
local_last_changed_at : timestampl;
}

Table Draft

@EndUserText.label : 'Demande de conges Draft"
@AbapCatalog.tableCategory : #TRANSPARENT
define table zdraft_leave_req {
key client : abap.clnt not null;
key request_uuid : sysuuid_x16 not null;
request_id : abap.numc(10);
employee_id : abap.numc(8);
employee_name : abap.char(80);
leave_type : abap.char(2);
start_date : abap.dats;
end_date : abap.dats;
days_requested : abap.int4;
reason : abap.string(500);
workflow_status : abap.char(2);
current_approver : abap.char(12);
approval_level : abap.int1;
// Champs Draft-Admin
"%admin" : include sych_bdl_draft_admin_inc;
}

CDS Interface View

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Demande de conges - Interface View"
define root view entity ZI_LeaveRequest
as select from zleave_request
{
key request_uuid as RequestUUID,
request_id as RequestId,
employee_id as EmployeeId,
employee_name as EmployeeName,
leave_type as LeaveType,
start_date as StartDate,
end_date as EndDate,
days_requested as DaysRequested,
reason as Reason,
// Statut du workflow
workflow_status as WorkflowStatus,
current_approver as CurrentApprover,
approval_level as ApprovalLevel,
// Historique d'approbation
submitted_at as SubmittedAt,
submitted_by as SubmittedBy,
approved_l1_at as ApprovedL1At,
approved_l1_by as ApprovedL1By,
approved_l2_at as ApprovedL2At,
approved_l2_by as ApprovedL2By,
rejected_at as RejectedAt,
rejected_by as RejectedBy,
rejection_reason as RejectionReason,
// Champ calcule pour le texte du statut
case workflow_status
when 'DR' then 'Brouillon"
when 'SU' then 'Soumis"
when 'A1' then 'Approuve (Niveau 1)"
when 'AC' then 'Actif"
when 'RJ' then 'Refuse"
else 'Inconnu"
end as WorkflowStatusText,
// Criticality pour l'affichage du statut
case workflow_status
when 'DR' then 0 // Neutre
when 'SU' then 2 // Warning (jaune)
when 'A1' then 2 // Warning (jaune)
when 'AC' then 3 // Success (vert)
when 'RJ' then 1 // Error (rouge)
else 0
end as StatusCriticality,
// Champs administratifs
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.lastChangedBy: true
last_changed_by as LastChangedBy,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
local_last_changed_at as LocalLastChangedAt
}

Behavior Definition avec Actions de workflow

La Behavior Definition est le coeur du workflow. Nous y definissons toutes les transitions de statut comme Actions avec Feature Control :

managed implementation in class zbp_i_leave_request unique;
strict ( 2 );
with draft;
define behavior for ZI_LeaveRequest alias LeaveRequest
persistent table zleave_request
draft table zdraft_leave_req
lock master total etag LastChangedAt
authorization master ( instance, global )
etag master LastChangedAt
{
// Operations standard
create;
update;
delete;
// Proprietes des champs
field ( readonly ) RequestId, CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
field ( readonly ) WorkflowStatus, CurrentApprover, ApprovalLevel;
field ( readonly ) SubmittedAt, SubmittedBy, ApprovedL1At, ApprovedL1By;
field ( readonly ) ApprovedL2At, ApprovedL2By, RejectedAt, RejectedBy;
field ( numbering : managed ) RequestId;
// ═══════════════════════════════════════════════════════════════════
// ACTIONS DU WORKFLOW
// ═══════════════════════════════════════════════════════════════════
// Demandeur : Soumettre
action ( features : instance ) submit result [1] $self;
// Approbateur Niveau 1 (Responsable) : Approuver ou Refuser
action ( features : instance ) approveL1 result [1] $self;
action ( features : instance ) rejectL1
parameter ZA_RejectionInput
result [1] $self;
// Approbateur Niveau 2 (RH) : Approbation ou refus final
action ( features : instance ) approveL2 result [1] $self;
action ( features : instance ) rejectL2
parameter ZA_RejectionInput
result [1] $self;
// Demandeur : Resoumettre une demande refusee
action ( features : instance ) resubmit result [1] $self;
// Demandeur : Retirer (avant soumission)
action ( features : instance ) withdraw result [1] $self;
// ═══════════════════════════════════════════════════════════════════
// VALIDATIONS
// ═══════════════════════════════════════════════════════════════════
validation validateDates on save { field StartDate, EndDate; }
validation validateDaysRequested on save { field DaysRequested; }
validation validateLeaveType on save { field LeaveType; }
// ═══════════════════════════════════════════════════════════════════
// DETERMINATIONS
// ═══════════════════════════════════════════════════════════════════
determination setInitialStatus on modify { create; }
determination calculateDays on modify { field StartDate, EndDate; }
determination setEmployeeData on modify { create; }
// ═══════════════════════════════════════════════════════════════════
// ACTIONS DRAFT
// ═══════════════════════════════════════════════════════════════════
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare {
validation validateDates;
validation validateDaysRequested;
validation validateLeaveType;
}
// Mapping
mapping for zleave_request
{
RequestUUID = request_uuid;
RequestId = request_id;
EmployeeId = employee_id;
EmployeeName = employee_name;
LeaveType = leave_type;
StartDate = start_date;
EndDate = end_date;
DaysRequested = days_requested;
Reason = reason;
WorkflowStatus = workflow_status;
CurrentApprover = current_approver;
ApprovalLevel = approval_level;
SubmittedAt = submitted_at;
SubmittedBy = submitted_by;
ApprovedL1At = approved_l1_at;
ApprovedL1By = approved_l1_by;
ApprovedL2At = approved_l2_at;
ApprovedL2By = approved_l2_by;
RejectedAt = rejected_at;
RejectedBy = rejected_by;
RejectionReason = rejection_reason;
CreatedBy = created_by;
CreatedAt = created_at;
LastChangedBy = last_changed_by;
LastChangedAt = last_changed_at;
LocalLastChangedAt = local_last_changed_at;
}
}

Abstract Entity pour le motif de refus

@EndUserText.label: 'Motif de refus"
define abstract entity ZA_RejectionInput
{
@UI.multiLineText: true
@EndUserText.label: 'Motif de refus"
RejectionReason : abap.string(500);
}

Behavior Implementation

L’implementation contient la logique du workflow avec les transitions de statut, le Feature Control et les validations :

CLASS lhc_leaverequest DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
CONSTANTS:
" Statuts du workflow
BEGIN OF gc_status,
draft TYPE c LENGTH 2 VALUE 'DR',
submitted TYPE c LENGTH 2 VALUE 'SU',
approved1 TYPE c LENGTH 2 VALUE 'A1',
active TYPE c LENGTH 2 VALUE 'AC',
rejected TYPE c LENGTH 2 VALUE 'RJ',
END OF gc_status.
" Determinations
METHODS setInitialStatus FOR DETERMINE ON MODIFY
IMPORTING keys FOR LeaveRequest~setInitialStatus.
METHODS calculateDays FOR DETERMINE ON MODIFY
IMPORTING keys FOR LeaveRequest~calculateDays.
METHODS setEmployeeData FOR DETERMINE ON MODIFY
IMPORTING keys FOR LeaveRequest~setEmployeeData.
" Validations
METHODS validateDates FOR VALIDATE ON SAVE
IMPORTING keys FOR LeaveRequest~validateDates.
METHODS validateDaysRequested FOR VALIDATE ON SAVE
IMPORTING keys FOR LeaveRequest~validateDaysRequested.
METHODS validateLeaveType FOR VALIDATE ON SAVE
IMPORTING keys FOR LeaveRequest~validateLeaveType.
" Actions du workflow
METHODS submit FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~submit RESULT result.
METHODS approveL1 FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~approveL1 RESULT result.
METHODS rejectL1 FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~rejectL1 RESULT result.
METHODS approveL2 FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~approveL2 RESULT result.
METHODS rejectL2 FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~rejectL2 RESULT result.
METHODS resubmit FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~resubmit RESULT result.
METHODS withdraw FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~withdraw RESULT result.
" Feature Control
METHODS get_instance_features FOR INSTANCE FEATURES
IMPORTING keys REQUEST requested_features FOR LeaveRequest RESULT result.
" Authorization
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR LeaveRequest RESULT result.
METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION
IMPORTING REQUEST requested_authorizations FOR LeaveRequest RESULT result.
" Methodes utilitaires
METHODS get_current_user
RETURNING VALUE(result) TYPE syuname.
METHODS get_current_timestamp
RETURNING VALUE(result) TYPE timestampl.
METHODS get_manager_for_employee
IMPORTING employee_id TYPE numc8
RETURNING VALUE(result) TYPE syuname.
METHODS is_current_user_approver
IMPORTING request TYPE zi_leaverequest
RETURNING VALUE(result) TYPE abap_boolean.
ENDCLASS.
CLASS lhc_leaverequest IMPLEMENTATION.
METHOD setInitialStatus.
" Definir les valeurs initiales lors de CREATE
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus ApprovalLevel )
WITH VALUE #( FOR key IN keys (
%tky = key-%tky
WorkflowStatus = gc_status-draft
ApprovalLevel = 0
) ).
ENDMETHOD.
METHOD calculateDays.
" Calculer le nombre de jours de conge a partir des dates de debut et fin
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( StartDate EndDate )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
IF <request>-StartDate IS NOT INITIAL
AND <request>-EndDate IS NOT INITIAL.
DATA(days) = <request>-EndDate - <request>-StartDate + 1.
IF days > 0.
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( DaysRequested )
WITH VALUE #( (
%tky = <request>-%tky
DaysRequested = days
) ).
ENDIF.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD setEmployeeData.
" Determiner les donnees de l'employe a partir des donnees utilisateur
DATA(current_user) = get_current_user( ).
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( EmployeeId EmployeeName )
WITH VALUE #( FOR key IN keys (
%tky = key-%tky
EmployeeId = current_user
EmployeeName = current_user " Simplifie - normalement depuis les RH
) ).
ENDMETHOD.
METHOD validateDates.
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( StartDate EndDate )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(today) = cl_abap_context_info=>get_system_date( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
" La date de debut doit etre dans le futur
IF <request>-StartDate < today.
APPEND VALUE #(
%tky = <request>-%tky
%element-StartDate = if_abap_behv=>mk-on
) TO failed-leaverequest.
APPEND VALUE #(
%tky = <request>-%tky
%state_area = 'VALIDATE_DATES"
%element-StartDate = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'La date de debut doit etre dans le futur"
)
) TO reported-leaverequest.
ENDIF.
" La date de fin doit etre apres la date de debut
IF <request>-EndDate < <request>-StartDate.
APPEND VALUE #(
%tky = <request>-%tky
%element-EndDate = if_abap_behv=>mk-on
) TO failed-leaverequest.
APPEND VALUE #(
%tky = <request>-%tky
%state_area = 'VALIDATE_DATES"
%element-EndDate = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'La date de fin doit etre apres la date de debut"
)
) TO reported-leaverequest.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD validateDaysRequested.
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( DaysRequested LeaveType )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
" Verifier le nombre maximum de jours de conge
DATA(max_days) = SWITCH i(
<request>-LeaveType
WHEN 'VA' THEN 30 " Vacances
WHEN 'SP' THEN 5 " Conge special
WHEN 'SI' THEN 180 " Maladie
ELSE 30
).
IF <request>-DaysRequested > max_days.
APPEND VALUE #(
%tky = <request>-%tky
%element-DaysRequested = if_abap_behv=>mk-on
) TO failed-leaverequest.
APPEND VALUE #(
%tky = <request>-%tky
%state_area = 'VALIDATE_DAYS"
%element-DaysRequested = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Maximum { max_days } jours autorises pour ce type de conge|
)
) TO reported-leaverequest.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD validateLeaveType.
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( LeaveType )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(valid_types) = VALUE string_table( ( `VA` ) ( `SP` ) ( `SI` ) ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
IF NOT line_exists( valid_types[ table_line = <request>-LeaveType ] ).
APPEND VALUE #(
%tky = <request>-%tky
%element-LeaveType = if_abap_behv=>mk-on
) TO failed-leaverequest.
APPEND VALUE #(
%tky = <request>-%tky
%state_area = 'VALIDATE_TYPE"
%element-LeaveType = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Type de conge invalide (VA, SP, SI autorises)"
)
) TO reported-leaverequest.
ENDIF.
ENDLOOP.
ENDMETHOD.
"═══════════════════════════════════════════════════════════════════════
" ACTIONS DU WORKFLOW
"═══════════════════════════════════════════════════════════════════════
METHOD submit.
" Soumettre la demande : DRAFT → SUBMITTED
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( WorkflowStatus EmployeeId )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
DATA(timestamp) = get_current_timestamp( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
" Seules les demandes en brouillon peuvent etre soumises
IF <request>-WorkflowStatus <> gc_status-draft.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Seuls les brouillons peuvent etre soumis"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" Determiner le responsable
DATA(manager) = get_manager_for_employee( CONV #( <request>-EmployeeId ) ).
" Effectuer la transition de statut
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover ApprovalLevel SubmittedAt SubmittedBy )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-submitted
CurrentApprover = manager
ApprovalLevel = 1
SubmittedAt = timestamp
SubmittedBy = current_user
) ).
ENDLOOP.
" Retourner les donnees mises a jour
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
METHOD approveL1.
" Approbation Niveau 1 : SUBMITTED → APPROVED1
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
DATA(timestamp) = get_current_timestamp( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
" Verification : statut correct et approbateur
IF <request>-WorkflowStatus <> gc_status-submitted.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'La demande ne peut pas etre approuvee dans ce statut"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
IF NOT is_current_user_approver( <request> ).
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Vous n etes pas autorise comme approbateur"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" Determiner l'approbateur RH (Niveau 2)
DATA(hr_approver) = CONV syuname( 'HR_APPROVER' ). " Simplifie
" Transition de statut : SUBMITTED → APPROVED1
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover ApprovalLevel ApprovedL1At ApprovedL1By )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-approved1
CurrentApprover = hr_approver
ApprovalLevel = 2
ApprovedL1At = timestamp
ApprovedL1By = current_user
) ).
ENDLOOP.
" Retourner le resultat
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
METHOD rejectL1.
" Refus Niveau 1 : SUBMITTED → REJECTED
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
DATA(timestamp) = get_current_timestamp( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>)
WHERE %tky = <request>-%tky.
" Motif de refus depuis le parametre
DATA(rejection_reason) = <key>-%param-RejectionReason.
IF <request>-WorkflowStatus <> gc_status-submitted.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'La demande ne peut pas etre refusee dans ce statut"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
IF NOT is_current_user_approver( <request> ).
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Vous n etes pas autorise comme approbateur"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" Transition de statut : SUBMITTED → REJECTED
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover RejectedAt RejectedBy RejectionReason )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-rejected
CurrentApprover = <request>-EmployeeId " Retour au demandeur
RejectedAt = timestamp
RejectedBy = current_user
RejectionReason = rejection_reason
) ).
ENDLOOP.
ENDLOOP.
" Retourner le resultat
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
METHOD approveL2.
" Approbation Niveau 2 (RH) : APPROVED1 → ACTIVE
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
DATA(timestamp) = get_current_timestamp( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
IF <request>-WorkflowStatus <> gc_status-approved1.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'La demande doit d abord etre approuvee par le responsable"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
IF NOT is_current_user_approver( <request> ).
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Vous n etes pas autorise comme approbateur RH"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" Approbation finale : APPROVED1 → ACTIVE
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover ApprovedL2At ApprovedL2By )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-active
CurrentApprover = '' " Workflow termine
ApprovedL2At = timestamp
ApprovedL2By = current_user
) ).
ENDLOOP.
" Retourner le resultat
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
METHOD rejectL2.
" Refus Niveau 2 (RH) : APPROVED1 → REJECTED
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
DATA(timestamp) = get_current_timestamp( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>)
WHERE %tky = <request>-%tky.
DATA(rejection_reason) = <key>-%param-RejectionReason.
IF <request>-WorkflowStatus <> gc_status-approved1.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'La demande ne peut pas etre refusee dans ce statut"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" APPROVED1 → REJECTED
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover RejectedAt RejectedBy RejectionReason )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-rejected
CurrentApprover = <request>-EmployeeId
RejectedAt = timestamp
RejectedBy = current_user
RejectionReason = rejection_reason
) ).
ENDLOOP.
ENDLOOP.
" Retourner le resultat
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
METHOD resubmit.
" Resoumettre une demande refusee : REJECTED → SUBMITTED
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( WorkflowStatus EmployeeId )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
DATA(timestamp) = get_current_timestamp( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
IF <request>-WorkflowStatus <> gc_status-rejected.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Seules les demandes refusees peuvent etre resoumises"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
DATA(manager) = get_manager_for_employee( CONV #( <request>-EmployeeId ) ).
" Reinitialiser et resoumettre
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover ApprovalLevel
RejectedAt RejectedBy RejectionReason SubmittedAt SubmittedBy )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-submitted
CurrentApprover = manager
ApprovalLevel = 1
RejectedAt = VALUE #( ) " Reset
RejectedBy = '"
RejectionReason = '"
SubmittedAt = timestamp
SubmittedBy = current_user
) ).
ENDLOOP.
" Retourner le resultat
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
METHOD withdraw.
" Retirer une demande soumise : SUBMITTED → DRAFT
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( WorkflowStatus EmployeeId )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
" Seules les demandes soumises peuvent etre retirees
IF <request>-WorkflowStatus <> gc_status-submitted.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Seules les demandes soumises peuvent etre retirees"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" Seul le demandeur peut retirer
IF CONV syuname( <request>-EmployeeId ) <> current_user.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Seul le demandeur peut retirer la demande"
)
) TO reported-leaverequest.
APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest.
CONTINUE.
ENDIF.
" Retour a Draft
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus CurrentApprover ApprovalLevel
SubmittedAt SubmittedBy )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-draft
CurrentApprover = '"
ApprovalLevel = 0
SubmittedAt = VALUE #( )
SubmittedBy = '"
) ).
ENDLOOP.
" Retourner le resultat
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(updated_requests).
result = VALUE #( FOR req IN updated_requests (
%tky = req-%tky
%param = req
) ).
ENDMETHOD.
"═══════════════════════════════════════════════════════════════════════
" FEATURE CONTROL
"═══════════════════════════════════════════════════════════════════════
METHOD get_instance_features.
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( WorkflowStatus EmployeeId CurrentApprover )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
DATA(is_owner) = xsdbool( CONV syuname( <request>-EmployeeId ) = current_user ).
DATA(is_approver) = is_current_user_approver( <request> ).
APPEND VALUE #(
%tky = <request>-%tky
" Update/Delete seulement pour le proprietaire en statut Draft
%update = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-draft
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
%delete = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-draft
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" Submit : seulement pour le proprietaire en statut Draft
%action-submit = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-draft
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" Withdraw : seulement pour le proprietaire en statut Submitted
%action-withdraw = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-submitted
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" Resubmit : seulement pour le proprietaire en statut Rejected
%action-resubmit = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-rejected
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" ApproveL1/RejectL1 : seulement pour l'approbateur en statut Submitted
%action-approveL1 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-submitted
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
%action-rejectL1 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-submitted
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" ApproveL2/RejectL2 : seulement pour l'approbateur en statut Approved1
%action-approveL2 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-approved1
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
%action-rejectL2 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-approved1
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
) TO result.
ENDLOOP.
ENDMETHOD.
METHOD get_instance_authorizations.
" Verification des autorisations analogue au Feature Control
READ ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( WorkflowStatus EmployeeId CurrentApprover )
WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DATA(current_user) = get_current_user( ).
LOOP AT requests ASSIGNING FIELD-SYMBOL(<request>).
DATA(is_owner) = xsdbool( CONV syuname( <request>-EmployeeId ) = current_user ).
DATA(is_approver) = is_current_user_approver( <request> ).
APPEND VALUE #(
%tky = <request>-%tky
%update = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-draft
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%delete = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-draft
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-submit = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-draft
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-approveL1 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-submitted
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-rejectL1 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-submitted
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-approveL2 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-approved1
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-rejectL2 = COND #(
WHEN is_approver = abap_true AND <request>-WorkflowStatus = gc_status-approved1
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-withdraw = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-submitted
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
%action-resubmit = COND #(
WHEN is_owner = abap_true AND <request>-WorkflowStatus = gc_status-rejected
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
) TO result.
ENDLOOP.
ENDMETHOD.
METHOD get_global_authorizations.
" Autorisation CREATE pour tous les utilisateurs
result = VALUE #(
%create = if_abap_behv=>auth-allowed
).
ENDMETHOD.
"═══════════════════════════════════════════════════════════════════════
" METHODES UTILITAIRES
"═══════════════════════════════════════════════════════════════════════
METHOD get_current_user.
result = cl_abap_context_info=>get_user_technical_name( ).
ENDMETHOD.
METHOD get_current_timestamp.
GET TIME STAMP FIELD result.
ENDMETHOD.
METHOD get_manager_for_employee.
" Implementation simplifiee - en realite depuis les donnees RH
" ou la structure organisationnelle
result = 'MANAGER01'.
ENDMETHOD.
METHOD is_current_user_approver.
DATA(current_user) = get_current_user( ).
result = xsdbool( request-CurrentApprover = current_user ).
ENDMETHOD.
ENDCLASS.

Bonnes pratiques pour les scenarios de workflow

A FAIRE : Verifier explicitement les transitions de statut

" BIEN : Verification explicite du statut avant chaque transition
IF <request>-WorkflowStatus <> gc_status-submitted.
APPEND VALUE #(
%tky = <request>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Statut invalide pour cette action"
)
) TO reported-leaverequest.
RETURN.
ENDIF.

A FAIRE : Garder le Feature Control et l’Authorization synchronises

" BIEN : Meme logique pour les deux
DATA(can_approve) = xsdbool(
is_approver = abap_true AND
<request>-WorkflowStatus = gc_status-submitted
).
" Feature Control
%action-approveL1 = COND #(
WHEN can_approve = abap_true
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" Authorization
%action-approveL1 = COND #(
WHEN can_approve = abap_true
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )

A FAIRE : Documenter completement l’historique du workflow

" BIEN : Toutes les actions du workflow avec timestamp et utilisateur
MODIFY ENTITIES OF zi_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( WorkflowStatus ApprovedL1At ApprovedL1By )
WITH VALUE #( (
%tky = <request>-%tky
WorkflowStatus = gc_status-approved1
ApprovedL1At = get_current_timestamp( )
ApprovedL1By = get_current_user( )
) ).

A NE PAS FAIRE : Dupliquer la logique de statut

" MAUVAIS : Meme verification a plusieurs endroits
METHOD approveL1.
IF <request>-WorkflowStatus = 'SU'. " Chaine codee en dur
...
ENDIF.
ENDMETHOD.
METHOD get_instance_features.
IF <request>-WorkflowStatus = 'SU'. " Encore codee en dur
...
ENDIF.
ENDMETHOD.
" BIEN : Utiliser des constantes
CONSTANTS:
BEGIN OF gc_status,
submitted TYPE c LENGTH 2 VALUE 'SU',
END OF gc_status.

Possibilites d’extension

Le workflow presente peut etre etendu :

ExtensionDescription
Notification par e-mailInformer automatiquement les concernes lors des changements de statut
EscaladeTransmission automatique apres depassement de delai
Regles de suppleancePermettre l’approbation par un suppleant
Approbation parallelePlusieurs approbateurs simultanement
Workflows conditionnelsDifferents niveaux d’approbation selon la duree du conge

Resume

Dans cet article, nous avons implemente un workflow d’approbation complet pour les demandes de conges :

  • Le Draft Handling permet la sauvegarde intermediaire des demandes avant soumission
  • Les Actions basees sur le statut controlent les transitions du workflow (Submit, Approve, Reject)
  • Le Feature Control affiche seulement les actions pertinentes pour le statut et l’utilisateur respectifs
  • L’Authorization garantit que seuls les utilisateurs autorises peuvent executer les actions
  • L’historique du workflow documente toutes les etapes d’approbation avec timestamps

Ce pattern est transposable a n’importe quel scenario d’approbation : validations de commandes, notes de frais, demandes de modification, etc.

Sujets connexes