RAP es la respuesta moderna de SAP a la pregunta: cómo escribo un Business Object transaccional en ABAP que se exponga automáticamente como OData v4 y se renderice con Fiori Elements. La respuesta cabe en cinco archivos: una CDS de datos, un Behavior Definition, una implementación local, un Service Definition y un Service Binding. El framework se encarga del resto: locking, draft, validaciones, autorización, transaccionalidad.
Procedural, 2000+ líneas, lock manuales, sin draft, expuesto solo a usuarios SAPGUI. Mantener un nuevo campo era días de trabajo.
200 líneas declarativas. CRUD generado. Draft, lock, validaciones, autorización: todo configuración. Service OData v4 listo en minutos.
Mismo BO compilando en S/4 Cloud sin tocar nada. Cero modificaciones. Cero acoplamiento al estándar. Upgrade transparente.
Ocho capítulos teóricos. Pulsa cualquier tarjeta para desplegar Teoría · Práctica · Ejemplo real en código.
RAP — Restful ABAP Programming Model — es el framework oficial de SAP, a partir de S/4HANA 1909, para construir Business Objects transaccionales ABAP, exponerlos como OData v4 y consumirlos desde Fiori Elements. Sustituye a BOPF (Business Object Processing Framework) y a SADL. Encaja en S/4HANA on-premise (con ABAP for Cloud Development opcional), en S/4HANA Cloud y en Steampunk/Embedded Steampunk.
Managed: RAP genera el código de persistencia sobre una tabla DDIC creada por ti. Ideal para BOs greenfield. Unmanaged: tú escribes el CRUD entero en handlers; ideal para envolver BAPIs o código legacy. Managed with additional save: RAP gestiona la persistencia y tú añades lógica en el saver (típicamente para cross-BO updates).
" 1) Managed — RAP persiste managed implementation in class zbp_i_pr_head unique; " 2) Unmanaged — yo persisto (llamo a BAPI) unmanaged implementation in class zbp_i_gr_head unique; " 3) Managed con saver adicional managed with additional save implementation in class zbp_i_po_head unique;
El modelo de datos de un BO RAP se expresa como un grafo de CDS jerárquico. La raíz se declara con define root view entity; los hijos se enlazan a la raíz con composition [0..*] of (relación de pertenencia) y con association (relación a otros BOs). Cada nivel requiere un orden de keys consistente. Anotaciones obligatorias: @AccessControl.authorizationCheck, @Semantics.systemDateTime.* para los campos administrativos. Opcionales pero recomendadas: @UI.headerInfo, @UI.lineItem, @UI.identification, @Search.searchable.
En el papel: rectángulo "Cabecera" con keys → flecha "composition" → rectángulo "Items" con keys compuestas. Para cada caja, listar campos. Solo entonces se empieza a teclear.
@AccessControl.authorizationCheck: #CHECK define root view entity ZI_Order as select from zorder composition [0..*] of ZI_Order_Item as _Item { key order_id as OrderId, customer as Customer, _Item } define view entity ZI_Order_Item as select from zorder_item association to parent ZI_Order as _Order on $projection.OrderId = _Order.OrderId { key order_id, key item_no, material, _Order }
El .bdef.asbdef es la pieza más importante de un BO RAP: define qué se puede hacer de forma declarativa. Sus secciones esenciales son: tipo de implementación (managed/unmanaged); CRUD (create / update / delete); field control (readonly, mandatory, numbering); validations (reglas que se ejecutan en save); determinations (cálculos en modify u on save); actions (operaciones invocables); y las asociaciones con su semántica.
Crear la BDEF desde ADT con clic derecho en la CDS → New Behavior Definition. ADT genera un esqueleto con el tipo y la entidad. Editar añadiendo CRUD, field control y validations. Activar.
managed implementation in class zbp_i_order unique; strict ( 2 ); define behavior for ZI_Order alias Order persistent table zorder lock master authorization master ( instance ) etag master LastChangedAt { create; update; delete; field ( readonly, numbering:managed ) OrderId; field ( mandatory ) Customer; validation validateCustomer on save { create; update; } determination calcTotal on modify { field _Item; } action ( features:instance ) Confirm result [1] $self; association _Item { create; } }
La Behavior Implementation es la clase ABAP que da vida al BDEF. Se compone de dos tipos de clases locales:
Local Handler Class (lhc): implementa los handlers de validations, determinations y actions. Recibe keys con las instancias afectadas; debe rellenar failed, mapped y reported.
Local Saver Class (lsc): implementa el ciclo de save (cleanup, finalize, save_modified, adjust_numbers) — utilizado cuando hay efectos cruzados con otros BOs o numeración late.
Pulsar F2 en una validation del BDEF abre el quick-fix "Implement validation in class…". ADT genera el método dentro de la lhc class. El cuerpo lee con READ ENTITIES OF … IN LOCAL MODE, comprueba, y rellena los appends a failed y reported.
METHOD validateCustomer. READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order FIELDS ( Customer ) WITH CORRESPONDING #( keys ) RESULT DATA(ls). LOOP AT ls INTO DATA(l). IF l-Customer IS INITIAL. APPEND VALUE #( %tky = l-%tky ) TO failed-order. APPEND VALUE #( %tky = l-%tky %msg = NEW zcm_order( textid = zcm_order=>customer_missing severity = if_abap_behv_message=>severity-error ) ) TO reported-order. ENDIF. ENDLOOP. ENDMETHOD.
Dos archivos minúsculos publican el BO al mundo. El Service Definition (.srvd) selecciona qué CDS forman parte del API (la raíz expuesta y, opcionalmente, las hijas). El Service Binding (.srvb) hace la publicación y elige el sabor: OData v4 UI (con draft, anotaciones UI, para Fiori Elements) o OData v4 Web API (sin draft, para sistemas externos M2M).
Crear el .srvd → New ▸ Service Definition. Crear el .srvb → New ▸ Service Binding tipo OData v4 UI. Activar, Publicar local. Botón derecho en el binding → Open Fiori Elements App Preview.
@EndUserText.label: 'Service de pedidos' define service ZSD_ORDER { expose ZC_Order as Order; expose ZC_Order_Item as OrderItem; }
El binding se configura visualmente en ADT — propiedades: Binding Type = OData v4 UI, Service Name = ZUI_ORDER_O4, Category = Business Service. Publicar local.
Draft permite guardar borradores en una tabla paralela (_d) antes de activar el registro como persistente. Es indispensable en móvil y en formularios largos. Locking evita colisiones: optimista via ETag (compara timestamp/version al guardar; si cambió, falla con error 409) y pesimista con lock SAP clásico (reserva el registro durante la edición). RAP elige el comportamiento según las cláusulas etag y lock del BDEF.
Para habilitar draft en un BO managed: añadir with draft al inicio del BDEF, crear la tabla draft (ZORDER_D con misma estructura + 4 campos administrativos draft), añadir draft table … draft action Edit/Activate/Discard/Resume/Prepare. Para locking optimista: definir etag master LastChangedAt.
managed implementation in class zbp_i_order unique; with draft; define behavior for ZI_Order persistent table zorder draft table zorder_d lock master total etag LocalLastChangedAt etag master LastChangedAt { create; update; delete; draft action Edit; draft action Activate optimized; draft action Discard; draft action Resume; draft determine action Prepare; }
DCL (Data Control Language) define en una vista define role las reglas de visibilidad por fila. El aspecto pfcg_auth mapea un objeto de autorización SAP clásico a campos de la CDS. Para autorizaciones por instancia (ej: "puedo aprobar éste pero no aquél"), el BDEF declara authorization master ( instance ) y la implementación expone un handler FOR INSTANCE AUTHORIZATION que decide por cada combinación key + acción.
Crear archivo .dcls.asdcls al lado de la CDS. Definir define role. Activar. La DCL se aplica automáticamente cuando se consulta la CDS desde RAP/Fiori. Verificar el efecto cambiando los datos del usuario (SU01) y reabriendo la app.
@MappingRole: true define role ZI_PO_Auth { grant select on ZI_PO_Head where ( CompanyCode ) = aspect pfcg_auth( M_BEST_BSA, BUKRS, ACTVT = '03' ) and ( PurchasingOrg ) = aspect pfcg_auth( M_BEST_EKO, EKORG, ACTVT = '03' ); }
Composition: la relación cabecera-items dentro del mismo BO, declarada en CDS como composition; transaccional (un commit atómico). Eventos: notificaciones publicadas a SAP Event Mesh cuando ocurre algo significativo; consumibles desde BTP, AWS, Azure, otros SAP. Numbering: early (cliente asigna), late (server asigna al save), managed (RAP asigna).
Declarar eventos en el BDEF con event. Implementar la publicación en el handler que corresponda (saver finalize o action). Mapear el evento en Communication Arrangement → Event Mesh queue. Consumir desde una iflow CPI con adaptador AMQP.
define behavior for ZI_PO_Head { create; update; delete; action SendToSupplier result [1] $self; // declarar el evento event POSent parameter ZS_POSentParam; }
" en el handler SendToSupplier: RAISE ENTITY EVENT ZI_PO_Head~POSent FROM VALUE #( ( %key = ls_po-%key %param = VALUE #( PoId = ls_po-PoId Supplier = ls_po-Supplier ) ) ).
Independientemente de la complejidad, todo Business Object RAP se compone exactamente de estas cinco piezas. Si entiendes éstas, entiendes RAP.
Estructura de datos: cabecera, items, asociaciones, composición.
.ddls.asddlsDefine la forma de los datos del BO y el grafo de relaciones. Es el "esqueleto" sobre el que se monta todo lo demás.
define root view entity.composition [0..*] of y association to parent.@AccessControl.authorizationCheck: #CHECK.@Semantics.systemDateTime.*.provider contract transactional_query.@AccessControl.authorizationCheck: #CHECK define root view entity ZI_Order as select from zorder composition [0..*] of ZI_Order_Item as _Item { key order_id, customer, _Item }
Qué se puede hacer: CRUD, actions, validations, draft.
.bdef.asbdefDeclara las reglas: qué operaciones se permiten, qué campos son obligatorios, qué validaciones, qué actions, si hay draft, cómo se hace lock.
managed implementation in class …define behavior for ZI_Xxx con persistent table, lock, authorization, etag.create / update / delete.readonly / mandatory / numbering.validation / determination / action.managed implementation in class zbp_i_order unique; strict ( 2 ); define behavior for ZI_Order alias Order persistent table zorder lock master etag master LastChangedAt { create; update; delete; field ( mandatory ) Customer; association _Item { create; } }
Lógica ABAP de validations, determinations, actions.
CLASS lhc_*Es el código. Implementa todo lo que el BDEF declara: cada validation, cada determination, cada action.
keys y rellena failed, mapped, reported.READ ENTITIES OF … IN LOCAL MODE.MODIFY ENTITIES OF … IN LOCAL MODE.CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS validateCustomer FOR VALIDATE ON SAVE IMPORTING keys FOR Order~validateCustomer. ENDCLASS. CLASS lhc_order IMPLEMENTATION. METHOD validateCustomer. " lectura, chequeo, append failed/reported ENDMETHOD. ENDCLASS.
Selección de qué CDS se exponen como API.
.srvd.srvdSelecciona qué entidades CDS forman parte del API. Es la "lista de invitados". Sin él, las CDS no son visibles desde fuera, aunque el BDEF las soporte.
@EndUserText.label: 'Service de pedidos' define service ZSD_ORDER { expose ZC_Order as Order; expose ZC_Order_Item as OrderItem; }
Publica el servicio: OData v4 UI o Web API.
.srvb.srvbPublica el service definition como un endpoint OData real, accesible por URL. Aquí eliges el sabor.
Clic derecho → Open Fiori Elements App Preview abre la app generada por SAP en runtime. Cero JavaScript escrito a mano.
// El .srvb se edita visualmente en ADT // Propiedades clave: Service Name = ZUI_ORDER_O4 Binding Type = OData V4 UI Category = Business Service Version = 0001 // Resultado en runtime: URL = /sap/opu/odata4/sap/zui_order_o4/srvd/sap/zsd_order/0001/ // Endpoint accesible desde: - Fiori Elements Preview - Postman / curl - BAS Apps - SAP Build Apps
Procure-to-Pay (P2P) es el ciclo más representativo del módulo MM/FI-AP: alguien necesita comprar, se aprueba, se pide, se recibe, se paga la factura y se ejecuta el pago al proveedor. En el estándar SAP esto se hace con transacciones distintas (ME51N, ME21N, MIGO, MIRO, F110). Vamos a reconstruir las cinco etapas como cinco Business Objects RAP encadenados por _PrecedingDocument.
_PrecedingDocument. En un proyecto real, lo correcto sería consumir las APIs released de SAP (API_PURCHASEREQ_PROCESS_SRV, API_PURCHASEORDER_PROCESS_SRV, API_MATERIAL_DOCUMENT_SRV, API_SUPPLIERINVOICE_PROCESS_SRV, API_PAYMENTREQUEST_SRV). El didáctico no contradice el correcto; demuestra la mecánica RAP completa de extremo a extremo.
ZPR_RELEASED a Event Mesh cuando se ejecuta ZRelease con éxito. Lo escucha el iflow de notificación al comprador.En Eclipse → File ▸ New ▸ ABAP Package. Nombre ZPR_PURCHASEREQ, software component ZLOCAL, application component MM-PUR, structure package no. Asigna transport request.
Botón derecho sobre el paquete → New ▸ Database Table. Crea ZPR_HEAD y ZPR_ITEM con los campos del SSD. Campos administrativos obligatorios: client, created_by, created_at, last_changed_by, last_changed_at, local_last_changed_at.
Nuevo Data Definition ZI_PR_Head y ZI_PR_Item (interface, root y child con composition). Después ZC_PR_Head y ZC_PR_Item (consumption) con anotaciones @UI completas.
Botón derecho sobre ZI_PR_Head → New Behavior Definition. Tipo Managed. Editar el .bdef tal como se ve en la pestaña BDEF.
En la BDEF, F2 sobre validation y determination → ADT genera el esqueleto de la clase ZBP_I_PR_HEAD. Implementar handlers en la clase local lhc_PrHead.
New ▸ Service Definition ZSD_PR. New ▸ Service Binding ZUI_PR_O4 tipo OData v4 UI. Activate, Publish.
Botón derecho sobre el Service Binding → Open Fiori Elements App Preview. SAP genera la app Fiori Elements en runtime. Verificar List Report + Object Page con secciones, sin haber escrito ni una línea de JavaScript.
Para producción: usar SAP Fiori Tools en BAS para empaquetar la app como BSP, generar manifest.json, registrar tile en catalog Z, asignar a grupo, y a rol PFCG.
Liberar el transport request desde SE10 (on-prem) o vía gCTS si está configurado. Importar en QAS, ejecutar ATC y ABAP Unit, importar en PRD.
Estructura jerárquica: una raíz (head) con composición hacia los items. Las anotaciones @UI controlarán el comportamiento de Fiori Elements en runtime.
@AbapCatalog.sqlViewName: 'ZIPRHEAD' @AccessControl.authorizationCheck: #CHECK @EndUserText.label: 'PR head — interface' @VDM.viewType: #BASIC @ObjectModel.semanticKey: ['ReqId'] define root view entity ZI_PR_Head as select from zpr_head composition [0..*] of ZI_PR_Item as _Item { key req_id as ReqId, requester as Requester, plant as Plant, status as Status, total_estimated as TotalEstimated, currency as Currency, // admin @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, _Item }
@UI.headerInfo: { typeName: 'Solicitud', typeNamePlural: 'Solicitudes', title: { value: 'ReqId' }, description: { value: 'Requester' } } @UI.presentationVariant: [{ sortOrder: [{ by: 'ReqId', direction: #DESC }] }] @Search.searchable: true define root view entity ZC_PR_Head provider contract transactional_query as projection on ZI_PR_Head { @UI.selectionField: [{ position: 10 }] @UI.lineItem: [{ position: 10, label: 'Solicitud' }] @UI.identification: [{ position: 10 }] key ReqId, @UI.lineItem: [{ position: 20, label: 'Solicitante' }] @UI.identification: [{ position: 20 }] Requester, @UI.lineItem: [{ position: 30, label: 'Centro' }] @UI.selectionField: [{ position: 20 }] Plant, @UI.lineItem: [{ position: 40, label: 'Estado', criticality: 'StatusCriticality' }] Status, @UI.lineItem: [{ position: 50, label: 'Total estim.' }] @Semantics.amount.currencyCode: 'Currency' TotalEstimated, Currency, @UI.facet: [{ id: 'Items', type: #LINEITEM_REFERENCE, label: 'Posiciones', targetElement: '_Item' }] _Item : redirected to composition child ZC_PR_Item }
Declara qué se puede hacer con el BO: CRUD, draft, field control, validations, determinations, actions. Cero lógica imperativa aquí.
managed implementation in class zbp_i_pr_head unique; strict ( 2 ); with draft; define behavior for ZI_PR_Head alias PrHead persistent table zpr_head draft table zpr_head_d lock master total etag LocalLastChangedAt authorization master ( instance ) etag master LastChangedAt { create; update; delete; field ( readonly, numbering : managed ) ReqId; field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt, LocalLastChangedAt, TotalEstimated, Status; field ( mandatory ) Requester, Plant, Currency; // reglas de negocio validation validateHeader on save { create; update; } determination calculateTotal on modify { field _Item; } determination setStatusDraft on save { create; } // acción action ( features:instance, authorization:instance ) Release result [1] $self; draft action Edit; draft action Activate optimized; draft action Discard; draft action Resume; draft determine action Prepare; // composición association _Item { create ( features:instance ); with draft; } } define behavior for ZI_PR_Item alias PrItem persistent table zpr_item draft table zpr_item_d lock dependent by _Head authorization dependent by _Head { update; delete; field ( readonly, numbering:managed ) ReqId, ItemNo; field ( mandatory ) Material, Qty, Unit, UnitPrice, DeliveryDate, CostCenter; validation validateItem on save { create; update; field Qty, DeliveryDate, UnitPrice; } association _Head { with draft; } }
Aquí va la lógica ABAP de validations, determinations y actions. Cada handler responde a una entidad concreta. RAP entrega los registros afectados; el handler los lee, valida, y devuelve failed, mapped y reported.
CLASS lhc_prhead DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS: validateHeader FOR VALIDATE ON SAVE IMPORTING keys FOR PrHead~validateHeader, calculateTotal FOR DETERMINE ON MODIFY IMPORTING keys FOR PrHead~calculateTotal, setStatusDraft FOR DETERMINE ON SAVE IMPORTING keys FOR PrHead~setStatusDraft, Release FOR MODIFY IMPORTING keys FOR ACTION PrHead~Release RESULT result. ENDCLASS. CLASS lhc_prhead IMPLEMENTATION. METHOD validateHeader. READ ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead FIELDS ( Requester Plant Currency ) WITH CORRESPONDING #( keys ) RESULT DATA(heads). LOOP AT heads INTO DATA(ls_head). IF ls_head-Requester IS INITIAL OR ls_head-Plant IS INITIAL OR ls_head-Currency IS INITIAL. APPEND VALUE #( %tky = ls_head-%tky ) TO failed-prhead. APPEND VALUE #( %tky = ls_head-%tky %msg = NEW zcm_pr( textid = zcm_pr=>mandatory_missing severity = if_abap_behv_message=>severity-error ) ) TO reported-prhead. ENDIF. ENDLOOP. ENDMETHOD. METHOD calculateTotal. READ ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead BY \_Item FIELDS ( Qty UnitPrice ) WITH CORRESPONDING #( keys ) RESULT DATA(items) LINK DATA(links). DATA updates TYPE TABLE FOR UPDATE zi_pr_head. LOOP AT links INTO DATA(lk) GROUP BY lk-source-%tky. DATA(total) = REDUCE p( INIT s = 0 FOR ls IN items WHERE ( ReqId = lk-source-ReqId ) NEXT s = s + ls-Qty * ls-UnitPrice ). APPEND VALUE #( %tky = lk-source-%tky TotalEstimated = total %control-TotalEstimated = if_abap_behv=>mk-on ) TO updates. ENDLOOP. MODIFY ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead UPDATE FIELDS ( TotalEstimated ) WITH updates. ENDMETHOD. METHOD setStatusDraft. MODIFY ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead UPDATE FIELDS ( Status ) WITH VALUE #( FOR k IN keys ( %tky = k-%tky Status = 'DRAFT' %control-Status = if_abap_behv=>mk-on ) ). ENDMETHOD. METHOD Release. MODIFY ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead UPDATE FIELDS ( Status ) WITH VALUE #( FOR k IN keys ( %tky = k-%tky Status = 'RELEASED' %control-Status = if_abap_behv=>mk-on ) ). READ ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(refreshed). result = VALUE #( FOR ls IN refreshed ( %tky = ls-%tky %param = ls ) ). ENDMETHOD. ENDCLASS.
Simula la app Fiori Elements que generaría RAP para este BO. Toca los botones, edita campos, guarda, libera. El estado se mantiene en memoria del navegador; sirve como demo sin necesidad de un sistema SAP.
| Solicitud | Solicitante | Centro | Items | Estado | Total estim. |
|---|---|---|---|---|---|
| 10000123 | M. Álvarez | 1000 | 2 | Released | 1 240,00 EUR |
| 10000122 | J. Pérez | 2000 | 1 | Draft | 340,00 EUR |
| 10000121 | L. Gómez | 1000 | 3 | Released | 2 875,50 EUR |
_PrecedingPR → ZI_PR_Head (debe estar en estado RELEASED).ZPO_RELEASED, ZPO_SENT a Event Mesh.ZPO_HEAD (po_id, supplier, currency, payment_terms, status, ref_pr_id) y ZPO_ITEM (po_id, item_no, material, qty, unit, net_price, delivery_date).
En ZI_PO_Head añadir association [0..1] to ZI_PR_Head as _PrecedingPR on $projection.RefPrId = _PrecedingPR.ReqId.
Necesario porque en el saver hay que actualizar el status de la PR origen (interferencia con otro BO). Implementar finalize en la saver class.
Llama internamente a la API IDoc estándar (MASTER_IDOC_DISTRIBUTE en on-prem) o a una iflow CPI (en cloud). Marca el PO como SENT y publica el evento.
Lee el TotalEstimated de la PR via association _PrecedingPR; calcula el delta; si supera el umbral, reporta error.
Verificar que la Object Page muestra la facet "PR original" con navegación de vuelta a la fase 1.
@AccessControl.authorizationCheck: #CHECK @EndUserText.label: 'PO head — interface' define root view entity ZI_PO_Head as select from zpo_head composition [0..*] of ZI_PO_Item as _Item association [0..1] to ZI_PR_Head as _PrecedingPR on $projection.RefPrId = _PrecedingPR.ReqId association [0..1] to I_Supplier as _Supplier on $projection.Supplier = _Supplier.Supplier { key po_id as PoId, supplier as Supplier, currency as Currency, payment_terms as PaymentTerms, status as Status, ref_pr_id as RefPrId, total_net as TotalNet, @Semantics.systemDateTime.createdAt: true created_at as CreatedAt, @Semantics.systemDateTime.lastChangedAt: true last_changed_at as LastChangedAt, _Item, _PrecedingPR, _Supplier }
managed with additional save implementation in class zbp_i_po_head unique; strict ( 2 ); define behavior for ZI_PO_Head alias PoHead persistent table zpo_head lock master etag master LastChangedAt { create; update; delete; field ( readonly, numbering:managed ) PoId; field ( readonly ) Status, TotalNet, CreatedAt, LastChangedAt; field ( mandatory ) Supplier, Currency, PaymentTerms; validation validateBudget on save { create; update; } determination setStatusOpen on save { create; } determination calcTotal on modify { field _Item; } action ( features:instance ) SendToSupplier result [1] $self; action ( features:instance ) CancelOrder result [1] $self; association _Item { create; } }
CLASS lsc_po_head_saver DEFINITION INHERITING FROM cl_abap_behavior_saver. PROTECTED SECTION. METHODS finalize REDEFINITION. METHODS adjust_numbers REDEFINITION. ENDCLASS. CLASS lsc_po_head_saver IMPLEMENTATION. METHOD finalize. " 1) leer todos los PO recién creados/modificados que tengan ref_pr_id READ ENTITIES OF zi_po_head IN LOCAL MODE ENTITY PoHead ALL INSTANCES FIELDS ( RefPrId Status ) RESULT DATA(po_all). " 2) para cada PR origen, marcar status='CONVERTED' LOOP AT po_all INTO DATA(ls_po) WHERE RefPrId IS NOT INITIAL. MODIFY ENTITIES OF zi_pr_head IN LOCAL MODE ENTITY PrHead UPDATE FIELDS ( Status ) WITH VALUE #( ( %tky = VALUE #( ReqId = ls_po-RefPrId ) Status = 'CONVERTED' %control-Status = if_abap_behv=>mk-on ) ). ENDLOOP. ENDMETHOD. METHOD adjust_numbers. " Asignar PO_ID definitivos cuando la numeración es late. " En este caso usamos managed numbering, así que aquí no hace falta nada. RETURN. ENDMETHOD. ENDCLASS.
METHOD SendToSupplier. " 1) cambiar status a SENT MODIFY ENTITIES OF zi_po_head IN LOCAL MODE ENTITY PoHead UPDATE FIELDS ( Status ) WITH VALUE #( FOR k IN keys ( %tky = k-%tky Status = 'SENT' %control-Status = if_abap_behv=>mk-on ) ). " 2) publicar evento READ ENTITIES OF zi_po_head IN LOCAL MODE ENTITY PoHead ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(refreshed). LOOP AT refreshed INTO DATA(ls_po). cl_zpo_event_pub=>publish_sent( iv_po_id = ls_po-PoId iv_supplier = ls_po-Supplier ). ENDLOOP. result = VALUE #( FOR ls IN refreshed ( %tky = ls-%tky %param = ls ) ). ENDMETHOD.
Simulación de la app Fiori Elements para el BO Pedido. Selecciona la PR fuente, ajusta proveedor y precios, y observa la cadena de status hasta el envío de IDoc al proveedor.
| # | Material | Cant. | UM | Precio | Entrega | Importe |
|---|---|---|---|---|---|---|
| 10 | MAT-001 · Tornillo M6 | 10 | UN | 35,50 | 15.06.2026 | 355,00 |
| 20 | MAT-002 · Engranaje 30T | 5 | UN | 125,00 | 20.06.2026 | 625,00 |
| 30 | MAT-009 · Caja embalaje | 50 | UN | 148,90 | 20.06.2026 | 7 445,00 |
_PrecedingPO → ZI_PO_Head (status SENT).ZGR_POSTED publicado al confirmar.Para BO sobre BAPIs estándar. La persistencia la hace SAP, no tú. En el saver: llamar a BAPI_GOODSMVT_CREATE en finalize(), tras commit lógico.
On-prem: SELECT FROM matdoc (S/4) o mkpf+mseg (ECC). Cloud: usar I_MaterialDocument released VDM.
a) Validar items contra PO. b) Construir tabla GOODSMVT_ITEM. c) BAPI_GOODSMVT_CREATE con GM_CODE='01'. d) BAPI_TRANSACTION_COMMIT WAIT='X'. e) Retornar mat_doc en RESULT.
SE37 con datos manuales primero. Después, ejecutar desde Fiori Preview. Si falla, revisar logs en SLG1.
@AccessControl.authorizationCheck: '#CHECK' @EndUserText.label: 'GR head over MATDOC' define root view entity ZI_GR_Head as select from matdoc association [0..1] to ZI_PO_Head as _PrecedingPO on $projection.RefPoId = _PrecedingPO.PoId { key mblnr as MaterialDocument, key mjahr as DocumentYear, bldat as DocumentDate, ebeln as RefPoId, werks as Plant, _PrecedingPO }
unmanaged implementation in class zbp_i_gr_head unique; strict ( 2 ); define behavior for ZI_GR_Head alias GrHead lock master { create; action ( features:instance ) PostGR result [1] $self; field ( mandatory ) RefPoId, Plant; field ( readonly, numbering:managed ) MaterialDocument, DocumentYear; validation validateAgainstPO on save { create; } }
METHOD PostGR. DATA: ls_header TYPE bapi2017_gm_head_01, ls_code TYPE bapi2017_gm_code, lt_items TYPE TABLE OF bapi2017_gm_item_create, lt_return TYPE TABLE OF bapiret2, ls_ret TYPE bapi2017_gm_head_ret. ls_code = VALUE #( gm_code = '01' ). ls_header = VALUE #( pstng_date = sy-datum doc_date = sy-datum ). CALL FUNCTION 'BAPI_GOODSMVT_CREATE' EXPORTING goodsmvt_header = ls_header goodsmvt_code = ls_code IMPORTING goodsmvt_headret = ls_ret TABLES goodsmvt_item = lt_items return = lt_return. IF line_exists( lt_return[ type = 'E' ] ). CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'. ELSE. CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'. ENDIF. ENDMETHOD.
Ajusta cantidades recibidas, contabiliza y observa el movimiento 101.
| # | Material | Pedido | Recibido | UM | Almacén | OK |
|---|---|---|---|---|---|---|
| 10 | MAT-001 · Tornillo M6 | 10 | UN | 0001 - Materiales | OK | |
| 20 | MAT-002 · Engranaje 30T | 5 | UN | 0001 - Materiales | OK | |
| 30 | MAT-009 · Caja embalaje | 50 | UN | 0002 - Embalajes | Parcial |
_PrecedingPO y _PrecedingGR.ZINV_POSTED publicado al contabilizar.Calcular en CDS los tres totales (sumar EKPO, MSEG, RSEG) y exponerlos como campos virtuales con anotación @UI.dataPoint.
Determination de validación en save: si delta > tolerancia, status='HOLD', si no, llamar BAPI_INCOMINGINVOICE_CREATE.
El BAPI genera el documento FI con BELNR.
Publicar evento ZINV_POSTED a Event Mesh.
@AccessControl.authorizationCheck: '#CHECK' define root view entity ZI_Invoice_Head as select from zinv_head association [0..1] to ZI_PO_Head as _PrecedingPO on $projection.RefPoId = _PrecedingPO.PoId { key invoice_id as InvoiceId, supplier_invoice as SupplierInvoice, ref_po_id as RefPoId, invoice_amount as InvoiceAmount, currency as Currency, status as Status, @UI.dataPoint: { title: 'PO total', criticality: 'MatchCriticality' } _PrecedingPO.TotalNet as PoTotal, _PrecedingPO }
unmanaged implementation in class zbp_i_invoice_head unique; strict ( 2 ); define behavior for ZI_Invoice_Head alias Invoice lock master { create; field ( mandatory ) Supplier, SupplierInvoice, InvoiceAmount, Currency; field ( readonly ) Status, FiDocNo; action ( features:instance ) PostInvoice result [1] $self; action ( features:instance ) Release result [1] $self; validation threeWayMatch on save { create; } }
METHOD threeWayMatch. READ ENTITIES OF zi_invoice_head IN LOCAL MODE ENTITY Invoice ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(invs). LOOP AT invs INTO DATA(ls_inv). SELECT SUM( gr~entry_qnt * po~net_price ) FROM mseg AS gr JOIN ekpo AS po ON gr~ebeln = po~ebeln AND gr~ebelp = po~ebelp WHERE gr~mblnr = @ls_inv-RefGrId AND gr~bwart = '101' INTO @DATA(expected). DATA(delta) = abs( ls_inv-InvoiceAmount - expected ). IF delta > expected * 0.01. MODIFY ENTITIES OF zi_invoice_head IN LOCAL MODE ENTITY Invoice UPDATE FIELDS ( Status ) WITH VALUE #( ( %tky = ls_inv-%tky Status = 'HOLD' %control-Status = if_abap_behv=>mk-on ) ). ENDIF. ENDLOOP. ENDMETHOD.
Modifica el importe factura y observa cómo el semáforo se vuelve rojo si supera la tolerancia del 1%.
_PrecedingInvoices [0..*] → ZI_Invoice_Head (status = POSTED, fecha vencimiento ≤ fecha pago).ZPAY_PROPOSED, ZPAY_EXECUTED, ZPAY_FAILED.ZPAY_HEAD (run_id, run_date, payment_date, company_code, status, created_by, approved_by) y ZPAY_ITEM (run_id, item_no, supplier, invoice_id, fi_doc, amount, discount, net_amount, house_bank, payment_method, included).
ZI_PaymentRun_Head con composition a ZI_PaymentRun_Item. En el item, association to ZI_Invoice_Head as _Invoice. En cabecera, association to I_CompanyCode.
Tres actions: Propose (precarga facturas elegibles), Approve (4-eyes), Execute (contabiliza FI + genera SEPA).
Validation impide ejecutar si created_by = approved_by o approved_by IS INITIAL.
Recorre items incluidos: BAPI_ACC_DOCUMENT_POST con asiento (Acreedor | Banco). Tras éxito, agrupa por banco y genera fichero SEPA XML via cl_dmee_facade.
Sociedad de pago (T042B), Variante de pago (T042Z), Formato DMEE (árbol DMEE2 para SEPA), Banco origen (T012/T012K) con IBAN.
Crear propuesta 25.05.2026 sociedad 1000, ver facturas precargadas, excluir alguna, aprobar (otro usuario), ejecutar. Verificar fichero en AL11.
@AccessControl.authorizationCheck: '#CHECK' @UI.headerInfo: { typeName: 'Propuesta pago', typeNamePlural: 'Propuestas' } define root view entity ZI_PaymentRun_Head provider contract transactional_query as select from zpay_head composition [0..*] of ZI_PaymentRun_Item as _Item association [0..1] to I_CompanyCode as _CompanyCode on $projection.CompanyCode = _CompanyCode.CompanyCode { key run_id as RunId, run_date as RunDate, payment_date as PaymentDate, company_code as CompanyCode, status as Status, @Semantics.amount.currencyCode: 'Currency' total_amount as TotalAmount, currency as Currency, created_by as CreatedBy, approved_by as ApprovedBy, _Item, _CompanyCode }
define view entity ZI_PaymentRun_Item as select from zpay_item association to parent ZI_PaymentRun_Head as _Head on $projection.RunId = _Head.RunId association [1..1] to ZI_Invoice_Head as _Invoice on $projection.InvoiceId = _Invoice.InvoiceId { key run_id, key item_no, supplier, invoice_id, fi_doc, amount, discount, net_amount, house_bank, payment_method, included, _Head, _Invoice }
managed implementation in class zbp_i_paymentrun_head unique; strict ( 2 ); define behavior for ZI_PaymentRun_Head alias PayRun persistent table zpay_head lock master authorization master ( instance ) etag master LastChangedAt { create; update; delete; field ( readonly, numbering:managed ) RunId; field ( readonly ) Status, TotalAmount, CreatedBy, ApprovedBy; field ( mandatory ) CompanyCode, PaymentDate, Currency; validation validateRunDate on save { create; update; } validation validateFourEyes on save { update; field ApprovedBy; } determination setInitialStatus on save { create; } determination recalcTotal on modify { field _Item; } action ( features:instance ) Propose parameter ZS_ProposeParam result [1] $self; action ( features:instance ) Approve result [1] $self; action ( features:instance ) Execute result [1] $self; action ( features:instance ) Cancel result [1] $self; event PaymentProposed parameter ZS_PaymentEventParam; event PaymentExecuted parameter ZS_PaymentEventParam; association _Item { create; } }
METHOD Propose. READ ENTITIES OF zi_paymentrun_head IN LOCAL MODE ENTITY PayRun ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(runs). LOOP AT runs INTO DATA(ls_run). " seleccionar facturas elegibles SELECT i~invoice_id, i~supplier, i~invoice_amount, i~currency, i~invoice_date, l~payment_method, l~house_bank, l~cash_disc_days, l~cash_disc_pct FROM zinv_head AS i JOIN lfb1 AS l ON i~supplier = l~lifnr AND l~bukrs = @ls_run-CompanyCode WHERE i~status = 'POSTED' AND i~due_date <= @ls_run-PaymentDate AND l~zahls = ' ' ORDER BY i~supplier INTO TABLE @DATA(eligible). DATA items_to_create TYPE TABLE FOR CREATE zi_paymentrun_head\_Item. DATA n TYPE i VALUE 0. LOOP AT eligible INTO DATA(ls_e). n = n + 10. DATA(lv_disc) = COND p( WHEN ls_e-invoice_date + ls_e-cash_disc_days >= ls_run-PaymentDate THEN ls_e-invoice_amount * ls_e-cash_disc_pct / 100 ELSE 0 ). APPEND VALUE #( %tky = ls_run-%tky %target = VALUE #( ( %cid = |ITEM_{ n }| ItemNo = n Supplier = ls_e-supplier InvoiceId = ls_e-invoice_id Amount = ls_e-invoice_amount Discount = lv_disc NetAmount = ls_e-invoice_amount - lv_disc HouseBank = ls_e-house_bank PaymentMethod = ls_e-payment_method Included = abap_true ) ) ) TO items_to_create. ENDLOOP. MODIFY ENTITIES OF zi_paymentrun_head IN LOCAL MODE ENTITY PayRun CREATE BY \_Item FIELDS ( ItemNo Supplier InvoiceId Amount Discount NetAmount HouseBank PaymentMethod Included ) WITH items_to_create. MODIFY ENTITIES OF zi_paymentrun_head IN LOCAL MODE ENTITY PayRun UPDATE FIELDS ( Status ) WITH VALUE #( ( %tky = ls_run-%tky Status = 'PROPOSED' %control-Status = if_abap_behv=>mk-on ) ). RAISE ENTITY EVENT zi_paymentrun_head~PaymentProposed FROM VALUE #( ( %key = ls_run-%key %param = VALUE #( RunId = ls_run-RunId ItemCount = lines( eligible ) ) ) ). ENDLOOP. ENDMETHOD.
METHOD Execute. READ ENTITIES OF zi_paymentrun_head IN LOCAL MODE ENTITY PayRun BY \_Item ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(items). DATA sepa_payments TYPE cl_dmee_facade=>tt_payments. LOOP AT items INTO DATA(ls_it) WHERE Included = abap_true. DATA doc_header TYPE bapiache09. DATA doc_accountap TYPE TABLE OF bapiacap09. DATA doc_accountgl TYPE TABLE OF bapiacgl09. DATA doc_currency TYPE TABLE OF bapiaccr09. DATA ret TYPE TABLE OF bapiret2. DATA belnr TYPE belnr_d. doc_header = VALUE #( username = sy-uname comp_code = ls_it-_Head-CompanyCode doc_date = sy-datum pstng_date = sy-datum doc_type = 'KZ' ). APPEND VALUE #( itemno_acc = 1 vendor_no = ls_it-Supplier gl_account = '1611000000' ) TO doc_accountap. APPEND VALUE #( itemno_acc = 2 gl_account = '1131000000' hbkid = ls_it-HouseBank ) TO doc_accountgl. APPEND VALUE #( itemno_acc = 1 amt_doccur = -ls_it-NetAmount currency = ls_it-_Head-Currency ) TO doc_currency. APPEND VALUE #( itemno_acc = 2 amt_doccur = ls_it-NetAmount currency = ls_it-_Head-Currency ) TO doc_currency. CALL FUNCTION 'BAPI_ACC_DOCUMENT_POST' EXPORTING documentheader = doc_header IMPORTING obj_key = belnr TABLES accountgl = doc_accountgl accountpayable = doc_accountap currencyamount = doc_currency return = ret. IF line_exists( ret[ type = 'E' ] ). CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'. ELSE. CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'. APPEND VALUE #( supplier = ls_it-Supplier amount = ls_it-NetAmount currency = ls_it-_Head-Currency iban = cl_zpay_helper=>get_iban( ls_it-Supplier ) reference = |INV { ls_it-InvoiceId }| ) TO sepa_payments. ENDIF. ENDLOOP. " Generación del fichero SEPA XML pain.001 IF sepa_payments IS NOT INITIAL. cl_dmee_facade=>create_sepa_file( it_payments = sepa_payments iv_format = 'SEPA_CT' iv_path = |/usr/sap/Z/pay/sepa_{ sy-datum }.xml| ). ENDIF. MODIFY ENTITIES OF zi_paymentrun_head IN LOCAL MODE ENTITY PayRun UPDATE FIELDS ( Status ) WITH VALUE #( FOR k IN keys ( %tky = k-%tky Status = 'EXECUTED' %control-Status = if_abap_behv=>mk-on ) ). RAISE ENTITY EVENT zi_paymentrun_head~PaymentExecuted FROM VALUE #( FOR k IN keys ( %key = k-%key %param = VALUE #( RunId = k-RunId ItemCount = lines( sepa_payments ) ) ) ). ENDMETHOD.
Simula "Manage Payment Proposals". Pulsa Proponer para precargar facturas vencidas, marca cuáles incluir, simula la aprobación 4-ojos, ejecuta el pago. Observa la generación del fichero SEPA y la contabilización en FI.
| Proveedor | Factura | Doc FI | Vencimiento | Importe | Dto. P.P. | Neto | |
|---|---|---|---|---|---|---|---|
| Pulsa Proponer para precargar las facturas vencidas con fecha ≤ 25.05.2026 | |||||||
Todo BO RAP es CDS + BDEF + Implementación + Service Def + Binding. Si lo asimilas, las 5 fases de este curso son variaciones de la misma melodía.
Managed: greenfield (PR, PO, PaymentRun). Unmanaged: te apoyas en BAPIs existentes (GR, Invoice). Elegir bien evita reinventar la rueda.
F1: managed + draft + action. F2: managed with additional save + cross-BO. F3: unmanaged + BAPI legacy. F4: validation con dataPoints UI. F5: action compleja + 4-eyes + integración batch + Event Mesh.
Cada BO publica eventos: ZPR_RELEASED, ZPO_SENT, ZGR_POSTED, ZINV_POSTED, ZPAY_EXECUTED. Es el cemento que une el ciclo P2P con CPI, BTP y Tesorería.
Las APIs released de SAP son la opción correcta en S/4 Cloud. Este curso enseña a construir el modelo equivalente para entender el motor.
① Side effects y determine actions. ② Numbering early. ③ Eventos a Event Mesh real. ④ Joule for Developers para generar la mitad del código.
Tomar las 5 fases y, en grupo de 2-3 personas, construir el ciclo completo en un sistema BTP Trial ABAP Cloud: tablas, CDS, BDEF, implementaciones, services, bindings y previews. Crear datos demo y mostrar el flujo punta a punta desde Solicitud hasta Pago SEPA. Entregable: vídeo de 5 minutos demostrando los Fiori Elements generados sin haber escrito una línea de JavaScript.