Los Approval-Workflows son un elemento central en aplicaciones empresariales. Solicitudes de vacaciones, liberaciones de pedidos o liquidaciones de gastos de viaje pasan por varias etapas de aprobacion antes de ser finalmente aprobados. El RESTful ABAP Programming Model (RAP) ofrece con Draft Handling la base ideal para tales workflows: las solicitudes pueden guardarse como borrador, enviarse y ser procesadas por diferentes aprobadores.
En este articulo implementamos un workflow de solicitud de vacaciones completo con aprobacion multinivel. Aprenderas como definir Actions basadas en estado, usar Feature Control para control dinamico de UI contextual y mapear todo el proceso de aprobacion en RAP.
Vision General del Workflow
Nuestra solicitud de vacaciones pasa por las siguientes fases:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ DRAFT │────▶│ SUBMITTED │────▶│ APPROVED │────▶│ ACTIVE ││ (Entwurf) │ │ (Eingereicht)│ │(Genehmigt L1)│ │ (Final) │└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ ▼ ▼ ▼┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ DISCARDED │ │ REJECTED │ │ REJECTED ││ (Verworfen) │ │(Abgelehnt L1)│ │(Abgelehnt L2)│└─────────────┘ └─────────────┘ └─────────────┘| Estado | Descripcion | Siguientes Acciones |
|---|---|---|
DRAFT | Empleado crea/edita solicitud | Submit, Discard |
SUBMITTED | Enviada, esperando supervisor | Approve, Reject (Level 1) |
APPROVED | Aprobada por supervisor, esperando HR | Approve, Reject (Level 2) |
ACTIVE | Aprobada finalmente, activa en el sistema | - |
REJECTED | Rechazada, de vuelta al solicitante | Resubmit, Discard |
Modelo de Datos
Tabla de Base de Datos para Solicitudes de Vacaciones
@EndUserText.label : 'Urlaubsantrag'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENTdefine 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=Vacation, SP=Special, SI=Sick start_date : abap.dats; end_date : abap.dats; days_requested : abap.int4; reason : abap.string(500);
// Status-Management workflow_status : abap.char(2); // DR/SU/A1/AC/RJ current_approver : abap.char(12); approval_level : abap.int1; // 1=Manager, 2=HR
// Approval-Historie 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);
// Administrative Felder created_by : abap.char(12); created_at : timestampl; last_changed_by : abap.char(12); last_changed_at : timestampl; local_last_changed_at : timestampl;}Tabla Draft
@EndUserText.label : 'Urlaubsantrag Draft'@AbapCatalog.tableCategory : #TRANSPARENTdefine 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;
// Draft-Admin-Felder "%admin" : include sych_bdl_draft_admin_inc;}CDS Interface View
@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Urlaubsantrag - 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,
// Workflow-Status workflow_status as WorkflowStatus, current_approver as CurrentApprover, approval_level as ApprovalLevel,
// Approval-Historie 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,
// Berechnetes Feld für Status-Text case workflow_status when 'DR' then 'Entwurf' when 'SU' then 'Eingereicht' when 'A1' then 'Genehmigt (Level 1)' when 'AC' then 'Aktiv' when 'RJ' then 'Abgelehnt' else 'Unbekannt' end as WorkflowStatusText,
// Criticality für Status-Anzeige case workflow_status when 'DR' then 0 // Neutral when 'SU' then 2 // Warning (gelb) when 'A1' then 2 // Warning (gelb) when 'AC' then 3 // Success (grün) when 'RJ' then 1 // Error (rot) else 0 end as StatusCriticality,
// Administrative Felder @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 con Workflow-Actions
La Behavior Definition es el nucleo del workflow. Aqui definimos todas las transiciones de estado como Actions con Feature Control:
managed implementation in class zbp_i_leave_request unique;strict ( 2 );with draft;
define behavior for ZI_LeaveRequest alias LeaveRequestpersistent table zleave_requestdraft table zdraft_leave_reqlock master total etag LastChangedAtauthorization master ( instance, global )etag master LastChangedAt{ // Standard-Operationen create; update; delete;
// Feld-Eigenschaften 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;
// ═══════════════════════════════════════════════════════════════════ // WORKFLOW ACTIONS // ═══════════════════════════════════════════════════════════════════
// Antragsteller: Einreichen action ( features : instance ) submit result [1] $self;
// Genehmiger Level 1 (Vorgesetzter): Genehmigen oder Ablehnen action ( features : instance ) approveL1 result [1] $self; action ( features : instance ) rejectL1 parameter ZA_RejectionInput result [1] $self;
// Genehmiger Level 2 (HR): Final genehmigen oder ablehnen action ( features : instance ) approveL2 result [1] $self; action ( features : instance ) rejectL2 parameter ZA_RejectionInput result [1] $self;
// Antragsteller: Abgelehnten Antrag erneut einreichen action ( features : instance ) resubmit result [1] $self;
// Antragsteller: Zurück an Entwurf (vor Submit) 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; }
// ═══════════════════════════════════════════════════════════════════ // DRAFT ACTIONS // ═══════════════════════════════════════════════════════════════════
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 para Motivo de Rechazo
@EndUserText.label: 'Ablehnungsgrund'define abstract entity ZA_RejectionInput{ @UI.multiLineText: true @EndUserText.label: 'Ablehnungsgrund' RejectionReason : abap.string(500);}Behavior Implementation
La implementacion contiene la logica del workflow con transiciones de estado, Feature Control y validaciones:
CLASS lhc_leaverequest DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. CONSTANTS: " Workflow-Status 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.
" Workflow Actions 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.
" Hilfsmethoden 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. " Initiale Werte bei CREATE setzen 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. " Anzahl Urlaubstage aus Start- und Enddatum berechnen 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. " Mitarbeiterdaten aus Benutzerstamm ermitteln 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 " Vereinfacht - normalerweise aus HR-Stamm ) ). 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>). " Startdatum muss in der Zukunft liegen 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 = 'Das Startdatum muss in der Zukunft liegen' ) ) TO reported-leaverequest. ENDIF.
" Enddatum muss nach Startdatum liegen 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 = 'Das Enddatum muss nach dem Startdatum liegen' ) ) 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>). " Maximale Urlaubstage prüfen DATA(max_days) = SWITCH i( <request>-LeaveType WHEN 'VA' THEN 30 " Vacation WHEN 'SP' THEN 5 " Special Leave WHEN 'SI' THEN 180 " Sick Leave 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 = |Maximal { max_days } Tage für diesen Urlaubstyp erlaubt| ) ) 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 = 'Ungültiger Urlaubstyp (VA, SP, SI erlaubt)' ) ) TO reported-leaverequest. ENDIF. ENDLOOP. ENDMETHOD.
"═══════════════════════════════════════════════════════════════════════ " WORKFLOW ACTIONS "═══════════════════════════════════════════════════════════════════════
METHOD submit. " Antrag einreichen: 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>). " Nur Draft-Anträge können eingereicht werden IF <request>-WorkflowStatus <> gc_status-draft. APPEND VALUE #( %tky = <request>-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Nur Entwürfe können eingereicht werden' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
" Manager ermitteln DATA(manager) = get_manager_for_employee( CONV #( <request>-EmployeeId ) ).
" Status-Transition durchführen 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.
" Aktualisierte Daten zurückgeben 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. " Level-1-Genehmigung: 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>). " Prüfung: korrekter Status und Genehmiger IF <request>-WorkflowStatus <> gc_status-submitted. APPEND VALUE #( %tky = <request>-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Antrag kann in diesem Status nicht genehmigt werden' ) ) 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 = 'Sie sind nicht als Genehmiger berechtigt' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
" HR-Genehmiger ermitteln (Level 2) DATA(hr_approver) = CONV syuname( 'HR_APPROVER' ). " Vereinfacht
" Status-Transition: 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.
" Ergebnis zurückgeben 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. " Level-1-Ablehnung: 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.
" Ablehnungsgrund aus Parameter 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 = 'Antrag kann in diesem Status nicht abgelehnt werden' ) ) 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 = 'Sie sind nicht als Genehmiger berechtigt' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
" Status-Transition: 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 " Zurück an Antragsteller RejectedAt = timestamp RejectedBy = current_user RejectionReason = rejection_reason ) ). ENDLOOP. ENDLOOP.
" Ergebnis zurückgeben 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. " Level-2-Genehmigung (HR): 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 = 'Antrag muss zuerst vom Vorgesetzten genehmigt werden' ) ) 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 = 'Sie sind nicht als HR-Genehmiger berechtigt' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
" Final Approval: 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 abgeschlossen ApprovedL2At = timestamp ApprovedL2By = current_user ) ). ENDLOOP.
" Ergebnis zurückgeben 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. " Level-2-Ablehnung (HR): 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 = 'Antrag kann in diesem Status nicht abgelehnt werden' ) ) 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.
" Ergebnis zurückgeben 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. " Abgelehnten Antrag erneut einreichen: 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 = 'Nur abgelehnte Anträge können erneut eingereicht werden' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
DATA(manager) = get_manager_for_employee( CONV #( <request>-EmployeeId ) ).
" Reset und erneut einreichen 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.
" Ergebnis zurückgeben 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. " Eingereichten Antrag zurückziehen: 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>). " Nur eingereichte Anträge können zurückgezogen werden IF <request>-WorkflowStatus <> gc_status-submitted. APPEND VALUE #( %tky = <request>-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Nur eingereichte Anträge können zurückgezogen werden' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
" Nur Antragsteller darf zurückziehen 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 = 'Nur der Antragsteller kann den Antrag zurückziehen' ) ) TO reported-leaverequest. APPEND VALUE #( %tky = <request>-%tky ) TO failed-leaverequest. CONTINUE. ENDIF.
" Zurück auf 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.
" Ergebnis zurückgeben 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 nur für Eigentümer im Draft-Status %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: nur für Eigentümer im Draft-Status %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: nur für Eigentümer im Submitted-Status %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: nur für Eigentümer im Rejected-Status %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: nur für Genehmiger im Submitted-Status %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: nur für Genehmiger im Approved1-Status %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. " Authorization-Prüfung analog zu 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. " CREATE-Berechtigung für alle Benutzer result = VALUE #( %create = if_abap_behv=>auth-allowed ). ENDMETHOD.
"═══════════════════════════════════════════════════════════════════════ " HELPER METHODS "═══════════════════════════════════════════════════════════════════════
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. " Vereinfachte Implementierung - in der Realität aus HR-Stammdaten " oder Organisationsstruktur ermitteln result = 'MANAGER01'. ENDMETHOD.
METHOD is_current_user_approver. DATA(current_user) = get_current_user( ). result = xsdbool( request-CurrentApprover = current_user ). ENDMETHOD.
ENDCLASS.Projection View con UI-Annotations
La Projection View expone el workflow para Fiori Elements:
@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Urlaubsantrag - Projection'@Metadata.allowExtensions: truedefine root view entity ZC_LeaveRequest provider contract transactional_query as projection on ZI_LeaveRequest{ @UI.facet: [ { id: 'General', type: #COLLECTION, label: 'Allgemein', position: 10 }, { id: 'Dates', type: #FIELDGROUP_REFERENCE, targetQualifier: 'Dates', parentId: 'General', label: 'Zeitraum', position: 10 }, { id: 'Details', type: #FIELDGROUP_REFERENCE, targetQualifier: 'Details', parentId: 'General', label: 'Details', position: 20 }, { id: 'Workflow', type: #COLLECTION, label: 'Workflow', position: 20 }, { id: 'Status', type: #FIELDGROUP_REFERENCE, targetQualifier: 'Status', parentId: 'Workflow', label: 'Status', position: 10 }, { id: 'History', type: #FIELDGROUP_REFERENCE, targetQualifier: 'History', parentId: 'Workflow', label: 'Historie', position: 20 } ]
@UI.hidden: true key RequestUUID,
@UI.lineItem: [{ position: 10, importance: #HIGH }] @UI.selectionField: [{ position: 10 }] RequestId,
@UI.lineItem: [{ position: 20 }] @UI.fieldGroup: [{ qualifier: 'Details', position: 10 }] EmployeeName,
@UI.lineItem: [{ position: 30 }] @UI.fieldGroup: [{ qualifier: 'Details', position: 20 }] @Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_LeaveType', element: 'LeaveType' }}] LeaveType,
@UI.lineItem: [{ position: 40 }] @UI.fieldGroup: [{ qualifier: 'Dates', position: 10 }] StartDate,
@UI.lineItem: [{ position: 50 }] @UI.fieldGroup: [{ qualifier: 'Dates', position: 20 }] EndDate,
@UI.lineItem: [{ position: 60 }] @UI.fieldGroup: [{ qualifier: 'Dates', position: 30 }] DaysRequested,
@UI.fieldGroup: [{ qualifier: 'Details', position: 30 }] @UI.multiLineText: true Reason,
// Workflow-Status mit Criticality @UI.lineItem: [{ position: 70, criticality: 'StatusCriticality', importance: #HIGH }] @UI.fieldGroup: [{ qualifier: 'Status', position: 10, criticality: 'StatusCriticality' }] @UI.selectionField: [{ position: 20 }] WorkflowStatusText,
@UI.hidden: true WorkflowStatus,
@UI.hidden: true StatusCriticality,
@UI.fieldGroup: [{ qualifier: 'Status', position: 20 }] CurrentApprover,
// Workflow-Historie @UI.fieldGroup: [{ qualifier: 'History', position: 10 }] SubmittedAt,
@UI.fieldGroup: [{ qualifier: 'History', position: 20 }] SubmittedBy,
@UI.fieldGroup: [{ qualifier: 'History', position: 30 }] ApprovedL1At,
@UI.fieldGroup: [{ qualifier: 'History', position: 40 }] ApprovedL1By,
@UI.fieldGroup: [{ qualifier: 'History', position: 50 }] ApprovedL2At,
@UI.fieldGroup: [{ qualifier: 'History', position: 60 }] ApprovedL2By,
@UI.fieldGroup: [{ qualifier: 'History', position: 70 }] RejectedAt,
@UI.fieldGroup: [{ qualifier: 'History', position: 80 }] RejectedBy,
@UI.fieldGroup: [{ qualifier: 'History', position: 90 }] @UI.multiLineText: true RejectionReason,
// Workflow-Actions in der Line Item @UI.lineItem: [ { type: #FOR_ACTION, dataAction: 'submit', label: 'Einreichen', position: 10 }, { type: #FOR_ACTION, dataAction: 'approveL1', label: 'Genehmigen', position: 20 }, { type: #FOR_ACTION, dataAction: 'rejectL1', label: 'Ablehnen', position: 30 }, { type: #FOR_ACTION, dataAction: 'withdraw', label: 'Zurückziehen', position: 40 } ]
// Actions auf Object Page @UI.identification: [ { type: #FOR_ACTION, dataAction: 'submit', label: 'Einreichen', position: 10 }, { type: #FOR_ACTION, dataAction: 'approveL1', label: 'Genehmigen (L1)', position: 20 }, { type: #FOR_ACTION, dataAction: 'rejectL1', label: 'Ablehnen (L1)', position: 30 }, { type: #FOR_ACTION, dataAction: 'approveL2', label: 'Final Genehmigen', position: 40 }, { type: #FOR_ACTION, dataAction: 'rejectL2', label: 'Final Ablehnen', position: 50 }, { type: #FOR_ACTION, dataAction: 'withdraw', label: 'Zurückziehen', position: 60 }, { type: #FOR_ACTION, dataAction: 'resubmit', label: 'Erneut Einreichen', position: 70 } ] EmployeeId,
// Administrative Felder @UI.hidden: true CreatedBy, @UI.hidden: true CreatedAt, @UI.hidden: true LastChangedBy, @UI.hidden: true LastChangedAt, @UI.hidden: true LocalLastChangedAt, @UI.hidden: true ApprovalLevel}Projection Behavior Definition
projection implementation in class zbp_c_leaverequest unique;strict ( 2 );use draft;
define behavior for ZC_LeaveRequest alias LeaveRequest{ use create; use update; use delete;
// Workflow Actions use action submit; use action approveL1; use action rejectL1; use action approveL2; use action rejectL2; use action resubmit; use action withdraw;
// Draft Actions use action Edit; use action Activate; use action Discard; use action Resume; use action Prepare;}Mejores Practicas para Escenarios de Workflow
HACER: Verificar transiciones de estado explicitamente
" BIEN: Verificacion explicita de estado antes de cada transicionIF <request>-WorkflowStatus <> gc_status-submitted. APPEND VALUE #( %tky = <request>-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = 'Ungültiger Status für diese Aktion' ) ) TO reported-leaverequest. RETURN.ENDIF.HACER: Mantener Feature Control y Authorization sincronizados
" BIEN: Misma logica para ambosDATA(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 )HACER: Documentar completamente el historial del workflow
" BIEN: Registrar todas las acciones del workflow con timestamp y usuarioMODIFY 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( ) ) ).NO HACER: Duplicar logica de estado
" MAL: Misma verificacion en multiples lugaresMETHOD approveL1. IF <request>-WorkflowStatus = 'SU'. " String hardcodeado ... ENDIF.ENDMETHOD.
METHOD get_instance_features. IF <request>-WorkflowStatus = 'SU'. " De nuevo hardcodeado ... ENDIF.ENDMETHOD.
" BIEN: Usar constantesCONSTANTS: BEGIN OF gc_status, submitted TYPE c LENGTH 2 VALUE 'SU', END OF gc_status.Posibilidades de Extension
El workflow mostrado puede extenderse:
| Extension | Descripcion |
|---|---|
| Notificacion por Email | Informar automaticamente a los involucrados cuando cambia el estado |
| Escalacion | Reenvio automatico despues de tiempo excedido |
| Reglas de Sustitucion | Permitir aprobacion por suplentes |
| Aprobacion Paralela | Multiples aprobadores simultaneamente |
| Workflows Condicionales | Diferentes niveles de aprobacion segun duracion de las vacaciones |
Resumen
En este articulo implementamos un workflow de aprobacion completo para solicitudes de vacaciones:
- Draft Handling permite el almacenamiento intermedio de solicitudes antes de enviarlas
- Actions basadas en estado controlan las transiciones del workflow (Submit, Approve, Reject)
- Feature Control muestra solo las acciones relevantes para el estado y usuario actual
- Authorization asegura que solo usuarios autorizados puedan ejecutar acciones
- Historial del Workflow documenta todos los pasos de aprobacion con timestamp
El patron puede transferirse a cualquier escenario de aprobacion: liberacion de pedidos, liquidacion de gastos de viaje, solicitudes de cambio y mas.
Temas Relacionados
- Draft Handling en RAP - Fundamentos de la funcionalidad Draft
- RAP Authorization - Autorizaciones en ABAP Cloud
- RAP Actions y Functions - Implementar logica de negocio
- Feature Control y Side Effects - Control dinamico de UI
- RAP Determinations y Validations - Calculos automaticos y verificaciones