Crear paso a paso un servicio RAP managed con borrador
De una tabla de base de datos a una aplicación Fiori Elements funcional: un objeto de negocio RAP managed completo con borrador, validación, determinaciones y acciones de aprobar/rechazar.
En este artículo construimos una aplicación RAP (ABAP RESTful Application Programming Model) pequeña pero completa: una app de solicitud de equipos donde los empleados piden un portátil o un monitor y un responsable aprueba o rechaza la solicitud.
Al final tendrás un list report + object page de Fiori Elements con borrador (draft), una validación, dos determinaciones y dos acciones. Sin una sola línea de código UI5.
Requisitos previos: S/4HANA 2022 o superior (on-premise o private cloud) o el entorno ABAP de SAP BTP, y ABAP Development Tools (Eclipse o VS Code). Todo aquí usa ABAP for Cloud Development y solo APIs liberadas (released).
Los objetos que vamos a crear
| Objeto | Tipo | Propósito |
|---|---|---|
ZEQUIP_REQ |
Tabla de base de datos | Datos persistentes |
ZR_EquipmentRequest |
CDS root view entity | Objeto de negocio base |
ZR_EquipmentRequest |
Behavior definition | Comportamiento transaccional + borrador |
ZBP_R_EquipmentRequest |
Clase | Implementación del comportamiento |
ZC_EquipmentRequest |
Proyección CDS + behavior | Vista específica del servicio |
ZC_EquipmentRequest |
Metadata extension | Anotaciones de UI |
ZUI_EQUIPMENTREQUEST |
Service definition | Qué se expone |
ZUI_EQUIPMENTREQUEST_O4 |
Service binding | OData V4 – UI |
La convención de nombres es sencilla: ZR_ para el objeto de negocio raíz, ZC_ para proyecciones, ZBP_ para behavior pools y ZUI_ para servicios de UI.
1. Tabla de base de datos
La clave es un UUID (sysuuid_x16), lo que permite que RAP asigne las claves automáticamente (managed numbering). El número legible request_id se rellena aparte al guardar.
Los cinco últimos campos son campos administrativos. Gracias a los elementos de datos abp_* y a las anotaciones @Semantics del siguiente paso, el framework los rellena por ti; además son necesarios para el ETag y el borrador.
@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. Vista CDS raíz
La vista raíz da a los campos nombres legibles en CamelCase y marca los campos administrativos con anotaciones @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
Es el corazón del objeto de negocio. Clic derecho en la vista raíz → New Behavior Definition y adáptala:
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;
}
}
Algunos puntos a destacar:
strict ( 2 )activa las comprobaciones del modo estricto más reciente; úsalo siempre en desarrollos nuevos.with draft+draft table zequip_req_d: la tabla de borrador aún no existe. Coloca el cursor sobre su nombre y usa el quick fix (Ctrl+1) para generarla.etag masterprotege frente a actualizaciones perdidas;total etages obligatorio con borrador.features : instanceen las acciones significa que decidimos por registro si los botones están activos.draft determine action Prepareejecuta la validación mientras el usuario aún está en borrador, así los errores aparecen antes de guardar.
4. Implementación del comportamiento
Usa el quick fix sobre el nombre de la clase en la primera línea para crear ZBP_R_EquipmentRequest. La clase global queda vacía:
CLASS zbp_r_equipmentrequest DEFINITION PUBLIC ABSTRACT FINAL
FOR BEHAVIOR OF zr_equipmentrequest.
ENDCLASS.
CLASS zbp_r_equipmentrequest IMPLEMENTATION.
ENDCLASS.
Toda la lógica está en la pestaña Local Types de la clase:
" 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.
Qué hace cada método:
get_instance_features– Approve y Reject solo están activos mientras el estado es Abierto.approve/reject– actualizan el estado con EML (MODIFY ENTITIES ... IN LOCAL MODE) y devuelven el registro modificado para que la pantalla se refresque al instante.setinitialvalues– al crear: estado Abierto y moneda por defecto.setrequestid– al guardar: asigna el siguiente número. MAX + 1 vale para una demo; en producción usa un objeto de rango de números.validatecost– rechaza costes cero o negativos y asocia el mensaje al campoEstimatedCost.
IN LOCAL MODEomite las comprobaciones de autorización y de feature control. Por eso podemos cambiarStatusaunque sea de solo lectura para el usuario.
5. Capa de proyección
La proyección define qué ve un servicio concreto. provider contract transactional_query es obligatorio en proyecciones transaccionales.
@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
}
El behavior de proyección simplemente reutiliza lo que ofrece el objeto 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. Anotaciones de UI en una metadata extension
Deja las anotaciones de UI fuera de la vista CDS y ponlas en una metadata extension. La vista queda limpia y la capa de UI puede cambiar sin tocar el modelo de datos.
@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 y binding
@EndUserText.label: 'Equipment Request - UI Service'
define service ZUI_EQUIPMENTREQUEST {
expose ZC_EquipmentRequest as EquipmentRequest;
}
Ahora crea un service binding ZUI_EQUIPMENTREQUEST_O4 de tipo OData V4 – UI, actívalo y pulsa Publish. Selecciona la entidad EquipmentRequest y haz clic en Preview: la app Fiori Elements se abre en el navegador.
8. Pruébalo
- Haz clic en Create. Se crea un borrador y el Status ya es Abierto.
- Deja el coste en 0 y pulsa Create otra vez → el mensaje de validación aparece en el campo.
- Introduce un coste y guarda. La solicitud recibe su número (
RequestID). - Selecciona la línea y pulsa Approve. El estado cambia y ambos botones se desactivan.
Próximos pasos
- Sustituir MAX + 1 por un objeto de rango de números.
- Añadir un control de acceso (DCL) y una comprobación de autorización real.
- Mover los textos a una clase de mensajes en lugar de
new_message_with_text. - Escribir pruebas ABAP Unit con EML y los test doubles de RAP.
- Ejecutar el ATC con la variante cloud readiness antes del transporte.
El código fuente completo está en los bloques de arriba: cópialos en orden y actívalos. ¿Preguntas o ideas? Estaré encantado de leerlas.
Comentarios