← Tous les articles

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.

RAPCDSABAP CloudFiori ElementsOData V4

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.

zequip_req.tabl
@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.

zr_equipmentrequest.ddls
@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 :

zr_equipmentrequest.bdef
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 master protège contre les mises à jour perdues ; total etag est obligatoire avec le brouillon.
  • features : instance sur les actions signifie que nous décidons, enregistrement par enregistrement, si les boutons sont actifs.
  • draft determine action Prepare exé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 :

zbp_r_equipmentrequest.global.abap
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 :

zbp_r_equipmentrequest.clas.abap
" 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 champ EstimatedCost.

IN LOCAL MODE ignore les contrôles d'autorisation et de feature control. C'est pourquoi nous pouvons modifier Status, 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.

zc_equipmentrequest.ddls
@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 :

zc_equipmentrequest.bdef
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.

zc_equipmentrequest.ddlx
@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

zui_equipmentrequest.srvd
@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.

Le list report généré par l'aperçu Fiori Elements
Le list report généré par l'aperçu Fiori Elements

8. À vous de tester

  1. Cliquez sur Create. Un brouillon est créé et le Status vaut déjà Ouvert.
  2. Laissez le coût à 0 et cliquez de nouveau sur Create → le message de validation s'affiche sur le champ.
  3. Saisissez un coût et enregistrez. La demande reçoit son numéro (RequestID).
  4. Sélectionnez la ligne et cliquez sur Approve. Le statut change et les deux boutons se désactivent.
L'object page avec les actions Approve et Reject
L'object page avec les actions Approve et Reject

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

Laisser un commentaire