İçeriğe geç

RAP Proje Örneği: Başlık ve Kalemleriyle Bir Masraf Formu Uygulaması

RAP rehberinde kavramları tek bir varlık üzerinden anlattım. Gerçek uygulamalar ise nadiren tek tablodan oluşur: başlık ve kalemler vardır, toplam kalemlerden hesaplanır, ekranın düzeni de en az kurallar kadar önemlidir. Bu yazıda küçük ama eksiksiz bir projeyi, bir masraf formu uygulamasını baştan sona kuruyorum.

Mustafa Önder Mustafa Önder  ·  4 Ekim 2026  ·  25 dk okuma

İçindekiler

  1. Proje Kapsamı: Ne Yapıyoruz?
  2. Nesne Listesi ve İsimlendirme
  3. Adım 1: İki Tablo ve Bir Composition
  4. Adım 2: Başlık ve Kalem İçin Behavior Definition
  5. Adım 3: Kalemden Başlığa — Para Birimi ve Toplam
  6. Adım 4: Onaya Gönder, Onayla, Reddet
  7. Adım 5: Projeksiyon ve Servis
  8. Adım 6: Metadata Extension ile Ekran
  9. Adım 7: Kalemlerin Okuma Yetkisi
  10. ADT'de Oluşturma Sırası
  11. Test Senaryoları
  12. Bilerek Kapsam Dışı Bıraktıklarım
  13. Başlık–Kalem Yapısında Sık Hatalar
  14. Sonuç

SAP RAP rehberinde draft destekli bir izin talebi üzerinden RAP'in katmanlarını, behavior definition'ı, determination, validation ve action kavramlarını, EML'i ve unmanaged save'i anlattım. Bu yazı onun devamı: aynı kavramları tekrar açıklamak yerine, gerçek bir projede ilk gün karşınıza çıkan sorulara odaklanıyorum. Kalemler başlığa nasıl bağlanır? Toplam nerede hesaplanır? Kullanıcı bir kalemi değiştirdiğinde ekran nasıl güncellenir? Kalemlerin yetkisi kimden gelir?

Bu bir örnek proje. Gerçek bir müşteri uygulaması değil; senaryo, RAP'in başlık–kalem yapısındaki kararlarını göstermek için seçildi. Kod blokları okunabilirlik için kısaltıldı. Örnek, ABAP Cloud dil sürümüyle güncel bir S/4HANA ya da SAP BTP ABAP Environment sistemi varsayılarak yazıldı. Side effects gibi bazı BDEF özellikleri eski sürümlerde bulunmaz; kendi sürümünüzde kontrol edin.

Proje Kapsamı: Ne Yapıyoruz?

Senaryo herkesin tanıdığı bir süreç: çalışan bir masraf formu açar, harcamalarını kalem kalem girer ve formu onaya gönderir. Yönetici formu onaylar ya da gerekçe yazarak reddeder. Uygulamadan beklenenler:

  • Başlık: çalışan, açıklama, para birimi, toplam tutar, durum ve ret gerekçesi.
  • Kalemler: harcama tarihi, masraf türü, tutar ve not. Bir formda tek para birimi olur; kalemler başlığın para birimini kullanır.
  • Toplam: kullanıcı yazmaz, kalemlerden hesaplanır ve kalem değiştikçe ekranda güncellenir.
  • Durum akışı: Açık → Onaya gönderildi → Onaylandı / Reddedildi. Reddedilen form düzeltilip yeniden gönderilebilir.
  • Kurallar: kalem tutarı sıfırdan büyük olmalı, kalemi olmayan form onaya gönderilemez, ret gerekçesi zorunlu.
  • Draft: kullanıcı formu yarım bırakıp sonra devam edebilmeli.

Nesne Listesi ve İsimlendirme

Bir RAP projesine nesne listesiyle başlamak, hem tahmin yapmayı hem de ekip içinde iş bölmeyi kolaylaştırır. Bu proje için hepsi tek bir pakette (ZEXPENSE) duran nesneler şunlar:

Masraf formu RAP projesindeki nesneler, türleri ve görevleri
NesneTürGörevi
ZEXP_REP, ZEXP_ITEMVeritabanı tablosuBaşlık ve kalem verisi
ZEXP_REP_D, ZEXP_ITEM_DDraft tablosuKaydedilmemiş değişiklikler (ADT üretir)
ZR_ExpenseReport, ZR_ExpenseItemCDS view entityİş nesnesinin veri modeli (interface katmanı)
ZR_ExpenseReportBehavior definitionDavranış sözleşmesi; iki varlık tek BDEF'te
ZBP_R_EXPENSEREPORTABAP sınıfı (behavior pool)Determination, validation ve action kodu
ZD_RejectReasonAbstract entityRet aksiyonunun parametresi
ZC_ExpenseReport, ZC_ExpenseItemProjection view + projection BDEFFiori uygulamasına açılan dilim
ZC_ExpenseReport, ZC_ExpenseItemMetadata extensionListe, nesne sayfası ve düğmelerin düzeni
ZUI_EXPENSE, ZUI_EXPENSE_O4Service definition / bindingOData V4 - UI servisi
ZR_EXPENSEREPORT, ZR_EXPENSEITEMAccess control (DCL)Okuma yetkisi
ZEXPENSEMesaj sınıfıDoğrulama ve aksiyon mesajları

Ön ekler SAP'nin kendi geliştirmelerinde kullandığı alışkanlığı izliyor: R_ iş nesnesinin yeniden kullanılabilir katmanı, C_ belirli bir uygulama için projeksiyon. Ekibinizin yerleşik bir isimlendirme kuralı varsa onu uygulayın; önemli olan, nesne adından katmanın anlaşılabilmesidir.

Adım 1: İki Tablo ve Bir Composition

Başlık tablosu, rehberdeki gibi RAP'in yönetici alanlarını içerir. Yeni olan, para birimine bağlı tutar alanıdır: @Semantics.amount.currencyCode tutarın hangi para birimi alanına göre yorumlanacağını söyler.

@EndUserText.label : 'Masraf formu başlığı'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zexp_rep {
  key client            : abap.clnt not null;
  key report_uuid       : sysuuid_x16 not null;
  employee_id           : abap.char(8);
  description           : abap.char(80);
  @Semantics.amount.currencyCode : 'zexp_rep.currency_code'
  total_amount          : abap.curr(15,2);
  currency_code         : abap.cuky;
  status                : abap.char(1);
  reject_reason         : abap.char(100);
  local_created_by      : abp_creation_user;
  local_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;
}

Kalem tablosu kendi UUID anahtarını ve başlığa işaret eden report_uuid alanını taşır. Alt varlıkta oluşturan/değiştiren bilgisine gerek yok; eşzamanlılık kontrolü için local_last_changed_at yeterli:

@EndUserText.label : 'Masraf formu kalemi'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zexp_item {
  key client            : abap.clnt not null;
  key item_uuid         : sysuuid_x16 not null;
  report_uuid           : sysuuid_x16;
  expense_date          : abap.dats;
  expense_type          : abap.char(2);
  @Semantics.amount.currencyCode : 'zexp_item.currency_code'
  amount                : abap.curr(15,2);
  currency_code         : abap.cuky;
  note                  : abap.char(80);
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
}

İlişki CDS katmanında kurulur. Kök varlık kalemleri composition ile tanımlar; kalem de kendi ebeveynini association to parent ile bilir. Bu iki satır RAP'e "kalem başlığın parçasıdır" der: kalem başlıksız var olamaz, başlık silinince kalemleri de silinir, kilit ve draft başlıktan yönetilir.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Masraf formu'
define root view entity ZR_ExpenseReport
  as select from zexp_rep
  composition [0..*] of ZR_ExpenseItem as _Item
{
  key report_uuid           as ReportUuid,
      employee_id           as EmployeeId,
      description           as Description,
      @Semantics.amount.currencyCode: 'CurrencyCode'
      total_amount          as TotalAmount,
      currency_code         as CurrencyCode,
      status                as Status,
      reject_reason         as RejectReason,
      @Semantics.user.createdBy: true
      local_created_by      as LocalCreatedBy,
      @Semantics.systemDateTime.createdAt: true
      local_created_at      as LocalCreatedAt,
      @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,

      _Item
}
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Masraf formu kalemi'
define view entity ZR_ExpenseItem
  as select from zexp_item
  association to parent ZR_ExpenseReport as _Report
    on $projection.ReportUuid = _Report.ReportUuid
{
  key item_uuid             as ItemUuid,
      report_uuid           as ReportUuid,
      expense_date          as ExpenseDate,
      expense_type          as ExpenseType,
      @Semantics.amount.currencyCode: 'CurrencyCode'
      amount                as Amount,
      currency_code         as CurrencyCode,
      note                  as Note,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at as LocalLastChangedAt,

      _Report
}

Kalemin anahtarı yalnızca kendi UUID'si. ReportUuid anahtar değil, sıradan bir alan; değerini kalem başlık üzerinden oluşturulurken RAP kendisi doldurur. Bu yüzden kalem kodunda başlığı bulmak için ReportUuid'ye güvenebiliriz.

Adım 2: Başlık ve Kalem İçin Behavior Definition

İki varlık tek bir behavior definition içinde tanımlanır. Rehberdeki tek varlıklı örnekle karşılaştırıldığında yeni olan satırları aşağıda tek tek açıklıyorum:

managed implementation in class zbp_r_expensereport unique;
strict ( 2 );
with draft;

define behavior for ZR_ExpenseReport alias ExpenseReport
persistent table zexp_rep
draft table zexp_rep_d
lock master
total etag LastChangedAt
authorization master ( global, instance )
etag master LocalLastChangedAt
{
  create;
  update;
  delete;
  association _Item { create; with draft; }

  field ( numbering : managed, readonly ) ReportUuid;
  field ( readonly ) TotalAmount, Status, RejectReason,
                     LocalCreatedBy, LocalCreatedAt,
                     LocalLastChangedBy, LocalLastChangedAt, LastChangedAt;
  field ( mandatory ) EmployeeId, Description, CurrencyCode;

  determination setInitialStatus on modify { create; }
  determination syncItemCurrency on modify { field CurrencyCode; }

  action ( features : instance ) submit result [1] $self;
  action ( features : instance ) approve result [1] $self;
  action ( features : instance ) reject parameter ZD_RejectReason result [1] $self;
  internal action recalcTotal;

  side effects
  {
    field CurrencyCode affects entity _Item;
  }

  draft action Edit;
  draft action Activate optimized;
  draft action Discard;
  draft action Resume;
  draft determine action Prepare
  {
    validation ExpenseItem~validateAmount;
  }

  mapping for zexp_rep
  {
    ReportUuid         = report_uuid;
    EmployeeId         = employee_id;
    Description        = description;
    TotalAmount        = total_amount;
    CurrencyCode       = currency_code;
    Status             = status;
    RejectReason       = reject_reason;
    LocalCreatedBy     = local_created_by;
    LocalCreatedAt     = local_created_at;
    LocalLastChangedBy = local_last_changed_by;
    LocalLastChangedAt = local_last_changed_at;
    LastChangedAt      = last_changed_at;
  }
}

define behavior for ZR_ExpenseItem alias ExpenseItem
persistent table zexp_item
draft table zexp_item_d
lock dependent by _Report
authorization dependent by _Report
etag master LocalLastChangedAt
{
  update;
  delete;
  association _Report { with draft; }

  field ( numbering : managed, readonly ) ItemUuid;
  field ( readonly ) ReportUuid, CurrencyCode, LocalLastChangedAt;
  field ( mandatory ) ExpenseDate, ExpenseType, Amount;

  determination setCurrency on modify { create; }
  determination recalcReportTotal on modify { create; field Amount; }
  validation validateAmount on save { create; field Amount; }

  side effects
  {
    field Amount affects field _Report.TotalAmount;
  }

  mapping for zexp_item
  {
    ItemUuid           = item_uuid;
    ReportUuid         = report_uuid;
    ExpenseDate        = expense_date;
    ExpenseType        = expense_type;
    Amount             = amount;
    CurrencyCode       = currency_code;
    Note               = note;
    LocalLastChangedAt = local_last_changed_at;
  }
}
  • association _Item { create; with draft; }: Kalem yalnızca başlık üzerinden oluşturulur; bu yüzden kalem varlığında tek başına create yok. with draft, başlık draft'tayken kalemin de draft olarak oluşturulmasını sağlar. Kalem tarafındaki association _Report { with draft; } da aynı şeyi ters yönde söyler.
  • lock dependent by _Report, authorization dependent by _Report: Kalemin kendi kilidi ve yetki kontrolü yoktur; ikisi de başlıktan gelir. Kullanıcı bir kalemi değiştirdiğinde kilitlenen şey bütün formdur.
  • authorization master ( global, instance ): Rehberde yalnız instance kullanmıştım. global, henüz kaydı olmayan işlemleri (form oluşturmak gibi) kontrol eder; instance ise belirli bir formdaki değişiklik ve aksiyonları. Oluşturma yetkisini ayrıca kısıtlamak istiyorsanız ikisi birlikte gerekir.
  • internal action recalcTotal: Dışarıya açılmayan, yalnızca nesnenin kendi kodundan çağrılabilen bir aksiyon. Toplam hesaplamasını tek bir yerde tutmak için kullanıyorum.
  • parameter ZD_RejectReason: Ret aksiyonu bir parametre alır. Fiori Elements bu parametre için düğmeye basıldığında otomatik bir giriş penceresi açar.
  • side effects: Ekrana "şu alan değişince şunu yeniden oku" der. Kalemde tutar değişince başlıktaki toplam, başlıkta para birimi değişince kalem tablosu yenilenir. Bu olmadan değer arka planda doğru hesaplanır, ama kullanıcı ekranı yenileyene kadar eski rakamı görür.
  • validation ExpenseItem~validateAmount: Alt varlığın doğrulaması, başlığın Prepare aksiyonuna varlık adı önekiyle eklenir. Böylece kalem hataları da draft aşamasında görünür.

Kalemin para birimi salt okunurdur: kullanıcı yazmaz, başlıktan kopyalanır. Bu bilinçli bir sadeleştirme; yabancı para birimli fişleri desteklemek kur çevrimi gerektirir ve bu örneğin kapsamında değil.

Ret parametresi: abstract entity

Aksiyon parametreleri veritabanında karşılığı olmayan bir abstract entity ile tanımlanır:

@EndUserText.label: 'Ret gerekçesi'
define abstract entity ZD_RejectReason
{
  @EndUserText.label: 'Gerekçe'
  RejectReason : abap.char(100);
}

Adım 3: Kalemden Başlığa — Para Birimi ve Toplam

Behavior pool iki handler sınıfı içerir: biri başlık, biri kalem için. ADT, BDEF'teki her determination, validation ve action için metot iskeletini hızlı düzeltmeyle üretir:

CLASS lhc_expensereport DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    CONSTANTS:
      BEGIN OF status,
        open      TYPE c LENGTH 1 VALUE 'O',
        submitted TYPE c LENGTH 1 VALUE 'S',
        approved  TYPE c LENGTH 1 VALUE 'A',
        rejected  TYPE c LENGTH 1 VALUE 'R',
      END OF status.

    METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION
      IMPORTING REQUEST requested_authorizations FOR ExpenseReport RESULT result.
    METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
      IMPORTING keys REQUEST requested_authorizations FOR ExpenseReport RESULT result.
    METHODS get_instance_features FOR INSTANCE FEATURES
      IMPORTING keys REQUEST requested_features FOR ExpenseReport RESULT result.
    METHODS set_initial_status FOR DETERMINE ON MODIFY
      IMPORTING keys FOR ExpenseReport~setInitialStatus.
    METHODS sync_item_currency FOR DETERMINE ON MODIFY
      IMPORTING keys FOR ExpenseReport~syncItemCurrency.
    METHODS submit FOR MODIFY
      IMPORTING keys FOR ACTION ExpenseReport~submit RESULT result.
    METHODS approve FOR MODIFY
      IMPORTING keys FOR ACTION ExpenseReport~approve RESULT result.
    METHODS reject FOR MODIFY
      IMPORTING keys FOR ACTION ExpenseReport~reject RESULT result.
    METHODS recalc_total FOR MODIFY
      IMPORTING keys FOR ACTION ExpenseReport~recalcTotal.
ENDCLASS.

CLASS lhc_expenseitem DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS set_currency FOR DETERMINE ON MODIFY
      IMPORTING keys FOR ExpenseItem~setCurrency.
    METHODS recalc_report_total FOR DETERMINE ON MODIFY
      IMPORTING keys FOR ExpenseItem~recalcReportTotal.
    METHODS validate_amount FOR VALIDATE ON SAVE
      IMPORTING keys FOR ExpenseItem~validateAmount.
ENDCLASS.

set_initial_status ve approve rehberdeki kalıbın aynısı olduğu için burada tekrar etmiyorum. Yetki metotlarında da yeni bir şey yok: kendi yetki nesnenizle AUTHORITY-CHECK yapıp sonucu result'a yazarsınız.

Kalemin para birimi başlıktan gelir

Yeni bir kalem oluşturulduğunda başlığın para birimi kaleme kopyalanır. Başlıkta para birimi sonradan değişirse, ikinci determination bütün kalemleri günceller. İkisi de aynı fikre dayanır: kalemin ReportUuid'si ve draft durumu, doğru başlığı bulmak için yeterlidir.

METHOD set_currency.
  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem
      FIELDS ( ReportUuid ) WITH CORRESPONDING #( keys )
    RESULT DATA(items).

  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem BY \_Report
      FIELDS ( CurrencyCode ) WITH CORRESPONDING #( keys )
    RESULT DATA(reports).

  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem
      UPDATE FIELDS ( CurrencyCode )
      WITH VALUE #( FOR item IN items
                    ( %tky         = item-%tky
                      CurrencyCode = reports[ %is_draft  = item-%is_draft
                                              ReportUuid = item-ReportUuid ]-CurrencyCode ) ).
ENDMETHOD.

" lhc_expensereport içinde
METHOD sync_item_currency.
  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      FIELDS ( CurrencyCode ) WITH CORRESPONDING #( keys )
    RESULT DATA(reports).

  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport BY \_Item
      FIELDS ( ReportUuid ) WITH CORRESPONDING #( keys )
    RESULT DATA(items).

  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem
      UPDATE FIELDS ( CurrencyCode )
      WITH VALUE #( FOR item IN items
                    ( %tky         = item-%tky
                      CurrencyCode = reports[ %is_draft  = item-%is_draft
                                              ReportUuid = item-ReportUuid ]-CurrencyCode ) ).
ENDMETHOD.

Karşılaştırmaya %is_draft'ı katmak önemlidir: aynı formun aynı anda bir aktif, bir de draft hâli bulunabilir ve ikisinin ReportUuid'si aynıdır.

Toplam tek bir yerde hesaplanır

Toplamın hesaplandığı yer recalcTotal iç aksiyonudur. Kalem tarafındaki determination yalnızca "ebeveynimin toplamını yeniden hesapla" der; hesabın kendisini bilmez:

" lhc_expenseitem içinde
METHOD recalc_report_total.
  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem BY \_Report
      FIELDS ( ReportUuid ) WITH CORRESPONDING #( keys )
    RESULT DATA(reports).

  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      EXECUTE recalcTotal FROM CORRESPONDING #( reports ).
ENDMETHOD.

" lhc_expensereport içinde
METHOD recalc_total.
  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      FIELDS ( TotalAmount ) WITH CORRESPONDING #( keys )
    RESULT DATA(reports).

  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport BY \_Item
      FIELDS ( ReportUuid Amount ) WITH CORRESPONDING #( keys )
    RESULT DATA(items).

  LOOP AT reports ASSIGNING FIELD-SYMBOL(<report>).
    CLEAR <report>-TotalAmount.
    LOOP AT items INTO DATA(item)
         WHERE ReportUuid = <report>-ReportUuid
           AND %is_draft  = <report>-%is_draft.
      <report>-TotalAmount += item-Amount.
    ENDLOOP.
  ENDLOOP.

  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      UPDATE FIELDS ( TotalAmount ) WITH CORRESPONDING #( reports ).
ENDMETHOD.

Bu ayrımın faydası, toplamı yeniden hesaplatması gereken başka bir yer çıktığında görülür: aşağıdaki onaya gönderme aksiyonu da aynı iç aksiyonu çağırır. Hesap kuralı değişirse (örneğin bazı masraf türleri toplama dahil edilmeyecekse) tek bir metodu değiştirirsiniz.

Kalem silindiğinde ne olur? recalcReportTotal'ın tetikleyicileri kalem oluşturma ve tutar değişikliğidir; silme bu listede yok. Draft'ta bir kalem silindiğinde ekrandaki ve kaydedilen toplam, başka bir kalem değişene kadar eski kalabilir. Bu örnekte onaya giden rakamın her zaman doğru olmasını, submit aksiyonunda toplamı bir kez daha hesaplatarak garanti ediyorum. Silme anında da güncellenmesini istiyorsanız, kullandığınız sürümün determination tetikleyicilerinde alt varlık için delete desteğini kontrol edin.

Kalem doğrulaması ve %path

Validation kalıbı rehberdekiyle aynı; tek fark alt varlıklarda mesajın %path ile ebeveyne bağlanmasıdır. Fiori Elements, kalem tablosundaki hatalı satırı bu yol bilgisiyle bulur:

METHOD validate_amount.
  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem
      FIELDS ( Amount ) WITH CORRESPONDING #( keys )
    RESULT DATA(items).

  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseItem BY \_Report
      FROM CORRESPONDING #( items )
    LINK DATA(links).

  LOOP AT items INTO DATA(item).
    APPEND VALUE #( %tky        = item-%tky
                    %state_area = 'VALIDATE_AMOUNT' ) TO reported-expenseitem.

    IF item-Amount <= 0.
      APPEND VALUE #( %tky = item-%tky ) TO failed-expenseitem.
      APPEND VALUE #( %tky            = item-%tky
                      %state_area     = 'VALIDATE_AMOUNT'
                      %msg            = new_message(
                                          id       = 'ZEXPENSE'
                                          number   = '001'
                                          severity = if_abap_behv_message=>severity-error )
                      %path           = VALUE #( expensereport-%tky =
                                          links[ KEY id source-%tky = item-%tky ]-target-%tky )
                      %element-Amount = if_abap_behv=>mk-on ) TO reported-expenseitem.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

Adım 4: Onaya Gönder, Onayla, Reddet

Onaya gönderme: toplamı tazele, boş formu reddet

submit iki iş yapar: toplamı kalemlerden yeniden hesaplatır ve kalemi olmayan formları reddeder. Hata alan formlar failed ve reported'a yazılır, yalnızca geçerli olanların durumu değişir:

METHOD submit.
  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      EXECUTE recalcTotal FROM CORRESPONDING #( keys ).

  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport BY \_Item
      FIELDS ( ReportUuid ) WITH CORRESPONDING #( keys )
    RESULT DATA(items).

  DATA(valid_keys) = keys.
  LOOP AT valid_keys INTO DATA(report_key).
    IF NOT line_exists( items[ ReportUuid = report_key-ReportUuid ] ).
      APPEND VALUE #( %tky = report_key-%tky ) TO failed-expensereport.
      APPEND VALUE #( %tky = report_key-%tky
                      %msg = new_message(
                               id       = 'ZEXPENSE'
                               number   = '002'
                               severity = if_abap_behv_message=>severity-error ) )
             TO reported-expensereport.
      DELETE valid_keys.
    ENDIF.
  ENDLOOP.

  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      UPDATE FIELDS ( Status )
      WITH VALUE #( FOR valid IN valid_keys
                    ( %tky   = valid-%tky
                      Status = status-submitted ) ).

  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      ALL FIELDS WITH CORRESPONDING #( valid_keys )
    RESULT DATA(reports).

  result = VALUE #( FOR report IN reports
                    ( %tky   = report-%tky
                      %param = report ) ).
ENDMETHOD.

Buradaki kontrol için draft durumunu karşılaştırmaya katmadım; çünkü aşağıdaki feature control, aksiyonu yalnızca kaydedilmiş (aktif) formlarda açıyor.

Parametreli ret

Parametre, her anahtar satırında %param altında gelir. Gerekçe boşsa form reddedilmez:

METHOD reject.
  DATA(valid_keys) = keys.
  LOOP AT valid_keys INTO DATA(report_key).
    IF report_key-%param-RejectReason IS INITIAL.
      APPEND VALUE #( %tky = report_key-%tky ) TO failed-expensereport.
      APPEND VALUE #( %tky = report_key-%tky
                      %msg = new_message(
                               id       = 'ZEXPENSE'
                               number   = '003'
                               severity = if_abap_behv_message=>severity-error ) )
             TO reported-expensereport.
      DELETE valid_keys.
    ENDIF.
  ENDLOOP.

  MODIFY ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      UPDATE FIELDS ( Status RejectReason )
      WITH VALUE #( FOR valid IN valid_keys
                    ( %tky         = valid-%tky
                      Status       = status-rejected
                      RejectReason = valid-%param-RejectReason ) ).

  " Sonuç, submit'teki gibi güncel kayıtla doldurulur
ENDMETHOD.

Hangi düğme ne zaman açık?

Durum akışı feature control ile kurulur. Draft'taki bir formda üç aksiyon da kapalıdır; kullanıcı önce kaydetmelidir:

METHOD get_instance_features.
  READ ENTITIES OF zr_expensereport IN LOCAL MODE
    ENTITY ExpenseReport
      FIELDS ( Status ) WITH CORRESPONDING #( keys )
    RESULT DATA(reports).

  result = VALUE #( FOR report IN reports
    LET is_active  = xsdbool( report-%is_draft = if_abap_behv=>mk-off )
        can_submit = COND #( WHEN is_active = abap_true
                               AND ( report-Status = status-open
                                  OR report-Status = status-rejected )
                             THEN if_abap_behv=>fc-o-enabled
                             ELSE if_abap_behv=>fc-o-disabled )
        can_decide = COND #( WHEN is_active = abap_true
                               AND report-Status = status-submitted
                             THEN if_abap_behv=>fc-o-enabled
                             ELSE if_abap_behv=>fc-o-disabled )
    IN ( %tky            = report-%tky
         %action-submit  = can_submit
         %action-approve = can_decide
         %action-reject  = can_decide ) ).
ENDMETHOD.

Mesaj sınıfı ZEXPENSE'te üç mesaj var: 001 "Tutar sıfırdan büyük olmalı", 002 "Kalemi olmayan form onaya gönderilemez", 003 "Ret gerekçesi zorunludur".

Adım 5: Projeksiyon ve Servis

Projeksiyon view'larında composition, redirected to ile yeni katmana yönlendirilir. Bu yapılmazsa servis, projeksiyon yerine alttaki interface varlığına giden bir ilişki görür:

@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'Masraf formu - projeksiyon'
define root view entity ZC_ExpenseReport
  provider contract transactional_query
  as projection on ZR_ExpenseReport
{
  key ReportUuid,
      EmployeeId,
      Description,
      TotalAmount,
      @Consumption.valueHelpDefinition: [{ entity: { name: 'I_CurrencyStdVH', element: 'Currency' } }]
      CurrencyCode,
      Status,
      RejectReason,
      LocalLastChangedAt,
      LastChangedAt,

      _Item : redirected to composition child ZC_ExpenseItem
}
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'Masraf formu kalemi - projeksiyon'
define view entity ZC_ExpenseItem
  as projection on ZR_ExpenseItem
{
  key ItemUuid,
      ReportUuid,
      ExpenseDate,
      ExpenseType,
      Amount,
      CurrencyCode,
      Note,
      LocalLastChangedAt,

      _Report : redirected to parent ZC_ExpenseReport
}

Para birimi alanındaki değer yardımı, SAP'nin yayımladığı standart para birimi view'ını kullanır. Masraf türü için de sabit değerli bir domain ve onun üzerine küçük bir değer yardımı view'ı eklenir; yazıyı kısa tutmak için burada göstermiyorum.

Projection BDEF'te iki yeni satır var: use side effects ve kalem için use association. İç aksiyon recalcTotal burada yer almaz; dışarıya açılmaz:

projection;
strict ( 2 );
use draft;
use side effects;

define behavior for ZC_ExpenseReport alias ExpenseReport
use etag
{
  use create;
  use update;
  use delete;

  use action submit;
  use action approve;
  use action reject;

  use action Edit;
  use action Activate;
  use action Discard;
  use action Resume;
  use action Prepare;

  use association _Item { create; with draft; }
}

define behavior for ZC_ExpenseItem alias ExpenseItem
use etag
{
  use update;
  use delete;

  use association _Report { with draft; }
}
@EndUserText.label: 'Masraf formu servisi'
define service ZUI_EXPENSE {
  expose ZC_ExpenseReport as ExpenseReport;
  expose ZC_ExpenseItem   as ExpenseItem;
}

Service definition'dan OData V4 - UI türünde ZUI_EXPENSE_O4 binding'i oluşturup yayımladığınızda, binding ekranındaki Preview ile çalışan bir List Report / Object Page uygulaması açılır. Ama ekranın kullanılabilir olması için bir adım daha gerekiyor.

Adım 6: Metadata Extension ile Ekran

Annotation'sız bir önizleme sütunsuz bir liste gösterir; hangi sütunların, filtrelerin, bölümlerin ve düğmelerin görüneceğini metadata extension belirler. Ekran tarifini CDS view'ın içine değil ayrı bir nesneye koymak, veri modelini kalabalıklaştırmadan ekranı değiştirebilmeyi sağlar. Fiori Elements'in bu yaklaşımını Fiori Elements mi, Freestyle mı? yazısında ayrıntılı anlattım.

@Metadata.layer: #CORE
@UI.headerInfo: {
  typeName: 'Masraf Formu',
  typeNamePlural: 'Masraf Formları',
  title: { type: #STANDARD, value: 'Description' },
  description: { type: #STANDARD, value: 'EmployeeId' }
}
annotate entity ZC_ExpenseReport with
{
  @UI.facet: [
    { id: 'General', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
      label: 'Genel Bilgiler', position: 10 },
    { id: 'Items', purpose: #STANDARD, type: #LINEITEM_REFERENCE,
      label: 'Kalemler', position: 20, targetElement: '_Item' }
  ]
  @UI.hidden: true
  ReportUuid;

  @UI: { lineItem:       [ { position: 10 } ],
         identification: [ { position: 10 } ],
         selectionField: [ { position: 10 } ] }
  EmployeeId;

  @UI: { lineItem:       [ { position: 20 } ],
         identification: [ { position: 20 } ] }
  Description;

  @UI: { lineItem:       [ { position: 30 } ],
         identification: [ { position: 30 } ] }
  TotalAmount;

  @UI.identification: [ { position: 40 } ]
  CurrencyCode;

  @UI: { lineItem:       [ { position: 40 },
                           { type: #FOR_ACTION, dataAction: 'approve', label: 'Onayla' },
                           { type: #FOR_ACTION, dataAction: 'reject',  label: 'Reddet' } ],
         identification: [ { position: 50 },
                           { type: #FOR_ACTION, dataAction: 'submit',  label: 'Onaya Gönder' },
                           { type: #FOR_ACTION, dataAction: 'approve', label: 'Onayla' },
                           { type: #FOR_ACTION, dataAction: 'reject',  label: 'Reddet' } ],
         selectionField: [ { position: 20 } ] }
  Status;

  @UI.identification: [ { position: 60 } ]
  RejectReason;
}

Bu dosyanın üç önemli parçası var:

  • @UI.facet: Nesne sayfasının bölümleri. İlki identification ile işaretli alanları bir form olarak gösterir; ikincisi #LINEITEM_REFERENCE ve targetElement: '_Item' ile kalem tablosunu ekler. Tablonun sütunları kalem varlığının kendi lineItem annotation'larından gelir.
  • #FOR_ACTION: Aksiyonları düğme olarak yerleştirir. lineItem içindekiler listede (yönetici formu açmadan, listeden seçerek onaylayabilir), identification içindekiler nesne sayfasının başlığında görünür. Düğmenin açık ya da kapalı olmasını annotation değil, feature control belirler.
  • @UI.headerInfo: Nesne sayfasının başlığı ve sayfa türünün adı. Tutar sütununda para biriminin yanında görünmesi için ek bir şey gerekmez; @Semantics.amount.currencyCode interface katmanından gelir.

Kalem tarafı daha kısadır:

@Metadata.layer: #CORE
@UI.headerInfo: {
  typeName: 'Masraf Kalemi',
  typeNamePlural: 'Masraf Kalemleri',
  title: { type: #STANDARD, value: 'ExpenseType' }
}
annotate entity ZC_ExpenseItem with
{
  @UI.facet: [
    { id: 'Item', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
      label: 'Kalem', position: 10 }
  ]
  @UI.hidden: true
  ItemUuid;

  @UI.hidden: true
  ReportUuid;

  @UI: { lineItem:       [ { position: 10 } ],
         identification: [ { position: 10 } ] }
  ExpenseDate;

  @UI: { lineItem:       [ { position: 20 } ],
         identification: [ { position: 20 } ] }
  ExpenseType;

  @UI: { lineItem:       [ { position: 30 } ],
         identification: [ { position: 30 } ] }
  Amount;

  @UI: { lineItem:       [ { position: 40 } ],
         identification: [ { position: 40 } ] }
  Note;
}

Önizlemede artık şunu görürsünüz: çalışan ve duruma göre filtrelenebilen bir form listesi; formu açınca genel bilgiler ve kalem tablosu; kalem eklendikçe kendiliğinden güncellenen toplam; duruma göre açılıp kapanan düğmeler ve "Reddet"e basıldığında gerekçe isteyen bir pencere. Bunların hiçbiri için tek satır JavaScript yazılmadı.

Adım 7: Kalemlerin Okuma Yetkisi

Başlığın okuma yetkisini rehberde pfcg_auth ile anlattım. Başlık–kalem yapısında sık atlanan nokta, kalem varlığının da bir role ihtiyaç duymasıdır. #CHECK ile işaretli bir varlık için hiç rol yoksa, okuma kısıtlanmaz. Servis kalemleri ayrı bir varlık olarak da açtığı için, başlığı göremeyen biri kalemleri doğrudan sorgulayabilir.

Çözüm, kalemin koşullarını yeniden yazmak değil, başlıktan devralmaktır:

@EndUserText.label: 'Masraf kalemi okuma yetkisi'
@MappingRole: true
define role ZR_EXPENSEITEM {
  grant select on ZR_ExpenseItem
    where inheriting conditions from entity ZR_ExpenseReport
      replacing { root with _Report };
}

replacing { root with _Report }, başlık rolündeki koşulların kalemde _Report ilişkisi üzerinden uygulanmasını sağlar. Başlığın kuralı değiştiğinde kalemin kuralı da kendiliğinden değişir. Projeksiyon view'ları için de aynı fikirle, ilgili interface varlığından devralan birer rol yazılır.

ADT'de Oluşturma Sırası

Nesneler birbirine bağımlı olduğu için sıra önemlidir. Benim izlediğim sıra:

  1. Paket ve mesaj sınıfı.
  2. İki tablo; aktive edin.
  3. İki interface view. Composition ve to-parent ilişkisi birbirine işaret ettiği için ikisini birlikte aktive edin.
  4. Abstract entity ZD_RejectReason.
  5. Behavior definition. draft table satırlarındaki uyarıların hızlı düzeltmesi iki draft tablosunu, sınıf adındaki uyarınınki de behavior pool'u üretir. Eksik metotlar yine hızlı düzeltmeyle eklenir.
  6. Behavior pool'daki kod; BDEF ile birlikte aktive edin.
  7. Projeksiyon view'ları, metadata extension'lar ve projection BDEF.
  8. Access control rolleri.
  9. Service definition, OData V4 - UI service binding, yayımlama ve önizleme.

Sürümünüze bağlı olarak ADT'de, bir tablodan başlayıp bu iskeletin büyük kısmını tek adımda üreten bir sihirbaz da bulunur. İlk projede yine de nesneleri elle kurmanızı öneririm: üretilen kodu anlamadan geçmek, ilk hatada ne yapacağınızı bilmemek demektir.

Test Senaryoları

Projeyi teslim etmeden önce önizleme üzerinde elle, en kritik kuralları da ABAP Unit ile doğrularım. Bu proje için asgari liste:

Masraf formu projesi için test senaryoları ve beklenen sonuçlar
SenaryoBeklenen
Yeni form, 100 ve 250 tutarlı iki kalemToplam 350; her kalemden sonra ekranda güncellenir
Başlıkta para birimini değiştirBütün kalemlerin para birimi de değişir
Tutarı 0 olan kalemle kaydetForm kaydedilmez; ilgili kalemin tutar alanı hatalı işaretlenir
Kalemi olmayan formu onaya gönderHata mesajı; durum değişmez
Draft'taki formOnaya Gönder, Onayla ve Reddet kapalı
Onaya gönderilmiş formOnaya Gönder kapalı; Onayla ve Reddet açık
Gerekçe yazmadan reddetHata mesajı; durum değişmez
Kalem sil, kaydet, onaya gönderOnaya giden toplam kalan kalemlerle tutarlı
Yetkisi olmayan kullanıcıyla kalem varlığını doğrudan sorgulaBaşlığı göremediği formların kalemleri de gelmez

Otomatik teste ilk aday validate_amount ve submit'teki boş form kuralıdır; ikisi de saf iş kuralıdır ve sonuçları failed / reported üzerinden doğrulanabilir. Bağımlılıkları test double'larla kırma yaklaşımını ABAP Unit yazısında anlattım.

Bilerek Kapsam Dışı Bıraktıklarım

Bir örnek projeyi küçük tutmak, neyin eksik olduğunu açıkça söylemeyi gerektirir. Gerçek bir masraf uygulamasında şunlar da olurdu:

Okunabilir form numarası

UUID anahtar kullanıcıya gösterilmez. Gerçek projede numara aralığından alınan ayrı bir form numarası eklenir; numaranın boşluksuz olması yasal bir zorunluluksa numaranın kaydetme aşamasında verilmesi (late numbering) gerekir.

Gönderilmiş formu düzenlemeye kapatmak

Bu örnekte onaya gönderilmiş bir form hâlâ düzenlenebilir. Gerçek projede düzenleme (Edit) ve kalem ekleme de feature control ile duruma bağlanır.

Kur çevrimi, belge eki ve gerçek onay akışı

Yabancı para birimli fişler, fiş görüntüsünün eklenmesi, yöneticinin otomatik belirlenmesi, vekâlet ve çok seviyeli onay. Onay akışı büyüdüğünde bunu aksiyonlarla değil, bir iş akışı çözümüyle yönetmek daha doğrudur.

Muhasebeye aktarım ve bildirim

Onaylanan formdan muhasebe belgesi oluşturmak, SAP'nin yayımladığı API'ler üzerinden yapılmalıdır; tablolara doğrudan yazılmaz. Onay anında başka sistemleri haberdar etmek için RAP business event'leri kullanılabilir; bunu SAP entegrasyon senaryoları yazısında anlattım.

Başlık–Kalem Yapısında Sık Hatalar

with draft'ı unutmak

Draft destekli bir nesnede association _Item { create; } yazıp with draft'ı atlamak, kalemlerin draft akışına katılmamasına yol açar. Hata çoğu zaman ilk kalem eklenmeye çalışıldığında ortaya çıkar. Projeksiyondaki use association için de aynı kural geçerlidir.

Kalem doğrulamalarını Prepare'e eklememek

Başlığın Prepare listesine yalnızca başlık doğrulamalarını yazmak sık görülür. ExpenseItem~validateAmount gibi alt varlık doğrulamaları oraya eklenmezse, kalem hataları draft aşamasında görünmez.

Toplamı her yerde ayrı hesaplamak

Toplamı kalem determination'ında, aksiyonda ve bir raporda ayrı ayrı hesaplamak, kural değiştiğinde birinin unutulmasıyla sonuçlanır. Hesap tek bir iç aksiyonda durmalı, diğer yerler onu çağırmalıdır.

Side effects'i projeksiyonda açmamak

BDEF'te side effects tanımlayıp projection BDEF'e use side effects yazmamak, ekranın değişiklikten haberdar olmamasına yol açar. Değer arka planda doğrudur ama kullanıcı onu yenileyene kadar göremez; bu da "toplam yanlış" hata kaydı olarak geri döner.

Kalem varlığına rol yazmamak

Başlık rolü kalemleri korumaz. Kalem için başlıktan devralan bir rol yazılmazsa, servisin açtığı kalem varlığı yetki kısıtlaması olmadan okunabilir.

Draft ve aktif kaydı karıştırmak

Kalemi başlığıyla eşleştirirken yalnızca ReportUuid'ye bakmak, aynı formun draft ve aktif hâllerini karıştırabilir. Eşleştirmeye %is_draft'ı katın ya da anahtarları %tky ile taşıyın.

Sonuç

Tek varlıklı bir RAP örneğinden gerçek bir projeye geçerken öğrenilecek yeni kavram sayısı aslında az: composition, ebeveyne bağlı kilit ve yetki, iç aksiyon, side effects ve metadata extension. Asıl iş, bunları doğru yere koymaktır. Kurallar interface katmanında, hesaplar tek bir yerde, ekran tarifi ayrı bir dosyada, yetki de başlıktan devralınmış olarak durmalı.

Bu iskelet masraf formuna özgü değil: satın alma ön talebi, bakım bildirimi ya da sözleşme onayı gibi başlık–kalem yapısındaki çoğu süreç aynı kalıpla kurulur. Kendi sisteminizde denerken nesneleri ADT'nin ürettiği iskeletle birleştirin ve önce test senaryoları tablosundaki maddeleri elle geçin. Geliştirme tarafında sunduğum kapsamı SAP geliştirme hizmet sayfasında bulabilirsiniz.

RAP ile ilk projenizi kurarken mimari, kod incelemesi veya ekip içi rehberlik desteği alın.

Ön Görüşme Talep Et
← Blog'a Dön

İletişim

Projeleriniz için iletişime geçin.