Créer pas à pas un service RAP managed avec brouillon
D'une table de base de données à une application Fiori Elements fonctionnelle — un objet métier RAP managed complet avec brouillon, validation, déterminations et actions d'approbation/rejet.
Dans cet article, nous construisons une petite application RAP (ABAP RESTful Application Programming Model) mais complète : une application de demande d'équipement où les employés demandent un ordinateur portable ou un écran, et où un responsable approuve ou rejette la demande.
À la fin, vous aurez un list report + object page Fiori Elements avec gestion du brouillon (draft), une validation, deux déterminations et deux actions. Sans une seule ligne de code UI5.
Prérequis : S/4HANA 2022 ou plus récent (on-premise ou private cloud) ou l'environnement ABAP de SAP BTP, ainsi que les ABAP Development Tools (Eclipse ou VS Code). Tout est écrit en ABAP for Cloud Development, avec uniquement des API publiées (released).
Les objets à créer
| Objet | Type | Rôle |
|---|---|---|
ZEQUIP_REQ |
Table de base de données | Données persistantes |
ZR_EquipmentRequest |
CDS root view entity | Objet métier de base |
ZR_EquipmentRequest |
Behavior definition | Comportement transactionnel + brouillon |
ZBP_R_EquipmentRequest |
Classe | Implémentation du comportement |
ZC_EquipmentRequest |
Projection CDS + behavior | Vue propre au service |
ZC_EquipmentRequest |
Metadata extension | Annotations UI |
ZUI_EQUIPMENTREQUEST |
Service definition | Ce qui est exposé |
ZUI_EQUIPMENTREQUEST_O4 |
Service binding | OData V4 – UI |
La convention de nommage est simple : ZR_ pour l'objet métier racine, ZC_ pour les projections, ZBP_ pour les behavior pools et ZUI_ pour les services UI.
1. Table de base de données
La clé est un UUID (sysuuid_x16), ce qui permet à RAP d'attribuer les clés automatiquement (managed numbering). Le numéro lisible request_id est renseigné séparément lors de l'enregistrement.
Les cinq derniers champs sont des champs administratifs. Grâce aux éléments de données abp_* et aux annotations @Semantics de l'étape suivante, le framework les remplit pour vous ; ils sont aussi nécessaires pour l'ETag et le brouillon.
@EndUserText.label : 'Equipment Request'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zequip_req {
key client : abap.clnt not null;
key request_uuid : sysuuid_x16 not null;
request_id : abap.numc(10);
description : abap.char(80);
equipment_type : abap.char(20);
@Semantics.amount.currencyCode : 'zequip_req.currency_code'
estimated_cost : abap.curr(15,2);
currency_code : abap.cuky;
status : abap.char(1);
requested_by : abp_creation_user;
created_at : abp_creation_tstmpl;
local_last_changed_by : abp_locinst_lastchange_user;
local_last_changed_at : abp_locinst_lastchange_tstmpl;
last_changed_at : abp_lastchange_tstmpl;
}
2. Vue CDS racine
La vue racine donne aux champs des noms lisibles en CamelCase et marque les champs administratifs avec des annotations @Semantics.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Equipment Request - Root'
define root view entity ZR_EquipmentRequest
as select from zequip_req
{
key request_uuid as RequestUUID,
request_id as RequestID,
description as Description,
equipment_type as EquipmentType,
@Semantics.amount.currencyCode: 'CurrencyCode'
estimated_cost as EstimatedCost,
currency_code as CurrencyCode,
status as Status,
@Semantics.user.createdBy: true
requested_by as RequestedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.localInstanceLastChangedBy: true
local_last_changed_by as LocalLastChangedBy,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
local_last_changed_at as LocalLastChangedAt,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt
}
3. Behavior definition
C'est le cœur de l'objet métier. Clic droit sur la vue racine → New Behavior Definition, puis adaptez-la :
managed implementation in class zbp_r_equipmentrequest unique;
strict ( 2 );
with draft;
define behavior for ZR_EquipmentRequest alias EquipmentRequest
persistent table zequip_req
draft table zequip_req_d
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master ( global )
{
field ( numbering : managed, readonly ) RequestUUID;
field ( readonly ) RequestID, Status, RequestedBy, CreatedAt,
LocalLastChangedBy, LocalLastChangedAt, LastChangedAt;
field ( mandatory ) Description, EquipmentType;
create;
update;
delete;
action ( features : instance ) approve result [1] $self;
action ( features : instance ) reject result [1] $self;
determination setInitialValues on modify { create; }
determination setRequestID on save { create; }
validation validateCost on save { create; field EstimatedCost, CurrencyCode; }
draft action Activate optimized;
draft action Discard;
draft action Edit;
draft action Resume;
draft determine action Prepare { validation validateCost; }
mapping for zequip_req
{
RequestUUID = request_uuid;
RequestID = request_id;
Description = description;
EquipmentType = equipment_type;
EstimatedCost = estimated_cost;
CurrencyCode = currency_code;
Status = status;
RequestedBy = requested_by;
CreatedAt = created_at;
LocalLastChangedBy = local_last_changed_by;
LocalLastChangedAt = local_last_changed_at;
LastChangedAt = last_changed_at;
}
}
Quelques points importants :
strict ( 2 )active les contrôles du mode strict le plus récent — à utiliser pour tout nouveau développement.with draft+draft table zequip_req_d: la table de brouillon n'existe pas encore. Placez le curseur sur son nom et utilisez le quick fix (Ctrl+1) pour la générer.etag masterprotège contre les mises à jour perdues ;total etagest obligatoire avec le brouillon.features : instancesur les actions signifie que nous décidons, enregistrement par enregistrement, si les boutons sont actifs.draft determine action Prepareexécute la validation pendant que l'utilisateur est encore en brouillon : les erreurs apparaissent avant l'enregistrement.
4. Implémentation du comportement
Utilisez le quick fix sur le nom de classe de la première ligne pour créer ZBP_R_EquipmentRequest. La classe globale reste vide :
CLASS zbp_r_equipmentrequest DEFINITION PUBLIC ABSTRACT FINAL
FOR BEHAVIOR OF zr_equipmentrequest.
ENDCLASS.
CLASS zbp_r_equipmentrequest IMPLEMENTATION.
ENDCLASS.
Toute la logique se trouve dans l'onglet Local Types de la classe :
" Local types (tab "Local Types" in ADT)
CLASS lhc_equipmentrequest DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
CONSTANTS:
BEGIN OF c_status,
open TYPE c LENGTH 1 VALUE 'O',
approved TYPE c LENGTH 1 VALUE 'A',
rejected TYPE c LENGTH 1 VALUE 'X',
END OF c_status.
METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION
IMPORTING REQUEST requested_authorizations FOR EquipmentRequest RESULT result.
METHODS get_instance_features FOR INSTANCE FEATURES
IMPORTING keys REQUEST requested_features FOR EquipmentRequest RESULT result.
METHODS approve FOR MODIFY
IMPORTING keys FOR ACTION EquipmentRequest~approve RESULT result.
METHODS reject FOR MODIFY
IMPORTING keys FOR ACTION EquipmentRequest~reject RESULT result.
METHODS setinitialvalues FOR DETERMINE ON MODIFY
IMPORTING keys FOR EquipmentRequest~setInitialValues.
METHODS setrequestid FOR DETERMINE ON SAVE
IMPORTING keys FOR EquipmentRequest~setRequestID.
METHODS validatecost FOR VALIDATE ON SAVE
IMPORTING keys FOR EquipmentRequest~validateCost.
ENDCLASS.
CLASS lhc_equipmentrequest IMPLEMENTATION.
METHOD get_global_authorizations.
" Demo: everyone may create/change/delete.
" In a real app check an authorization object here (AUTHORITY-CHECK).
result = VALUE #( %create = if_abap_behv=>auth-allowed
%update = if_abap_behv=>auth-allowed
%delete = if_abap_behv=>auth-allowed ).
ENDMETHOD.
METHOD get_instance_features.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( Status ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
" Approve / reject only while the request is still open
result = VALUE #( FOR req IN requests
LET enabled = COND #( WHEN req-Status = c_status-open
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
IN ( %tky = req-%tky
%action-approve = enabled
%action-reject = enabled ) ).
ENDMETHOD.
METHOD approve.
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( Status )
WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = c_status-approved ) ).
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(requests).
result = VALUE #( FOR req IN requests ( %tky = req-%tky %param = req ) ).
ENDMETHOD.
METHOD reject.
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( Status )
WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = c_status-rejected ) ).
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(requests).
result = VALUE #( FOR req IN requests ( %tky = req-%tky %param = req ) ).
ENDMETHOD.
METHOD setinitialvalues.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( Status CurrencyCode ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DELETE requests WHERE Status IS NOT INITIAL.
CHECK requests IS NOT INITIAL.
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( Status CurrencyCode )
WITH VALUE #( FOR req IN requests
( %tky = req-%tky
Status = c_status-open
CurrencyCode = COND #( WHEN req-CurrencyCode IS INITIAL
THEN 'EUR'
ELSE req-CurrencyCode ) ) ).
ENDMETHOD.
METHOD setrequestid.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( RequestID ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DELETE requests WHERE RequestID IS NOT INITIAL.
CHECK requests IS NOT INITIAL.
" Demo only: MAX + 1. Use a number range object in production.
SELECT SINGLE FROM zequip_req
FIELDS MAX( request_id )
INTO @DATA(max_id).
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( RequestID )
WITH VALUE #( FOR req IN requests INDEX INTO i
( %tky = req-%tky RequestID = max_id + i ) ).
ENDMETHOD.
METHOD validatecost.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( EstimatedCost CurrencyCode ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
LOOP AT requests INTO DATA(req).
" Clear old messages of this validation (draft state messages)
APPEND VALUE #( %tky = req-%tky %state_area = 'VALIDATE_COST' )
TO reported-equipmentrequest.
IF req-EstimatedCost <= 0.
APPEND VALUE #( %tky = req-%tky ) TO failed-equipmentrequest.
APPEND VALUE #( %tky = req-%tky
%state_area = 'VALIDATE_COST'
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Estimated cost must be greater than zero' )
%element-EstimatedCost = if_abap_behv=>mk-on )
TO reported-equipmentrequest.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
Le rôle de chaque méthode :
get_instance_features– Approve et Reject ne sont actifs que si le statut est Ouvert.approve/reject– mettent à jour le statut avec EML (MODIFY ENTITIES ... IN LOCAL MODE) et renvoient l'enregistrement modifié, pour un rafraîchissement immédiat de l'écran.setinitialvalues– à la création : statut Ouvert et devise par défaut.setrequestid– à l'enregistrement : attribue le numéro suivant. MAX + 1 suffit pour une démo ; en production, utilisez un objet de tranche de numéros.validatecost– refuse un coût nul ou négatif et rattache le message au champEstimatedCost.
IN LOCAL MODEignore les contrôles d'autorisation et de feature control. C'est pourquoi nous pouvons modifierStatus, pourtant en lecture seule pour l'utilisateur.
5. Couche de projection
La projection définit ce qu'un service donné peut voir. provider contract transactional_query est obligatoire pour les projections transactionnelles.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Equipment Request - Projection'
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZC_EquipmentRequest
provider contract transactional_query
as projection on ZR_EquipmentRequest
{
key RequestUUID,
@Search.defaultSearchElement: true
RequestID,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
Description,
EquipmentType,
EstimatedCost,
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_CurrencyStdVH', element: 'Currency' } }]
CurrencyCode,
Status,
RequestedBy,
CreatedAt,
LocalLastChangedAt,
LastChangedAt
}
Le behavior de projection réutilise simplement ce que propose l'objet de base :
projection;
strict ( 2 );
use draft;
define behavior for ZC_EquipmentRequest alias EquipmentRequest
use etag
{
use create;
use update;
use delete;
use action approve;
use action reject;
use action Activate;
use action Discard;
use action Edit;
use action Resume;
use action Prepare;
}
6. Annotations UI dans une metadata extension
Gardez les annotations UI hors de la vue CDS et placez-les dans une metadata extension. La vue reste propre et la couche UI peut évoluer sans toucher au modèle de données.
@Metadata.layer: #CUSTOMER
@UI.headerInfo: { typeName: 'Equipment Request',
typeNamePlural: 'Equipment Requests',
title: { type: #STANDARD, value: 'Description' },
description: { value: 'RequestID' } }
annotate entity ZC_EquipmentRequest with
{
@UI.facet: [{ id: 'General', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
label: 'General Information', position: 10 }]
@UI.hidden: true
RequestUUID;
@UI: { lineItem: [{ position: 10 }], identification: [{ position: 10 }],
selectionField: [{ position: 10 }] }
RequestID;
@UI: { lineItem: [{ position: 20 }], identification: [{ position: 20 }] }
Description;
@UI: { lineItem: [{ position: 30 }], identification: [{ position: 30 }],
selectionField: [{ position: 20 }] }
EquipmentType;
@UI: { lineItem: [{ position: 40 }], identification: [{ position: 40 }] }
EstimatedCost;
@UI: { lineItem: [{ position: 50 },
{ type: #FOR_ACTION, dataAction: 'approve', label: 'Approve' },
{ type: #FOR_ACTION, dataAction: 'reject', label: 'Reject' }],
identification: [{ position: 50 },
{ type: #FOR_ACTION, dataAction: 'approve', label: 'Approve' },
{ type: #FOR_ACTION, dataAction: 'reject', label: 'Reject' }],
selectionField: [{ position: 30 }] }
Status;
@UI: { lineItem: [{ position: 60 }], identification: [{ position: 60 }] }
RequestedBy;
}
7. Service definition et binding
@EndUserText.label: 'Equipment Request - UI Service'
define service ZUI_EQUIPMENTREQUEST {
expose ZC_EquipmentRequest as EquipmentRequest;
}
Créez ensuite un service binding ZUI_EQUIPMENTREQUEST_O4 de type OData V4 – UI, activez-le et cliquez sur Publish. Sélectionnez l'entité EquipmentRequest puis Preview : l'application Fiori Elements s'ouvre dans le navigateur.
8. À vous de tester
- Cliquez sur Create. Un brouillon est créé et le Status vaut déjà Ouvert.
- Laissez le coût à 0 et cliquez de nouveau sur Create → le message de validation s'affiche sur le champ.
- Saisissez un coût et enregistrez. La demande reçoit son numéro (
RequestID). - Sélectionnez la ligne et cliquez sur Approve. Le statut change et les deux boutons se désactivent.
Pour aller plus loin
- Remplacer MAX + 1 par un objet de tranche de numéros.
- Ajouter un contrôle d'accès (DCL) et une vraie vérification d'autorisation.
- Déplacer les textes dans une classe de messages au lieu de
new_message_with_text. - Écrire des tests ABAP Unit avec EML et les test doubles RAP.
- Lancer l'ATC avec la variante cloud readiness avant le transport.
Le code source complet se trouve dans les blocs ci-dessus : copiez-les dans l'ordre et activez. Des questions ou des idées ? Je serai ravi d'en discuter.
Commentaires