İçindekiler
- Proje Kapsamı: Ne Yapıyoruz?
- Nesne Listesi ve İsimlendirme
- Adım 1: İki Tablo ve Bir Composition
- Adım 2: Başlık ve Kalem İçin Behavior Definition
- Adım 3: Kalemden Başlığa — Para Birimi ve Toplam
- Adım 4: Onaya Gönder, Onayla, Reddet
- Adım 5: Projeksiyon ve Servis
- Adım 6: Metadata Extension ile Ekran
- Adım 7: Kalemlerin Okuma Yetkisi
- ADT'de Oluşturma Sırası
- Test Senaryoları
- Bilerek Kapsam Dışı Bıraktıklarım
- Başlık–Kalem Yapısında Sık Hatalar
- 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?
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:
| Nesne | Tür | Görevi |
|---|---|---|
ZEXP_REP, ZEXP_ITEM | Veritabanı tablosu | Başlık ve kalem verisi |
ZEXP_REP_D, ZEXP_ITEM_D | Draft tablosu | Kaydedilmemiş değişiklikler (ADT üretir) |
ZR_ExpenseReport, ZR_ExpenseItem | CDS view entity | İş nesnesinin veri modeli (interface katmanı) |
ZR_ExpenseReport | Behavior definition | Davranış sözleşmesi; iki varlık tek BDEF'te |
ZBP_R_EXPENSEREPORT | ABAP sınıfı (behavior pool) | Determination, validation ve action kodu |
ZD_RejectReason | Abstract entity | Ret aksiyonunun parametresi |
ZC_ExpenseReport, ZC_ExpenseItem | Projection view + projection BDEF | Fiori uygulamasına açılan dilim |
ZC_ExpenseReport, ZC_ExpenseItem | Metadata extension | Liste, nesne sayfası ve düğmelerin düzeni |
ZUI_EXPENSE, ZUI_EXPENSE_O4 | Service definition / binding | OData V4 - UI servisi |
ZR_EXPENSEREPORT, ZR_EXPENSEITEM | Access control (DCL) | Okuma yetkisi |
ZEXPENSE | Mesaj 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şınacreateyok.with draft, başlık draft'tayken kalemin de draft olarak oluşturulmasını sağlar. Kalem tarafındakiassociation _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ızinstancekullanmıştım.global, henüz kaydı olmayan işlemleri (form oluşturmak gibi) kontrol eder;instanceise 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ığınPrepareaksiyonuna 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.
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. İlkiidentificationile işaretli alanları bir form olarak gösterir; ikincisi#LINEITEM_REFERENCEvetargetElement: '_Item'ile kalem tablosunu ekler. Tablonun sütunları kalem varlığının kendilineItemannotation'larından gelir.#FOR_ACTION: Aksiyonları düğme olarak yerleştirir.lineItemiçindekiler listede (yönetici formu açmadan, listeden seçerek onaylayabilir),identificationiç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.currencyCodeinterface 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:
- Paket ve mesaj sınıfı.
- İki tablo; aktive edin.
- İki interface view. Composition ve to-parent ilişkisi birbirine işaret ettiği için ikisini birlikte aktive edin.
- Abstract entity
ZD_RejectReason. - Behavior definition.
draft tablesatı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. - Behavior pool'daki kod; BDEF ile birlikte aktive edin.
- Projeksiyon view'ları, metadata extension'lar ve projection BDEF.
- Access control rolleri.
- 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:
| Senaryo | Beklenen |
|---|---|
| Yeni form, 100 ve 250 tutarlı iki kalem | Toplam 350; her kalemden sonra ekranda güncellenir |
| Başlıkta para birimini değiştir | Bütün kalemlerin para birimi de değişir |
| Tutarı 0 olan kalemle kaydet | Form kaydedilmez; ilgili kalemin tutar alanı hatalı işaretlenir |
| Kalemi olmayan formu onaya gönder | Hata mesajı; durum değişmez |
| Draft'taki form | Onaya Gönder, Onayla ve Reddet kapalı |
| Onaya gönderilmiş form | Onaya Gönder kapalı; Onayla ve Reddet açık |
| Gerekçe yazmadan reddet | Hata mesajı; durum değişmez |
| Kalem sil, kaydet, onaya gönder | Onaya giden toplam kalan kalemlerle tutarlı |
| Yetkisi olmayan kullanıcıyla kalem varlığını doğrudan sorgula | Baş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