概要
今回は、FioriアプリケーションからDeepInsertメソッドを使って受注を登録します。
前回の記事:Deep Insertを使ったデータの登録(2)
ステップ
- ベースとなるMaster Detailアプリの登録
- 見た目の調整
- 登録処理の実装
登録処理を実装するにあたってベースはかならずしもMaster Detailでなくてもよいのですが、今後色々な処理を実装する土台にしたいのでこの形にしました。
1. ベースとなるMaster Detailアプリの登録
新規のプロジェクトをテンプレートから作ります。
SAP Fiori Master-Detail Applicationを選択します。
プロジェクト名、タイトル、ネームスペースを入力します。
前回までで作ったODataサービスを選択します。
一覧に表示する項目を選択します。
この時点でいったん実行してみます。
ポップアップが表示されたら、flpSandbox.htmlを選択します。
トラブルシューティング
デバッグでネットワークタブを見てみると、$batchリクエストで2つのクエリを実行していました。
GET SalesOrderSet/$count HTTP/1.1
GET SalesOrderSet?$skip=0&$top=20&$orderby=SoId%20asc HTTP/1.1
まずSalesOrderSetの件数を確認し、次に先頭20件を取得していることがわかります。最初のクエリで全件検索になっているため時間がかかって返ってこないようです。
対応
どうやらBAPI_EPM_SO_GET_LISTのパフォーマンスが良くないようなので、暫定対応として$countが指定された場合(すなわち全件検索になる場合)は、DBから直接データを取得するようにしました。
GET_ENTITYSETメソッドの頭に以下のコードを追加します。
"$countが指定された場合
IF io_tech_request_context->has_count( ) = abap_true.
SELECT * FROM snwd_so INTO TABLE lt_so.
MOVE-CORRESPONDING lt_so TO et_entityset.
RETURN.
ENDIF.
これで、エラーならずにデータが表示できるようになりました。
2. 見た目の調整
ここは好みの世界なので、飛ばしても大丈夫です。
2-1. リストの表示
デフォルトでは、Master(左側のリスト)を下にスクロールすると数秒後に次の20件が表示されるという動きになっています。これを、「さらに表示」というリンクを押すことにで次の20件を表示させるように変更します。
Master.view.xmlで、Listコントロールのプロパティを変更します。
<List
id="list"
width="auto"
class="sapFDynamicPageAlignContent"
省略
growing="true"
growingThreshold="20" ←追加
growingScrollToLoad="false" ←falseに変更
updateFinished="onUpdateFinished"
selectionChange="onSelectionChange">
結果、「さらに表示」というリンクと現在の件数が表示されます。
2-2. テキスト
デフォルトではリストのタイトル等がカッコ(<>)で囲まれており、暫定であることを表しています。これをきちんとした形で表示させるため、i18nファイルを修正します。
結果、以下のテキストが変更されました。
3. 登録処理の実装
ここからが本題です。登録部分の処理構造は以下のようになります。
主な登場人物は、登録用フォームを持つビューと、登録処理を実行するコントローラです。
コントローラ側でビュー専用のワークエリアであるビューモデルを定義します。
登録用フォームに入力して保存すると、ビューモデルからデータを取得してODataに渡します。
ステップ
3-1. 登録用のビュー、コントローラーを作成
3-2. Routingを追加
3-3. Masterビューに登録用ボタンとメソッドを追加
3-4. 登録用のビューを作りこむ
3-5. 登録処理を実装
3-1.登録用のビュー、コントローラーを作成
Detailビューとコントローラーをコピー&ペーストして、CreateOrderのビューとコントローラーを作成します。
CreateOrder.view.xml
<mvc:View controllerName="train.Train_01_SalesOrder.controller.CreateOrder" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc"
xmlns:semantic="sap.f.semantic" xmlns:footerbar="sap.ushell.ui.footerbar">
<semantic:SemanticPage id="createOrderPage" busy="{createOrderView>/busy}" busyIndicatorDelay="{createOrderView>/delay}">
<semantic:titleHeading>
<Title text="Create a new Order"/>
</semantic:titleHeading>
<semantic:headerContent>
</semantic:headerContent>
<semantic:content>
</semantic:content>
</semantic:SemanticPage>
</mvc:View>
不要な部分は削って骨格だけにします。
コントローラーの名前はCreateOrderに変更します。また、{}で囲まれた部分(ビューモデルをバインドしている部分)についても、モデル名をcreateOrderに変更します。
CreateOrder.controller.js
/*global location */
sap.ui.define([
"train/Train_01_SalesOrder/controller/BaseController",
"sap/ui/model/json/JSONModel",
"train/Train_01_SalesOrder/model/formatter"
], function (BaseController, JSONModel, formatter) {
"use strict";
return BaseController.extend("train.Train_01_SalesOrder.controller.CreateOrder", {
formatter: formatter,
/* =========================================================== */
/* lifecycle methods */
/* =========================================================== */
onInit: function () {
// Model used to manipulate control states. The chosen values make sure,
// detail page is busy indication immediately so there is no break in
// between the busy indication for loading the view's meta data
var oViewModel = new JSONModel({
busy: false,
delay: 0,
lineItemListTitle: this.getResourceBundle().getText("detailLineItemTableHeading")
});
this.getRouter().getRoute("createOrder").attachPatternMatched(this._onObjectMatched, this);
this.setModel(oViewModel, "createOrderView");
this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this));
},
/* =========================================================== */
/* event handlers */
/* =========================================================== */
/* =========================================================== */
/* begin: internal methods */
/* =========================================================== */
_onObjectMatched: function (oEvent) {
},
_onMetadataLoaded: function () {
}
});
});
コントローラーも不要な部分を削って骨格だけにします。
ビューモデルの名前やルート名など、detailという名前がついていた部分はcreateOrderに変更します。
3-2. Routingを追加
MasterビューからCreateOrderビューに遷移するためのRoutingを追加します。
manifest.jsonに以下のようにrouteとtargetを追加します。
"routes": [
省略
{
"pattern": "CreateOrder",
"name": "createOrder",
"target": [
"master",
"createOrder"
]
}
],
"targets": {
省略
"createOrder": {
"viewName": "CreateOrder",
"viewId": "createOrder",
"viewLevel": 1,
"controlAggregation": "midColumnPages"
},
3-3. Masterビューに登録用ボタンとメソッドを追加
Master.view.xml
<headerToolbar>
<OverflowToolbar>
省略
<Button
id="groupButton"
press="onOpenViewSettings"
icon="sap-icon://group-2"
type="Transparent"/>
<Button ←追加
id="creatOrderButton"
press="onCreateOrder"
icon="sap-icon://create"
type="Transparent"/>
</OverflowToolbar>
</headerToolbar>
Master.controller.js
onCreateOrder: function(){
this.getRouter().navTo("createOrder");
},
onCreateOrderメソッドを実装します。RouterのnavToメソッドでCreateOrderビューに遷移します。
この時点で登録ボタンを押すと以下のようになります。(MasterとDetailが表示された状態からでないと遷移しません)
3-4. 登録用のビューを作りこむ
登録用のビューを以下のような形にします。明細の上の+ボタンを押すと明細が追加されるようにします。
CreateOrder.view.xml
<mvc:View controllerName="train.Train_01_SalesOrder.controller.CreateOrder" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc"
xmlns:semantic="sap.f.semantic" xmlns:footerbar="sap.ushell.ui.footerbar"
xmlns:f="sap.ui.layout.form" xmlns:l="sap.ui.layout">
<semantic:SemanticPage id="createOrderPage" busy="{createOrderView>/busy}" busyIndicatorDelay="{createOrderView>/delay}">
<semantic:titleHeading>
<Title text="Create a new Order"/>
</semantic:titleHeading>
<semantic:headerContent>
<f:SimpleForm id="idHeader" editable="true" layout="ResponsiveGridLayout" title="Header" labelSpanXL="3" labelSpanL="3"
labelSpanM="3" labelSpanS="12" adjustLabelSpan="false" emptySpanXL="4" emptySpanL="4" emptySpanM="4" emptySpanS="0" columnsXL="1"
columnsL="1" columnsM="1" singleContainerFullSize="false">
<f:content>
<Label text="Note"/>
<Input id="note" value="{createOrderView>/salesOrder/Note}"/>
<Label text="Currency"/>
<Input value="{createOrderView/salesOrder/CurrencyCode}"/>
</f:content>
</f:SimpleForm>
</semantic:headerContent>
<semantic:content>
<Table id="idItems" inset="false" items="{createOrderView>/salesOrder/ToSalesOrderItem}">
<headerToolbar>
<Toolbar>
<content>
<Title text="Items"/>
<Button icon="sap-icon://add" press="onAddItem"></Button>
</content>
</Toolbar>
</headerToolbar>
<columns>
<Column>
<Text text="Product Id"/>
</Column>
<Column>
<Text text="Net Amount"/>
</Column>
<Column>
<Text text="Tax Amount"/>
</Column>
<Column>
<Text text="Quantity"/>
</Column>
<Column>
<Text text="Unit"/>
</Column>
</columns>
<items>
<ColumnListItem>
<cells>
<Input value="{createOrderView>ProductId}"></Input>
<Input type="Number" value="{createOrderView>NetAmount}"></Input>
<Input type="Number" value="{createOrderView>TaxAmount}"></Input>
<Input type="Number" value="{createOrderView>Quantity}"></Input>
<Text text="{createOrderView>QuantityUnit}"></Text>
</cells>
</ColumnListItem>
</items>
</Table>
</semantic:content>
<semantic:addAction>
<semantic:AddAction icon="sap-icon://save" press="onSave"/>
</semantic:addAction>
</semantic:SemanticPage>
</mvc:View>
SimpleFormとTableにビューモデルの項目をバインドしています。ビューモデルはこのあとコントローラーの中で定義します。
CreateOrder.controller.js
onInit: function () {
// Model used to manipulate control states. The chosen values make sure,
// detail page is busy indication immediately so there is no break in
// between the busy indication for loading the view's meta data
var oViewModel = new JSONModel({
busy: false,
delay: 0,
lineItemListTitle: this.getResourceBundle().getText("detailLineItemTableHeading"),
salesOrder: {
"SoId": "",
"CreatedBy": "",
"ChangedBy": "",
"CreatedByBp": false,
"ChangedByBp": false,
"Note": "",
"BuyerId": "100000005",
"BuyerName": "TECUM",
"CurrencyCode": "",
"LifecycleStatus": "",
"BillingStatus": "",
"DeliveryStatus": "",
"ToSalesOrderItem": []
}
});
this.getRouter().getRoute("createOrder").attachPatternMatched(this._onObjectMatched, this);
this.setModel(oViewModel, "createOrderView");
this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this));
},
onInitメソッドの中でビューモデルを定義している箇所があるので、ここにsalesOrderという名前で受注の項目を追加します。明細はToSalesOrderItemという名前の配列とします。"ToSalesOrderItem"はODataで定義したナビゲーションプロパティの名前です。
このビューモデルはcreateOrderViewという名前でビューにセットされ、ビュー側から{createOrderView>/salesOrder/xxx}という形で参照できます。
明細追加用のメソッド(onAddItem)も同じコントローラーの中で実装します。
/* =========================================================== */
/* event handlers */
/* =========================================================== */
onAddItem: function(){
var oViewModel = this.getView().getModel("createOrderView");
var items = oViewModel.getProperty("/salesOrder/ToSalesOrderItem");
var itemData = {
"SoId": "",
"ProductId": "HT-1040",
"NetAmount": 0,
"TaxAmount":0,
"Quantity":1,
"QuantityUnit": "EA"
};
items.push(itemData);
oViewModel.setProperty("/salesOrder/ToSalesOrderItem", items);
}
この結果、+ボタンを押すと明細行が追加されるようになります。
3-5. 登録処理を実装
最後に保存ボタンを押したときのメソッドonSaveで登録処理を実装します。これも前のステップと同じコントローラーに実装します。
onSave: function(){
//デフォルトモデルであるODataを取得
var oDataObject = this.getView().getModel();
//ビューモデルからODataに送る項目(DeepInsert用の形)を取得
var oViewModel = this.getView().getModel("createOrderView");
var payload = oViewModel.getProperty("/salesOrder");
//登録処理を実行→DeepInsertが実行される
oDataObject.create("/SalesOrderSet", payload, {
//成功の場合
success: function(){
sap.m.MessageToast.show("Order has been created successfully");
},
//エラーの場合
error: function(){
sap.m.MessageBox.error("An eror occurred");
}
});
}
登録ボタンを押したところ、エラーになりました。
コンソールを見ると、以下のようなメッセージが出ていました。
Failed to read property 'Quantity' at offset '179'
トラブルシューティング
Quantityの属性がおかしいようなので、デバッガを置いてpayloadの値を確認してみます。
コンソールでpayloadを見ると、Quantityだけ数値型になっています。
これを文字型に変えて続行すると、正常に登録されました。(以下はQuantityを文字型に変換しているところ)
対応
また、登録された受注番号が確認できるようにレスポンスから受注番号を取得してメッセージに表示するようにしました。
oDataObject.create("/SalesOrderSet", payload, {
//成功の場合
success: function(oResponse){
sap.m.MessageToast.show(oResponse.SoId + " has been created successfully");
},
結果
正常に登録され、受注番号が返ってくるようになりました。
登録された受注がDB上でも確認できます。
感想
3回の記事で、Deep InsertのOData側とFiori側の実装を行いました。
OData側では標準のBAPIを使いましたが、BAPIの動作がよくわからないこともあり、一応登録はされるもののデータに欠けがあるようです。
今回の目的であるDeep Insertの動作確認はできましたが、試しで作るときにはアドオンテーブルを使ったほうがわかりやすいと思いました。
参考
SAP Gateway $batch from OData | DEEP ENTITY using OData Service | SAP Netweaver OData Training