Certificationも取ったことだし、とりあえずList Reportでいい感じのSales Order Listでも作ってみた

# はじめに

この記事はの[SAP Advent Calendar 2023](https://adventar.org/calendars/8870 "SAP Advent Calendar 2023")12月10日分の記事として執筆しています。

ま～ず見てくださ～い。↓

ﾃﾞｰﾝ!!!!!!
＿人人人人人人人人人人人人人人人人人人人人人人人人人人＿
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/95894d1c-57f2-899c-ccae-227e3ab03b23.png)
￣Y^Y^Y^Y^Y^Y^^Y^Y^Y^Y^^Y^Y^Y^Y^^Y^Y^Y^Y^^Y^Y^Y^Y^^Y￣
ﾄﾞﾔｱ!

というわけで、認定資格も取ったことなので、Fiori ElementsのList ReportとObject Pageを利用していい感じのSales Order Listでも作ってみました。
なんてったってCertifyされてるんで、シンプルな見る専アプリくらい余裕っしょw
いわゆるRAPという方法で開発しています。
デプロイまではしていません。
CDS Annotationを使ってEclipseで完結する方法と、EclipseでODataをPublishして以降は、BAS（ないしVS Code）でService ModelerやGuided Developmentを駆使してUIを作り込む方法とがありますが、今回は前者で作ってみました。エディター2つも使うのもめんどくさいので。。

(上記2つのFiori Elements開発手法をどのように使い分ければよいか、については[こちら](https://answers.sap.com/questions/13174977/local-annotations-vs-external-annotations.html)のAnswerがわかりやすいです。基本はCDS Annotationで作り、それができない or 1つのOData Serviceで複数のアプリを作りたい、といった場合にannotation modelerを使うと良いそうです。)

# 前提
* S/4HANAオンプレ版を使えること
* EclipseにABAP ADTをインストールしてあること

# ファイル構成

```
Business Service
|-Service Bindings
    S_SB_SO
|-Service Definition
    Z_SD_SO
  
Core Data Services
|-Data Definitions
    Z_C_SOH
    Z_C_SOI
    Z_C_SOTYPE_VH
    Z_C_CUSTOMER_VH

Metadata Extension
 ZM_ME_SOH
 ZM_ME_SOI
```

## Service Bindings
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/639aa83c-5e7d-a61b-9d74-99527c282d82.png)





## Service Definition
```abap:S_SD_SO
@EndUserText.label: 'Service Definition for SalesOrder'
define service Z_SD_SO {
  expose Z_C_SOH;
  expose Z_C_SOI;
  expose Z_C_CUSTOMER_VH;
  expose Z_C_SOType_VH;
}
```
## Data Definitions
```abap:Z_C_SOH
@AbapCatalog.sqlViewName: 'ZCSOH_231201'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'SalesOrderHeader'
@Metadata.allowExtensions: true
@OData.publish: true
@Search.searchable: true
define view Z_C_SOH as select from I_SalesOrder
inner join tvakt as TVAKT 
  on I_SalesOrder.SalesOrderType = tvakt.auart and tvakt.spras = 'E'
association [0..*] to I_SalesOrderPartner as _Partner_AG
  on $projection.SalesOrder = _Partner_AG.SalesOrder and _Partner_AG.PartnerFunction = 'AG'
association [0..*] to I_SalesOrderPartner as _Partner_WE
  on $projection.SalesOrder = _Partner_WE.SalesOrder and _Partner_WE.PartnerFunction = 'WE'
association [0..*] to Z_C_SOI as _SOI
  on $projection.SalesOrder = _SOI.SalesOrder
association [0..1] to Z_C_CUSTOMER_VH as _Customer on $projection.SoldToParty = _Customer.Customer
{
    @Search.defaultSearchElement: true
    key I_SalesOrder.SalesOrder,
    @Consumption.valueHelpDefinition: [{ entity: { name: 'Z_C_SOType_VH', element: 'SalesOrderType' } }]
    @ObjectModel.text.element: ['SalesOrderTypeDescription']
    I_SalesOrder.SalesOrderType,
    tvakt.bezei as SalesOrderTypeDescription,
    I_SalesOrder.SoldToParty,
    @Consumption.valueHelpDefinition: [{ entity: { name: 'Z_C_CUSTOMER_VH', element: 'Customer' } }]
    @ObjectModel.text.element: ['FullName_AG']
    _Partner_AG.Customer as Customer_AG,
    _Partner_AG.FullName as FullName_AG,
    @Consumption.valueHelpDefinition: [{ entity: { name: 'Z_C_CUSTOMER_VH', element: 'Customer' } }]
    @ObjectModel.text.element: ['FullName_WE']
    _Partner_WE.Customer as Customer_WE,
    _Partner_WE.FullName as FullName_WE,    
    I_SalesOrder.SalesOrderDate,
    _SOI.SalesOrderItem,
    _SOI.Material,
    _SOI.MaterialName,
    _SOI.Plant,
    _SOI.StorageLocation,
    _SOI.SalesOrderItemText,
    /* Associations */
    @ObjectModel.association.type: [#TO_COMPOSITION_CHILD]
    _Partner_AG,
    _Partner_WE,
    _SOI,
    _Customer
}

```

```abap:Z_C_SOI
@AbapCatalog.sqlViewName: 'ZCSOI'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'SalesOrderItem'
@Metadata.allowExtensions: true
@OData.publish: true
@Search.searchable: true
define view Z_C_SOI as select from I_SalesOrderItem
  association [1..1] to Z_C_SOH    as _SOH on $projection.SalesOrder = _SOH.SalesOrder
  association [0..*] to I_MaterialText as _MaterialText on $projection.Material = _MaterialText.Material and _MaterialText.Language = 'E'
{
    @Search.defaultSearchElement: true
    key SalesOrder,
    key SalesOrderItem,
    Material,
    _MaterialText.MaterialName,
    Plant,
    StorageLocation,
    SalesOrderItemText,
    @Semantics.quantity.unitOfMeasure : 'OrderQuantityUnit'
    OrderQuantity,
    OrderQuantityUnit,
    @Semantics.amount.currencyCode : 'TransactionCurrency'
    NetPriceAmount,
    @Semantics.amount.currencyCode : 'TransactionCurrency'
    NetAmount,
    TransactionCurrency,
    /* Associations */
    @ObjectModel.association.type: [#TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT]
    _SOH,
    _MaterialText
}

```

```abap:Z_C_CUSTOMER_VH
@AbapCatalog.sqlViewName: 'ZCCUSTOMERVH'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Customer Value Help'
define view Z_C_CUSTOMER_VH as select from I_Customer
{
    key Customer,
    CustomerName,
    CustomerAccountGroup,
    Country,
    BusinessType
    /* Associations */
}

```

```abap:Z_C_SOType_VH
@AbapCatalog.sqlViewName: 'ZCSOTYPEVH'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'SalesOrderType Value Help'
@Search.searchable: true
@ObjectModel.representativeKey: 'SalesOrderType'
@ObjectModel.resultSet.sizeCategory: #XS //ドロップダウンにする
define view Z_C_SOType_VH as select from tvak as TVAK
inner join tvakt as TVAKT 
  on TVAK.auart = tvakt.auart and tvakt.spras = 'E'
{
    @Search.defaultSearchElement: true
    @ObjectModel.text.element: ['SalesOrderTypeDescription']
    key TVAK.auart as SalesOrderType,
    TVAKT.bezei as SalesOrderTypeDescription
}
where tvakt.bezei is not initial

```


## Metadata Extension
```ABAP:Z_ME_SOH
@Metadata.layer: #CORE
@UI: {
 headerInfo: { 
    typeName: 'SOList', 
    typeNamePlural: 'Sales Orders', 
    title: { type: #STANDARD, value: 'SalesOrder' } ,
    description: { value: 'Customer_AG' }
    } 
 }
annotate view Z_C_SOH
    with 
{
    @UI.facet: [
        { 
            id:              'HeaderFacet',
            type:            #COLLECTION,
            purpose:         #STANDARD,
            label:           'Order Information',
            position: 10
         },
        // Header Info Group
        { 
            id: 'BasicGroup',
            parentId: 'HeaderFacet',
            type: #FIELDGROUP_REFERENCE,
            purpose: #STANDARD,
            label: 'Basic Information',
            targetQualifier: 'BasicFieldGroup'    ,
            position: 10        
        },
        // Customer Group
        { 
            id: 'CustomerGroup',
            type: #FIELDGROUP_REFERENCE,
            parentId: 'HeaderFacet',
            purpose: #STANDARD,
            label: 'Customer Group',
            targetQualifier: 'CustomerFieldGroup',
            position: 20             
        },
        { 
            id: 'ItemFacet',
            purpose: #STANDARD,
            type: #LINEITEM_REFERENCE,
            label: 'Item',
            position: 20,
            targetElement: '_SOI'
         }
    ]
    @UI.selectionField: [{position: 10 }]
    @UI.lineItem: [{position:10 }] 
    @UI.identification: [{ position: 10, label: 'SalesOrder' }]
    SalesOrder;
    @UI: { 
        selectionField: [{position: 20 }],
        lineItem: [{ position: 20, importance: #HIGH }],
        fieldGroup: [{ qualifier: 'BasicFieldGroup', position: 20, importance: #MEDIUM }]
    }    
    SalesOrderType;
    @UI: { 
        selectionField: [{position: 30 }],
        lineItem: [{ position: 30, importance: #HIGH }],
        fieldGroup: [{ qualifier: 'CustomerFieldGroup', position: 10, importance: #MEDIUM }]
    }
    @EndUserText.label: 'Sold to Party'
    Customer_AG;
   @UI: { 
        selectionField: [{position: 40 }],
        lineItem: [{ position: 40, importance: #HIGH }],
        fieldGroup: [{ qualifier: 'CustomerFieldGroup', position: 20, importance: #MEDIUM }]
    }    
    @EndUserText.label: 'Ship to Party'
    Customer_WE;
   @UI: { 
        lineItem: [{ position: 50, importance: #HIGH }]
    }    
    SalesOrderItem;
   @UI: { 
        lineItem: [{ position: 60, importance: #HIGH }]
    }    
    Material;
}
```
```ABAP:Z_ME_SOI
@Metadata.layer: #CORE
@UI: { 
    headerInfo: { 
        typeName: 'Item', 
        typeNamePlural: 'Items',
        title: { value: 'SalesOrderItem' },
        description: { value: 'Material' }
    }
}
annotate view Z_C_SOI
    with 
{
    @UI.lineItem: [ { position: 10 } ]
    SalesOrderItem;
    @UI.lineItem: [ { position: 20 } ]
    Material;
    @UI.lineItem: [ { position: 30 } ]
    MaterialName;
    @UI.lineItem: [ { position: 40 } ]
    Plant;
    @UI.lineItem: [ { position: 50 } ]
    SalesOrderItemText;
    @UI.lineItem: [ { position: 60 } ]
    OrderQuantity;
}
```

## Interface Viewについて

[こちら](https://blogs.sap.com/2019/10/29/part7.-how-to-search-for-sap-standard-cds-views/)のブログでも紹介されている、標準のCDS ViewをInterfaceViewとして使っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/73b61e24-2c70-faa1-05cc-6522cdb498eb.png)

[I_SalesOrder](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/ee6ff9b281d8448f96b4fe6c89f2bdc8/955a323799624bd79221780675ada090.html#prerequisites "I_SalesOrder")

[I_SalesOrderPartner](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/ee6ff9b281d8448f96b4fe6c89f2bdc8/faa38d278eb3489693e02d5fcfb7ddd1.html#purpose "I_SalesOrderPartner")

[I_SalesOrderItem](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/ee6ff9b281d8448f96b4fe6c89f2bdc8/4cb98651bcae44c3a2924681ca194988.html#purpose "I_SalesOrderItem")

[I_CUSTOMER](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/ee6ff9b281d8448f96b4fe6c89f2bdc8/a11849401368469fb9e7dfb34f44e9c7.html#purpose "I_CUSTOMER")

## 実行時の挙動
Z_C_SOHを選択してPreview↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/d52ac893-8709-6c3a-b933-d79ef8bab320.png)

初期表示時はこんな感じ↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/83fefb56-1fe4-1b6e-06bd-5a3b19d8c2e6.png)

Sales Order Typeはドロップダウンリスト形式で選択可能↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/c385ea30-903e-b120-aedf-33f7c601791e.png)

受注先・出荷先には検索ヘルプを導入↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/d40f4038-eb9b-55ae-3ad6-4b486535fff4.png)

Goを押下して検索結果を表示↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/edb80623-fe34-1605-4c01-6b24c9e877fb.png)

明細をクリックするとObject Pageに遷移する↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/ab843003-c905-c9ec-f938-cba7478ae3de.png)

# 工夫した点
## その１：明細レベルでの一覧出力
やっぱ受注一覧作るのであれば、明細レベルで一覧出力したいですよね。
というわけなので、Z_C_SOHはZ_C_SOIとAssociationで関連付けして、明細の項目もListReportの一覧に出るようにしました。

## その２：受注伝票番号と受注先・出荷先との紐付
受注に紐づく受注先（取引先機能：AG）と出荷先（取引先機能：WE）を列で出力してみました。SQLで言うと、子テーブルをJOINし子テーブルのPKを指定して列で出す感じですね。

## その３：伝票タイプはドロップダウンリスト形式で選択
検索項目のうち一つはドロップダウンリストにしたかったので、伝票タイプをそのようにしています。伝票タイプの標準CDS Viewは探せなかったので、CDS Viewの`Z_C_SOType_VH`だけはテーブルを直接指定しています。

# アプリを作ってみた感想
まあ認定資格は仮免許みたいなもので、取ったからと言ってFiori開発がサクサクできるわけでは全く無かったです。試行錯誤して手を動かし、ハマり苦しみながらようやく完成までこぎ着けたって感じです。
実際に動くモノを作らないと、習熟ってできないんだなと思いました。

Fiori ElementsのFloorPlanの中ではおそらくList Reportが最もメジャーで、多くの初学者が最初に作り始めることになると思います。List ReportはクラシックなABAPで言うところのALVのレポートプログラムに相当するFloor Planだと思います。

Fiori Elementsはローコードツール的なものの一つだと思っていて、ABAPでレポートを作り込むより簡単なのかなーと思っていたのですが、そんなことはなかったです。
CDS Viewに一つ項目を追加すると検索結果がでなくなるようになったり、Object Pageに明細を表示させるのに小一時間ハマったりで、だいぶ苦戦しました。
検索ヘルプについても、クラシックなABAPだととくに意識せず勝手に補完してくれていたのですが、RAPだとCDS Viewで作り込まないといけないですよね。
RAPとかFioriElementsも、クラシックなABAPと比べて開発が簡単になります！とかではなくで、イチから覚え直さないとダメそうです。

ただ、[@tamiさんの記事](https://qiita.com/tami)の解説が非常に丁寧で、[やりたいことに近い記事](https://qiita.com/tami/items/dffe55a5aa35214be323)が公開されていたので、とても参考になりました。
ここに感謝を申し上げます。

# 参考
https://qiita.com/tami/items/dffe55a5aa35214be323

https://qiita.com/tami/items/c6585f743936146a4ede

https://qiita.com/tami/items/87a4d18e9954cd4c34ee

https://developers.sap.com/tutorials/fiori-tools-rap-modify-list-report.html