Blog Feed

SAP Screen Personas – Easy Selection mittels Scripting

Mit diesem Blogbeitrag möchte ich euch mit dem Scripting innerhalb von SAP Screen Personas vertraut machen. Mittels Scripting ist es z.B. möglich, Bearbeitungen vom SAP-Standard im Hintergrund durchzuführen, Informationen von mehreren Transaktionen auf einer Maske zu verbinden und noch vieles mehr. Um euch aber erstmal im kleinen Rahmen mit dem Scripting vertraut zu machen, habe ich ein sehr einfaches Beispiel ausgesucht. Gearbeitet habe ich dieses mal in einem S/4HANA-System, um euch zu zeigen, dass es kaum Unterschiede im Vergleich zu R/3-Systemen im Umgang mit SAP Screen Personas gibt. Für das Beispiel werde ich euch zunächst das Set-up beschreiben, bevor es dann richtig zur Sache geht.

Set-up

Für mein kleines Beispiel habe ich mir einen Mitarbeiter vorgestellt, der sehr häufig mit der Transaktion ME2N (Einkaufsbelege nach Belegnummer) arbeitet.

Diese Transaktion ist ein Standardreport zum Anzeigen von beispielsweise Bestelldetails. Sie besteht aus zwei Teilen. Zum einen aus einer Selectionsmaske und zum Anderen aus einer Ergebnisansicht. Wenn ihr die Transaktion im Standard öffnet, so gelangt ihr zunächst auf eine unübersichtliche und sehr große Selektionsmaske.

In der Selektionsmaske ist das einzige Pflichtfeld Listumfang. Dieses Selektionskriterium bestimmt das Format des Reports. Für dieses Beispiel stelle ich mir einen Mitarbeiter vor, der lediglich mit den Selektionskriterien Belegart, Einkäufergruppe und Werk arbeitet und im Listumfang immer „ALV“ als Standardanzeige auswählt. Wenn ich meine Selektionen vorgenommen habe und den Report ausführe, so komme ich auf folgende Ergebnisansicht:

Möchte ich nun meine Selektionsbedingungen verändern, so muss ich immer wieder zurück auf den Selektionsbildschirm springen. Um genau das jedoch nicht machen zu müssen, können wir Screen Personas benutzen. Ziel soll es sein die neue Selektion direkt aus der Ergebnisansicht neu starten zu können. Dazu müssen wir zunächst die Transaktion optisch aufbereiten und danach mittels Scripting die Funktionalitäten ergänzen.

Transaktion optisch vorbereiten

Zunächst einmal habe ich die Selektionsmaske auf die Kriterien Belegart, Einkäufergruppe und Werk beschränkt und die Maske etwas aufgehübscht:

Wenn ihr nicht genau wisst, wie ihr mit SAP Screen Personas Transaktionen optisch bearbeiten und Objekte entfernen bzw. hinzufügen könnt, dann schaut doch nochmal hier vorbei:

https://developers4sap.blog/deine-ersten-schritte-mit-sap-screen-personas/

Ebenso habe ich in der Ergebnisansicht Felder und Labels hinzugefügt und optisch angepasst, sodass es erstmal so aussieht, als ob die Selektionsbedingungen auch von hier neu gestartet werden können:

Um nun das ganze mit Leben zu füllen, also die Selektion auch von der Ergebnisansicht aus zu ermöglichen, müssen wir in das Scripting einsteigen.

Das Scripting-Tool von SAP Screen Personas

Zunächst müsst ihr das Scripting Tool im Menü von SAP Screen Personas öffnen:

Gesagt getan – schon landet ihr auch direkt auf dem Scripting Menü:

Um ein neues Script zu erstellen müsst ihr einfach auf das + Symbol (1) klicken, dem Script einen Namen geben und schon kann es losgehen. Das Script könnt ihr natürlich komplett manuell runtercoden. Eine andere Möglichkeit wäre jedoch, das Script einfach durch das Bedienen der Transaktion wie von Zauberhand entstehen zu lassen. Dazu nutzt ihr einfach den Button Aufzeichnung starten (2). Möchtet ihr auf Eigenschaften von Objekten eingehen, z.B. um einen Wert zu kopieren oder die Hintergrundfarbe dynamisch zu wechseln, so nutzt ihr den Objektselektor (3.). Ein einfacher klick auf den Objektselektor, gefolgt von einem Klick auf des jeweilige Objekt genügt.

Wenn ihr nun schon etliche Zeilen gecoded habt, vergesst bloß nicht zwischendurch auch mal zu speichern (4) 😉

Easy Selection mittels Scripting

Als erstes werden wir ein kurzes Script für den Go-Button in der Selektionsmaske erstellen und dieses mit dem Button verbinden:

Als nächstes wollen wir die Selektion auch von der Ergebnisansicht her starten können. Dafür legt ihr euch ein neues Script an. Dann müsst ihr euch drei Variablen erstellen, in denen ihr die Werte der Selektionsmaske der Ergebnisansicht zwischenspeichern könnt. Diesen Variablen müsst ihr dann, wie im folgenden Gif gezeigt, mittels des Objektselektor den Inhalt der Textfelder zuordnen:

Im nächsten Schritt müsst ihr diese Werte in die eigentliche Selektionsmaske bringen und die Selektion neu starten. Hierzu nutzt ihr am besten die Möglichkeit Aufzeichnung starten, wechselt einmal zurück in die Selektionsmaske und beendet die Aufzeichnung wieder. Dann nutzt ihr den Objektselektor und übertragt den Inhalt der Variablen in das jeweilige Textfeld. Zum Abschluss geht ihr wieder auf Aufzeichnung starten, drückt den Go-Button und beendet dann eurer Script. Ihr solltet nun noch manuell folgende Codezeile hinzufügen:

session.findById("wnd[0]/usr/ctxtLISTU").text = "ALV";

Als kleine Übung könnt ihr – bevor ihr weiterlest – überlegen an welche Stelle im Code die Zeile muss und wofür diese Zeile gut ist…

Richtig! Wie zu Anfang beschrieben, ist in der Selektionsmaske das Pflichtfeld Listumfang, welche mittels der Codezeile mit dem Wert „ALV“ gefüllt wird. Wenn euer Code nun genau so aussieht, wie im Folgenden, dann habt ihr alles richtig gemacht.

var bgart = session.findById("wnd[0]/usr/subPersonas_159888175457714/boxPersonas_159887725488965/txtPersonas_159888188832820").text;
var ekgrp = session.findById("wnd[0]/usr/subPersonas_159888175457714/boxPersonas_159887725488965/txtPersonas_159888185323181").text;
var plant = session.findById("wnd[0]/usr/subPersonas_159888175457714/txtPersonas_159888185842577").text;

session.findById("wnd[0]/tbar[0]/btn[3]").press();

// An OnLoad script wnd[0]/scrptPersonas_0050568768B71EDABAF05D1B1E1E640A was executed.
// session.utils.executeScript("wnd[0]/scrptPersonas_0050568768B71EDABAF05D1B1E1E640A");

session.findById("wnd[0]/usr/ctxtLISTU").text = "ALV";

session.findById("wnd[0]/usr/ctxtS_BSART-LOW").text = bgart;
session.findById("wnd[0]/usr/ctxtS_EKGRP-LOW").text = ekgrp;
session.findById("wnd[0]/usr/ctxtS_WERKS-LOW").text = plant;

session.findById("wnd[0]/tbar[1]/btn[8]").press();

Jetzt müsst ihr das Script nur noch an den den Go-Button der Ergebnisansicht anbinden. Schon habt ihr mit einem einfachen Script eine „Easy Selection“ für die Transaktion ME2N gebaut, die wie folgt funktionieren dürfte:

Was geht sonst noch so?

Das Scripting bietet natürlich noch eine Fülle an Möglichkeiten. In den nächsten Blogartikeln werden wir zusammen ein Dashboard bauen und Informationen von verschiedenen Transaktionen auf einem Bildschirm mittels Scripting vereinen.

Nun wünsche ich euch erstmal fröhliches Ausprobieren und Scripten.

Wenn ihr noch irgendwelche Fragen oder Anregungen habt, dann nutzt doch einfach die Kommentarfunktion oder schreibt mir direkt via Mail: paul.holst@cgi.com

SAP Cloud Identity Services – Identity Authentication | Ein kurzer Überblick

Die steigende Anzahl der digitalen Technologien verändern die Landschaft von Unternehmen und bringen eine Reihe von Sicherheitsproblemen mit sich, mit denen sich sowohl mittelständische als auch große Konzerne sich gezwungenermaßen befassen müssen. Insbesondere wenn es um die Verwaltung von Benutzeridentitäten und den Zugriff auf Geschäftslösungen geht. Um diese Herausforderungen zu bewältigen, benötigen Unternehmen einen einheitlichen, umfassenden und zentralisierten Ansatz für das Identitätsmanagement und die Zugriffssteuerung. In diesem Artikel befassen wir uns mit dem SAP Identity Authentication Service (IAS) und machen uns einen Überblick über dessen Kernfunktionen und Integrationsmöglichkeiten.

SAP Cloud Identity Services

Die SAP hat angekündigt, 2020 stark in die Integration zu investieren. Security ist nur ein Aspekt der Integration, aber einer von hoher Bedeutung. Die Cloud Identity Services bieten hierbei die Kern- Sicherheitsfunktionen an: Benutzerauthentifizierung und Benutzerbereitstellung.

Derzeit umfasst die SAP Cloud Identity Services folgende zwei Dienste:

  • Identity Authentication
  • Identity Provisioning

Beide Services sind über den Service Catalog in der SAP Cloud Platform (weitere Infos zu SCP hier) verfügbar.

Identity Authentication

Oft wird der Frage nach der Authentifizierungsmethode in einer hybriden Systemlandschaft nicht die nötige Beachtung geschenkt. Wobei die Frage essentiell für die Gewährleistung der Zugriffssicherheit ist. Viele Unternehmen haben hybride Systemlandschaften bestehend aus SAP / Non-SAP Anwendungen, Standardsoftware, Eigenentwicklungen usw. die in der Cloud oder On-Premise laufen. Mit der Anzahl der Systeme erhöht sich auch die Anzahl der Zugangsdaten und somit auch der Aufwand für die Systeme. Hierbei kann es für den Anwender mal schnell unübersichtlich werden.

Ein einfacher Zugriff über einen Single Sign-On stellt in so einem Fall eine erhebliche Erleichterung dar.

Um die Benutzerverwaltung zu vereinfachen, lagern einige Cloudanwendungen ihren Authentifizierungsprozess an ein zentrales System aus. Der User wird dort authentifiziert und bekommt dann einen Sicherheits-Token. Mit diesem Token wird dann der Login-Prozess abgeschlossen. Eine ähnliche Funktion übernimmt auch der SAP Identity Authentication Service (IAS).

Doch was genau macht der Service?

SAP Identity Authentication Service (IAS)

Der Service fungiert als zentraler Identity Provider für die angeschlossenen Anwendungen. Die Hauptaufgabe ist es, Sicherheitstoken für authentifizierte Anwender für bestimmte Zielanwendungen bereitzustellen. Sie dient als zentrale Schnittstelle, die verschiedene Authentifizierungsmethoden für Benutzer implementiert. Gleichzeitig ermöglicht sie den Zugriff auf angeschlossene Systeme, unabhängig davon ob die Anwendungen in der Cloud oder On-Premise laufen.

Dies sind die Hauptfunktionen des Services:

  • Standardisierte Authentifizierungsmethoden für Cloud- und On-Premise Anwendungen
  • Zentralisierte Verwaltung
  • Risikobasierte Authentifizierungsregeln
  • Zwei-Faktor-Authentifizierung
  • Single-Sign-On-Funktionalität
  • Integrationsszenarien mit bereits vorhandenen IdPs
  • Einheitliche Benutzererfahrung
  • Flexible Option für die Benutzerverwaltung

Beispielszenario:

Mitarbeiter XY braucht für seine tägliche Arbeit Zugang zu einer bestimmten Geschäftsanwendung. Er öffnet den Web-Browser und ruft über das SAP Fiori Launchpad eine Anwendung auf. Daraufhin prüft die Anwendung ob bereits eine offene Sitzung für den Benutzer vorhanden ist oder ob ein Sicherheitstoken existiert. Falls kein Sicherheitstoken existiert, fragt die Anwendung beim Identity Authentication Service an und der Mitarbeiter kann sich authentifizieren.

Hierbei sind folgende Authentifizierungsmethoden möglich:

  • Benutzername / Passwort (Standard)
  • 509-Zertifikat
  • SPNEGO / Kerberos
  • 2-Faktor-Authentifizierung
  • Weiterleiten der Anfrage an einen vom Kunden konfigurierten Corporate Identity Provider (IdP)

Nach der erfolgreichen Authentifizierung erhält der Nutzer dann einen Sicherheitstoken vom Service. Dieser Token wird nun verwendet, um eine Verbindung mit der Anwendung herzustellen. Hierbei erstellt die Anwendung einen Sitzungstoken, der nur für die Anwendung gültig ist.

Hierbei sind folgende Typen möglich:

  • SAML 2.0 assertion
  • OpenID Connect (OIDC)

Sobald der Sicherheitstoken vom SAP IAS ausgestellt wurde, kann der Nutzer weitere verschiedene Geschäftsanwendungen aufrufen. Der Zugriff ist dann so lange möglich, bis der Webbrowser geschlossen wurde oder die Gültigkeitsdauer des Tokens überschritten ist.

Folgende Abbildung zeigt den Ablauf der Authentifizierung über SAP IAS:

SAP Identity Authentication

Anschließend stellt sich die Frage: Wo werden die Benutzerdaten denn nun gespeichert? Irgendwo muss ja der Abgleich zwischen den eingegebenen Werten und den Benutzerdaten stattfinden.

Hier bietet SAP IAS folgende 3 Optionen:

1. Nutzung des Standard IAS Benutzerspeichers

IAS nutzt den hierbei den eigenen Benutzerspeicher. Über eine Web UI kann der Admin dann User anlegen und verwalten. Es besteht die Möglichkeit Massenimports mithilfe von CSV Dateien durchzuführen.

2. Anbindung eines bereits bestehenden Benutzerspeichers

Falls es bereits einen Benutzerspeicher gibt, kann er mit SAP IAS gekoppelt werden. Beispiel: Active Directory (LDAP) oder ein HCM System. Bei dieser Variante gleicht SAP IAS die Anmeldedaten mit den Daten des Benutzerspeichers ab, um den Benutzer zu Authentifizieren.

3. Integration mit einem bereits existierendem Identity Provider

Hier ist ein bereits bestehender Identity Provider mit in den Authentifizierungsprozess integriert. In diesem Fall fungiert der SAP IAS als eine Art Proxy, welcher die Authentifizierungsanfragen an den hervorgesehenen Identity Provider weiterleitet. Der SAP IAS transformiert die Authentifizierungs-Token und leitet sie an die Zielanwendungen weiter.

Fazit und Ausblick

Abschließend kann man sagen, dass mit Hilfe von SAP Authentication Service, der Authentifizierungsprozess in einer hybriden Systemlandschaft zentralisiert und vereinfacht werden kann. Die Benutzerverwaltung wird durch einen einheitlichen Benutzerspeicher vereinfacht. Die Tatsache, dass mit SAP IAS sowohl SAP, als auch Non-SAP Systeme integriert werden können, hebt das Produkt von herkömmlichen Identity Providern ab.

 

Weitere Links zum Thema:

SAP Community Topics – SAP Cloud Identity Services (SAP IAS):

https://community.sap.com/topics/cloud-identity-services/identity-authentication

Blog-Artikel der SAP:

https://blogs.sap.com/2020/02/19/the-cloud-enterprise-security-suite-cloud-identity-services/

 

Bei weiteren Fragen könnt ihr gerne die Kommentarfunktion nutzen oder mich direkt anschreiben über elkhan.aliyev@cgi.com.

SAPUI5 ODataV2-Model – Refresh Performance

Einleitung

Wer schon einmal eine Applikation in SAPUI5 geschrieben hat und dabei auf einen OData-Service zurückgegriffen hat, wird sicherlich schon einmal über folgendes Problem gestolpert sein:

Die Daten der aktuell angezeigten Entität oder des Entitätssets haben sich geändert. Soweit so gut. Nur die Anzeige verändert sich leider nicht.

Wenn die Daten sich hinter dem OData-Service verändert haben, müssen wir die Daten in jedem Fall manuell erneut Laden. Doch auch, wenn die Daten in derselben Applikation geändert werden, von der wir sie wieder abrufen wollen, kann es vorkommen, dass nicht alle Daten auch in der View erneuert werden. Das kann zum Bespiel vorkommen, wenn wir Daten verändern, die über Mapping-Tabellen an die aktuell an die View gebundene Entität verknüpft sind.

Für dieses Problem gibt es zum Glück eine einfache Lösung: refresh.

Mit dem Aufruf

 this.getView().getModel().refresh();

erneuert sich das ODataModel ganz von selbst und die gebundenen Views werden automatisch mit den neuen Daten versorgt.

Dabei ist allerdings Vorsicht geboten!

Die Performance des Refreshs ist ein nicht zu vernachlässigendes Kriterium. Ein kleines Beispiel folgt.

Beispiel

Dies ist eine Beispiel SAPUI5-Anwendung auf Basis des Northwind-ODataV2 Services. Sie besteht aus drei IconTabFiltern in einer IconTabBar.

Übersicht SAPUI5 Anwendung <em>NorthwindSampleApplication</em>
Übersicht SAPUI5 Anwendung NorthwindSampleApplication

In jedem der IconTabFilter befindet sich eine Tabelle. Die erste ist folgendermaßen an das Entitätsset Products gebunden:

<Table
items="{path:'/Products',parameters:{expand:'Category,Order_Details,Supplier',select:'ProductID,ProductName,UnitsInStock,UnitPrice,Category/CategoryName,Supplier/City'}}"
noDataText="Keine Daten" id="list0">
    <headerToolbar>
        <Toolbar>
            <Title text="Products" level="H2"/>
        </Toolbar>
    </headerToolbar>
    <columns>
        <Column>
            <Text text="Id"/>
        </Column>
        <Column>
            <Text text="Product Name"/>
        </Column>
        <Column>
            <Text text="Stock"/>
        </Column>
        <Column>
            <Text text="Unit Price"/>
        </Column>
        <Column>
            <Text text="Category"/>
        </Column>
        <Column>
            <Text text="Supplier Origin"/>
        </Column>
    </columns>
    <items>
        <ColumnListItem type="Active" press="onPress" id="item0">
            <cells>
                <Label text="{ProductID}"/>
                <Label text="{ProductName}"/>
                <Label text="{UnitsInStock}"/>
                <Label text="{UnitPrice}€"/>
                <Label text="{Category/CategoryName}"/>
                <Label text="{Supplier/City}"/>
            </cells>
        </ColumnListItem>
    </items>
</Table>

In gleicher Weise ist die Tabelle im mittleren IconTabFilter an das Entitätsset Orders und die Tabelle im rechten IconTabFilter an das Entitätsset Invoices gebunden.

Beispielhaft habe ich eine Objektansicht für die Produkte angelegt, die beim Klicken auf ein spezielles Produkt aus der Liste geöffnet wird:

<em>NorthwindSampleApplication</em> - Product Overview
NorthwindSampleApplication – Product Overview

Auf dieser Produktseite befinden sich zwei Buttons:

  1. Allgemeines Refresh
  2. Spezielles Refresh

Das Ziel eines „Refreshs“, also der Erneuerung der lokalen Daten im lokalen OData-Model, an dieser Stelle kann nur sein, alle Daten auf dieser aktuell angezeigten Seite zu erneuern.

In diesem Fall handelt es sich dabei um das Produkt mit der productID „1“. In dieser speziellen View werden keine Daten aus anderen Entitäten dargestellt, somit benötigen wir auch keinen $expand Ausdruck.

Alle weiteren Daten, die in anderen Views als der aktuellen liegen müssen beim Laden dieser View nicht aktualisiert werden. Um die Aktualität der Daten kümmern wir uns bei öffnen dieser Views.

Auch ist es nicht nötig das gesamte Entitätsset zu Laden, sei es auch mit $skip und $top, wir benötigen genau ein Produkt.

Soviel zu den Erwartung, schauen wir uns einmal die Effekte der beiden Buttons an.

Das allgemeine Refresh

Refresh des ODataV2-Models - <em>this.getView().getModel().refresh();</em>
Refresh des ODataV2-Models

Beim Drücken des ersten Knopfes wird das gesamte Model erneuert („Allgemeines Refresh“). Dazu nutzen wir folgenden Code:

Ein Einzeiler mit großen Folgen, denn schaut man sich die Calls an, die das ODataV2-Model im Hintergrund tätigt, staunt man nicht schlecht:

Refresh Model - Batch Calls
Refresh Model – Batch Calls

Im Hintergrund werden vier Batch-Calls verschickt, die Laufzeit liegt insgesamt bei circa 1,5 Sekunden. Schaut man sich den Inhalt der Batch-Calls an, erklärt sich diese Laufzeit:

Model Refresh - Laden des gesamten Product Entitysets
Model Refresh – Laden des gesamten Product Entitysets

Ganz am Anfang wird ein $count auf das Entitätsset Products aufgerufen, anschließend die ersten 100 Einträge aus diesem Entitätsset. Sicher nicht was geplant war, denn wir befinden uns auf der Übersichtsseite eines speziellen Produkts. Da werden nicht die ersten 100 Produkte benötigt, den $count brauchen wir hier auch nicht (Wie ihr unnötige $counts generell verhindern könnt, lest ihr hier).
Aber es wird noch schlimmer, scrollt man ein wenig runter:

Model Refresh - Laden des gesamten Order Entitätssets
Model Refresh – Laden des gesamten Order Entitätssets

Im selben Batch wird auch noch ein $count auf das Entitätsset Orders gemacht, auch dort laden wir noch einmal die ersten 100 Einträge.

Scrollen wir noch ein Stück, finden wir dasselbe dort auch noch einmal für das Entitätsset Invoice , also noch ein $count und noch einmal 100 Einträge.

Das ODataV2-Model scheint bei einem Refresh auf sich selbst also implizit ein Refresh auf alle vorhandenen Bindings durchzuführen – Egal aus welcher View.

Somit werden hier 3 AggregationBindings (aus den 3 Tabellen) und ein ElementBinding erneuert, gebraucht wird hier allerdings nur das letzte.

Das spezielle Refresh

Im Gegensatz zum ersten Knopf sieht die Logik hinter dem zweiten Knopf folgendermaßen aus:

Refresh des ODataV2-Models - <em>this.getView().getElementBinding().refresh();</em>
Spezielles Refresh

Wieder ein Einzeiler. Allerdings bedienen wir uns jetzt des Refreshs des speziellen Bindings. In diesem Fall also des ElementBindings der View, also des speziellen Produkts.

Ein Blick auf die Batch-Calls dieses Refreshs zeigt die Wirkung:

Spezielles Refresh - Batch Calls
Spezielles Refresh – Batch Calls

Im Hintergrund wird genau ein Batch verschickt. Laufzeit: 248ms.

Der Inhalt des Batches sieht wie folgt aus:

Spezielles Refresh - Batch Call Inhalt
Spezielles Refresh – Batch Call Inhalt

Der Batch-Call besteht aus genau einem GET auf das „Produkt 1“.

Analog zum ElementBinding klappt das spezielle Refresh übrigens auch mit Bindings auf Entitätssets, z.B. bei Listen oder Tabellen ;).

Dazu einfach:

Refresh des ODataV2-Models - <em>this.getView().byId("myTable").getBinding("items").refresh();</em>
Spezielles Refresh für AggregationBindings

Fazit

Wer in seiner SAPUI5-Anwendung einmal auf das Problem trifft, sein ODataV2-Model erneuern zu müssen, sollte darauf achten, welche Daten tatsächlich erneuert werden müssen. Das allgemeine Refresh ist vermutlich in wenigen der Fälle die Antwort.

Schon in diesem kleinen Beispiel ist der Zeitvorteil messbar, ohne dass die Menge der Datensätze sich besonders an der Laufzeit der Batch-Calls bemerkbar macht. Denn die Datensätze des Northwind OData-Services sind vergleichsweise klein:

  • /Products/$count:  77
  • /Orders/$count: 830
  • /Invoices/$count: 2155

Vor allem durch den sehr geringen Aufwand beim Programmieren lohnt es sich kurz darüber nachzudenken, ob das allgemeine Refresh an dieser Stelle jetzt wirklich nötig ist.

Deine ersten Schritte mit SAP Screen Personas

In diesem Blogbeitrag möchte ich euch mit dem kostenlosen Add-On „SAP Screen Personas“ vertraut machen. Ihr werdet sehen, wie einfach es ist Oberflächen umzugestalten. Ich werde euch zeigen, wie ihr überflüssige Inhalte ausblendet, Corporate Identity integrieren und zusätzliche Funktionen in eine Transaktion einbauen könnt.

Wenn ihr zufällig auf diesen Blogbeitrag gestoßen seid, euch fragt was SAP Screen Personas überhaupt ist und wofür das ganze gut sein soll, dann schaut doch einfach einmal in meinen letzten Blogbeitrag:

https://developers4sap.blog/sap-screen-personas-die-alternative-zu-fiori-und-co/

Bevor ihr starten könnt, müsst ihr das kostenlose Add-On „SAP Screen Personas“ von SAP installiert haben und einige Einstellungen vornehmen. Die wichtigsten Konfigurationsschritte könnt ihr dem folgenden Artikel im SAP Help Portal entnehmen:

https://help.sap.com/viewer/9db44532734f4718b91e460c020307fe/3.0.7/en-US/4a8b1bf790a545fa939b8183bdac0ca8.html

Achtung: Um SAP Screen Personas nutzen zu können, müsst ihr über einen SAP WebGUI-Zugang verfügen. Im normalen SAP GUI ist dies nicht möglich. Sobald ihr aber über den Zugang verfügt, müsst ihr nur die Transaktion, die ihr bearbeiten wollt, aufrufen und schon könnt ihr damit beginnen, eure ersten Flavor zu erstellen.

Ein Flavor erstellen? Was ist das überhaupt?

Flavor erstellen

Ein Flavor ist eine Variante einer bestehenden Transaktion. Wollt ihr eine bestimmte Transaktion optisch aufpimpen, so müsst ihr zunächst ein neues Flavor erstellen. Um euch das zu zeigen, habe ich zunächst die Transaktion MD04 (Bedarfs-/Bestandsliste) ausgewählt. Nun müsst ihr den Mauszeiger ganz nach oben an die blaue Leiste führen, sodass das Symbol erscheint. Nach einem einfachen Klick darauf, öffnet sich das SAP Screen Personas Menü.

Mit einem Klick auf „Neues Flavor anlegen“ erstellt ihr euch eure eigene Maske der Transaktion, auf der ihr euch austoben dürft. Nun einfach noch einen Namen und eine Beschreibung für das Flavor eingeben und schon könnt ihr loslegen.

Wenn ihr bereits ein Flavor besitzt und dieses nochmal bearbeiten wollt, so müsst ihr lediglich dieses Flavor in dem Menü auswählen und könnt mittels des Buttons in den Bearbeitungsmodus springen.

Objekte ausblenden, verschieben und hinzufügen

Wenn ihr nun im Bearbeitungsmodus seid, könnt ihr direkt starten, die für euch überflüssigen Objekte auszublenden und die übrigen neu anzuordnen. Zum Ausblenden klickt ihr einfach auf das jeweilige Objekt (z.B. auf einen Button). Nun könnt ihr entweder in der Menüleiste unter dem Reiter „Home“ den Ausblenden-Button betätigen oder ihr nutzt die Entf-Taste auf eurer Tastatur. Um die Platzhalter der ausgeblendeten Objekte verschwinden zu lassen, betätigt ihr einfach das Drop-Down-Menü neben dem Ausblenden-Button und wählt „Hide all placeholders“ an.

Die übrig gebliebenen Objekte könnt ihr nun entweder per „Drag-and-Drop“ an die gewünschte Position schieben oder ihr nutzt ebenfalls unter dem Reiter „Home“ die Funktion zum Ändern der Größe und Position.

Unter dem Reiter „Einfügen“ findet ihr sämtliche Objekte, um die ihr euer Flavor ergänzen könnt. Hierzu gehören u.a. Labels, Gruppenrahmen, Auswahlknöpfe und diverse Buttons.

Für mein kleines Beispiel füge ich einen Gruppenrahmen hinzu, in dem ich zwei Transaktionsbuttons integriere. Mit einem Gruppenrahmen könnt ihr mehrere Objekte optisch zusammen bündeln und diese dann auch gemeinsam bearbeiten. Ein Transaktionsbutton ist ein einfacher Druckknopf, der euch zu einer anderen Transaktion führen soll.

Zunächst erstelle ich also zwei Transaktionsbuttons. Hierzu klicke ich unter dem Reiter „Einfügen“ auf „Transaktionsbutton“, trage den Namen ein, der auf dem Button stehen soll und gebe die Transaktion ein, zu der geführt werden soll. In meinem Beispiel mache ich dies für die Transaktionen ME53N (Bestellanforderung) und ME23N (Bestellung). Diese beiden Transaktionsbuttons ziehe ich nun per Drag-and-Drop in den angelegten Gruppenrahmen. Zuletzt positioniere ich den Gruppenrahmen mit den beiden intigrierten Transaktionsbuttons an eine freie Stelle.

Corporate Identity

Zur Corporate Identity gehört auch immer das Corporate Design, also das einheitliche Nutzen von Farben und Formen, das Firmenlogo und auch das einheitliche Gestallten von internen und externen Dokumenten. Mit SAP Screen Personas könnt ihr recht einfach ein einheitliches und auf das Unternehmen zugeschnittenes Design eurer benutzen Transaktionen gestallten.

Farben und Formen von Objekten könnt Ihr unter dem Reiter „Home“ sehr einfach und ganz nach eurer Corporate Identity anpassen. Wie immer reicht dafür ein einfacher Klick auf das jeweilige Objekt und schon könnt ihr dieses umgestalten.

Hintergrundbilder könnt ihr ebenso einfach über den Menü-Button „Assign Background“ für jedes Objekt festlegen. Bevor ihr jedoch ein eigenes Bild für den Hintergrund benutzen könnt, müsst ihr dieses über die Admin Transaktion /PERSONAS/ADMIN zur Verfügung stellen. Das macht ihr ganz einfach über die Funktion „Neue Ressource über Upload anlegen“.

Mein Ergebnis von zehn Minuten Arbeit könnt ihr im nachfolgenden Bild betrachten:

Ihr könnt z.B. den Gruppenrahmen „Schnellzugriff – Transaktionen“ sehen, in dem die beiden Transaktionsbuttons („Bestellung“ und „Bestellanforderung“) integriert sind. Außerdem habe ich auch Formen und Farben einzelner Objekte gemäß der Standards von CGI angepasst und ein Hintergrundbild mit dem CGI-Logo hinzugefügt.

Tabellen anpassen

Als letztes möchte ich euch noch zeigen, wie ihr Tabellen anpassen und ändern könnt. Dafür müsst ihr zunächst die Tabelle bzw. das Grid in eurer Transaktion auswählen. Dann könnt ihr über den Reiter „Tabellen“ eure Anpassungen vornehmen.

Zum einen könnt Ihr Spaten verschieben, ausblenden oder umbenennen. Dafür müsst ihr nur die jeweilige Spalte anklicken und könnt dann die Menü-Optionen verwenden. Zum anderen könnt ihr aber auch bedingte Formatierungen verwenden, um bestimmte Inhalte oder gar ganze Zeilen hervorzuheben.

Dafür wählt ihr einfach den Menü-Punkt „Bedingte Formatierung“ an und definiert eure Regeln.

Ich habe als Beispiel einfach einmal für die Spalte „Verfügbare Menge“ Regeln entworfen, die je nach Wert die Farbe der Zellen verändern. Natürlich ist es auch möglich statt der Zellenfarbe auch die Farbe der ganzen Spalte oder der Schrift zu formatieren. Probiert das doch einfach selber einmal aus.

War das schon alles, was SAP Screen Personas kann?

Nein, natürlich war das noch nicht alles, was SAP Screen Personas kann. Wie die Überschrift schon sagt, sind dies aber die ersten wichtigen Schritte im Umgang mit SAP Screen Personas.

Wie ihr euch eigene Dashboards erstellt und mit der Hilfe von Scripting Informationen von mehreren Transaktionen auf einem Bildschirm bündelt, werde ich euch in weiteren Blogartikeln zeigen.

Nun wünsche ich euch erstmal fröhliches Ausprobieren und Umgestallten.

Wenn ihr noch irgendwelche Fragen oder Anregungen habt, dann nutzt doch einfach die Kommentarfunktion oder schreibt mir direkt via Mail: paul.holst@cgi.com

Custom Code Migration für SAP S/4HANA – Teil 1

Immer mehr Unternehmen stehen in den Startlöchern für die Migration zu S/4HANA.
Ein Teil dieser großen Systemumstellung ist die Custom Code Migration, oder auf Deutsch: die Überführung der Eigenentwicklungen in die S/4 Welt.

Da kommen einem direkt einige Fragen in den Sinn:

  • Warum muss ich das machen?
  • Was genau bedeutet das?
  • Welche Programmteile sind betroffen und wie finde ich diese?

Ich freue mich darauf, diesen Fragen in diesem Blog nachzugehen! Du bist hier also genau richtig als Programmierer oder als Projektmitglied einer S/4 Migration.

Hintergrund zur Custom Code Migration:

Im Verlaufe einer Systemmigration zu einem S/4 System müssen wir auch die Eigenentwicklungen überführen. Woran liegt es, dass der Code nicht einfach übernommen werden kann? Und warum stellt die SAP keine Abwärtskompatibilität her?

Hierfür schauen wir uns zunächst folgendes Bild an:

Übersicht Systemmigration, Custom Code Migration, Quelle: www.sap.com
Übersicht Systemmigration, Custom Code Migration, Quelle: http://www.sap.com

Auf der linken Seite sehen wir ein reguläres R/3-System. Unser Custom Code greift auf Tabellen, Datentypen, Standard-Reports oder Bausteine zu. Nach der Migration zu S/4 (rechte Seite des Bildes) bestehen diese aber teilweise nicht mehr (wurden durch effizientere ersetzt), wurden verändert oder sind an anderer Stelle zu finden.
Genau das ist der Grund, weshalb wir die Custom Code Migration benötigen.

 

Nützliche Informationen vorab:

Einige spannenden Informationen möchte ich an dieser Stelle zusammenfassen:

  • Kein S/4HANA ohne Custom Code Migration möglich (wenn es Eigenentwicklungen gibt).
  • Die SAP stellt eine „Simplification-Database“ (SD) pro S/4 HANA Version zur Verfügung. Diese listet auf, welche Änderungen es zwischen den Systemen gibt. (Die aktuelle Version gibt es hier.)
  • Mit dem ABAP Test Cockpit (ATC) stellt die SAP ein mächtiges Tool zur Verfügung, dass auch für die Überführung der Eigenentwicklungen hilfreich ist.
  • Durch die Anpassungen des Codes können deutliche Performance Verbesserungen erreicht werden (besonders durch kluge Nutzung der HANA Datenbank).

 

ABAP Test Cockpit (ATC) für die Custom Code Migration

Das ATC ist ein sehr hilfreiches Tool bei der Überführung der ABAP-Entwicklungen. Gefüttert mit der erwähnten Simplification-Database durchforstet der ATC einen spezifizierbaren Code-Bereich und findet automatisiert verbesserungsbedürftige Codezeilen.

Über die Transaktion „ATC“ (mind. Releasestand SAP_BASIS 7.51) wird das ABAP Test Cockpit aufgerufen und sieht dann wie folgt aus:

Oberfläche des ABAP Test Cockpits
Oberfläche des ABAP Test Cockpits

Hier können wir unter anderem Testläufe einplanen, Ergebnisse betrachten oder verschiedene Testverfahren einstellen. In der Regel dauert ein Testlauf mehrere Stunden und sollte deshalb gut vorbereitet sein. Ein besonders wichtiger Punkt ist dabei die Konfiguration des ATC. Hierzu werden hier in Kürze noch weiter Infos veröffentlichen- bleib dran :-).

Das Ergebnis des ATC ist eine Auflistung der Code-Fundstellen, die angepasst werden müssen.

Wirklich toll an dem ATC ist, dass dieser auf einem alleinstehenden System betrieben werden kann. Das hat drei entscheidende Vorteile:

  • Die zu testenden Systeme müssen nicht den Releasestand 7.51 erreichen
  • Nur auf einem System muss der ATC installiert und konfiguriert werden
  • Wir erhalten eine sehr präzise Übersicht für alle zu testenden Systeme in dem zentralen Check-System.

Dies wird in der folgenden Grafik deutlicher:

Funktionsweise des ATC, Quelle: www.sap.com
Funktionsweise des ATC, Quelle: http://www.sap.com

Abweichend zu der Grafik ist es bereits ab der SAP_BASIS-Version 7.51 möglich das ABAP Test Cockpit zu nutzen. Wenn das geschehen ist, haben wir die Möglichkeit über RFC-Verbindungen alle betreffenden Systeme anzuschließen. Damit sind wir befähigt die Custom Code Analyse auf allen angeschlossenen Systemen per Remote durchzuführen.

Damit haben wir nun einen Überblick zur Vorbereitung der Custom Code Migration für SAP S/4HANA erhalten.
Im Teil 2 werde ich detaillierter auf die praktische Anwendung des ATCs, die Fundstellenliste im Checksystem, die Navigation zum Target-System und Beispiele für die Codeanpassungen eingehen – es bleibt spannend!

 

Empfohlene ergänzende Hinweise gibt es hier!

Blog-Artikel der SAP: „SAP S/4HANA System Conversion – Custom code adaptation process“
SAP Note: „SAP S/4HANA: Empfehlungen zur Anpassung von benutzerdefiniertem Quelltext“

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion oder schreibt mir direkt an henning.gerdes@cgi.com

 

Komm zu Uns!
Du programmierst, bist ABAP oder FIORI-interessiert und hast Lust coole Projekte mit uns zu realisieren?
Dann schau doch mal in unserer Stellenbeschreibung vorbei.

 

SAP Screen Personas – die Alternative zu Fiori und Co

Um individuelle Lösungen bereitzustellen, die User Experience zu erhöhen, Unternehmensprozesse zu optimieren und um Inhalte auf mobilen Endgeräten in der richtigen Qualität zur Verfügung zu stellen, wird in der SAP-Welt häufig mit Fiori-Apps gearbeitet. Das dies aus Aufwands- und Kostensicht nicht immer der geeignete Weg ist, wollen wir in diesem und folgenden Blogbeiträgen zusammen erfahren. Das Werkzeug was wir euch vorstellen wollen ist SAP Screen Personas.

Status Quo mit Fiori und Co.

In der SAP Welt gibt es weit mehr als 200.000 Standard-Transaktionen. Jedoch existieren nur knapp über 10.000 Standard Fiori-Apps. Es kommen zwar nach und nach immer mehr Standard Fiori-Apps hinzu. Dennoch wird es noch sehr lange dauern bis diese nahezu das gesamte SAP-Spektrum abdecken. Dies entspricht dann auch lediglich dem SAP-Standard-Spektrum. Kundeneigene Transaktionen (Z-Transaktionen) bleiben hiervon komplett unberührt.

Möchten wir gewährleisten, dass Geschäftsprozesse einem ganzheitlichen und modernen Stil entsprechen und zusätzlich gemäß des Lean-Ansatzes optimal auf die kundenspezifischen Bedürfnisse ausgerichtet sind, so ist es unumgänglich sich mit eigenem Design zu befassen. Auf der einen Seite haben wir natürlich die Möglichkeit eigene Fioris zu entwickeln. Dies ist jedoch meist mit großem Aufwand verbunden, denn im Wesentlichen wird hier etwas komplett neues aufgebaut. Das ergibt Sinn, wenn man eine völlig neue Logik aufbauen und flexibel gestalten möchte. Sollen hingegen die vorhandenen Standard-Transaktionen modernisiert und auf den jeweiligen Nutzer angepasst werden, so sind Fiori und Co. ggf. die falschen Ansätze, da diese mit viel Kosten- und Zeitaufwand verbunden sind.

Prozesse mit SAP Screen Personas

Mit weit geringerem Aufwand können wir jedoch mit SAP Screen Personas bestehende Prozesse verbessern:

SAP Screen Personas ist ein kostenloses Add-On von SAP. Es ist ab Netweaver 7.4 kompatibel und ermöglicht uns bspw. bekannte SAP Transaktionen in der Optik zu modernisieren und den Informationsüberfluss zu reduzieren. Es ermöglicht uns folglich den Content in optimal zusammengefasster Form, dem End-User zur Verfügung zu stellen. SAP Screen Personas setzt dabei auf die vorhandene Businesslogik der jeweiligen Transaktion.

Transaktionen, wie z.B. die ME53N (Anzeige Bestellanforderungen), besitzen über 15 verschiedene Reiter, über 100 Ein- und Ausgabefelder und Tabellen mit über 25 Spalten. Der Grund hierfür ist, dass Standard-Transaktionen für jeden Nutzer aus verschiedenen Branchen anwendbar sein müssen. Einzelne Kunden benötigen zumeist jedoch nur sehr wenige Informationen innerhalb einer solchen Standard-Transaktion. Mit SAP Screen Personas lassen sich solche komplexen Transaktionen ohne Probleme und in wenigen Minuten reduzieren. Nicht genutzte Informationen müssen so auch gar nicht zur Anzeige kommen. Das kann uns viele Klicks ersparen und steigert die Übersichtlichkeit. Unternehmen können so bspw. langfristig den Einarbeitungs-/Trainingsaufwand im Umgang mit der Transaktion reduzieren.

Dies ist selbstverständlich nur das einfachste Beispiel inwieweit wir die UX verbessern und die Produktivität steigern können.

Fazit und Ausblick

Zusammengefasst können wir also sagen, dass sich SAP Screen Personas besonders dann eignet, wenn Geschäftsprozesse nicht fundamental neu gestaltet werden sollen. Also kurz gesagt eine UI5-Entwicklung finanziell keinen Sinn ergibt, die UX und die Produktivität jedoch deutlich nach Optimierungen schreien. Das SAP UI5 und Fioris natürlich auch zahlreiche Vorteile bieten, haben wir ja schon zu Genüge aufgezeigt. Stöbert dazu doch einfach mal in unserem Blog:

https://developers4sap.blog/category/fiori/

Wenn ihr euch auch mal in der Praxis mit SAP Screen Personas beschäftigen wollt, könnt ihr euch ja schon einmal mit der kostenlosen Installation beschäftigen. Die SAP bietet dazu zahlreiche Hilfen an. Z.B. findet ihr unter folgendem Link auf dem SAP Help Portal eine detaillierte Beschreibung der Installation:

https://help.sap.com/viewer/ea09d73800744056a761296a70e357f2/Current/en-US/3463c03dd76349388ce30024bf14b6f1.html

Wenn ihr es nun nicht mehr aushaltet und direkt eure ersten Schritte mit SAP Screen Personas starten wollt, dann schaut doch einfach einmal im folgendem Blogartikel vorbei:

https://developers4sap.blog/deine-ersten-schritte-mit-sap-screen-personas/

Wenn ihr noch irgendwelche Fragen habt, dann nutzt doch einfach die Kommentarfunktion oder schreibt mir direkt via Mail: paul.holst@cgi.com

OData Services mit CDS Views erstellen, Teil 2

In dem vorherigen Teil der Blog-Serie habe ich über den technologischen Wandel auf der Hardware- und Software-Ebene im SAP-Umfeld erzählt und erläutert, was für einen Einfluss er auf die Entwicklung der OData-Services hat. In diesem Teil möchte ich in die technische Detail gehen und erklären, wie man anhand CDS-Views einen oData-Service erstellt.

Die Core Data Services gliedern die Datenbanktabellen als VDMs (Virtual Data Model) ein. Dadurch strukturiert man die Daten als logische Einheiten aus einer geschäftlichen Perspektive. Das liefert einen konsistenten Datenmodel und hilft uns einen Überblick auf die Daten zu schaffen. Die VDMs basieren auf 4 Schichten:

  • Datenbanktabellen (1.Schicht)
  • Basic Interface Views (2.Schicht)
  • Composite Interface Views (3.Schicht)
  • Consumption Views (4.Schicht)

Aufbau des Virtual Data Models in SAP S/4HANA

In diesem Blog-Eintrag konzentrieren wir uns auf den obersten Schicht des VDMs, da die oData-Services in den Consumption Views bereitgestellt werden.

Consumption Views:

Consumption-Views werden auf die Composite Interface Views gebaut und bilden den obersten Schicht des VDMs. Im Gegensatz zu Basic und Composite Views sind die Consumption Views für Wiederverwendung nicht geeignet. Sie sind erstellt, um die benötigten Daten für eine Anwendung oder ein System bereitzustellen. Es gibt 3 Arten von Consumption Views:

  • transkationale Consumption View
  • analytische Consumption Views (CUBE)
  • Web Service API für remote APIs

Um Eine Consumption View als oData-Service freizugegeben, öffnet man zuerst im ABAP Deveopement Tools die relevante DDL-Source. In der DDL-Source fügt man vor DEFINE die Annotation „@OData.publish: true“ hinzu:

Wenn DDL-Source aktiviert wird, schickt ADT eine Aktivierungsanfrage an SADL-Framework und generiert SADL-Framework die benötigten SAP Gateway Artefakte für die Aktivierung des oData-Services.

Ab diesem Punkt kann man wie gewöhnt  den oData-Service in der Transaktion /IWFND/MAINT_SERVICE aktivieren.

OData Services mit CDS-Views erstellen, Teil 1

In diesem Blog werden wir diskutieren, welche fachlichen und technischen Änderungen die CDS-Views beim Erstellen von OData-Services verursacht haben. Seitdem SAP seine neue UI-Technologie SAPUI5 eingeführt hat, ist der Begriff OData ein fester Bestandteil der Anwendungsentwicklung geworden, um die Geschäftsdaten im Web sinnvoll zu modellieren und zu konsumieren.

Bisher haben wir die OData-Services in R/3 in der Transaktion SEGW angelegt und die Geschäftslogik des Web-Services in SAP Workbench implementiert. Wie man einen OData-Service anlegen kann und welche Erstellungsmöglichkeiten zur Verfügung stehen, können Sie hier finden.

Im Laufe der Zeit hat SAP die Funktionalitäten und die Tools für die OData-Entwicklung verbessert und erweitert. Die weitreichendsten Änderungen kamen mit der Einführung der neuen Datenbanktechnologie HANA und der Erweiterung der bestehenden ERP-Software-Produktlinie mit S/4HANA. Das war eine grundlegende Änderung auf der Hardware- und Software-Ebene. Selbstverständlich hat die neukonzipierte In-Memory Datenbank HANA von uns ABAP- und Frontend-Entwicklern seinen Tribut gefordert. ABAP-Entwicklung unter SAP HANA folgt nicht mehr dem ressourcenintensiven „Data to Code“-Prinzip, wobei man alle anwendungsrelevanten Daten für Verarbeitung zuerst auf den Application-Server laden muss. Wir können jetzt mit CDS-Views auch die komplexeren Kalkulationen auf der Datenbank-Ebene machen. Dieses Prinzip überwindet den Bottle-Neck zwischen der Datenbank und dem Application-Server und verkürzt die Lese- und Verarbeitungszeiten von komplexen und großen Datenmengen.

Code-to-Data Prinzip bei SAP S/4HANA
Die neue Code-to-Data Prinzip bei SAP S/4HANA

Dieser Paradigmenwechsel hat auch die Entwicklung der OData-Services beeinflusst. Die CDS-Views haben das Erstellen der OData-Services auf der Datenbank-Ebene ermöglicht. Weiterhin kann man jetzt sogar die Benutzeroberflächen auf der Datenbank programmieren! Dadurch kann man sich die Geschwindigkeit der HANA-DB zunutze machen.

Systemanforderungen

Obwohl die CDS-Views grundsätzlich für die HANA-Systeme gedacht sind, stehen sie für Nicht-SAP-DBs und ältere SAP-Systeme zur Verfügung. Die Anforderungen für das Erstellen von OData-Services anhand CDS-Views für SAP-Systeme sind wie folgt:

  • SAP NetWeaver 7.4 SP05
  • SAP NetWeaver 7.50, SP01, oder höher.
  • S/4HANA
  • SAP HANA SPS6
  • SAP Business Suite EHP7 (Suite on HANA)
  • SAP Business Warehouse 7.3

 

ADT & CDS-Annotationen

Die CDS-Views werden nicht im ABAP Workbench programmiert. SAP hat die benötigten Programmiertools für HANA unter den Namen ADT (ABAP Developement Tools) für Eclipse IDE als Plug-Ins zur Verfügung gestellt (https://tools.hana.ondemand.com/#abap). Das heißt nicht, dass man jetzt völlig außerhalb der SAP-Kontext arbeiten muss aber es ist sicherlich eine Vereinfachung für die Neugeneration-Programmierer, die sich bereits während des Studiums mit Eclipse auseinandersetzt haben.

Weiterhin kann man im OData-Service durch die neueingeführten SAP-Annotationen die Darstellung von den Benutzeroberflächen festlegen. Filtereigenschaften von den Tabellen, Draft-Handling, Eingabeprüfungen, semantische Zusammenhänge zwischen Werte und ihre Einheiten uvm. können hier definiert werden. Das heißt, dass die CDS-Annotationen genug Daten liefern, die in der SAP Web IDE die Nutzung der App-Templates ermöglichen. Dadurch kann man mit wenig Programmieraufwand innerhalb wenigen Minuten eine Fiori-Anwendung auf die Beine bringen.

Alles was oben steht, wird nach der Erstellung einer CDS-View mit einer einzigen SAP-Annotation als OData-Service zur Verfügung gestellt: @OData.publish: true. Wenn die CDS-View als Repository Objekt erzeugt wird, kann der generierte OData-Service wie gewöhnt in der Transaktion /IWFND/MAINT_SERVICE gefunden werden.

Obwohl die obengenannten Änderungen uns Entwickler herausfordern, bringen sie die viele Vorteile mit. In dem zweiten Teil des Blogs werde ich die technischen Aspekten erklären, wie die CDS-Views aufgebaut sind und wie man anhand der CDS-View einen OData-Service erzeugen kann.

In dem zweiten Teil dieser Blog-Serie werden wir in die technische Details gehen und zeigen, wie man einen OData-Service mit CDS Views erstellt.

 

CORRESPONDING – MAPPING: nicht namensgleiche Felder mappen

Ich bin letztens auf ein Problem gestoßen, auf das bestimmt viele schon einmal gestoßen sind: Ich habe zwei Tabellen und möchte die Werte von der einen Tabelle in die andere übertragen. Easy going möchte man meinen, ich kann ja einfach MOVE-CORRESPONDING verwenden. Aber was macht man, wenn ein oder mehrere der Felder nicht namensgleich sind wie bei den beiden untenstehenden Tabellen?

lt_table1
name
pernr
validfrom
validto
lt_table2
name
pernr
vdatu
bdatu

Natürlich könnte ich umständlich über die Tabellen loopen und darin die entsprechenden Felder mappen. Aber ich war auf der Suche nach einer besseren Lösung und bin auf den Komponentenoperator CORRESPONDING gestoßen, allerdings mit dem optionalen Zusatz MAPPING.

Nach diesem Zusatz kann eine Mapping-Regel angegeben werden, die die Default-Regel, dass nur gleichnamige Felder einander zugewiesen werden, überschreibt.
So konnte ich angeben welche Felder gemappt werden sollen und war in der Lage, mein Problem mit nur einer Zeile zu lösen.

lt_table1 = CORRESPONDING #( lt_table2 MAPPING validfrom = vdatu validto = bdatu ).

SAP on your mobile device – Entwicklungsmöglichkeiten für die Mobile-App-Erstellung – Teil 2 – Fiori Client

Im ersten Teil haben wir uns mit der Erstellung von Hybrid Apps beschäftigt. Heute schauen wir uns die Erstellung des Fiori Clients an, über den ich bereits hier berichtet habe. Der Fiori Client ist eine gute Browser-Alternative, mit dem wir das Fiori-Launchpad in einen mobilen Container packen können. Um was es sich genau beim Fiori Client handelt zeige ich dir in diesem Beitrag. Lies diesen Beitrag gerne, bevor du anfängst den Fiori Client eigenständig zu bauen.

Fiori Client

Ich stelle dir die Entwicklungsmöglichkeit mit der Verwendung eines Services in der SAP Cloud Platform vor. Es gibt zudem eine lokale Entwicklungsumgebung, auf die ich hier jedoch nicht eingehe. Beide Möglichkeiten bringen Vor- und Nachteile mit sich, daher wirst du im Anschluss für dich selbst am Besten entscheiden können, welche sinnvoll für dich ist.

Entwicklung in der SAP Cloud Platform

Mit Hilfe der SAP Cloud Platform kannst du dir schnell deinen eigenen Standard Fiori Client erstellen, mit eigenen Icons und eigenem Splashscreen.

Öffne deine SAP Cloud Platform Cockpit und aktiviere den Service „Mobile Services“. Diesen Service hatten wir bereits ersten Teil verwendet.

Anschließend springen wir in den Service.

Im Mobile Service navigieren wir zu „Mobile Applications“->“Native/Hybrid“.

Da wir einen neuen Fiori Client erstellen möchten, klicken wir auf „New“.

Die folgenden Einstellungen müssen eingetragen werden:

  • Config Templates: SAP Fiori
  • ID: eine App-ID, die der Signatur bzw. dem Provisioning Profile entspricht
  • Name: der Titel der App
  • Description: eine passende Beschreibung zur App
  • Vendor: der Name / die ID deiner Organisation

Im Bereich „Assigned Features“ finden sich alle zur Verfügung Plugins bzw. Features.

Hier entfernen wir ein automatisch hinzugefügtes Feature. Die Connectivity Funktion wird benötigt, wenn wir passende Destinations eingerichtet haben und anbinden wollen. Dies ist in diesem Beispiel nicht der Fall.

Man gelangt automatisch zurück in die Übersicht unserer soeben erstellen Applikation. Wir hätten die Möglichkeit über das Plus-Zeichen weitere Funktionalitäten der App hinzuzufügen.

Folgende Features lassen sich implementieren:

  • App Update
  • Sample Back End
  • Client Resources
  • Connectivity
  • Document Repository
  • JSON Storage
  • Offline

Für unser Szenario lassen wir unsere Applikation jedoch schlank und bauen keine weiteren ein. Im nächsten Schritt müssen wir den Cloud Build anstoßen, in dem wir auch Daten zum Fiori Launchpad eingeben.

Im folgenden Dialog wählen wir zunächst Custom Fiori Client aus.

Anschließend können die Parameter entsprechend angepasst werden und bei Fiori Destination Name wählen wir Custom URL aus und geben in der Fiori Destination URL den Link zum entsprechenden Launchpad ein. Falls du kein passendes Launchpad zur Verfügung hast, kannst du an dieser Stelle https:\\www.google.de verwenden.

Anschließend klicken wir im Dialog weiter auf „Next“. Der Dialog „Android Builds“ kann ausgelassen werden. Im Bereich „Multimedia“ können wir App Icons ersetzen.

Anschließend müssen wir die Signing Profiles auswählen, mit denen die Applikation gebaut werden soll.

Solltest du momentan kein iOS-Signing Profile zur Verfügung haben, dann lass dir einfach ein entsprechendes Android-Profil generieren.

Die entsprechenden Build Options sprechen für sich. Solltest du beispielsweise den Privacy Screen ausschalten wollen, dann setze einen entsprechenden Haken.

Klicke auf „Finish“, um den Build Job anzulegen.

Im unteren Bereich wird der Build Job angezeigt. Mit diesem Job kann man nun die einzelnen Applikationen erstellen, indem man auf „Build“ klickt.

Der Erstellungsprozess kann eine Weile dauern.  Nach einiger Zeit springt der Status des Jobs auf grün und die Install Funktion steht zur Verfügung.

Die ipa-Datei für iOS und die apk-Datei für Android werden direkt in der SAP Cloud Platform bereitgestellt.

Scanne den Barcode für dein entsprechendes Gerät und du gelangst zur Seite für die Installation. Sobald du auf „Install“ klickst wird die Applikation heruntergeladen und installiert.

Viel Spaß mit deinem ersten Fiori Client, den du über den Mobile Service erstellt hast.

Solltest du noch weitere Fragen haben, nutze gerne die Kommentar-Funktion oder schreibe mich direkt per Mail an: tim.schomaker@cgi.com

Freue dich schon auf die nächsten Teile 3 und 4 zum Thema mobile Entwicklung. 🙂

SAP Cloud Application Programming Model: Custom Logic

Moin! Wir kehren heute mal wieder zur SAP Cloud Foundry zurück um unseren OData Service mit Custom Logic zu erweitern. Benjamin hatte hier ja schon beschrieben, wie wir mit dem SAP Cloud Application Programming Models und Core Data Services (CDS) einfach einen OData Service erstellen können. Wenn Du den Beitrag noch nicht kennst, dann lauf mal schnell rüber und lerne dort die Grundlagen, auf denen wir diesmal aufbauen. Ich warte hier so lange.

Wieder da? Sehr schön, dann fangen wir mal an!
Wie schon im letzten Beitrag bleiben wir diesmal bei DB und Service und lassen die UI erstmal außen vor. Stattdessen beschäftigen wir uns damit, wie wir unsere Services mit Custom Logic erweitern können, wenn uns die generierten CDS nicht reichen. Dazu gebe ich zwei Beispiele, wie wir eigene Logik in unseren OData Service einbauen können.

[0]: Vorraussetzungen

Dieser Blog geht davon aus, dass du den ersten Teil des Beitrags durchgearbeitet hast. Außerdem solltest du zumindest über grundlegende Java-Kenntnisse verfügen. Das trifft auf dich zu? Dann können wir ja gleich loslegen!

[1]: Hooks

CDS stellen uns eine Reihe von „Hooks“ zur Verfügung, für die wir eigene Handler-Methoden schreiben können, die dann je nach Hook zu unterschiedlichen Zeitpunkten im Ablauf der Transaktion durchlaufen werden. Wir beschränken uns hier auf die folgenden Hooks, die wir für jede Operation für jede Entität ausbilden können.

  • Before<Operation> Hook –> wird vor Ausführung der Operation (CRUD) durchlaufen, Annotation @Before<Operation>
  • After<Operation> Hook –> wird nach der Ausführung der Operation (aber noch vor Commit/Rollback) durchlaufen, Annotation @After<Operation>

Die Web IDE stellt uns hierfür einen hilfreichen Wizard zur Verfügung: Wir klicken mit der rechten Maustaste auf unseren srv-Ordner und wählen New -> Entity Operation Hooks aus.

Im Wizard wählen wir die gewünschten Entitäten aus und drücken auf Next und dann Finish. Und schwups, legt uns die Web IDE an der richtigen Stelle eine Java-Klasse mit auskommentierten Methoden-Stubs an:

Wir sehen hier, dass die Methoden alle schon die nötigen Annotationen haben und diese die Entität und unseren Service-Namen als Parameter haben. Um eine Methode zu nutzen, kommentieren wir diese und alle benötigten Importe einfach ein. Lass deiner Fantasie vollen lauf, was die Implementierung deiner Custom Logic angeht! Bei mir ist das einfach eine Validierung, dass die Menge an OrderItems größer Null ist. Zugegebenermaßen nicht sehr einfallsreich, aber dir fällt da sicher mehr ein. Zur Anregung findest du hier die Javadocs für das SAP Cloud Application Programming Model.

@BeforeCreate(entity = "OrderItems", serviceName = "CatalogService")
public BeforeCreateResponse beforeCreateOrderItems(CreateRequest req, ExtensionHelper helper) {
	EntityData data = req.getData();

	if (Integer.parseInt(data.getElementValue("OI_Quantity").toString()) < 1) {
		return BeforeCreateResponse.setError(ErrorResponse.getBuilder()
                           .setMessage("Quantity must be greater 0").response());
	} else {
		return BeforeCreateResponse.setSuccess().response();
	}
}

Die Möglichen Return-Statements sind uns in den Stubs dabei sogar schon angelegt, wir wählen einfach aus, was wir wo brauchen und passen den Fehlertext an.

[2]: Generic Operation Overrides

Bisher haben wir gesehen, wie wir uns vor und nach einer Operation in die Transaktion einhängen können und z.B. Validierungen druchführen können. Aber manchmal ist es nötig, dass wir die Operation selbst an unsere eigenen Wünsche anpassen. Hierzu können wir die generischen Operationen überschreiben, indem wir eine entsprechend annotierte Methode anlegen und implementieren. Auch hierfür stellt uns die Web IDE freundlicherweise einen Wizard zur Verfügung, auch wenn wir diesmal noch etwas mehr Hand anlegen müssen.

Wieder klicken wir mit der rechten Maustaste auf den srv-Ordner. Diesmal wählen wir aber New -> Custom Entity Operations aus. Wieder wählen wir die gewünschten Entitäten aus und drücken Next sowie Finish.

Die generierte Datei stellt uns wieder auskommentierte Funktionen zur Verfügung, die auch annotiert sind. Allerdings fehlt uns in der Signatur der Methoden noch ein Input-Parameter, der ExtensionHelper. Dies habe ich hier für die Methode zum Überschreiben der read-Operation hinzugefügt.

Jetzt geht es wieder daran, dass wir unsere gewünschte Logik für die Operation implementieren. Ich bleibe wieder bei einem einfachen Beispiel: Ich möchte das Produkt nur dann zurückgeben, wenn es auch vorrätig ist.

@Read(entity = "Products", serviceName = "CatalogService")
public ReadResponse readProducts(ReadRequest req, ExtensionHelper extensionHelper) {
    DataSourceHandler handler = extensionHelper.getHandler();

    EntityMetadata entityMetadata = req.getEntityMetadata();
    try {
	EntityData entityData = handler.executeRead(entityMetadata.getName(), req.getKeys(),
				entityMetadata.getFlattenedElementNames());

	if (Integer.parseInt(entityData.getElementValue("P_UnitsInStock").toString()) < 1) {
	    ErrorResponse errorResponse = ErrorResponse.getBuilder()
			.setMessage("Requested product not available or not in stock.")
                        .setStatusCode(404).response();
	    return ReadResponse.setError(errorResponse);
	} else {
	    return ReadResponse.setSuccess().setData(entityData).response();
	}

    } catch (DatasourceException ex) {
	ErrorResponse er = ErrorResponse.getBuilder().setMessage(ex.getMessage())
                                                     .setStatusCode(404).response();
	return ReadResponse.setError(er);
    }
}

Wenn wir uns die Methode nun anschauen, dann sehen wir, dass wir uns neben dem Handler, der die read-Operation zur Verfügung stellt, noch die Enititäts-Metadaten aus dem Request holen. Dies ist nötig, da wir für den Handler den Namen der Entität, sowie die flache Form von komplexen oder strukturierten Elementen benötigen. Im Fehlerfall oder wenn das Produkt nicht vorrätig ist, geben wir eine Fehlermeldung zurück, ansonsten die Daten, die wir gelesen haben.

Nun machen wir noch ein Build und starten unseren Java Service. Zu einem späteren Zeitpunkt schauen wir uns noch ein paar andere Möglichkeiten an, wie wir unser Projekt weiterentwickeln können, aber jetzt ist erst mal Kaffeezeit!

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

TMS Auftragsmonitor Ausgabe

Transport Management System: TMS Auftragsmonitor

Die Übersicht über SAP Transportaufträge im Transport Management System zu behalten, ist ohne ChaRM nicht einfach. In welche Systeme, welche Aufträge importiert sind, wird häufig manuell und fehleranfällig über eine Schattenbuchhaltung in Excel realisiert. In diesem Beitrag implementieren wir einen TMS Auftragsmonitor, der uns die Übersicht über die Importe liefert. Damit wird kein Auftrag mehr irgendwo vergessen. Dies ist der 2. Teil unserer TMS Helpertools, nachdem wir im 1. Teil einen Transport von Kopien Erzeuger implementiert haben.

Benutzung des TMS Auftragsmonitor

Beim Start des Programms ZBC_TMS_REQUEST_MONITOR öffnet sich der Selection Screen. Über das Start- und Endedatum wählst Du den zu analysierenden Zeitraum aus. Das Programm analysiert alle in diesem Zeitraum freigegebenen Aufträge. Optional kannst Du die Selektion auf ein oder mehrere Transportaufträge weiter einschränken.

TMS Auftragsmonitor Selektion

Das Programm selektiert die Aufträge und ermittelt aus der TMS Konfiguration die beteiligten Systeme (Test, PreProd, Produktion…). Für jedes gefundene System liest das Programm per Remote Function Call die Import Historie.

Die Ausgabe des Programms ist ein ALV Grid, in dem eine einzelne Zeile den Importstatus des Auftrags in den beteiligten Systemen anzeigt. Auf der Abbildung sieht man, dass die Systemlandschaft in diesem Fall aus dem Testsystem CC2, einem weiteren Testsystem CC7, dem PreProd System PL2 und dem Produktivsystem PC2 besteht. Für jedes System ist der Import Returncode sowie der Importzeitpunkt sichtbar. Die Erzeugung der Grid Spalten erfolgt dynamisch, da die SAP Systemlandschaft bei Dir sicherlich anders aussieht.

TMS Auftragsmonitor Ausgabe

In unserem Beispiel ist noch kein Auftrag in CC7 importiert. Der Auftrag EC2K915770 ist in alle Systeme bis zur Produktion PC2 importiert. Der Auftrag EC2K915837 ist nur in das CC2 importiert. Die Importe in die Folgesysteme sind noch ausstehend. Im Grid siehst Du ausstehende Importe daran, dass die Zellen leer sind.

Über Hyperlinks im Grid kann zum einen zum Transportauftrag und zum anderen zum Transportprotokoll navigiert werden. Die Abbildung zeigt den Aufrag EC2K915837. Gut zu sehen ist, dass er für den Import in PC2 und PL2 vorgemerkt ist und in CC2 bereits importiert wurde. Dies ist also ein Auftrag, der das Produktivsystem noch nicht erreicht hat und im Status Test ist.

Transportprotokoll

TMS Import Historie

Die Importhistorie ist im SAPGUI in Transaktion STMS nach Auswahl des Systems und Wählen des Menüpunkt Springen\Import Historie zugreifbar. Das Programm kann also aus dem Entwicklungssystem heraus ermitteln, wo die Aufträge bereits importiert wurden. Deshalb ist dieses Programm vollständig funktionsfähig, wenn Du es in Deinem Entwicklungssystem implementierst.

Importhistorie

Implementierung

Den Code kannst Du hier herunterladen. Das Programm ZBC_TMS_REQUEST_MONITOR besteht aus den folgenden Teilen

  • Klasse LCL_UI_CTR: UI Logik zur Darstellung der Infos im Grid
  • Klasse LCL_TMS_CTR: Auswertung der Aufträge und Import Historien
  • Interface LIF_CONST: Typen und Konstanten, welche durch UI- und TMS-Controller benutzt werden
  • Klasse LCL_TEST: Unit Test Klasse für den TMS-Controller
  • Klasse LCX_T100: Ausnahmeklasse, welche eine T100-Nachricht kapselt

TMS Auftragsmonitor Implementierung

Implementierung Importhistorie auswerten

Die Logik zur Auswertung der Import Historie steckt zum größten Teil in der Methode lcl_tms_ctr, query_request. Die Aufträge werden aus den Tabellen e070 und e07t selektiert. Die Importhistorie für einen Zeitraum und einem SAP System geschieht über Funktionsbaustein TMS_TM_GET_HISTORY. Intern nutzt der Baustein RFCs, um via den RFC-Destinationen TMSADM* die Informationen aus den Systemen zu ermitteln. Das ist übrigens unter dem Sicherheitsaspekt kein Problem, da der verwendete Systembenutzer TMSADM im Mandanten 000 nur die relevanten TMS-Funktionsbausteine aufrufen kann und sonst nichts.

  METHOD query_request.
    DATA lt_request TYPE lif_const=>tt_request.
    DATA lt_sysnam TYPE lif_const=>tt_synam.
    DATA lv_domnam TYPE tmsdomnam.
    FIELD-SYMBOLS <lv_sysnam> TYPE tmssysnam.
    DATA lv_start_date TYPE d.
    DATA lv_start_time TYPE t VALUE '000000'.
    DATA lv_end_date TYPE d.
    DATA lv_end_time TYPE t VALUE '235959'.
    DATA lt_alog TYPE tmstpalogs.
    DATA ls_alert TYPE stmscalert.

    REFRESH et_request_mon.

*   Aufträge gemäß Selektion selektieren
    SELECT
      e070~trkorr
      e070~trfunction
      e070~as4date
      e070~as4time
      e07t~as4text
     FROM e070 LEFT OUTER JOIN e07t ON
      e070~trkorr = e07t~trkorr
*     Keine Selektion über e07t~langu, weil es unterschiedliche
*     Sprachen sein können
     INTO TABLE lt_request WHERE
        e070~trfunction IN (lif_const=>gc_enum_trfunction-workbench_request, lif_const=>gc_enum_trfunction-customizing_request) AND
        e070~trstatus = lif_const=>gc_enum_trstatus-released AND
        e070~trkorr IN it_rng_trkorr AND
        e070~as4date BETWEEN iv_start_date AND iv_end_date.

    IF sy-subrc <> 0.
      lcx_t100=>raise_exc_by_msg( |Keine freigegebenen Cust/Wrkb-Aufträge in Zeitraum gefunden| ).
    ENDIF.

*   Aus der TMS Konfiguration die beteiligten System sowie die Domäne ermitteln
    read_tms_config(
      IMPORTING
        et_sysnam = lt_sysnam
        ev_domnam = lv_domnam ).

    lv_start_date = iv_start_date.
    lv_end_date = iv_end_date.

*   Für jeden System die Importhistorie ermitteln
    LOOP AT lt_sysnam ASSIGNING <lv_sysnam>.

      CALL FUNCTION 'TMS_TM_GET_HISTORY'
        EXPORTING
          iv_system     = <lv_sysnam>
          iv_domain     = lv_domnam
        IMPORTING
          et_tmstpalog  = lt_alog
          es_exception  = ls_alert
        CHANGING
          cv_start_date = lv_start_date
          cv_start_time = lv_start_time
          cv_end_date   = lv_end_date
          cv_end_time   = lv_end_time
        EXCEPTIONS
          alert         = 1
          OTHERS        = 2.

      IF sy-subrc <> 0.
*       Adaptiert aus FuBa TMW_TM_GET_HISTORY.
*       Nachricht TP 702: Keine Protokolleinträge im selektierten Bereich
        IF ( lt_alog IS INITIAL AND
           ls_alert-msgid = 'TP' AND
           ls_alert-msgno = '702' ) OR
           ls_alert-error <> 'OK'. "Fehler bei Zugriff auf das System

*         System ignorieren
          CONTINUE.
        ELSE.
          lcx_t100=>raise_exc_by_sy( ).
        ENDIF.
      ENDIF.

*     Sortierung des alogs wegen Binary Search in Methode fill_request_mon
      SORT lt_alog BY trkorr.

*     Ausgabetabelle in Parameter et_request_mon füllen
      fill_request_mon(
        EXPORTING
          iv_sysnam = <lv_sysnam>
          it_request = lt_request
          it_alog = lt_alog
        CHANGING
          ct_request_mon = et_request_mon ).

    ENDLOOP.
  ENDMETHOD.

Die Methode fill_request_mon bereitet die Infos aus der Importhistorie auf. Der folgende Debugger Screenshot zeigt die in dieser Methode aufgebaute Datenstruktur.

TMS Auftragsmonitor Interne Datenstruktur

Implementierung UI Grid aufbauen

Die Implementierung des UI Controllers ist nicht ganz so einfach, weil die Datenstruktur für das Grid nicht statisch definierbar ist. Für jedes SAP System der Transportdomäne werden 2 Spalten für Returncode und Importzeitpunkt benötigt. Der Code nutzt deshalb exzessiv dynamische Programmierung zur Erzeugung von Datentypen -objekten (Klassen cl_abap_structdescr etc., ABAP-Kommando CREATE DATA). Die folgende Methode  lcl_ui_ctr, init_ui_data_structure zeigt exemplarisch den dynamischen Aufbau der UI Struktur für das ALV Grid. Ergebnis der Methode ist eine Referenz auf eine interne Tabelle, welche die Felder passend zur SAP Systeminfrastruktur enthält.

  METHOD init_ui_data_structure.
    DATA lr_tabledescr TYPE REF TO cl_abap_tabledescr.
    DATA lr_structdescr TYPE REF TO cl_abap_structdescr.
    DATA lt_comp TYPE abap_component_tab.
    DATA ls_comp TYPE abap_componentdescr.

*   Jedes Feld aus Type lif_const=>ts_request aufnehmen
    ls_comp-name = 'TRKORR'.
    ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'TRKORR' ).
    APPEND ls_comp TO lt_comp.

    ls_comp-name = 'TRFUNCTION'.
    ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'TRFUNCTION' ).
    APPEND ls_comp TO lt_comp.

    ls_comp-name = 'AS4DATE'.
    ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'AS4DATE' ).
    APPEND ls_comp TO lt_comp.

    ls_comp-name = 'AS4TIME'.
    ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'AS4TIME' ).
    APPEND ls_comp TO lt_comp.

    ls_comp-name = 'AS4TEXT'.
    ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'AS4TEXT' ).
    APPEND ls_comp TO lt_comp.

*   Jetzt für jedes System die Infos aus lif_const=>ts_request_imp aufnehmen
    LOOP AT it_sysnam ASSIGNING FIELD-SYMBOL().
      ls_comp-name = get_fieldname(
          iv_fieldname = 'RETCODE'
          iv_sysnam    =  ).
      ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'TRRETCODE' ).
      APPEND ls_comp TO lt_comp.

      ls_comp-name = get_fieldname(
          iv_fieldname = 'TRTIME'
          iv_sysnam    =  ).
      ls_comp-type ?= cl_abap_elemdescr=>describe_by_name( 'TSTAMP' ).
      APPEND ls_comp TO lt_comp.

    ENDLOOP.

    lr_structdescr = cl_abap_structdescr=>create(
        p_components = lt_comp ).

    lr_tabledescr = cl_abap_tabledescr=>create(
        p_line_type  = lr_structdescr ).

    CREATE DATA rr_request_mon_ui TYPE HANDLE lr_tabledescr.

  ENDMETHOD.

Die folgende Abbildung aus dem Debugger zeigt die gefüllte interne Tabelle, mit welcher das Grid befüllt wird.

TMS Auftragsmonitor UI Struktur

Fazit

Der TMS Auftragsmonitor liefert eine gute Übersicht, in welche Zielsysteme freigegebene Transportaufträge noch zu transportieren sind. Er ist ein Workaround, wenn kein ChaRM eingesetzt wird. In unseren Projekten finden wir jedoch häufig diese Situation vor und müssen entsprechend damit umgehen. Von der Implementierung her zeigt er, wie man in ABAP gegen dynamische Datenstrukturen programmiert.

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion!

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP
SAP Transaktion ME21N

Ranorex Testautomatisierung mit SAP Funktionsbausteinen

Ranorex ist primär ein Werkzeug für den Test auf Ebene von User Interfaces (UI). SAP lässt sich mit Ranorex analog zu Web und Windows Anwendungen auf dieser Ebene testen. Eine bessere Alternative zum Test auf UI-Ebene ist der Test auf Serviceebene gemäß der Test Pyramide. Vorteile sind die bessere Wartbarkeit der Testfälle sowie die schnellere Ausführung. Im SAP wird die Serviceebene häufig durch Funktionsbausteine implementiert. In diesem Beitrag beschreibe ich die Ranorex Testautomatisierung mit SAP Funktionsbausteinen.

Services im SAP

Im SAP wird die Serviceebene häufig durch Funktionsbausteine implementiert. Z.B. implementiert der Funktionsbaustein BAPI_PO_CREATE1 das Anlegen einer Bestellung. Der Funktionsbaustein ist das Pendant auf Service Ebene zur SAP-Transaktion ME21N, über welche eine Bestellung auf UI-Ebene angelegt wird. Eine Automatisierung der SAP-Transaktion ME21N ist aufwendig und fehleranfällig, weil sie sehr verschachtelt ist und viele Tabellen benutzt, in denen die UI-Objektadressierung schwierig ist.

SAP Transaktion ME21N

Die folgende Abbildung zeigt die Schnittstelle des Funktionsbaustein BAPI_PO_CREATE1. Die Schnittstelle besteht aus Vielzahl von Struktur- und Tabellenparametern. Bevor man den Baustein via Ranorex automatisiert aufruft, sollte man zunächst die richtige Parametrisierung ermitteln, da dies in vielen Fällen komplex ist. Deshalb ist es sinnvoll, den Funktionsbaustein zunächst im SAP via ABAP Unit Tests zu testen.

Funktionsbaustein BAPI_PO_CREATE1

Ranorex Testautomatisierung mit SAP Funktionsbausteinen

Damit Ranorex auf SAP-Funktionsbausteine zugreifen kann, muss der kostenlose SAP Connector for Microsoft .NET in die Ranorex Solution eingebunden werden. Der Connector stellt eine API zur Verfügung, die in einem Ranorex Coding Module verwendet werden kann.

Voraussetzung für den Zugriff auf einen Funktionsbaustein ist dessen Remote-Fähigkeit (RFC). Für einen eigenentwickelten Funktionsbaustein ist dies durch ein simples Aktivieren dieser Eigenschaft bei der Definition des Funktionsbausteins zu erreichen. Bei einem durch SAP entwickelten Baustein ist dies unter Umständen nicht der Fall. Hier muss der aufzurufende SAP-Funktionsbaustein durch einen eigenentwickelten RFC-Funktionsbaustein umhüllt werden. Für eine Vielzahl von Testszenarien existieren aber bereits RFC-Funktionsbausteine von SAP selber.

Einbinden des SAP Connectors in die Ranorex Solution

Der Connector besteht aus 4 DLLs. In die Ranorex Solution sind sapnco.dll und sapnco_utils.dll als References aufzunehmen.

Solution Reference SAP .Net Connector

Implementierung des Testfalls als Ranorex Coding Module

Der Aufruf des SAP Funktionsbausteins wird in Ranorex als Coding Module implementiert.

Die Connector-API ist im Namespace SAP.Middleware.Connector implementiert. Deshalb wird dieser zunächst mit der using-Direktive bekannt gemacht.

Über den RfcDestinationManager werden die Verbindungsinformationen zum SAP gelesen. Wie diese konfiguriert werden, wird weiter unten erläutert.

Ein SAP-Funktionsbaustein wird im Connector als IRfcFunction verfügbar gemacht. In diesem Fall ist zu beachten, dass neben dem Funktionsbaustein BAPI_PO_CREATE1 auch der Funktionsbaustein BAPI_TRANSACTION_COMMIT benötigt. Der Hintergrund ist, dass der Funktionsbaustein BAPI_PO_CREATE1 kein Commit der übergebenen Daten auf der SAP-Datenbank durchführt. Dies muss durch einen expliziten Aufruf von BAPI_TRANSACTION_COMMIT durchgeführt werden.  Über den RfcSessionManager wird dem Connector mitgeteilt, dass beide Aufrufe auf der gleichen SAP-Verbindung ausführen soll.

Auf die Parameter des Funktionsbausteins wird über IrRfcStructure und IrfcTable zugegriffen. In diesem Beispiel sind die folgenden Parameter zu füllen. Die Füllung der Parameter ist abhängig vom Testfall und den Einstellungen des SAP-Systems.

  • POHEADER: Kopfdaten der Bestellung wie Lieferant, Buchungskreis etc.
  • POHEADERX: Flags, welche Kopfdaten gefüllt wurden
  • POITEM: Positionsdaten der Bestellung wie Material, Menge und Werk
  • POITEMX: Flags, welche Positionsdaten gefüllt wurden

Die im Code sichtbaren Variablen wie DocType, CompCode etc. sind Modulvariablen, damit das Code Module über eine Data Source parametrisiert werden kann.

private void bapiPurchaseOrderCreate()
        {
        	const string TRUE = "X";
            RfcDestination destination = RfcDestinationManager.GetDestination("NCO_TESTS");
            IRfcFunction bapiPoCreate = destination.Repository.CreateFunction("BAPI_PO_CREATE1");
            IRfcFunction bapiTransCommit = destination.Repository.CreateFunction("BAPI_TRANSACTION_COMMIT");
            
            RfcSessionManager.BeginContext(destination);
            
            try {
                IRfcStructure poHeader = bapiPoCreate.GetStructure("POHEADER");
                poHeader.SetValue("DOC_TYPE", DocType);
                poHeader.SetValue("COMP_CODE", CompCode);
                poHeader.SetValue("PURCH_ORG", PurchOrg);
                poHeader.SetValue("PUR_GROUP", PurGroup);
                poHeader.SetValue("VENDOR", Vendor);
                
                IRfcStructure poHeaderX = bapiPoCreate.GetStructure("POHEADERX");
                poHeaderX.SetValue("DOC_TYPE", TRUE);
                poHeaderX.SetValue("PURCH_ORG", TRUE);
                poHeaderX.SetValue("COMP_CODE", TRUE);
                poHeaderX.SetValue("PURCH_ORG", TRUE);
                poHeaderX.SetValue("PUR_GROUP", TRUE);
                poHeaderX.SetValue("VENDOR", TRUE);
                
                IRfcTable poItems = bapiPoCreate.GetTable("POITEM");
                poItems.Append();
                poItems.SetValue("PO_ITEM", PoItem1);
                poItems.SetValue("MATERIAL", Material1);
                poItems.SetValue("PLANT", Plant1);
                poItems.SetValue("QUANTITY", Quantity1);
                
                IRfcTable poItemsX = bapiPoCreate.GetTable("POITEMX");
                poItemsX.Append();
                poItemsX.SetValue("PO_ITEM", "00001");
                poItemsX.SetValue("MATERIAL", TRUE);
                poItemsX.SetValue("PLANT", TRUE);
                poItemsX.SetValue("QUANTITY", TRUE);
                
                bapiPoCreate.Invoke(destination);
                
                IRfcTable bapiReturnTab = bapiPoCreate.GetTable("RETURN");
                checkReturn( bapiReturnTab );
                
                string poNumber = bapiPoCreate.GetString("EXPPURCHASEORDER");
                Ranorex.Report.Success("PoNumber:" + poNumber);
                
                bapiTransCommit.Invoke(destination);
            }    
            finally
            {
                RfcSessionManager.EndContext(destination);
            }
        }

Durch den Aufruf der Invoke-Methode auf dem IRfcFunction-Objekt wird der Aufruf durchgeführt. Die Nummer der Bestellung wird über den Parameter EXPPURCHASEORDER zurückgegeben und als Success-Meldung in das Ranorex Log ausgegeben.

Als letzter Schritt wird über RfcSessionManager.EndContext die SAP-Verbindung ordnungsgemäß abgebaut.

Die Ermittlung, ob der Aufruf erfolgreich war, ist in Methode checkReturn implementiert. SAP RFC-Funktionsbausteine melden üblicherweise über einen RETURN-Parameter zurück, ob der Aufruf erfolgreich war. Der RETURN-Parameter bildet eine Meldung ab. Eine Fehlermeldung wird mit dem TYPE = E signalisiert. Die Methode checkReturn iteriert über die Tabelleneinträge und prüft das Feld TYPE einer Tabellenzeile. Ist die Meldung ein Fehler, wird eine Ausnahme mit dem Meldungstext geworfen. In allen anderen Fällen wird die Meldung in das Ranorex Log ausgegeben. Über den RETURN-Parameter wird auch die Erfolgsmeldung (hier: „Normalbestellung unter der Nummer 4500017782 angelegt“) zurückgeliefert.

private static void checkReturn(IRfcTable bapiReturnTab) {
			for (int i=0; i<bapiReturnTab.Count; ++i) {
				bapiReturnTab.CurrentIndex = i;
				string msg = bapiReturnTab.GetString("MESSAGE");
                Ranorex.Report.Info(msg);
                string msgType = bapiReturnTab.GetString("TYPE");
                
                if (msgType == "E") {
                    throw new System.Exception(msg);
                }
			}
        }

Die folgende Abbildung zeigt den Aufbau des RETURN-Parameters.

SAP Return Parameter

Konfiguration der SAP-Verbindung

Die Konfiguration der SAP-Verbindungsparameter (Hostname, Benutzer, Kennwort…) erfolgt über die app.config-Datei in der Ranorex Solution, welche beim Build der Solution in die Datei bin\Debug\<RanorexSolution>.exe.config kopiert wird.

Die ConfigSections aus diesem Beispiel können ohne Anpassung übernommen werden. Die Verbindung zum SAP wird in einem destinations-XML-Element definiert. In der Abbildung sind 3 verschiedene Destinations sichtbar, die sich in ihrer Parametrisierung unterscheiden. Durch den Code benutzt wird lediglich die Destination NCO_TESTS. Hier sind neben den SAP-Systeminformationen ASHOST und und SYSNR auch die Benutzerinformationen USER, PASSWD, CLIENT und LANG einzugeben. Die Destination NCO_LB_TESTS ist ein Beispiel, wenn der SAP-Zugriff über den SAP Message Server erfolgen soll.

Jede Ranorex Solution besitzt eine Standard app.config, welche nur das XML-Element startup enthält. Die Reihenfolge, wie die für den Connector erforderlichen XML-Elemente in die app.config eingefügt werden, ist relevant.

app.configfür Connector

Fazit

Das war es schon! Die Ausführung des Testfalls sollte das Ergebnis so aussehen wie in der folgenden Abbildung. Wir haben gesehen, dass Ranorex nicht nur für das UI Testing, sondern auch für die Testautomatisierung auf Service Ebene geeignet ist. Im Unterschied zum UI Testing braucht man hierfür jedoch Entwicklerkenntnisse in .Net.

SAP Service Test Ausführung

 

 

Veröffentlicht in ABAP
TOC Erzeuger Selektion

Transport Management System: Transport von Kopien Erzeuger

Eine typische Situation bei der ABAP Entwicklung ist, dass ein IT-Test wegen fehlender Testdaten nicht im Entwicklungssystem, sondern nur im Testsystem möglich ist. Steht ChaRM zur Verfügung, ist das schnell gemacht: ChaRM transportiert die Entwicklung als Transport von Kopien mit dem Transport Management System. Wenn nun aber kein ChaRM verfügbar ist, muss man den Transport von Kopien Transportauftrag dauernd manuell anlegen. In diesem Beitrag bauen wir einen kleinen Transport von Kopien Erzeuger, der uns diese manuelle fehleranfällige Tätigkeit abnimmt.

Benutzung des Transport von Kopien Erzeuger

Beim Start des Programms ZBC_TMS_TOC_CREATE öffnet sich der Selection Screen. Auf dem Screen wählt man über die Suchhilfe ein oder mehrere Aufträge aus. Das Coding prüft bei Erzeugung des Aufrags, dass nur änderbare Workbench- bzw. Customizing-Aufträge ausgewählt wurden.

TOC Erzeuger Selektion

Nach der Erzeugung des Transport von Kopien Auftrags wird dieser direkt geöffnet, damit man das Ergebnis nochmal prüfen kann. Auf der folgenden Abbildung sind gut die CORR MERGE Einträge zu sehen, welche die Quellaufträge und deren Aufgaben referenzieren.

TOC Erzeuger Ergebnis

Wenn der Auftrag ok ist, kannst Du ihn über das Menü freigeben und danach über Transaktion STMS in das Testsystem importieren.

TOC Erzeuger Freigabe

Implementierung

Den Code kannst Du hier herunterladen. Das Programm ZBC_TMS_TOC_CREATE besteht aus den folgenden Teilen

  • Klasse LCL_UI_CTR: UI Logik wie Suchhilfe und Öffnen des Auftrags.
  • Klasse LCL_TOC_CTR: Logik der Transport von Kopien Erzeugung
  • Interface LIF_CONST: Konstanten für Auftragsart und -status, welche das Coding benutzt.
  • Klasse LCL_TEST: Unit Test Klasse für die TOC Erzeugung

 

TOC Erzeuger Implementierung

Die wesentliche Logik des Programms steckt in der Methode LCL_TOC_CTR, CREATE_TOC. Die Methode erhält eine Tabelle von Auftragsnummern aus dem UI Controller. Zunächst prüft die Methode read_request, ob der Auftrag auch existiert. Weiterhin prüft die Logik, dass nur Workbench bzw. Customizing Aufträge übergeben wurden und dass alle Aufträge änderbar sind.

Für die übergebenen Aufträge ermittelt Funktionsbaustein TRINT_FOLLOW_STRKORR_BACKWARD die zugehörigen Aufgaben.

In Methode create_toc_init wird der intiale (=leere) TOC Auftrag angelegt. Das Hinzufügen der Objektlisten aus den Quellaufträgen zum TOC Auftrag macht der Funktionsbaustein TRINT_MERGE_COMMS.

  METHOD create_toc.
    FIELD-SYMBOLS <lv_trkorr> TYPE trkorr.
    DATA ls_request_header TYPE trwbo_request_header.
    DATA lt_request_header_src TYPE trwbo_request_headers.
    DATA lv_msg TYPE string.
    DATA lv_as4text TYPE as4text.
    DATA lt_e070 TYPE trwbo_t_e070.

    IF lines( it_trkorr ) = 0.
*     Wählen Sie einen vorhandenen Auftrag aus
      MESSAGE e773(tk) INTO lv_msg.
      raise_exc_by_sy( ).
    ENDIF.

*   Übergebene Aufträge prüfen
    LOOP AT it_trkorr ASSIGNING <lv_trkorr>.

      ls_request_header = read_request( <lv_trkorr> ).
      APPEND ls_request_header TO lt_request_header_src.

*     Prüfen ob Auftrag Workbench oder Customizing ist
      IF NOT ( ls_request_header-trfunction = lif_const=>gc_enum_trfunction-workbench_request OR
               ls_request_header-trfunction = lif_const=>gc_enum_trfunction-customizing_request ).

        MESSAGE e000(swlt) WITH |Auftrag { <lv_trkorr> } ist nicht Cust/Work| INTO lv_msg.
        raise_exc_by_sy( ).
      ENDIF.

      IF ls_request_header-trstatus <> lif_const=>gc_enum_trstatus-modifiable.
        MESSAGE e000(swlt) WITH |Auftrag { <lv_trkorr> } muss änderbar sein| INTO lv_msg.
        raise_exc_by_sy( ).
      ENDIF.

    ENDLOOP.

*   Aufgaben aller Quellaufträge sammeln
    CALL FUNCTION 'TRINT_FOLLOW_STRKORR_BACKWARD'
      CHANGING
        ct_requests = lt_request_header_src.

*   TOC Request erzeugen: Name wie der 1. Aufrag mit Prefix TOC
    lv_as4text  = |TOC { lt_request_header_src[ 1 ]-as4text }|.
    rs_request_header_toc = create_toc_init( lv_as4text ).

    MOVE-CORRESPONDING lt_request_header_src TO lt_e070.

*   Objektlisten aus den Quellaufgaben zum TOC Auftrag hinzufügen
    CALL FUNCTION 'TRINT_MERGE_COMMS'
      EXPORTING
        it_e070                = lt_e070
      CHANGING
        cs_target_request      = rs_request_header_toc
      EXCEPTIONS
        db_access_error        = 1
        invalid_target_request = 2
        invalid_source_request = 3
        OTHERS                 = 4.
    IF sy-subrc <> 0.
      raise_exc_by_sy( ).
    ENDIF.

  ENDMETHOD.

Anpassung des Zielsystems

Das Zielsystem des erzeugten TOC Auftrags musst Du bei der Implementierung des Programms auf Deinem System noch anpassen. Aktuell steht bei jedem Auftrag GWY als Zielsystem drin.

TOC Erzeuger Zielsystem

Im Code ist das die Methode lcl_toc_ctr, create_toc_init. Diese Methode erzeugt den intialen TOC Auftrag.

METHOD create_toc_init.

    CALL FUNCTION 'TR_INSERT_REQUEST_WITH_TASKS'
      EXPORTING
        iv_type           = lif_const=>gc_enum_trfunction-transport_of_copies
        iv_text           = iv_as4text
        iv_target         = 'GWY' "TODO: Zielsystem anpassen
      IMPORTING
        es_request_header = rs_request_header
      EXCEPTIONS
        insert_failed     = 1
        enqueue_failed    = 2
        OTHERS            = 3.
    IF sy-subrc <> 0.
      raise_exc_by_sy( ).
    ENDIF.

  ENDMETHOD.

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion!

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Veröffentlicht in ABAP

SAP on your mobile device – Entwicklungsmöglichkeiten für die Mobile-App-Erstellung – Teil 1 – Hybride Apps

Herzlich Willkommen in einer Genertion, in der man mit mobilen Geräten und Apps aufwächst!

Mittlerweile hat auch SAP den Shift verstanden und einen weiteren Fokus gesetzt. Die App-Entwicklung, beziehungsweise die mobile Entwicklung im SAP-Kontext hat mittlerweile einen Stellenwert. Vor allem auch die Kooperation mit Apple zeigt den erweiterten Fokus auf die mobile Branche.

In dieser Blogserie möchte ich dir die Entwicklungsmöglichkeiten vorstellen, sowie die einzelnen Apptypen. Wir beginnen mit hybriden Ansätzen und arbeiten uns zu nativen Apps vor.

 

Hybride App

Zunächst einmal die Hybrid App. Wie auch in der klassischen Mobilentwicklung ist im Kontext der SAP die Hybrid-App nicht außer Acht zu lassen. Da Fiori-Apps stets im Kontext von HTML5, Javascript und CSS und daher im Aufruf von Browsern zu betrachten sind, ist die Entwicklung von Hybrid-Apps die naheliegenste bei bereits existierend Fiori-Apps. Hierdurch gewährleistet man im Handumdrehen die Mobilisierung von bereits bestehenden Fiori-Apps, ohne großartig Nachentwicklungen durchführen zu müssen.

Was benötigst du für die Entwicklung einer Hybrid-App?

Zunächst aktiviere in deiner SAP Cloud Platform Umgebung den Mobile Service. Dieser ist zuständig für das letztliche builden der Applikation.

Konfiguriere ggf. in den Mobile Services

Anschließend öffne die WebIDE und aktiviere das Hybrid Application Toolkit in der WebIDE wie folgt. Vergiss das Aktivieren nicht zu sichern.

Bildergebnis für hybrid application toolkit

 

Lade die WebIDE neu. Mit einem Rechtsklick auf ein bereits bestehendes Fiori-Projekt wirst du erkennen, dass es eine neue Dropdown-Kategorie namens „Mobile“ gibt. Dies ist ein Feature, das wir durch das Aktivieren des Hybrid Application Toolkits bereitstellen konnten.

Du kannst das Projekt als Hybrid Mobile Project freigeben. Wichtig ist, dass es sich bei deiner App um eine Standalone-App handelt und die erstellte App kompatibel ist mit den Richtlinien für die Hybride App-Entwicklung.

In der unteren rechten Ecke bekommt ihr angezeigt, dass euer Fiori-Projekt mobilisiert wird.

Wenn euer Projekt vollständig mobilisiert wurde bekommt ihr den folgenden Hinweis in der oberen rechten Ecke eurer WebIDE.

Ein erneuter Rechtsklick auf das Projekt zeigt jetzt, dass es mehr Funktionen zum Mobile Development Feature gibt.

Ihr habt die Möglichkeiten unter „Select Cordova Plugins“ die Cordova Plugins auszuwählen, die ihr mit in der App haben möchtet oder nicht. Ähnlich zur lokalen Erstellung des Fiori Clients kann man mit dieser Funktion Plugins rauswerfen oder einbeziehen, um die App so schlank wie möglich zu halten, oder um wichtige Informationen mit einzubeziehen.

Unter „Build Packaged App“ kannst du die entsprechende App generieren mit Icons, dem Signing Profile und weiteren Einstellungsmöglichkeiten.

 

Für die Erstellung der Signing Profiles benötigst du einen Account bei developer.apple.com, solltest du deine Hybrid App auch für Apple-Geräte erstellen wollen. Mach dir die Entwicklung einfacher, indem du iOS abwählst, solltest du keinen Developer-Account bei Apple besitzen. Ein Android-Signing-Profile hingegen kannst du sehr einfach über das Fenster generieren lassen.

 

Klicke auf „Build“ im Confirmation-Reiter und die App wird im Hintergrund erstellt.

Der Mobile Service zeigt das Builden der App und das Sammeln der Cordova-Plugins an.

Dieser Prozess kann einige Minuten dauern. Schnapp dir einen Kaffee, oder einen Tee, oder beides! 🙂

Anschließend bekommst du das folgende Popup:

 

Scanne den QR-Code mit deinem Android- oder iOS-Gerät und melde dich mit deinen Credentials an. Anschließend wird die Applikation auf dein Gerät installiert und du kannst deine Fiori-Apps über die hybride App starten.

 

Das war der erste Teil der Reihe SAP on your mobile device – Entwicklungsmöglichkeiten für die Mobile-App-Erstellung. Ich wünsche dir auch viel Spaß mit den weiteren Teilen, in denen ich näher auf den Fiori Client, auf Native Apps und auf SAP Mobile Cards.

BOPF EPM Datengenerator

SAP BOPF: EPM Datengenerator

Beim Schreiben der beiden Blogs zum SAP BOPF API ( Teil 1, Teil 2 ) hatte ich zufällig gesehen, dass es den EPM Datengenerator  zum Füllen der EPM BOPF-Tabellen für Salesorder, Business Partner, Product etc. mit Demodaten gibt. /BOBF/EPM_DATA_GENERATOR ist ein ausführbares Programm und es gibt ein Problem: Das Programm bricht mit einem Laufzeitfehler ab. Das ist wirklich störend, weil man so die EPM BOPF Geschäftsobjekte nicht gut ausprobieren kann. Die EPM BOPF Geschäftsobjekte zeigen sehr viele Aspekte der BOPF Modellierung und Implementierung. In diesem Blogbeitrag wollen wir den EPM Datengenerator reparieren.

EPM Datengenerator Laufzeitfehler

Bei der Ausführung des Programms /BOBF/EPM_DATA_GENERATOR kommt es zu einem ASSERTION_FAILED-Laufzeitfehler. Problem ist die Transformation der XML-Produktdaten aus dem MIME-Repository in eine interne ABAP-Tabelle mit der Simple Transformation /bobf/tr_product_data.

BOPF EPM Datengenerator Dump

Irgendwo in den XML-Daten fehlt das XML-Element web_resource. Ich habe mir das im XML angeschaut und nicht gefunden. Das ist aber nicht das einzige Problem im Daten Generator: Wenn man diese Methode DESERIALIZE_PRODUCT_DATA überspringt, kommt der nächste Laufzeitfehler „13.0561555# nicht als Zahl interpretierbar“ in Methode map_bp_data. Irgendwo in den XML Business Partner Daten ist im Addressfeld latitude ein Tab-Zeichen am Ende der Zahl.

Diese Probleme sind auf den 1. Blick erstaunlich, weil der Standard EPM Datengenerator auf genau den gleichen XML-Daten aufsetzt. Der Standard EPM Datengenerator ist das Programm SEPM_DG_EPM_STD_CHANNEL, welches die SNWD_*-Tabellen füllt. Der Unterschied ist jedoch, dass die Transformation von XML nach ABAP nicht über Simple Transformations, sondern manuell erfolgt.

BOPF EPM Datengenerator Dump Debugger

Reparatur des EPM Datengenerator

Um die Probleme im Datengenerator zu beheben, bauen wir die Logik zum Lesen der Quelldaten und Transformieren in die Zielstrukturen einfach neu. Wir kopieren die Quellklasse /BOBF/CL_EPM_DATA_GENERATOR  in eine neue Klasse und implementieren 4 neue DESERIALIZE-Methoden. Den Code der kopierten Klasse kannst Du hier herunterladen.

BOPF EPM Datengenerator Neue ABAP Klasse

Die 4 neuen DESERIALIZE-Methoden setzen jetzt nicht mehr auf den XML-Daten auf, sondern selektieren aus den EPM Standard SNWD_*-Tabellen. Die Transformation in die Zielstrukturen erfolgt also nicht mehr mit Simple Transformations, sondern ganz stumpf in ABAP.  Den ganzen Rest der Klasse mit den Map- und Update-Methoden lassen wir unberührt.

BOPF EPM Datengenerator Execute Methode Neu

Die folgende Abbildung zeigt exemplarisch das Lesen der Produktdaten aus den Standard EPM Tabellen snwd_pd und snwd_texts. Die gelesenen Daten werden transformiert und in die Member Variable mt_products gespeichert. Die später im Rahmen der Excecute-Methode ausgerufene Methoden map_product_data und update_product_data mappen die Produktdaten auf die BOPF-Strukturen und schreiben sie in die EPM BOPF Tabellen /bobf/d_pr_root und /bobf/d_pr_name fort.

BOPF EPM Datengenerator DeserializeProduct Neu

Zusammenfassung

Der von SAP gelieferte BOPF EPM Datengenerator /BOBF/EPM_DATA_GENERATOR funktioniert nicht. In diesem Blog haben wir das durch eine partielle Neuimplementierung repariert. Damit auch auf Deinem System die EPM BOPF Tabellen mit Demodaten gefüllt werden, führe folgende Schritte aus:

  1. Programm SEPM_DG_EPM_STD_CHANNEL ausführen. Dies füllt die EPM Standardtabellen SNWD_*
  2. Reparierter BOPF EPM Datengenerator herunterladen und im System einspielen.
  3. Reparierter BOPF EPM Datengenerator ausführen: Execute Methode in der SE80 in der Testumgebung ausführen.

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP
BOPF Business Object Builder

SAP BOPF API Teil 2

Nachdem wir uns im 1. Teil im Allgemeinen mit der SAP BOPF API und im speziellen mit der Implementierung der CRUDQ-Operationen Create, Read und Update beschäftigt haben, folgt nun der 2. Teil mit den restlichen Operationen Delete und Query. Weitere Themen bei der Implementierung gegen die SAP BOPF API sind Assoziationen, Actions (entspricht OData Function Imports) und Ausnahme- und Transaktionsbehandlung.

Das Coding aus dem 1. Teil hat bereits diese Funktionalitäten umfasst. Du kannst ihn hier herunterladen und dann einfach in Deinem System implementieren. Die Unit Test Klasse findest Du hier.

1. DELETE_HEADER

Die delete_header Methode ist einfach zu implementieren. Das geschieht wieder über die uns wohlbekannte Methode modify des Service Managers.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->DELETE_HEADER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_KEY                         TYPE        /BOBF/CONF_KEY
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD delete_header.
    DATA lr_change   TYPE REF TO /bobf/if_tra_change.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.
    DATA lt_mod      TYPE /bobf/t_frw_modification.
    FIELD-SYMBOLS   <ls_mod> TYPE /bobf/s_frw_modification.

    APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
    <ls_mod>-node        = /bobf/if_epm_sales_order_c=>sc_node-root.
    <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_delete.
    <ls_mod>-key         = iv_key.

    mr_service_mgr->modify(
      EXPORTING
        it_modification = lt_mod
      IMPORTING
        eo_change       = lr_change
        eo_message      = lr_message ).

    raise_exc_by_bopf_msg( lr_message ).

  ENDMETHOD.

In der Modification Struktur sind der Knotentyp, der Change_mode sowie der Datenbankschlüssel zu füllen. Ein gute Hilfe, wie die Modification Struktur abhängig vom Szenario zu füllen ist, liefert übrigens die Dokumentation zu /bobf/s_frw_modification.

BOPF API Modification Struktur Doku

2. QUERY_HEADER

Die Query_header Methode erwartet für jedes selektierbare Feld eine Range-Tabelle. Auf Seiten der BOPF API benutzen wir die query Methode des Service Managers. Die query Methode erwartet im Parameter it_selection_parameters die Selektionsparameter als eine einzige Range-Tabelle.

Über den Parameter is_query_options können Parameter für das Paging (top, skip, maxRows) sowie Sortieren übergeben werden. Wir halten es hier einfach und benutzen nur maxRows.

Wichtig zu wissen ist, dass die query Methode direkt das Ergebnis des Datenbank SELECT zurückgibt. Es wird also nicht geprüft, ob ein zurückgegebener Datensatz in der aktuellen Transaktion bereits geändert wurde. Dieses Verhalten ist identisch zu persistenten ABAP Klassen und stellt somit keine Überraschung dar.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->QUERY_HEADER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_MAX_ROW                     TYPE        I (default =0)
* | [--->] IT_RNG_SO_ID                   TYPE        SABP_T_RANGE_OPTIONS(optional)
* | [--->] IT_RNG_SO_STATUS               TYPE        SABP_T_RANGE_OPTIONS(optional)
* | [--->] IT_RNG_LCHG_DATE_TIME          TYPE        SABP_T_RANGE_OPTIONS(optional)
* | [<---] ET_HEADER                      TYPE        /BOBF/T_EPM_SO_ROOT
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD QUERY_HEADER.
    DATA ls_query_options TYpe /bobf/s_frw_query_options.
    DATA lt_query_selparam TYPE /bobf/t_frw_query_selparam.
    field-SYMBOLS <ls_query_selparam> type /BOBF/S_FRW_QUERY_SELPARAM.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.

    ls_query_options-maximum_rows = iv_max_row.

*   Spezifischen Range SalesOrderId in die allgemeine Range Tabelle lt_query_selparam übernehmen
    LOOP AT it_rng_so_id ASSIGNING FIELD-SYMBOL(<ls_rng_so_id>).
      APPEND INITIAL LINE TO lt_query_selparam ASSIGNING <ls_query_selparam>.
      MOVE-CORRESPONDING <ls_rng_so_id> TO <ls_query_selparam>.
      <ls_query_selparam>-attribute_name = /bobf/if_epm_sales_order_c=>sc_node_attribute-root-so_id.
    ENDLOOP.

*   Spezifischen Range SalesOrder Status in die allgemeine Range Tabelle lt_query_selparam übernehmen
    LOOP AT it_rng_so_status ASSIGNING FIELD-SYMBOL(<ls_rng_so_status>).
      APPEND INITIAL LINE TO lt_query_selparam ASSIGNING <ls_query_selparam>.
      MOVE-CORRESPONDING <ls_rng_so_status> TO <ls_query_selparam>.
      <ls_query_selparam>-attribute_name = /bobf/if_epm_sales_order_c=>sc_node_attribute-root-so_status.
    ENDLOOP.

*   Spezifischen Range SalesOrder Change DateTime in die allgemeine Range Tabelle lt_query_selparam übernehmen
    LOOP AT it_rng_lchg_date_time ASSIGNING FIELD-SYMBOL(<ls_rng_lchg_date_time>).
      APPEND INITIAL LINE TO lt_query_selparam ASSIGNING <ls_query_selparam>.
      MOVE-CORRESPONDING <ls_rng_lchg_date_time> TO <ls_query_selparam>.
      <ls_query_selparam>-attribute_name = /bobf/if_epm_sales_order_c=>sc_node_attribute-root-lchg_date_time.
    ENDLOOP.

    mr_service_mgr->query(
      EXPORTING
        iv_query_key            = /bobf/if_epm_sales_order_c=>sc_query-root-select_by_elements
        it_selection_parameters = lt_query_selparam
        is_query_options        = ls_query_options
        iv_fill_data            = abap_true
      IMPORTING
        eo_message              = lr_message
        et_data                 = et_header ).

    raise_exc_by_bopf_msg( lr_message ).

  ENDMETHOD.

3. QUERY_ITEM_BY_HEADER_KEY

Eine Assoziation verknüpft BOPF Knotenelemente und wird datenbankseitig durch einen Fremdschlüssel abgebildet. Wir wollen hier die auf der Root-Knoten Salesorder Header definierte Assoziation ITEM benutzen, um ausgehend von einem Header Key die zugehörigen Salesorder Items zu  lesen.

BOPF API Assoziation

Die Methode query_item_by_header_key erhält den Schlüssel eines Salesorder Header und liest dann über die Assoziation Root->Item die Salesorder Items.  Typtechnisch werden die Items in einer interen Tabelle von Kombi-Strukturen zurückgegeben  (Tabellentyp /BOBF/T_EPM_SO_ITEM).

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->QUERY_ITEM_BY_HEADER_KEY
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_HEADER_KEY                  TYPE        /BOBF/CONF_KEY
* | [<---] ET_ITEM                        TYPE        /BOBF/T_EPM_SO_ITEM
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD query_item_by_header_key.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.

    mr_service_mgr->retrieve_by_association(
      EXPORTING
        iv_node_key             = /bobf/if_epm_sales_order_c=>sc_node-root "Source node
        it_key                  = VALUE #( ( key = iv_header_key ) )
        iv_association          = /bobf/if_epm_sales_order_c=>sc_association-root-item
        iv_fill_data            = abap_true
      IMPORTING
        eo_message              = lr_message
        et_data                 = et_item ).

    raise_exc_by_bopf_msg( lr_message ).

  ENDMETHOD.

4. Action CONFIRM_HEADER

Mit der Implementierung der CRUDQ-Operationen sind wir jetzt durch. Eine BOPF Action entspricht dem Funtion Import im OData Kontext. Wir wollen jetzt die Action CONFIRM definiert auf Header Ebene anbinden. Die Implementierung der Action in Klasse /BOBF/CL_EPM_SO_A_CHGE_STATUS ist schlicht: Das Datenbankfeld so_status wird ohne weitere Prüfung auf C gesetzt.

BOPF API Action

Die Action CONFIRM erwartet keine Import- und Exportparameter. Auf  Seite der BOPF API rufen wir deshalb auf dem Service Manager die Methode do_action ohne weitere Parameter nur mit dem Schlüssel der Salesorder auf.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->CONFIRM_HEADER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_KEY                         TYPE        /BOBF/CONF_KEY
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD confirm_header.
    DATA lr_change   TYPE REF TO /bobf/if_tra_change.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.

    mr_service_mgr->do_action(
      EXPORTING
        iv_act_key              = /bobf/if_epm_sales_order_c=>sc_action-root-confirm
        it_key                  = VALUE #( ( key = iv_key ) )
*        Action Confirm hat keine Import und Exportparameter
*        is_parameters           =
      IMPORTING
        eo_change               = lr_change
        eo_message              = lr_message ).

    raise_exc_by_bopf_msg( lr_message ).
  ENDMETHOD.

5. Ausnahmebehandlung

Die Methoden der BOPF API deklarieren keine Ausnahmen. Die Ausnahmebehandlung muss auf Seiten des Aufrufers durch Auswertung der Exportparameter eo_change und eo_message erfolgen. Über eo_change (Interface /BOBF/IF_TRA_CHANGE) lässt sich feststellen, ob es fehlgeschlagene Änderungen gab.

BOPF API Change Interface

Im Parameter eo_message wird ein (manchmal) Message Container mit Nachrichten zurückgegeben. Um nicht nach jedem BOPF API Aufruf die Ausnahmebehandlung erneut zu implementieren, habe ich die Methode RAISE_EXC_BY_BOPF_MSG implementiert. Der Aufrufer unserer DAO-Klasse soll im Fehlerfall eine klassenbasierte Ausnahme erhalten. Ich verwende hier die Demo Ausnahmeklasse CX_DEMO_DYN_T100, welche eine T100-Nachricht kapselt. In einem konkreten Anwendungsfall würde man eine Ausnahmeklasse analog zu CX_DEMO_DYN_T100 implementieren. Die Methode RAISE_EXC_BY_BOPF_MSG prüft, ob Fehlernachrichten vorliegen. Wenn ja, wird die 1. Fehlernachricht verwendet, um, die T100-Ausnahme zu erzeugen.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_RL_EPM_SO_DAO->RAISE_EXC_BY_BOPF_MSG
* +-------------------------------------------------------------------------------------------------+
* | [--->] IR_MESSAGE                     TYPE REF TO /BOBF/IF_FRW_MESSAGE
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD RAISE_EXC_BY_BOPF_MSG.
    DATA lt_message TYPE /bobf/t_frw_message_k.
    DATA lv_exc_msg(200) TYPE c.

    IF ir_message IS BOUND AND ir_message->check( ) EQ abap_true.
      ir_message->get_messages(
        EXPORTING
          iv_severity = /bobf/cm_frw=>co_severity_error
        IMPORTING
          et_message = lt_message ).

      lv_exc_msg = lt_message[ 1 ]-message->get_text( ).

*     &1&2&3&4
      MESSAGE e241(/BOBF/COM_GENERATOR) WITH lv_exc_msg+0(50) lv_exc_msg+50(50) lv_exc_msg+100(50) lv_exc_msg+150(50)
        INTO DATA(lv_sy_msg).

      raise_exc_by_sy( ).
    ENDIF.

  ENDMETHOD.

Für eine korrekte Ausnahmebehandlung reicht es nicht aus, nur auf die Parameter eo_change und eo_message zu schauen. Beispiele sind die read-Methoden READ_HEADER_BY_KEY und READ_HEADER_BY_SO_ID unserer DAO-Klasse, wo wir mittels der Service Manager Retrieve-Methode einen Datensatz lesen. Wird dieser nicht gefunden, wird keine Fehlernachricht gesetzt. Wir müssen deshalb hier prüfen, ob 1 Datensatz gefunden wurde.

    mr_service_mgr->retrieve(
      EXPORTING
        iv_node_key             = /bobf/if_epm_sales_order_c=>sc_node-root
        it_key                  = VALUE #( ( key = iv_key ) )
      IMPORTING
        eo_message              = lr_message
        et_data                 = lt_header ).

    raise_exc_by_bopf_msg( lr_message ).

    IF lines( lt_header ) = 0.
      raise_exc_by_msg( |Header { iv_key } nicht gefunden| ).
    ENDIF.

Für diesen Fall gibt es die Methode RAISE_EXC_BY_MSG. Sie erwartet einen String und wirft die T100-Ausnahme.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_RL_EPM_SO_DAO->RAISE_EXC_BY_MSG
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_MSG                         TYPE        CLIKE
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD raise_exc_by_msg.
    DATA lv_msg(200) TYPE c.

    lv_msg = iv_msg.

*   &1&2&3&4
    MESSAGE e241(/bobf/com_generator) WITH lv_msg+0(50) lv_msg+50(50) lv_msg+100(50) lv_msg+150(50)
      INTO DATA(lv_sy_msg).

    raise_exc_by_sy( ).

  ENDMETHOD.

Als 3. Möglichkeit eine T100-Ausnahme zu werfen, habe ich die Methode RAISE_EXC_BY_SY implementiert. Die Methode nimmt die T100-Nachricht aus den SY-Feldern. Diese Form von Ausnahmebehandlung brauchen wir, wenn wir Funktionsbausteine mit klassischer Ausnahmebehandlung aufrufen. In unserem Beispiel ist der Funktionsbaustein NUMBER_GET_NEXT, mit dem wir eine neue Salesorder Id ziehen.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_RL_EPM_SO_DAO->RAISE_EXC_BY_SY
* +-------------------------------------------------------------------------------------------------+
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD raise_exc_by_sy.

    RAISE EXCEPTION TYPE cx_demo_dyn_t100
      MESSAGE ID sy-msgid
      TYPE sy-msgty
      NUMBER sy-msgno
      WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.

  ENDMETHOD.

6. Transaktionsbehandlung

In der DAO-Klasse benutzen wir den Service Manager, um Datensätze zu lesen und zu schreiben. In der Praxis werde wir mehr als 1 DAO-Klasse haben, um z.B. die Zugriffe auf Items und Notes zu implementieren. Die Transaktionssteuerung sollte deshalb zentral außerhalb der DAO-Klassen erfolgen. In diesem Blog machen wir das in der setup-Methode der Unit-Test Klasse.

  METHOD setup.
    CREATE OBJECT mr_epm_so_dao.
    mr_transaction_mgr = /bobf/cl_tra_trans_mgr_factory=>get_transaction_manager( ).

*   Änderungen von der letzten Testmethode zurücksetzen. Ansonsten scheitern die
*   Testmethoden, weil diese auf der gleichen Salesorder rumändern
    mr_transaction_mgr->cleanup( ).
  ENDMETHOD.

Über /bobf/cl_tra_trans_mgr_factory=>get_transaction_manager( ) holen wir uns den den BOPF Transaction Manager. Das Speichern triggern wir in der Testmethode über die Methode save. Beim Commit work führt das BOPF Framework den generischen Verbucherbaustein /BOBF/CL_DAC_UPDATE aus, der pro zu ändernder Tabelle einmal aufgerufen wird. Der folgende Screenshot zeigt das Speichern im Debugger für die Unit Testmethode READ_UPDATE_HEADER. Standardmäßig verbucht der Transaction Manager synchron als local update task. Dies kann über den save-Parameter IV_TRANSACTION_PATTERN auf asynchrone Verbuchung umgestellt werden.

BOPF API Transaktion

7. SAP BOPF API Fazit

BOPF ist ein wichtiger Teil des ABAP Programming Model for SAP Fiori. BOPF steckt hinter den ObjectModel Annotations einer CDS View. Es gibt einer CDS View die Fähigkeit, nicht nur Daten zu lesen, sondern auch zu schreiben. Die in diesem Blog beschriebene BOPF API benötigt Du, wenn Du diese Geschäftsobjekte mittels Validations und Determinations erweiterst. Ein weiterer Anwendungsfall ist die Programmierung gegen die von SAP mit S/4HANA ausgelieferten Geschäftsobjekte.

Die  BOPF API ist ein bißchen tricky in ihrer Benutzung. Mit diesem Blog hoffe ich, dass ein bisschen klarer gemacht zu haben. Aufsetzend auf den von SAP ausgelieferten EPM Geschäftsobjekten lässt sich schnell das Know-How in der BOPF API als auch in BOPF im allgemeinen aufbauen.

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP

Test Seams: Mocking von Code Abhängigkeiten in ABAP Unit

ABAP Unit ist unter SAP Entwicklern wohl bekannt. In der Praxis wird ABAP Unit jedoch wenig eingesetzt. Ein häufiger Grund ist, dass der zu testende Code starke Abhängigkeiten zu Datenkonstellationen hat. Wenn man nun nicht die Möglichkeit hat, den Code für eine bessere Testbarkeit umzubauen, kann man Test Seams verwenden. Test Seams erlauben es, Quelltext im produktiven Teil des Programms auszutauschen. Das ermöglicht ein Mocking der Datenabhängigkeiten.

Systemvoraussetzungen für Test Seams

Um Test Seams nutzen zu können, benötigt man mindestens einen SAP Netweaver in der Version 7.5 oder aufwärts. Die Verwendung beschränkt sich auf Klassen und Funktionsgruppen.

Grundlagen der Test Seams

Test Seams bieten die Möglichkeit im produktiven Code Test-Code bzw. ein bestimmtes Verhalten zu injizieren. Das bedeutet, dass das System anstatt des produktiven Codes ein Test Seam ausführt. Das gilt nur, wenn man einen Unit-Test ausführt. Über einen normalen Aufruf über bspw. ein Programm wird der produktive Code ausgeführt.

Anwendung der Test Seams

Ein Test Seam umschließt den Code wie folgt.

TEST-SEAM [Test-Seam-Name].
*     produktiver Code
END-TEST-SEAM.

Bis jetzt der zu injizierende Code nicht bekannt, weshalb man eine Test Injection in der zugehörigen Unit-Test-Klasse definiert. Eine Test Injection ist der Code, welcher im Unit-Test anstatt des produktiven Codes ausgeführt wird. Allgemein definiert man eine Test Injection mit dem nachfolgenden Block.

TEST-INJECTION [Test-Seam-Name].
*    Injizierter Code
END-TEST-INJECTION.

Alle innerhalb des Test Seams sichtbaren Variablen sind innerhalb der Test Injection zugreifbar. In einer Test Injection kann man demzufolge also nicht auf die Variablen in der ABAP Unit Testklasse zugreifen.

Beispiele für Test Seams

Für die Beispiele habe ich die Klasse ZCL_MK_TEST_SEAM implementiert, welche nachfolgende Methoden und lokale Klassen beinhaltet. Die globale und lokale ABAP Unit Testklasse stehen als Download zur Verfügung. Die Logik setzt auf dem EPM Datenmodell (Tabellen SNWD_*) auf.

Test-Seams: Klassenimplementierung

Beispiel 1: Berechnung des Durchschnittsbruttobetrags von Kundenaufträgen

Berechnungen stellen häufig ein Problem hinsichtlich des Testens dar. Dabei ist die Berechnung selbst nicht das Problem, sondern die Datensätze, auf denen die Berechnung basiert. Hier eignen sich Test Seams hervorragend zum Mocken von Daten. Das bedeutet, dass man keine Datensätze aus der Datenbank lädt, sondern fiktive Datensätze bildet, um die Funktionalität zu testen. Hierzu ein kurzes Beispiel:

Die Methode GET_SO_AVERAGE gibt den Durchschnittswert der Bruttowerte der Kundenaufträge zurück. Die Methode enthält zwei weitere Funktionen: Die Selektion der Daten und die Berechnung selbst.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_MK_TEST_SEAM->GET_SO_AVERAGE
* +---------------------------------------------------------------------------------------------+
* | [<-()] RV_SUM_AMOUNT                  TYPE        SNWD_TTL_GROSS_AMOUNT
* +--------------------------------------------------------------------------------------
  METHOD GET_SO_AVERAGE.

    select_data( ).

    rv_sum_amount = calculate_so_average( ).

  ENDMETHOD.    

Der Fokus der Test Seams ist die Methode SELECT_DATA. Im produktiven Code ist es notwendig, dass alle Datensätze aus der Datenbank gelesen werden. Das bedeutet, dass der Durchschnittswert variabel zu den Daten der Datenbank ist. Deshalb setze ich hier das Test Seam average_selection ein.

    TEST-SEAM average_selection.
      SELECT * FROM snwd_so INTO TABLE mt_so_headers.
    END-TEST-SEAM.

Die Injection average_selection beschreibt die Datensätze, welche man mockt. Hierbei erzeuge ich bei einem Unit-Test drei Datensätze, welche immer dieselben Werte besitzen.
Mit der Methode CALCULATE_SO_AVERAGE wird der Durchschnitt der Brutto-Beträge berechnet.

    FIELD-SYMBOLS <fs_snwd_so> TYPE snwd_so.

    LOOP AT mt_so_headers ASSIGNING <fs_snwd_so>.
      rv_calculated_average = ( rv_calculated_average + <fs_snwd_so>-gross_amount ).
    ENDLOOP.

    rv_calculated_average = rv_calculated_average  / lines( mt_so_headers ).

Basierend auf der vorherigen Implementierung habe ich die Testmethode TEST_GET_SO_AVERAGE implementiert. Ich rufe die Methode GET_SO_AVERAGE auf und überprüfe anschließend, ob die Funktionalität richtig mit den gemockten Daten gerechnet hat.

*   Code injizieren
    TEST-INJECTION average_selection.
      FIELD-SYMBOLS <fs_snwd_so> TYPE snwd_so.

      APPEND INITIAL LINE TO mt_so_headers ASSIGNING <fs_snwd_so>.
      <fs_snwd_so>-gross_amount = 20.

      APPEND INITIAL LINE TO mt_so_headers ASSIGNING <fs_snwd_so>.
      <fs_snwd_so>-gross_amount = 50.

      APPEND INITIAL LINE TO mt_so_headers ASSIGNING <fs_snwd_so>.
      <fs_snwd_so>-gross_amount = 50.

    END-TEST-INJECTION.

*   Zu testenden Code ausführen
    DATA(lv_calculated_average) = mr_test_seam->get_so_average( ).

    cl_abap_unit_assert=>assert_equals(
      EXPORTING
        act                  = lv_calculated_average
        exp                  = 40  ).

Wenn die Funktionalität richtig rechnet, wird der Test erfolgreich sein.

Beispiel 2: Berechtigungsprüfung auf Business-Partner-ID

Berechtigungsprüfungen stellen häufig ein Problem für Unit-Tests dar, da der ausführende Benutzer nicht über die Berechtigung verfügt, die Funktionalität auszuführen. Hier finden Test Seams einen weiteren Anwendungsbereich. Nachfolgend findest du dafür ein Beispiel.

Die Methode HAS_AUTHORITY überprüft, ob der Benutzer für das Feld BP_ID der EPM-Business-Partner  Berechtigungen besitzt. Falls das der Fall ist, besitzt die Methode den Rückgabewert abap_true, andernfalls abap_false. Diese Überprüfung ist mit dem Test Seam authority umgeben. Bei der Ausführung des dazugehörigen Unit-Tests wird das Test Seam authority injiziert.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_MK_TEST_SEAM->HAS_AUTHORITY
* +---------------------------------------------------------------------------------------------+
* | [--->] IV_PARTNER_ID                  TYPE        SNWD_PARTNER_ID
* | [<-()] RV_HAS_AUTHORITY               TYPE        ABAP_BOOL
* +--------------------------------------------------------------------------------------
  METHOD has_authority.

    TEST-SEAM authority.

      AUTHORITY-CHECK OBJECT 'S_EPM_BP'
       ID 'EPM_BP_ID' FIELD iv_partner_id.
      IF sy-subrc = 0.
        rv_has_authority = abap_true.
      ELSE.
        rv_has_authority = abap_false.
      ENDIF.

    END-TEST-SEAM.

  ENDMETHOD.

Es ist nicht sichergestellt, dass der aufrufende Benutzer des Unit-Tests auch wirklich die Berechtigung für das Feld BP_ID besitzt, führt man die Injection authority aus.
Da ich Berechtigungen auf das EPM-Modell habe, setze ich den Wert in der Test Injection auf abap_false. In anderen Fällen könnte ich damit überprüfen, ob nur für ganz bestimmte Benutzer bspw. eine Transaktion aufgerufen werden kann.

Die Testmethode TEST_AUTHORITY_CHECK ruft die ursprünglich implementierte Funktionalität auf und vergleicht die Ergebnisse. Da das Test Seam den Wert abap_false für has_authority injiziert, wird der Test erfolgreich sein.

*   Code injizieren
    TEST-INJECTION authority.
      rv_has_authority = abap_false.
    END-TEST-INJECTION.

*   Zu testenden Code ausführen
    DATA(lv_has_authority) = mr_test_seam->has_authority( '4711' ).

    cl_abap_unit_assert=>assert_equals(
      EXPORTING
        act                  = lv_has_authority
        exp                  = abap_false ).

Fazit

Test Seams ermöglichen das Injizieren von Testcode im Rahmen von ABAP Unit. Dadurch kann man bspw. Datenabfragen mocken, Berechtigungen umgehen oder Berechnungen überprüfen. Insbesondere bei Anwendungen, die über Jahre hin gewachsen sind und der Code viele Abhängigkeiten besitzt, zeigen Test Seams ihre Stärken.

Ein alternativer Ansatz ist das Test Double-Framework von SAP. Allerdings können dabei nur Interfaces verwendet werden und nicht Klassen oder Funktionsgruppen, bei denen man direkt im Code die zu testenden Stellen kennzeichnet. Zudem ist es viel schwieriger, gegen die API des Test Double-Frameworks zu implementieren.

Insgesamt finde ich, dass sie eine richtig nützliche und pragmatische Erweiterung für Unit-Tests sind.

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion.

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Veröffentlicht in ABAP
BOPF Business Object Builder

SAP BOPF API Teil 1

Das Business Object Processing Framework (BOPF) ist ein Teil des ABAP Programming Model for SAP Fiori. BOPF steckt hinter den ObjectModel Annotations einer CDS View. BOPF gibt einer CDS View die Fähigkeit, nicht nur Daten zu lesen, sondern auch zu schreiben. In diesem Blog beschäftigen wir uns mit der SAP BOPF API. Wir implementieren CRUDQ Operationen auf dem ausgelieferten Demo Geschäftsobjekt /BOBF/EPM_SALES_ORDER und nutzen dabei die SAP BOPF API. In der SAP Doku ist das nur sehr theoretisch beschrieben. Deshalb dieser Blog, um das mal dem konkreten Beispiel EPM Salesorder auszuprobieren.

Die Relevanz von BOPF wird auch durch die 500 implementierten BOPF-Geschäftsobjekte in einem S/4HANA-System (siehe Transaktion BOBX) unterstrichen. In einem ECC6 EHP8 gibt es nur knapp 60 BOPF-Geschäftsobjekte.

Den Code kannst Du hier herunterladen und dann einfach in Deinem System implementieren. Die Unit Test Klasse findest Du hier.

1. SAP BOPF Demo Geschäftsobjekt /BOBF/EPM_SALES_ORDER

Die folgende Abbildung zeigt das Demo Geschäftsobjekt /BOBF/EPM_SALES_ORDER (Transaktion BOBX). Es besteht aus dem Root Knoten Salesorder Header (Datenbanktabelle /BOPF/D_SO_ROOT) und den Knoten für Items und Notes. Das einem Item zugeordnete Produkt ist im BOPF-Geschäftsobjekt /BOBF/EPM_PRODUCT implementiert. In diesem Blog arbeiten wir im nur mit dem Header.

Business Object Builder ABAP

Als Entwicklungsumgebung für BOPF kann man alternativ auch Eclipse ABAP Development Tools (ADT) verwenden. Die ganzen BOB*-Transaktionen sind also mittelfristig obsolet.

Business-Object-Builder-Eclipse

2. SAP BOPF API

Die BOPF API ist ziemlich schmal. Für die Implementierung der CRUDQ-Methoden benutzen wir den Service Manager /BOBF/IF_TRA_SERVICE_MANAGER. Wir werden hierfür die Methoden RETRIEVE, MODIFY und QUERY benutzen.

BOPF API Service Manager

Für die Transaktionssteuerung nutzen wir den Transaction Manager /BOBF/IF_TRA_TRANSACTION_MGR. In unserem sehr einfachen Fall brauchen wir nur die Methoden SAVE und CLEANUP.

BOPF API Transaction Manager

Wir werden noch sehen, dass die Programmierung gegen die BOPF API ein bisschen fummelig ist. Aus diesem Grund kapseln wir die Logik in eine Data Access Object Klasse. Die DAO Klasse stellt die CRUDQ-Methoden bereit und bietet dem Aufrufer eine vereinfachte API. Als Aufrufer implementieren wir hier eine Unit Test Klasse. In den folgenden Abschnitten gehen wir die Implementierung der CRUDQ-Methoden im Detail durch.

3. CREATE_HEADER

Die Methode create_header bekommt eine Struktur und speichert diese über die Service Manager Methode MODIFY.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->CREATE_HEADER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IS_HEADER                      TYPE        /BOBF/S_EPM_SO_ROOT
* | [<---] EV_KEY                         TYPE        /BOBF/CONF_KEY
* | [<---] EV_SO_ID                       TYPE        /BOBF/EPM_SO_ID
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD create_header.
    DATA lt_mod      TYPE /bobf/t_frw_modification.
    FIELD-SYMBOLS   <ls_mod> TYPE /bobf/s_frw_modification.
    DATA lr_change   TYPE REF TO /bobf/if_tra_change.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.
    DATA lr_so_root  TYPE REF TO /bobf/s_epm_so_root.
    DATA ls_so_root  TYPE /bobf/s_epm_so_root.

*   Key erzeugen
    MOVE-CORRESPONDING is_header TO ls_so_root.
    ev_key = ls_so_root-key = /bobf/cl_frw_factory=>get_new_key( ).

*   Fachliche SalesorderId erzeugen
*   Siehe Transaktion SNUM. Dort ein Intervall anlegen
    CALL FUNCTION 'NUMBER_GET_NEXT'
      EXPORTING
        nr_range_nr             = '01'
        object                  = '/BOBF/EPM'
      IMPORTING
        number                  = ev_so_id
      EXCEPTIONS
        interval_not_found      = 1
        number_range_not_intern = 2
        object_not_found        = 3
        quantity_is_0           = 4
        quantity_is_not_1       = 5
        interval_overflow       = 6
        buffer_overflow         = 7
        OTHERS                  = 8.

    IF sy-subrc <> 0.
      raise_exc_by_sy( ).
    ENDIF.

    ls_so_root-so_id = ev_so_id.

    GET REFERENCE OF ls_so_root INTO lr_so_root.

    APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
    <ls_mod>-node        = /bobf/if_epm_sales_order_c=>sc_node-root.
    <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
    <ls_mod>-key         = ls_so_root-key.
    <ls_mod>-data        = lr_so_root.

    mr_service_mgr->modify(
      EXPORTING
        it_modification = lt_mod
      IMPORTING
        eo_change       = lr_change
        eo_message      = lr_message ).

    raise_exc_by_bopf_msg( lr_message ).

  ENDMETHOD.

Die Methode zieht aus dem Nummernkreisobjekt /BOBF/EPM eine Nummer, welche als Salesorder Id verwendet wird. Damit das funktioniert, leg bitte in Transaktion SNUM ein Intervall an.

Nummernkreisobjekt /BOBF/EPM     Nummernkreisobjekt /BOBF/EPM - Intervall

Zurück zum Code: Als Typisierung der Struktur wird die sogenannte Kombi-Struktur verwendet. Dies ist eine von BOPF generierte Struktur, welche die persistenten Attribute mit den transienten Attributen zusammenfasst. In unserem Fall haben wir nur persistente Attribute.

Felder wie Zeitpunkt und Benutzer der letzten Änderung lassen sich durch das BOPF-Framework automatisch füllen. Hierfür ist die Determination ADMINISTRATIVE_DATA definiert. Die Implementierung der Determination wird in Klasse /BOBF/CL_LIB_D_ADMIN_DATA_TSM geliefert.

BOPF API Kombi Struktur

Die MODIFY-Methode des Service Managers kann ziemlich viel:

  • Create, Update und Delete
  • Bearbeitung eines einzelnen Satz oder Massenverarbeitung
  • Bearbeiten eines Felds einer Struktur oder für den ganzen Datensatz

Aus diesem Grund braucht die Modify-Methode eine genaue Spezifikation, was zu tun ist. Hier kommt die Modification Struktur /BOBF/S_FRW_MODIFICATION ins Spiel (Programmvariable <ls_mod>). In das Feld node wird der Knotentyp geschrieben. In key kommt der Datenbankschlüssel des Datensatzes rein. Das ist immer eine Guid. Jede durch BOPF gemanagte Tabelle ist datenbankseitig mit dem Guid-Feld DB_KEY als Primärschlüssel angelegt. Die folgende Abbildung zeigt die Datenbanktabelle /BOBF/D_SO_ROOT, in welcher ein Salesorder Header gespeichert wird.

BOPF API Datenbank Tabelle

4. READ_HEADER_BY_KEY

Das Lesen eines Datensatzes zu einem Knoten mit dem Primärschlüssel ist einfach. Die zentrale Service Manager Methode retrieve benötigt den node_key und in einer Tabelle die Liste der zu lesenden Datensätze. Hier ist gut zu sehen, dass mittels der Methode retrieve auch eine Tabelle von Headern gelesen werden kann.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->READ_HEADER_BY_KEY
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_KEY                         TYPE        /BOBF/CONF_KEY
* | [<---] ES_HEADER                      TYPE        /BOBF/S_EPM_SO_ROOT
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------
METHOD read_header_by_key.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.
    DATA lt_header TYPE /bobf/t_epm_so_root.

    mr_service_mgr->retrieve(
      EXPORTING
        iv_node_key             = /bobf/if_epm_sales_order_c=>sc_node-root
        it_key                  = VALUE #( ( key = iv_key ) )
      IMPORTING
        eo_message              = lr_message
        et_data                 = lt_header ).

    raise_exc_by_bopf_msg( lr_message ).

    IF lines( lt_header ) = 0.
      raise_exc_by_msg( |Header { iv_key } nicht gefunden| ).
    ENDIF.

    es_header = lt_header[ 1 ].
  ENDMETHOD.

Der Parameter iv_node_key ist ein bißchen irreführend. Es ist eigentlich der Knotentyp. Der Salesorder Header ist hier das Root Geschäftsobjekt. Wir müssen also die Konstante /bobf/if_epm_sales_order_c=>sc_node-root verwenden.

Für jedes BOPF Geschäftsobjekt existiert ein generiertes Konstanten Interface, welches hier /bobf/if_epm_sales_order_c heißt. Dieses Konstanteninterface definiert diverse Konstanten: Actions, Assoziationen, Knotentypen, Actions usw. Hinter einem Großteil der hier definierten Konstanten stecken Guids. Dieses Interface wird uns bei der Programmierung gegen die BOPF API ständig begleiten.

BOPF API Const Interface

5. READ_HEADER_BY_SO_ID

Guids sind für den Anwender schwer lesbar. Um dem Anwender einen lesbaren Schlüssel zu bieten, bietet BOPF den Alternative Key an. Der Alternative Key am Salesorder Knoten ist das Feld SO_ID, welches wir beim CREATE_HEADER aus dem Nummernkreis gefüllt hatten.

BOPF API Alternative Key

Um einen Datensatz mittels Alternative Key zu lesen, müssen wir zunächst diesen Key in die Guid umwandeln, welche als Datenbank Key benutzt wird. Das geschieht über mr_service_mgr->convert_altern_key. Danach können wir via Methode retrieve den Datensatz lesen. Auch hier ist wieder gut die Nutzung des Konstanten Interface /bobf/if_epm_sales_order_c zu sehen.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->READ_HEADER_BY_SO_ID
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_SO_ID                       TYPE        /BOBF/EPM_SO_ID
* | [<---] ES_HEADER                      TYPE        /BOBF/S_EPM_SO_ROOT
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------
  METHOD read_header_by_so_id.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.
    DATA lt_header TYPE /bobf/t_epm_so_root.
    DATA lt_alt_key_so_id TYPE /bobf/t_epm_k_sales_order_id.
    DATA lt_key TYPE /bobf/t_frw_key.

    lt_alt_key_so_id = VALUE #( ( iv_so_id ) ).

    mr_service_mgr->convert_altern_key(
      EXPORTING
        iv_node_key          =  /bobf/if_epm_sales_order_c=>sc_node-root
        iv_altkey_key        =  /bobf/if_epm_sales_order_c=>sc_alternative_key-root-sales_order_id
        it_key               =  lt_alt_key_so_id
        iv_check_existence   = abap_true
      IMPORTING
         et_key              = lt_key
         eo_message          = lr_message  ).

    raise_exc_by_bopf_msg( lr_message ).

    IF lt_key[ 1 ]-key IS INITIAL.
      raise_exc_by_msg( |Header { iv_so_id } nicht gefunden| ).
    ENDIF.

    mr_service_mgr->retrieve(
      EXPORTING
        iv_node_key             = /bobf/if_epm_sales_order_c=>sc_node-root
        it_key                  = lt_key
      IMPORTING
        eo_message              = lr_message
        et_data                 = lt_header ).

    raise_exc_by_bopf_msg( lr_message ).

    es_header = lt_header[ 1 ].
  ENDMETHOD.

6. Update_header

Die update_header Methode erhält wie die Create Methode die Kombi-Struktur. Als kleine Sonderlocke bauen wir in diese Methode ein, dass der Schlüssel als Gui oder alternativ über den Alternative Key so_id übergeben werden kann. Wir setzen hierbei auf die bereits implementierte Methode read_header_by_so_id auf. Der Code ist ziemlich ähnlich zum Create. Wir müssen also die Modification Struktur /BOBF/S_FRW_MODIFICATION füllen.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_RL_EPM_SO_DAO->UPDATE_HEADER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IS_HEADER                      TYPE        /BOBF/S_EPM_SO_ROOT
* | [!CX!] CX_DEMO_DYN_T100
* +--------------------------------------------------------------------------------------
  METHOD update_header.
    DATA lt_mod      TYPE /bobf/t_frw_modification.
    FIELD-SYMBOLS   <ls_mod> TYPE /bobf/s_frw_modification.
    DATA lr_change   TYPE REF TO /bobf/if_tra_change.
    DATA lr_message  TYPE REF TO /bobf/if_frw_message.
    DATA ls_so_root  TYPE /bobf/s_epm_so_root.
    DATA ls_so_root_pers  TYPE /bobf/s_epm_so_root.

*   Wenn technischer Schlüssel leer, versuch per SO_ID zu lesen
    IF is_header-key IS INITIAL.
      CALL METHOD read_header_by_so_id(
        EXPORTING
          iv_so_id  = is_header-so_id
        IMPORTING
          es_header = ls_so_root_pers ).

      MOVE-CORRESPONDING is_header TO ls_so_root.
      ls_so_root-key = ls_so_root_pers-key.
      ls_so_root-root_key = ls_so_root_pers-root_key.
    ELSE.
      MOVE-CORRESPONDING is_header TO ls_so_root.
    ENDIF.

    APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
    <ls_mod>-node        = /bobf/if_epm_sales_order_c=>sc_node-root.
    <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_update.
    <ls_mod>-key         = ls_so_root-key.
*    Nur das Feld net_amount updaten. Das geht nicht, weil das Feld net_amount schreibgeschützt ist
*    <ls_mod>-changed_fields = value #( ( /bobf/if_epm_sales_order_c=>sc_node_attribute-root-net_amount ) ).
    GET REFERENCE OF ls_so_root INTO <ls_mod>-data.

    mr_service_mgr->modify(
      EXPORTING
        it_modification = lt_mod
      IMPORTING
        eo_change       = lr_change
        eo_message      = lr_message ).

    raise_exc_by_bopf_msg( lr_message ).
  ENDMETHOD.

Über die interne Tabelle changed_fields in der Modification Struktur können wir der BOPF API mitteilen, dass wir nicht alle Attribute updaten wollen, sondern nur bestimmte Attribute. Dabei muss man beachten, dass man nicht jedes Feld updaten kann. Bei der Definition des Geschäftsobjekts kann man die Attributeigenschaften definieren. Hier siehst Du, dass z.B. das Attribut NET_AMOUNT schreibgeschützt ist. Wenn Du im Code den auskommentierten Code für die changed_fields einkommentierst, wird die BOPF API deshalb einen Fehler werden. Lässt Du den Parameter changed_fields weg, werden alle Attribute ohne Schreibschutz aktualisiert.

BOPF Knotenattribute

7. Das war der 1. Teil

Das war der 1. Teil zum Thema BOPF API. Wir haben gesehen, dass BOPF ein mächtiges Framework zur Implementierung von Geschäftsobjekten ist. Die BOPF API ist auf Grund ihrer Generizität (generische Parameter, wenige API-Methoden) bei der Benutzung zunächst nicht ganz einfach. Ich hoffe aber, dass ich mit mit diesem Blogbeitrag ein gute Grundlage hierfür gelegt habe.

Im 2. Teil, der bald folgt, beschäftigen wir uns mit  den restlichen CRUDQ-Funktionalitäten wie Delete und Query, Assoziationen, Actions ( entspricht OData Function Imports), Ausnahme- und Transaktionsbehandlung .

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion!

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP
ABAP OData Unit Test Klasse

OData ABAP Unit Test

Testautomatisierung von OData Services in Zeiten von Continuous Integration und Continuous Delivery ist essentiell. Die Gründe sind, dass eigenentwickelte SAPUI5 Apps zunehmend geschäftskritische Funktionalitäten implementieren und der manuelle Test zu aufwendig ist. Das ABAP OData Framework ist mit dem ABAP Unit Test Framework integriert. OData ABAP Unit Test wird in der Online Doku SAP Gateway Foundation nur sehr kurz beschrieben. Dennoch sieht es auf dem 1. Blick sehr einfach aus. In diesem Blog wollen wir OData ABAP Unit Test am Beispiel des GWSAMPLE_BASIC OData Service einmal ausprobieren. Die Funktionalität des GWSAMPLE_BASIC Service ist in der Data Provider Class /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT implementiert ( Siehe SEGW-Projekt /IWBEP/GWSAMPLE_BASIC ).

Integration einer Data Provider Class mit dem Unit Test Framework

Die Unit Test Integration zwischen der DPC-Klasse besteht SAP-seitig aus 2 Teilen:

  1. Methode /iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test: Jede DPC-Klasse erbt die Implementierung aus Klasse /IWBEP/CL_MGW_ABS_DATA. Sie dient dazu, ein DPC-Objekt so zu parametrisieren, dass es durch einen Unit Test aufrufbar ist.
  2. Klasse /IWBEP/CL_MGW_REQUEST_UNITTST: Die Klasse ist eine Fassade für den OData Request. Ein Objekt dieser Klasse wird aus dem init_dp_for_unit_test an den Unit Test zurückgeliefert und muss dann an die zu testende CRUDQ-Methode der DPC-Klasse übergeben werden.

Die Abbildung zeigt ausgehend von der DPC-Klasse des GWSAMPLE_BASIC einen Ausschnitt der Implementierung der Methode init_dp_for_unit_test.

ABAP OData Unit Test /IWBEP/IF_MGW_CONV_SRV_RUNTIME~INIT_DP_FOR_UNIT_TEST

Hands On

Um die Integration der DPC-Klasse mit dem ABAP Unit Test Framework auszuprobieren, lege eine Klasse ZCL_RL_ODATA_UNITTEST an. Der Klassentyp ist Testklasse und die Instanzerzeugung ist abstrakt. Im Regelfall wird keine globale Testklasse angelegt, weil der Unit Test Code als lokale Klasse bei der zu testenden Klasse hinterlegt ist. Die lokale Unit Test Klasse LCL_TEST enthält die Logik, um die Data Provider Class /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT zu testen.

ABAP OData Unit Test Klasse

Das Ergebnis sollte hiernach so aussehen. Die Unit Test Klasse besteht aus Methoden (Prefix TEST), welche die DPC-Funktionalität testen. Alle anderen sind Helper Methoden wie READ_ODATA_MODEL, welche durch die Testmethoden genutzt werden.

Für den Unit Test verwende ich die Entity BusinessPartner. Im Folgenden erkläre ich die Funktionalität der TEST-Methoden.

OData Unit Test Read (test_businesspartner_read)

Die Methode init_dp_for_unit_test erwartet eine Struktur, die aus einer Vielzahl von Felder besteht. Der OData Request wird also nicht als String wie im Gateway Client in der Form /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet(‚0100000001‘) übergeben. Die Befüllung der Felder ist abhängig von der CRUDQ-Methode. Immer zu füllen die Entity-Namen für Source und Target.

Durch Aufruf der DPC-Methode init_dp_for_unit_test wird der DPC für den Unit Test auf Basis der Struktur initialisiert. Beim Lesen einer Entity muss der Schlüssel über die Methode set_converted_keys in den Request geschrieben werden.

Die Member Variable mr_data_provider enthält ein DPC-Objekt. Über den Aufruf der /iwbep/if_mgw_appl_srv_runtime~get_entity führen wir die Anwendungslogik in Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, BUSINESSPARTNERS_GET_ENTITY aus.

Das wirkt alles ein wenig kompliziert, nicht wahr? Um besser zu verstehen, was hier eigentlich passiert, setze einen Breakpoint in diese Test Methode und debug einmal die Methode init_dp_for_unit_test und /iwbep/if_mgw_appl_srv_runtime~get_entity durch.

 
  METHOD test_businesspartner_read.
    DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst.
    DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit.
    DATA lr_exc_base TYPE REF TO /iwbep/cx_mgw_base_exception.
    DATA ls_ent_businesspartner_exp TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner.
    FIELD-SYMBOLS <ls_ent_businesspartner_act> TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner.

    ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.
    ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.

    lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context_unit ).

    TRY.
*       Schlüssel des Business Partners in den Request aufnehmen
        ls_ent_businesspartner_exp-bp_id = '0100000001'.
        GET REFERENCE OF ls_ent_businesspartner_exp INTO DATA(lr_ent_businesspartner_exp).
        lr_request_unittst->set_converted_keys( lr_ent_businesspartner_exp ).

*       Dies ruft die zu testende Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, BUSINESSPARTNERS_GET_ENTITY auf
        mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~get_entity(
          EXPORTING
            io_tech_request_context = lr_request_unittst
          IMPORTING
            er_entity              = DATA(lr_ent_businesspartner_act) ).

        ASSIGN lr_ent_businesspartner_act->* TO <ls_ent_businesspartner_act>.

*       Stimmt die erwartete BusPartnerId mit der tatsächlichen überein?
        cl_abap_unit_assert=>assert_equals(
            act                  = <ls_ent_businesspartner_act>-bp_id
            exp                  = ls_ent_businesspartner_exp-bp_id ).

      CATCH /iwbep/cx_mgw_base_exception INTO lr_exc_base.
        cl_abap_unit_assert=>fail(
            msg    = lr_exc_base->get_text( ) ).
    ENDTRY.

  ENDMETHOD.

OData Unit Test Update (test_businesspartner_update)

Es wird ein kleines Stück komplexer. Beim Update lesen wir zunächst den Business Partner von der Datenbank in die Entity-Struktur. Danach müssen wir die Entity Struktur in einen Entity Provider verpacken. Der Rest des Codes ist analog zum Read.

  
METHOD test_businesspartner_update.
    DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst.
    DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit.
    DATA ls_ent_businesspartner_exp TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner.
    FIELD-SYMBOLS <ls_ent_businesspartner_act> TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner.
    DATA lr_entry_provider TYPE REF TO /iwbep/if_mgw_entry_provider.

*   Business Partner von der Datenbank lesen und die TelefonNr verändern
    ls_ent_businesspartner_exp = read_business_partner( '0100000001' ).
    ls_ent_businesspartner_exp-phone_number = sy-uzeit.

*   Der Update erwartet die Daten verpackt in einen sogeannten Entity Provider
    lr_entry_provider = conv_entity_to_entry_provider( ls_ent_businesspartner_exp ).

*   Basisinformationen des OData Requests füllen
    ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.
    ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.

*   Unit Test Request Objekt erzeugen
    lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context_unit ).

*   Schlüssel des zu ändernden Business Partners in den Request aufnehmen
    GET REFERENCE OF ls_ent_businesspartner_exp INTO DATA(lr_ent_businesspartner_exp).
    lr_request_unittst->set_converted_keys( lr_ent_businesspartner_exp ).

    TRY.
*       Business Partner ändern
        mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~update_entity(
          EXPORTING
            iv_entity_name          = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|
            iv_entity_set_name      = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|
            iv_source_name          = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|
            io_data_provider        = lr_entry_provider
            io_tech_request_context = lr_request_unittst
          IMPORTING
            er_entity               = DATA(lr_ent_business_partner_act)  ).

        ASSIGN lr_ent_business_partner_act->* TO <ls_ent_businesspartner_act>.

*       Ist die Telefonnummer wirklich geändert?
        cl_abap_unit_assert=>assert_equals(
            act                  = <ls_ent_businesspartner_act>-phone_number
            exp                  = ls_ent_businesspartner_exp-phone_number ).

      CATCH /iwbep/cx_mgw_base_exception INTO DATA(lr_exc_base).
        cl_abap_unit_assert=>fail(
            msg    = lr_exc_base->get_text( ) ).
    ENDTRY.
  ENDMETHOD.

Probleme bei der Intialisierung des DPC-Objekts (test_init_dp_fail)

Die  von SAP bereitgestellte Methode init_dp_for_unit_test haben wir schon benutzt. Wir werden nun sehen, dass die Methode ganz schön zickig ist. Konkret heißt dies, dass ich in der Request Struktur keine Referenzen übergeben kann. Tut man dies dennoch, kommt es zu einer Ausnahme. Alle Referenzen in dieser Struktur wie z.B converted_keys können deshalb erst nach der Initialisierung in den OData Request gesetzt werden.

METHOD test_init_dp_fail.
    DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst.
    DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit.
    DATA ls_ent_businesspartner TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner.

*   Das ist ok: Struktur enthält keine Referenzen
    lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test(  VALUE #( )  ).

    GET REFERENCE OF ls_ent_businesspartner INTO ls_request_context_unit-technical_request-converted_keys.

*   Das geht nicht: Jede Referenz (ref to data als auch ref to object) führt zu Serialisierungs-Ausnahme
*   Methode /IWBEP/CL_MGW_REQUEST_UNITTST, MOVE_CORRESPONDING
*   'Im ST-Program /IWBEP/ST_ANY_DATA ist bei der Serialisierung ein Fehler aufgetreten.'
*   'Die Verwendung eines Referenztyps ist hier nicht unterstützt.'
    lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context_unit ).

  ENDMETHOD.

OData Unit Test Query Teil 1(test_businesspartner_query)

Nun wollen wir einmal die Query /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet?$top=5&$skip=10 testen. Damit der Query funktioniert, braucht das DPC-Objekt Metainformationen aus dem OData-Model. Die Methode init_dp_for_unit_test sieht aber nicht vor, dass man ein Model übergeben kann. Ich habe deshalb den Code kopiert und in der lokalen Methode init_dp_for_unit_test erweitert.

  METHOD test_businesspartner_query.
    DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit.
    DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst.
    FIELD-SYMBOLS  TYPE /iwbep/cl_gwsample_bas_mpc=>tt_businesspartner.
    DATA lr_exc_base TYPE REF TO /iwbep/cx_mgw_base_exception.
    DATA lr_odata_model TYPE REF TO /iwbep/if_mgw_odata_fw_model.

*   Basisinformationen des OData Requests füllen
    ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.
    ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.

*   Top und Skip im Request setzen
    ls_request_context_unit-paging-skip = 10.
    ls_request_context_unit-paging-top = 5.


*   OData Model lesen. Siehe Tabelle /IWBEP/I_MGW_OHD
    lr_odata_model = read_odata_model(
        iv_version = '0001'
        iv_technical_name	= '/IWBEP/GWSAMPLE_BASIC_MDL' ).


*    DPC-Objekt init: Standard. Keine Übergabe des Models möglich
*    lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( is_request_context = ls_request_context_unit ).

*   DPC-Objekt init selbstgemacht
    lr_request_unittst = init_dp_for_unit_test(
        is_request_context  = ls_request_context_unit
        ir_data_provider = mr_data_provider
        ir_odata_model = lr_odata_model ).

    TRY .
*       OData Query ausführen
        mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~get_entityset(
          EXPORTING
            io_tech_request_context   = lr_request_unittst
          IMPORTING
            er_entityset              = DATA(lr_entityset)  ).

        ASSIGN lr_entityset->* TO .

*       Mit der Top und Skip Parametriesierung ohne weitere Einschränkungen kommen hier 5 Sätze zurück
        cl_abap_unit_assert=>assert_equals(
            act                  = lines(  )
            exp                  = 5 ).

      CATCH /iwbep/cx_mgw_base_exception INTO lr_exc_base.
        cl_abap_unit_assert=>fail(
            msg    = lr_exc_base->get_text( ) ).
    ENDTRY.
  ENDMETHOD.

OData Unit Test Query Teil 2 (test_businesspartner_query_fai)

Jetzt lass uns einmal versuchen eine vollwertige Query wie /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet?$filter=BusinessPartnerID ge ‚0100000001‘ and BusinessPartnerID le ‚0100000010‘ zu implementieren.

Ich habe das nicht hingekriegt, diese Filterkriterien in den OData-Request reinzukriegen. Sie werden ignoriert mit dem Ergebnis, dass der Unit Test scheitert.

  
  METHOD test_businesspartner_query_fai.
    DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit.
    DATA lt_select_option	TYPE /iwbep/t_mgw_select_option.
    DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst.
    DATA lr_odata_model TYPE REF TO /iwbep/if_mgw_odata_fw_model.
    FIELD-SYMBOLS <lt_ent_businesspartner> TYPE /iwbep/cl_gwsample_bas_mpc=>tt_businesspartner.
    DATA lr_exc_base TYPE REF TO /iwbep/cx_mgw_base_exception.

*   Basisinformationen des OData Requests füllen
    ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|.
    ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.
    ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|.

*   OData Model lesen. Siehe Tabelle /IWBEP/I_MGW_OHD
    lr_odata_model = read_odata_model(
        iv_version = '0001'
        iv_technical_name	= '/IWBEP/GWSAMPLE_BASIC_MDL' ).


*   Selektionsoptionen für BusinessPartnerID ge '0100000001' and BusinessPartnerID le '0100000010' aufbauen
    APPEND INITIAL LINE TO lt_select_option ASSIGNING FIELD-SYMBOL(<ls_select_option>).
    <ls_select_option>-property = 'BusinessPartnerID'.
    APPEND INITIAL LINE TO <ls_select_option>-select_options ASSIGNING FIELD-SYMBOL(<ls_rng>).
    <ls_rng>-sign = 'I'.
    <ls_rng>-option = 'BT'.
    <ls_rng>-low = '0100000001'.
    <ls_rng>-high = '0100000010'.

    ls_request_context_unit-technical_request-filter_select_options = lt_select_option.

*    Alternative Möglichkeit des Setzen Filter geht auch nicht
*    DATA lr_filter TYPE REF TO /iwbep/cl_mgw_req_filter.
*    CREATE OBJECT lr_filter TYPE /iwbep/cl_mgw_req_filter
*      EXPORTING
*        it_filter_select_options = lt_select_option
*        iv_filter_string         = space.
*
**   Filter initialisieren
*    lr_filter->GET_CONVERSION_INFO( ).
*    lr_request_unittst->set_filter( lr_filter ).


*   DPC-Objekt init selbstgemacht
    lr_request_unittst = init_dp_for_unit_test(
        is_request_context  = ls_request_context_unit
        ir_data_provider = mr_data_provider
        ir_odata_model = lr_odata_model ).

    TRY .
*       OData Query ausführen
        mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~get_entityset(
          EXPORTING
            io_tech_request_context   = lr_request_unittst
          IMPORTING
            er_entityset              = DATA(lr_entityset)  ).

        ASSIGN lr_entityset->* TO <lt_ent_businesspartner>.

*       Mit dem Filter müssen genau 10 Sätze zurückkommen
        cl_abap_unit_assert=>assert_equals(
            act                  = lines( <lt_ent_businesspartner> )
            exp                  = 10 ).

      CATCH /iwbep/cx_mgw_base_exception INTO lr_exc_base.
        cl_abap_unit_assert=>fail(
            msg    = lr_exc_base->get_text( ) ).
    ENDTRY.

  ENDMETHOD.

Wenn man die OData Calls vom Gateway Client  als auch von dieser Testmethode einmal debugged, stellt man fest, dass das Problem tief im OData Framework in Methode /IWBEP/CL_MGW_FILTER_EXPRSN, GET_EXPRESSION_TREE liegt. Das Framework möchte die Filterparameter als Tabelle von Expressions bekommen. Die folgende Abbildung zeigt das benötigte Format. Dieses Format ist extrem kryptisch.

ABAP OData Unit Test QueryFiltrerExpression

Fazit

ABAP Unit Test für die Testautomatisierung einzusetzen, ist generell sehr sinnvoll. Im Kontext eines OData Service halte ich die Testautomatisierung der DPC_EXT-Klasse wegen der aufgezeigten Problemen nicht für sinnvoll.

Alternative Ansätze zur Testautomatisierung eines OData Service sind:

  1. Auslagern der Logik aus der DPC_EXT-Klasse in Data Access Object Klassen: Diese lassen sich dann problemlos mit ABAP Unit Testen, weil sie keine Abhängigkeiten zum OData Framework haben
  2. End2End-Test über externe Tools wie SOAPUI oder einem OData Client wie pyodata. Pyodata lässt sich aus dem SAP GitHub beziehen und hat den Anspruch, ein Enterprise-ready Python OData client zu sein.

ABAP OData Unit Test Ergebnisanzeige

 

Hast du noch Fragen zu OData oder zu anderen Themen?

Nutze gerne unsere Kommentarfunktion

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Veröffentlicht in ABAP

Cache im ABAP-Umfeld: Generische Implementierung

Caching ist eine Form des Zwischenspeicherns von Daten, sodass Daten, die bereits von einer Datenbank geladen worden sind, nicht erneut geladen und verarbeitet werden müssen. Dieses Konzept existiert bei SAP. Hauptanwendungsbereich ist dabei das Cachen von Stammdaten. Allerdings existiert für Daten, die nicht zu den Stammdaten zählen, kein Cache. Zudem kann ich diesen Cache durch verschiedene Methoden aushebeln, z. B. durch die Verwendung von Datenbank-JOINs. Daher ist es Aufgabe des Programmierers, die Anwendungslogik so zu gestalten, dass die Datenbankabfragen performant sind. Dennoch sind in der Praxis oftmals die Datenbankabfragen bzw. deren Häufigkeit an Aufrufen ein großes Performance-Problem.

Um dieses Problem zu schließen, haben wir als Acando einen generischen Cache implementiert, welcher für beliebige Daten verwendet werden kann. Die Implementierung der Cache-Klasse sowie die dazugehörigen Unit-Tests sind als Textdatei im Anhang zu finden.

1 Cache-Aufbau

Der generische Cache beinhaltet die nachfolgenden Methoden, welche die dargestellten Attribute und Typen benötigt.

Der wesentliche Bestandteil eines Caches sind dessen Einträge, weswegen ich nachfolgend näher darauf eingehe.

Typdefinitionen

Um klar festzulegen, welchen Typen der Schlüssel eines Eintrages hat, wird der Schlüssel mit dem Typen T_KEY festgelegt. T_KEY entspricht dabei dem Typen String.

Ein Cache-Eintrag (definiert als Typ TS_CACHE)  besteht aus folgenden Komponenten:

  • Key: Schlüssel (Typ T_KEY)
  • Type: Typ des Eintrags (Typ I)
  • Value: Wert des Eintrags (Typ XSTRING)
  • Size: Größe des Eintrages (Typ I)
  • Timestamp: Zeitpunkt, an dem der Eintrag geschrieben wurde (Typ TIMESTAMPL)

Anmerkung: Der Typ TT_CACHE entspricht dem Tabellentypen der Struktur TS_CACHE.

Man erkennt, dass die Typen generisch sind, was bedeutet, dass der Cache für jeden beliebigen Typ einsetzbar ist. Um unterscheiden zu können, von welchem Typ der Eintrag ist, gebe ich den Typ bei der Abfrage eine Eintrags bzw. beim Schreiben mit an.

Im optimalen Fall erstelle ich dazu ein Konstanten-Interface, bei dem ich eine eindeutige Bezeichnung und eine Nummer hinterlege (analog zu einer Enumeration in Sprachen wie Java, C#, etc.). Diesen Typen gibt man beim Lesen als auch beim Schreiben eines Cache-Eintrages an.

Durch die Verwendung des Typs XSTRING des Eintrags-Wertes ist die Generizität des Caches gewährleistet. Das zeigt nachfolgende Abbildung.

2 Cache-Benutzung

2.1 Schreiben von Cache-Einträgen (SET_ENTRY)

Damit ich einen Cache-Eintrag schreiben kann, benötige ich zuallererst die Daten, die ich schreiben möchte. Diese lese ich meistens aus der Datenbank oder ich benutze bereits berechnete Daten, etc.
Da ich für einen Eintrag die Bestandteile Key, Type und Value benötige, müssen diese vor dem Setzen eines Eintrages feststehen. D. h.  ich habe die Daten beschafft, den zugehörige Schlüssel festgelegt (ggf. berechnet) und einen Typ festgelegt.

2.2 Lesen von Cache-Einträgen (GET_ENTRY)

Das Lesen eines Cache-Eintrages bedeutet, dass mithilfe eines Schlüssels und eines Typs, die Werte, ohne erneutes Lesen aus der Datenbank, zurückgegeben werden. Zusätzlich setzt das System die Variable ev_not_found, welche bestimmt, ob das System einen Eintrag gefunden hat oder nicht. Falls dies nicht der Fall ist, muss das System den Eintrag schreiben.

2.3 Erstellen des Schlüssels

Bei der Erstellung des Schlüssels kann man sowohl einfache Variablen/Strukturen als auch Tabellen angeben. Ein häufiger Fall ist die Bildung eines Schlüssels über Variablen/Strukturen und Tabellen zugleich.

Allerdings besteht bei Tabellen die Herausforderung, jede kleinste Änderung in der Tabelle zu erkennen.

2.3.1 Ohne Hashberechnung

Bei einfachen Variablen (Typ String, I, etc.) oder Strukturen können diese direkt als Schlüssel des Eintrages angegeben werden. Lediglich tiefe Strukturen sollten vermieden werden.

2.3.2 Hash-Berechnung (CALC_HASH)

Die oben genannte Herausforderung, Änderungen in komplexen Datentypen festzustellen, löst man durch die Berechnung eines Hashs über die gesamte komplexe Struktur hinweg. Mit der statischen Methode calculate_hash_for_raw der Klasse cl_abap_message_digest lässt sich dieser unter Verwendung des SHA1-Algorithmus berechnen. Als Ergebnis erhält man einen String der Länge 40.

2.4 Leeren des Caches (RESET)

Da die Daten des Caches nach einer gewissen Zeit veraltet sind, empfiehlt es sich, ihn gelegentlich zu leeren. Die dafür implementierte Methode RESET erledigt das. Dabei besteht die Möglichkeit einen Typen als Parameter mit anzugeben, welcher die Einträge des Typs aus dem Cache entfernt. Falls ich den Typ nicht angegebe, leert die Methode den Cache komplett.

2.5 Verkleinern des Caches (CLEANUP)

Da der Cache aufgrund der Anzahl der Einträge zu groß für den Speicher werden kann, ist es notwendig, diesen beim Setzen von neuen Einträgen aufzuräumen. Das heißt, dass, wenn der Wert der Konstante GC_MAX_SIZE überschreitet, die Methode CLEANUP die Cache-Tabelle um den Wert der Konstanten GC_CLEANUP_SIZE verkleinert. Das erfolgt durch das Löschen von Einträgen, welche am ältesten sind (Überprüfung realisiert durch den Timestamp).

2.6 Aktivieren/Deaktivieren des Caches (SET_ENABLED)

Da man für manche Testzwecke eine Überprüfung ohne als auch mit Cache benötigt, ist es sinnvoll, den Cache aktivieren und deaktivieren zu können.

3 Cache-Beispiele

3.1 Test einfacher Schlüssel (TEST_METHOD_SIMPLE)

Das Beispiel zeigt eine kurze Beispiel-Implementierung des Caches. Dabei selektiere ich spezifisch aus den Flugdaten der SAP Demo-Daten – in diesem Fall nach einem Abflugsland. Zuerst überprüfe ich, ob der Eintrag vorhanden ist. Anfangs ist er das nicht, weshalb das System einen Cache-Eintrag schreibt. Daraufhin versuche ich nochmals den Eintrag zu lesen, was dieses Mal erfolgreich ist. Die darauffolgende Überprüfung stellt sicher, dass die Daten dieselben sind.

    DATA:lt_spfli_exp TYPE TABLE OF spfli,
         ls_spfli TYPE spfli,
         lv_country TYPE land1,
         lv_not_found TYPE abap_bool,
         lt_spfli_act TYPE TABLE OF spfli.

*& Initialisierung: Schlüssel für den Flugplan ist das Land
    lo_cache = zcl_cache=>get_instance( ).
    lv_country = 'DE'.

*& Eintrag vorhanden?
    lo_cache->get_entry(
      EXPORTING
        iv_type      = zcl_cache=>gc_enum_type-flight_schedule
        iv_key       = lv_country
      IMPORTING
        ev_value     = lt_spfli_act
        ev_not_found = lv_not_found  ).

*& Wenn Eintrag nicht gefunden wurde, beschaffe die Daten und setze einen Cache-Eintrag.
    IF lv_not_found EQ abap_true.

      SELECT * FROM spfli INTO TABLE lt_spfli_exp WHERE countryfr = lv_country.

      lo_cache->set_entry(
        EXPORTING
          iv_type  = zcl_cache=>gc_enum_type-flight_schedule
          iv_key   = lv_country
          iv_value = lt_spfli_exp ).

    ENDIF.

*& Eintrag lesen
    lo_cache->get_entry(
      EXPORTING
        iv_type      = zcl_cache=>gc_enum_type-flight_schedule
        iv_key       = lv_country
      IMPORTING
        ev_value     = lt_spfli_act
        ev_not_found = lv_not_found  ).

*&  Die beiden internen Tabellen müssen gleich sein
    cl_abap_unit_assert=>assert_equals(
        act            = lt_spfli_act
        exp            = lt_spfli_exp   ).

3.2 Test von GUIDs (TEST_METHOD_GUID)

Bei Verwendung von GUIDs als Schlüssel stößt der Cache anfangs an seine Grenzen. Nachfolgend habe ich ein Beispiel implementiert, worin ich GUIDs verwende und welche ich anfangs nicht kompilieren kann, da der Typ für den Schlüssel (RAW(16)) nicht mit dem Typen des Cache-Schlüssels (CLIKE) übereinstimmt. Lösung des Problems ist die Konvertierung in den Typ Char(32).

    CONSTANTS lc_key_type TYPE i VALUE '3'.
    DATA lt_bp TYPE STANDARD TABLE OF snwd_bpa WITH DEFAULT KEY.
    DATA ls_bp TYPE snwd_bpa.
    DATA lv_not_found TYPE abap_bool.

*    Typ Raw Länge 16
    DATA ls_cache_key_temp TYPE snwd_node_key.
    DATA ls_cache_key(32) TYPE c.

*    Hier wäre ein der Typ inkompatibel mit der Schnittstelle des Caches.
    ls_cache_key_temp = '000C296828551EE99F9629ACFDCB1537'.

*    Deshalb wird hier in einen Char(32) konvertiert.
    ls_cache_key = ls_cache_key_temp.

    SELECT * FROM snwd_bpa INTO TABLE lt_bp ORDER BY node_key.

    lo_cache = zcl_cache=>get_instance( ).

    lo_cache->get_entry(
      EXPORTING
        iv_type      = lc_key_type
        iv_key       = ls_cache_key
      IMPORTING
        ev_value     = ls_bp
        ev_not_found = lv_not_found
    ).

    IF lv_not_found EQ abap_true.

      READ TABLE lt_bp INTO ls_bp WITH KEY node_key = ls_cache_key
         BINARY SEARCH.

      IF sy-subrc = 0.
        lo_cache->set_entry(
          EXPORTING
            iv_type  = lc_key_type
            iv_key   = ls_cache_key
            iv_value = ls_bp
        ).
      ENDIF.
    ENDIF.

3.3 Test von komplexen Schlüsseln/Einträgen (TEST_METHOD_KOMPLEX)

Nachfolgendes Beispiel zeigt die Verwendung des Caches mit einem komplexen Schlüssel als auch komplexen Einträgen. Wie zu erkennen ist, besteht der Schlüssel aus zwei Ranges. Aus diesen erzeuge ich mittels der Methode CALC_HASH den Schlüssel für den Eintrag. Weiterhin existiert der Typ ts_cache_value, welcher beide Werte des Cache-Eintrags (bpheaderdata und bpcontactdata) hält.

TYPES:
*  Komplexer Cache Value bestehend aus 2 internen Tabellen
    BEGIN OF ts_cache_value,
      bpheaderdata TYPE STANDARD TABLE OF bapi_epm_bp_header WITH DEFAULT KEY,
      bpcontactdata TYPE STANDARD TABLE OF bapi_epm_bp_contact WITH DEFAULT KEY,
    END OF ts_cache_value.

    DATA lt_rng_bpid TYPE if_epm_bp_header=>tt_sel_par_bp_ids.
    DATA ls_rng_bpid LIKE LINE OF lt_rng_bpid.

    DATA lt_rng_companyname TYPE if_epm_bp_header=>tt_sel_par_company_names.
    DATA ls_rng_companyname LIKE LINE OF lt_rng_companyname.

    DATA lv_not_found TYPE abap_bool.
    DATA lv_cache_key TYPE string.
    DATA ls_cache_value TYPE ts_cache_value.

*   Ranges für Selektion aufbauen
    ls_rng_bpid-sign = 'I'.
    ls_rng_bpid-option = 'BT'.
    ls_rng_bpid-low = '0100000005'.
    ls_rng_bpid-high = '0100000020'.
    APPEND ls_rng_bpid TO lt_rng_bpid.

    ls_rng_companyname-sign = 'I'.
    ls_rng_companyname-option = 'CP'.
    ls_rng_companyname-low = 'A%'.
    APPEND ls_rng_companyname TO lt_rng_companyname.

    lo_cache = zcl_cache=>get_instance( ).

*   Hashwert für den komplexen Cachekey erzeugen
    lo_cache->calc_hash(
      EXPORTING
        iv_data   = lt_rng_bpid
        iv_data2  = lt_rng_companyname
      IMPORTING
        ev_hash   = lv_cache_key  ).

    lo_cache->get_entry(
      EXPORTING
        iv_type      = zcl_cache=>gc_enum_type-business_partner_list
        iv_key       = lv_cache_key
      IMPORTING
        ev_value     = ls_cache_value
        ev_not_found = lv_not_found   ).

    IF lv_not_found EQ abap_true.

      CALL FUNCTION 'BAPI_EPM_BP_GET_LIST'
        TABLES
          selparambpid        = lt_rng_bpid
          selparamcompanyname = lt_rng_companyname
          bpheaderdata        = ls_cache_value-bpheaderdata
          bpcontactdata       = ls_cache_value-bpcontactdata.

      lo_cache->set_entry(
        EXPORTING
          iv_type  = zcl_cache=>gc_enum_type-business_partner_list
          iv_key   = lv_cache_key
          iv_value = ls_cache_value  ).

    ENDIF.

Anschließend kann über die Variable ls_cache_value auf die jeweiligen gecachten Einträge zugriffen werden. In diesem Fall wäre das mit ls_cache_value-bpheaderdata und ls_cache_value-bpcontactdata.

4 Fazit des generischen Caches

Mithilfe des generischen Caches ist es möglich, Bewegungsdaten und Daten, die am SAP-Cache vorbei gehen (z. B. durch JOINs), im Speicher zu halten. Durch die Implementierung auf Basis des Singleton-Patterns ist ein Zugriff an jeder Programmstelle realisiert.

Weiterhin schließt der generische Cache die Lücke zum Standard-SAP Cache, welcher durch einige Möglichkeiten umgangen werden kann und wird.

Die generelle Schwierigkeit, dass die Daten im Cache aktuell sind, liegt in Verantwortung des Programmierers. Deshalb sollte vor längeren Verarbeitungsblöcken der Methodenaufruf RESET erfolgen.

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion oder schreibt mir direkt an michael.krause@cgi.com

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP
SAP BOPF

SAP BOPF: Einführung in das Business Object Processing Framework

Um Geschäftsanwendungen möglichst effizient und standardisiert modellieren und implementieren zu können hat SAP das Business Object Processing Framework für ABAP entwickelt.
SAP BOPF bietet viele Standardfunktionalitäten wie zum Beispiel das Lesen und Schreiben der Datenbanktabellen an. Dadurch kann ein Teil der Entwicklung standardisiert und Zeit eingespart werden. In diesem Beitrag werde ich die Grundlagen sowie die Vorteile und die Architektur des Frameworks vorstellen.

Voraussetzungen

Ursprünglich wurde das Framework von SAP für interne Entwicklungen, beispielsweise beim Transportmanagement, eingesetzt. Aufgrund der erhöhten Nachfrage wurde SAP BOPF dann auch für alle Entwickler veröffentlicht. Das Framework ist ab den folgenden Releases nutzbar:

  • SAP Business Suite EHP5 SP11 / EHP6 SP05 / EHP7
  • SAP NetWeaver 7.50

Überblick über das Programmiermodell

Im Zentrum von BOPF steht das Geschäftsobjekt. Dies kann zum Beispiel ein Produkt, ein Vertrag oder ähnliches sein.
Ein Geschäfsobjekt hält immer eine Menge an Knoten, welche die Logik des jeweiligen Geschäftsobjekts modellieren.
Diese Knoten sind hierarchisch angeordnet wobei auch Assoziationen realisierbar sind.
Jeder dieser Knoten ist mit einer eindeutigen, vom Framework generierten, ID ansprechbar.
Im Hintergrund wird für jeden dieser Knoten eine Tabelle sowie für jede Knoteninstanz ein Eintrag in der jeweiligen Tabelle angelegt.
Dabei geben die Attribute, die einem Knoten zugeordnet werden, die Spalten der jeweiligen Tabelle an.
Das Framework bietet die Möglichkeit, verschiedene Arten von Knoten, sogenannte Knotenentitäten, anzulegen:

  1. Aktion: In Aktionen wird das Verhalten eines Geschäftsobjekts implementiert. Aktionen werden explizit aufgerufen, zum Beispiel durch einen Klick eines Benutzers auf einen Button. Eine Aktion für das Geschäftsobjekt wäre beispielsweise „Vertrag verlängern“.
  2. Ermittlung: Ermittlungen werden im Gegensatz zu Aktionen nicht explizit, sondern automatisch angestoßen. Beispielsweise wenn ein Vertrag ausgelaufen ist.
  3. Validierung: Validierungen nutzt man zur Konsistenzprüfung von Knoten oder Aktionen.
  4. Abfrage: Um nach bestimmten Knoteninstanzen suchen zu können, können Abfragen verwendet werden.
  5. Assoziation: Mit Assoziationen können verschiedene Instanzen von Geschäftsobjekten zwischen verschiedenen Knoten verbunden werden.

Im folgenden Bild siehst du das eben Beschriebene zusammengefasst:

SAP BOPF: Überblick
SAP BOPF: Überblick

Beispiel

Damit du dir einen Eindruck machen kannst wie das in der Praxis aussieht, stellt SAP ein paar Beispielimplementierungen bereit, die du in der Transaktion BOBX (Framework zur Verwaltung von Business Objekten) findest. Hier kannst du alle bereits erstellten Geschäftsobjekte ansehen und bearbeiten. Im Bereich Transportierbare Business-Objekte -> Business-Objekte findest du zum Beispiel ein Geschäftsobjekt für Produkte (/BOBF/DEMO_PRODUCT). In der Detailansicht (Doppelklick auf das Business-Objekt) siehst du das Geschäftsobjekt und alle zugehörigen Knoten.

SAP BOPF: Ansicht eines Geschäftsobjekts
SAP BOPF: Ansicht eines Geschäftsobjekts

Unter „Knotenstruktur“ sind alle Knoten hierarchisch dargestellt. Hier kannst du auch neue Knoten zum Geschäftsobjekt hinzufügen. Unter „Knotenelemente“ sind noch einmal alle Knoten alphabetisch sortiert dargestellt. Hier können die einzelnen Knoten bearbeitet und um Funktionalität erweitert werden. Für den Wurzelknoten sind schon einige Funktionalitäten hinterlegt.

SAP BOPF: Detailansicht eines Knotens
SAP BOPF: Detailansicht eines Knotens

Um alle Produkte lesen zu können wurde beispielsweise eine Abfrage „SELECT_ALL“ angelegt. Die Verbindung zu einem beschreibenden Text in verschiedenen Sprachen ist über eine Assoziation gelöst. Interessant ist auch die Validierung „CHECK_ROOT“, in der eine Prüfung auf auf Vollständigkeit und Korrektheit eines Produktkopfes stattfindet. Du kannst dich gerne einmal in dem und den weiteren Business-Objekten umsehen. In den nächsten Beiträgen gehe ich dann darauf ein, wie man ein eigenes Business-Objekt mit Funktionalitäten erstellt.

Fazit und Ausblick

SAP BOPF bietet eine gute Möglichkeit, um Geschäftsanwendungen zu modellieren und zu implementieren. Zudem kann durch den Einsatz von BOPF die Entwicklung standardisiert werden, was auch dazu führt, dass Entwickler nicht immer in eine neue Architektur einarbeiten müssen. Meiner Meinung nach ist es sinnvoll, BOPF anzuwenden, sofern keine projektbedingten Rahmenbedingungen dagegen sprechen.
In den nächsten Blogartikeln zeige ich dir, wie du ein eigenes Geschäftsobjekt anlegst, verwaltest und damit arbeitest.

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion oder schreib mir direkt an adrian.gehrke@cgi.com

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Veröffentlicht in ABAP

ABAP in S/4HANA – Die SAP HANA Datenbank

S/4HANA ist im Moment DAS Thema im SAP Bereich sowohl für Unternehmen, die SAP Software verwenden, als auch für SAP-Entwickler und -Berater. Mit der für das Jahr 2025 gesetzten Deadline von SAP für den Auslauf des Supports für die bestehende ERP-Software steht für viele Unternehmen die Migration zu S/4HANA unmittelbar bevor.

Auch für uns ABAP-Entwickler wird sich mit dieser Migration etwas ändern und wir wollen, dass du bestens darauf vorbereitet bist. Deswegen beschäftigen wir uns in diesem ersten Artikel unserer Reihe mit den Grundlagen der HANA Datenbank. In weiteren Artikeln gehen wir dann genauer auf die Veränderungen in der ABAP-Programmierung ein.

S/4HANA und HANA Datenbank

Bereits 2015 erfolgte der Release von S/4HANA und stellt einen Meilenstein in der Evolution der SAP-ERP-Produktlinie dar: von R/2, R/3 über ECC zur vierten Produktgeneration S/4. S/4 nutzt dabei HANA als Datenbanktechnologie. Bei der Migration zu S/4 ist somit die Verwendung von anderen Datenbanken wie Oracle oder IBM nicht mehr möglich wie zuvor mit R/3. Das heißt, dass Unternehmen, die zu dem neuen System migrieren, auch ihre komplette Datenbank migrieren müssen.

Aufbau der HANA Datenbank

Im Gegensatz zu herkömmlichen relationalen Datenbanken, basiert die HANA Datenbank auf der In-Memory-Technologie und kann so kürzere Zugriffszeiten erreichen.

Die Daten werden nicht auf herkömmlichen Speichern wie Festplatten bereitgehalten, sondern in den Hauptspeicher (RAM) geladen. Zeiten für Datenzugriffe verkürzen sich damit auf ein Fünftel.
Natürlich nutzen auch traditionelle Datenbanken den Hauptspeicher. Bei einem Request werden die Daten von der Datenbank in den Hauptspeicher geladen. Wird der gleiche Request nochmals ausgeführt, werden die Daten dann aus dem Hauptspeicher gelesen. Für neue Requests erfolgt erneut ein Datenbankzugriff, was die Performance reduziert. Im Gegensatz dazu werden bei HANA beim ersten Zugriff, z.B. nach einem Systemstart, die Daten einmalig in den Hauptspeicher geladen und darauf zugegriffen.

Bei Datenbanken ohne In-Memory-Technologie stellen die häufigen Zugriffe auf die Festplatte den Performanceengpass dar. Mit der In-Memory-Technologie verschiebt sich dieser Engpass auf den Zugriff auf den Hauptspeicher. Dieser ist nämlich noch immer 4-60 Mal langsamer als der Zugriff auf den Cache.

Das Konzept der In-Memory-Technologie ist nicht neu, galt aber viele Jahre als Nischenprodukt. Lange waren die hohen Kosten, die mit der nötigen Größe des Hauptspeichers zusammenhängen, ein großer Nachteil der In-Memory-Datenbanken. Mittlerweile sind die RAM-Preise deutlich gesunken und somit In-Memory-Datenbanken für Unternehmen eine mögliche Alternative zu herkömmlichen Datenbanken.

Innovationen der In-Memory-Technologie

Datenlayout

Während traditionelle Datenbanken ihre Daten zeilenorientiert halten, kann SAP HANA Daten zusätzlich auch spaltenorientiert ablegen. SAP HANA bietet die Möglichkeit, das Ablageverfahren für jede Tabelle einzeln zu entscheiden. Beide Ansätze haben Vor- und Nachteile aber generell gilt:

Zeilenorientiert Spaltenorientiert
Schnelle schreibende Zugriffe Längere schreibende Zugriffe
Längeres Lesen der Daten Schnelleres Lesen der Daten

Komprimierung

Ziele der Komprimierung sind sowohl eine verkürzte Datenübertragungszeit als auch die Reduzierung des Speicherverbrauchs. Im Fall der In-Memory-Datenbank ist dies besonders wichtig ist, da die Datenbank alle Daten im RAM hält. Die HANA Datenbank verwendet dafür im speziellen die Dictionary-Komprimierung.

Partitionierung

Die Partitionierung kommt zum Einsatz, wenn große Datenmengen vorhanden sind. Sie ermöglicht eine leichtere Verwaltung der Daten. Man unterscheidet zwischen der vertikalen und horizontalen Partitionierung. HANA unterstützt die horizontale Partitionierung.

Auswirkungen auf die Anwendungsentwicklung

Die Entwicklung der Anwendungslogik von ABAP Programmen fand bisher größtenteils in der Applikationsschicht statt. Um von den zuvor beschriebenen Vorteile der HANA Datenbank bestmöglich profitieren zu können, sollte die Anwendungslogik, insbesondere komplexe Kalkulationen, in die Datenbankschicht integriert werden. Damit findet hier ein Paradigmenwechsel statt: Data-to-Code zu Code-to-Data. Die Daten werden nun nicht mehr aus der Datenbank gelesen und dann auf der Applikationsschicht verarbeitet, sondern direkt auf der Datenbank verarbeitet. Dieser Paradigmenwechsel wird auch als Code Pushdown bezeichnet, da der Code in die darunterliegende Datenbankschicht verlagert wird.

 

Fazit und Ausblick

S/4HANA ist die neue Business Suite von SAP und ist die größte Neuerung der ERP Plattform seit zwei Jahrzehnten. Sie verwendet die HANA Datenbank, die auf der In-Memory-Technologie basiert, die mittlerweile über das Nischendasein hinweg ist.

In den nächsten Beiträgen zeigen wir worauf man in der Programmierung mit der neuen In-Memory-Datenbank achten sollte.

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion oder schreib mir direkt an jennifer.schneidmueller@cgi.com

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

SAP Cloud Application Programming Model: OData Implementierung mit CDS

Moin, heute bauen wir ganz einfach einen OData Service in der SAP Cloud Foundry mit Hilfe des SAP Cloud Application Programming Models und Core Data Services (CDS). Groß programmieren müssen wir dafür nicht, es reicht im Grunde zwei Dateien zu editieren.
Die SAP Cloud Platform Business Application fasst die UI, das Datenbankmodell und den OData-Service in einem Projekt zusammen. Wir beschränken uns in diesem Beitrag auf die DB und den Service.

Ich habe ein Beispiel für dich vorbereitet, so dass wir uns den befüllten OData Service am Ende ansehen können.

[0]: Voraussetzungen

Um diesem Beispiel zu folgen benötigst du:

  • einen SAP-User
  • Zugriff auf die Cloud Foundry (die Trial-Version reicht aus)
  • es hilft wenn du mit der WebIDE umgehen kannst

Du hast noch gar nicht mit der SAP Cloud und der Web IDE gearbeitet? Dann guck dir mal diesen Beitrag von uns an, da arbeitest du erstmal mit einem fertigen OData Service und einem UI-Template. Falls du deine Cloud Foundry Trial noch nicht aktiviert hast, gehst du einfach in das Cloud Platform Cockpit (CPC) und klickst dort auf Cloud Foundry .

Bevor es aber richtig los gehen kann müssen wir unsere IDE noch etwas konfigurieren. Dafür gehst du einmal in die Einstellungen der WebIDE.

Die Einstellungen der WebIDE erreicht man über das Zahnrad-Icon
Da verstecken sich die Einstellungen.

Da findest du unter Worspace Preferences > Cloud Foundry die Einstellungen für die Cloud Foundry.

Den Zugang zur Cloud Foundry konfigurieren wir in den Einstellungen.

Über das Drop Down Menü wählst du den ...api.cf.eu10... Endpunkt aus. Die WebIDE fragt dich dann nach deinem Login-Daten. Hier meldest du dich mit der E-Mail und dem Passwort deines SAP-Accounts an.

Und das war es auch schon, jetzt kann es losgehen.

[1]: Projekt anlegen

Wechsel einmal in die Development Ansicht indem du auf das Symbol –> (</>) klickst.
Mit einem Rechtsklick auf Workspace > New > Project from Template. In dem PopUp stellst du die Category auf „All categories“ und suchst das SAP Cloud Platform Business Application Template.

Die Template-Suche der Web IDE

In diesem Beispiel möchte ich einen einfachen Online-Shop-Backend erstellen, also gebe ich als Projektname einfach „Shop“ ein. In dem nächsten Schritt kannst du die Einstellungen beibehalten.

Projekt erstellen Schritt 2 & 3
Projekt anlegen Schritt 2 & 3

Im letzten Schritt hast du die Wahl zwischen einem Java- oder NodeJS-OData Service. Mit NodeJS bekommst du einen OData Service in Version 4 mit Java Version 2.

Dabei solltest du bedenken, dass nicht alle UI5 Templates V4 unterstützen, wenn du also nach diesem Beitrag noch eine UI hinzufügen möchtest empfehle ich den Java Service.

Ich passe noch den Package-Namen an. Den Rest kannst du so lassen.

OData-Service Konfiguration
OData-Service: Java oder NodeJS.

Achtung! Der OData Service den wir jetzt anlegen läuft ohne Authentifizierung.

Wir müssen nur zwei cds Dateien editieren
Wir müssen nur zwei cds Dateien editieren.

Öffne mal die beiden Dateien die ich rot markiert habe. In der data-model.cds-Datei definieren wir gleich unsere Datenbankstruktur. Und in der cat-service.cds-Datei definieren wir danach unseren OData-Service.

[2]: OData Model & Service mit CDS

Die beiden Dateien sind zu diesem Zeitpunkt bereits mit Beispieldaten befüllt. Du kannst sie dir ruhig noch mal kurz ansehen, denn wir werden sie gleich durch ein etwas komplexeres Beispiel ersetzen.

Trotzdem geht es hier natürlich nicht um das Schema sondern um die Technologie. Wenn dir mein Beispiel also nicht gefällt kannst du auch etwas anderes benutzen.

Ich befülle die data-model.cds jetzt mit dem folgenden Inhalt.

namespace my.shop;

entity Customers {
    key C_ID            : Integer;
    C_Name              : String;
    C_Address           : String;
}

entity Vendors {
    key V_ID            : Integer;
    V_Name              : String;
}

entity Products {
    key P_ID            : Integer;
    P_Name              : String;
    P_Vendor            : Association to Vendors; 
    P_UnitsInStock      : Integer;
    P_Price             : Decimal(10,2); 
    P_EAN               : String;
}

entity Orders {
    key O_ID            : Integer;
    O_Items             : Composition of many OrderItems on O_Items.OI_Order = $self;
    O_Customer          : Association to Customers;
}
entity OrderItems {
    key OI_Order        : Association to Orders;
    OI_Product          : Association to Products; 
    OI_Quantity         : Integer;
}

Bevor du jetzt speicherst, müssen wir noch den Service konfigurieren, dafür passen wir die andere Datei an.

using my.shop as my from '../db/data-model';


service CatalogService {
    entity Customers        as projection on my.Customers;
    entity Vendors          as projection on my.Vendors;
    entity Products         as projection on my.Products;
    entity Orders           as projection on my.Orders;
    entity OrderItems       as projection on my.OrderItems;
}

Diese ist deutlich kürzer und recht übersichtlich oder? Wenn du dir das original Beispiel angesehen hast, dann ist dir bestimmt aufgefallen, dass da noch ein @readonly neben der Entity stand. Und daraus folgt natürlich, dass dieser Service auch Schreibzugriffe zulassen wird.

Wenn du mehr zu der Syntax erfahren willst kannst du mal hier nachlesen. Und das war es auch schon.

Ja wirklich, damit hast du einen OData-Service + Datenbank angelegt. Jetzt speicherst du einmal beide Dateien (Strg+Shift+S).

Jetzt öffnet sich die Konsole und es wird und die CDS Dateien werden kompiliert. Wenn hier ein Fehler auftritt, dann hast du dich vermutlich vertippt.

Über das Menü kann man auch manuell einen CDS Build auslösen.
Wenn das automatische bauen bei dir deaktiviert ist, kannst du es auch selber auslösen.

Hier noch mal der Hinweis, unser Dienst läuft ohne Authentifizierung jeder, der die URL kennt, kann Daten in die Datenbank schreiben.

Bevor wir zum nächsten Schritt gehen, sehen wir uns noch kurz an, was die WebIDE da gebaut hat.

Wirf auch mal einen Blick in /srv/src/resources/edmx/ da liegen zwei Dateien für den Service. Die Inhalte dürften dir bekannt vor kommen, schließlich stammen sie von den beiden Dateien ab, die wir vorher bearbeitet hatten.

[3]: Daten, Deploy und Erkundung

Bevor wir jetzt die DB und den Service in der Cloud deployen. Befüllen wir ihn noch mit Beispieldaten.

Für mein Beispiel habe ich bereits ein paar CSV-Dateien und die nötige Data.hdbtabledata. In einem Git Repository abgelegt, diese kannst du hier als zip-Datei laden.

In der WebIDE dem db Verzeichnis legst du einen neues Verzeichnis csv an. Über Rechtsklick auf csv > Import > File or Project lädst du die import.zip Datei hoch.

Das neue CSV-Verzeichnis.
Das neue CSV-Verzeichnis.

Jetzt musst du noch mal den CDS Build auslösen. Wenn der fertig ist, startest du per Rechtsklick auf das db-Verzeichnis Build > Build den Build-Prozess für die Datenbank.

Build für die Datenbank über Rechtsklick starten
Build für die Datenbank über Rechtsklick starten.

Je nach Auslastung der Cloud kann dieser Prozess ein paar Minuten dauern, steh mal auf und mach dir einen frischen Kaffee.

Der Java OData Service wird gestartet

 

Zum Abschluss starten wir den Java-Service. Wenn der Service gestartet ist, erscheint unten in der Console eine URL zu deinem neuen OData-Service.

URL zum Service

Wenn du die URL öffnest, kannst du dir deinen neuen Service ansehen. Hänge mal /$metadata an die URL damit kannst du die die Metadaten des Services ansehen.

Metadaten unseres OData-Service
Die Metadaten bekommst du, wenn du /$metadata an die URL hängst.

Die Datensätze kannst du dir ansehen, in dem du zum Beispiel /Products an die URL hängst.

Produkte Datensatz
Ein Produkt-Datensatz

Damit ist unser OData Service fertig und du hast wieder etwas gelernt. Wenn du noch Zeit hast, kannst du ja einen anderen Beitrag lesen.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

S/4HANA Adoption Starter Programm – in 90 Tagen zum Weg zum S/4HANA

Das Ende für den R/3-Support rückt immer näher, sodass ihr euch entscheiden müsst, wie ihr den S/4HANA-Umstieg schafft. Ihr wisst nicht so recht wie euer aktuelles SAP-System für den Umstieg auf das S/4-System für eine Konvertierung oder Datenmigration vorbereitet ist? Dann könnte das Adoption Starter Programm interessant für euch sein.

Was ist das Adoption Starter Programm?

Das S/4HANA Adoption Starter Programm hat als Ziel einen Plan für den Weg zu S/4HANA zu erarbeiten. Mit dem Adoption Starter Programm erarbeitet ihr in 40 bis 90 Tagen eine Roadmap für den Weg zu eurem S/4HANA-Systems. Gemeinsam mit der SAP analysiert ihr eure IT-Infrastruktur und eure aktuellen Geschäftsprozesse. Ihr wählt zudem eure Umstiegsstrategie in diesem Schritt. Mit Hilfe des Programms lässt sich gut erkennen, ob ihr eher den Brownfield- oder den Greenfield-Ansatz für den Umstieg wählen möchtet. Und das beste an dem Programm? SAP stellt das Adoption Starter Programm kostenlos zur Verfügung. Das Resultat des Adoption Starter Programms ist die Definition und der Projektablauf für den darauf folgenden Umstieg.

Beim Adoption Starter Programm arbeitet ihr nicht alleine an der Umstellung. Der ständige Erfahrungsaustausch mit anderen Unternehmen, die auch am S/4HANA Adoption Starter Programm teilnehmen, vereinfacht die Umsetzung und Planung. Wöchentlich finden Workshops statt, an denen ihr und die anderen Unternehmen teilnehmt. Eine Reisebereitschaft ist nicht von Nöten, die Konferenz findet telefonisch bzw. per Videokonferenz statt. Ihr organisiert euch dahingehend in eurem Unternehmen mit ausgewählten Teams und Stakeholdern, die an dem Umstieg beteiligt sein könnten.

Wichtig ist abzugrenzen, dass es sich beim Adoption Starter Programm nicht um die Implementierung und Umsetzung des S/4HANA-Systems handelt. Das Programm dient lediglich dazu sich vorzubereiten, das S/4-System und die dafür erforderlichen Schritte besser einschätzen zu können.

Schritte:

Folgende Schritte gehören zum Adoption Starter Programm:

  • Readiness-Check
  • Custom Code
  • Custom Vendor Integration (CVI) – siehe auch die Geschäftspartner Integration
  • Constistency Checks
  • Customizing
  • Convert Architecture
  • Activation and configuration of Fiori Apps

Beim Durchlauf holt ihr alle möglichen Beteiligten, die später an der S/4 Einführung beteiligt sind, von Grund auf ab. Ihr setzt den Grundstein für die technologische Umstellung. In dem 90-tägigen Programm startet ihr unter anderem mit dem SAP Transformation Navigator. Mit dem Navigator schaut ihr euch eure aktuelle Systemlandschaft an, sodass ihr anschließend mit dem Tool erfahrt, was ihr benötigen werdet.

Erfahrungsberichte Adoption Starter Programm

Die Otto Group zählt zu den Pionieren des Adoption Starter Programms . Die Otto Group hat mit Hilfe des 90-tägigen Workshops alle möglichen Stakeholder mit ins Bord genommen, sodass eine Umsetzungsstrategie erfolgreich ausgearbeitet werden konnte.

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

 

REST Services in ABAP

Wer SAPUI5 Apps implementiert kennt OData. OData ist ein Standard für die Implementierung von REST-Services. Dass es auch möglich ist, REST Services in ABAP nativ zu implementieren, ist unserer Erfahrung nach nicht so bekannt. Die Eclipse Plugins der ABAP Development Tools kommunizieren zum Beispiel über REST mit dem SAP ABAP. Du wolltest schon immer wissen, wie das eigentlich geht? Dann bist du hier richtig, denn wir wollen heute versuchen die OData Funktionalitäten CRUDQ (CREATE, READ, UPDATE, DELETE, QUERY) mit einem nativen REST Service zu implementieren – los geht’s!

Von Andreas Fischer & Paul Holst

REST steht erst einmal für Representational State Transfer und hat den Anspruch mit einer einheitlichen Schnittstelle über bereits vorhandene WWW Architektur eine vereinfachte Mashine-2-Mashine-Communication zu ermöglichen. REST selbst ist dabei weder Protokoll noch Standard und wird auch dadurch einzigartig, dass über die angesprochenen URIs keine Methodeninformationen übergeben werden (müssen), sondern nur eine Ressource. SAP liefert bereits die benötigten Grundstrukturen mit, die einfach implementiert werden können um REST Services zu erstellen.

Ziel unseres REST-Service ist es, mithilfe von HTTP-Requests die CRUDQ Funktionalitäten für Salesorder Header und die zugehörigen Items aus dem SAP EPM Model anzuwenden. 

REST Services in ABAP: Klassenmodell

Grundsätzlich besteht unser REST-Service aus zwei zusammenhängenden Klassen. Auf der einen Seite haben wir die Application-Class (ZCL_PH_REST_EPM_APP) auf der anderen Seite die Ressource-Class (ZCL_PH_REST_EPM_RES), welche wir für diesen hier als lokale Objekte illustrativ angelegt haben. Die Resource Klasse regelt dabei die zur Verfügung zu stellenden Datenquellen und DB-Operations, welche wiederum aus der Application-Klasse aufgerufen werden.

Man kann sich das Ganze auch etwas bildlicher vorstellen: Tun wir mal so, als sei der der Ablauf unseres REST Service eine Pizzabestellung. Statt umständlich loszufahren und an der Theke eine Pizza zu ordern und diese wieder mit nach Hause zu nehmen, ist es auch möglich den Online Weg zu nehmen. Als Kunde sind wir der Client, der eine Anfrage losschickt. Das tun wir zum Beispiel über das Portal, welches unser Lieferdienst (in unserem Beispiel der Server) zur Verfügung stellt. Unsere ZCL_PH_REST_EPM_APP Klasse fungiert (hat nichts mit Pizza Fungi zu tun) genau wie dieses Portal als Schablone für die Bestellung einer definierten Ressource. Danach wird diese Anfrage entgegengenommen und vom Personal des Ladens ZCL_PH_REST_EPM_RES bearbeitet, bis schließlich das Ergebnis, unsere Pizza bzw. unsere angeforderten Daten zu uns zurückgeschickt werden.

Die Application-Klasse konstruiert also den Rahmen und legt fest über welche URI welche Handler Klasse angesprochen werden soll. Die Handler Klassen sind hierbei als lokale Klassen der Resource Klasse implementiert.

 

Application Klasse ZCL_PH_REST_EPM_APP: Registrierung von URIs und Handler Klassen

Die Application-Klasse erbt von der von SAP zur Verfügung gestellten Klasse CL_REST_HTTP_HANDLER. Diese Klasse ermöglicht uns die Erstellung eines REST-Services für das HTTP-Protokoll.

REST Application Klasse

Zunächst haben wir die Tabelle (mt_resource_metadata) auf der Basis des Strukturtypen TS_RESOURCE_METADATA angelegt. Die Tabelle dient zum einen zur eindeutigen Definition / Bestimmung der URI und zum anderen der Angabe der genutzten lokalen Handler-Klasse. Im CONSTRUCTOR beginnt die Initialisierung der angesprochenen Tabelle.

Das 1. Url Pattern /header/{HEADER_KEY} wird beispielsweise für unsere GET Methode benötigt. Der Platzhalter {header_key} im Template dient uns – wie wir später sehen – zum Anlegen der eindeutigen URI gemäß des Angesprochenen Node_Key aus dem Sales Order Header bzw. der DB Tabelle snwd_so. Die spezifische Handler Klasse wird in der Resource Metadata dem jeweiligen URI Template zugeordnet. Der CONSTRUCTOR kann dabei natürlich durch beliebig vielen URI-Patterns erweitert werden. Möchtest du beispielsweise statt der der Header-Daten die ITEMS einer Sales-Order ansprechen, so müsstest du hier ein neues Template mit dazugehöriger Handler-Klasse definieren.

In der redefinierten Methode if_rest_application~get_root_handler wird unsere Ressource-Klasse angesprochen. Die Metadaten der Resource werden an den Constructor der Resource Klasse übergeben. Dies erleichtert dort die Delegation an die entsprechende Anwendungslogik.

  METHOD if_rest_application~get_root_handler.
    DATA lr_rest_handler TYPE REF TO cl_rest_router.
    data lT_PARAMETER	TYPE ABAP_PARMBIND_TAB.
    data lr_parameter TYPE REF TO data.

    constants: lc_handler_class TYPE SEOCLSNAME value 'ZCL_PH_REST_EPM_RES'.

    CREATE OBJECT lr_rest_handler.

    loop at mt_resource_metadata ASSIGNING FIELD-SYMBOL(<ls_resource_metadata>).

     get REFERENCE OF <ls_resource_metadata> INTO lr_parameter.

     lt_parameter = value #(
       ( name = 'IS_RESOURCE_METADATA'
         kind = cl_abap_objectdescr=>exporting
         value = lr_parameter )
         ).

     lr_rest_handler->attach(
      iv_template = <ls_resource_metadata>-template
      iv_handler_class = lc_handler_class
      iT_PARAMETER = lt_parameter  ).
    endloop.

    ro_root_handler = lr_rest_handler.

  ENDMETHOD.

Resource-Klasse ZCL_PH_REST_EPM_RES: Logik des REST-Services

In der Resource-Class ZCL_PH_REST_EPM_RES spielt die eigentliche Musik. Wie wir sehen, erbt die Klasse von der abstrakten Klasse CL_REST_RESOURCE. Für jede HTTP Method (GET, POST, PUT und DELETE) existiert eine korrespondierende Methode. Diese Methoden sind rezuimplementieren. Die folgende Abbildung zeigt den Aufbau der Resource Klasse und die Implementierung der GET-Methode. Gut zu sehen ist, wie auf Basis der Metainformationen an die lokale Handlerklasse delegiert. Im Rahmen dieser Delegation erfolgt auch die Umsetzung einer HTTP Method in die CRUDQ-Methode.  Beim GET kann entweder ein Function Import, eine Query oder ein Read vorliegen.

REST Resource Klasse

Helper Klasse LCL_HELPER

Die LCL_HELPER Klasse beinhaltet die Funktionen, die wir in allen Handler Klassen benötigen. Dies sind aktuell die Funktionen, um einen beliebige ABAP Datenstruktur in das JSON Format zu serialisieren bzw. eine JSON Nachricht in eine ABAP Datenstruktur zu deserialisieren. Wir benutzen hier die SAP Klasse /ui2/cl_json.

Handler Klasse LCL_BASE_HANDLER

Die Handler Klassen handeln den eigentlichen Teil der Datenbankzugriffe ab. Jede Handler Klasse erbt von LCL_BASE_HANDLER. LCL_BASE_HANDLER implementiert jede CRUDQ-Methode, indem sie eine NotImplemented Ausnahme wirft. Motivation hierfür, dass eine Handler nicht zwingend für alle CRUDQ-Methoden definiert ist. Konkrete Handler Klassen wie lcl_header_handler oder lcl_item_handler redefinieren diese Methode nach Bedarf.

Implementierung Read-Methode im Handler

Die Funktionsweise der Read-Methode lässt sich am besten an der Unit Test Methode header_read nachvollziehen.

  METHOD header_read.
    DATA lr_entity TYPE REF TO if_rest_entity.
    DATA lv_response TYPE string.

    mr_rest_client->set_request_uri( |/header/0050568500ED1EE2A5A7CF2ACDB80AD0| ).
    mr_rest_client->get( ).

    cl_abap_unit_assert=>assert_equals(
        act                  = mr_rest_client->get_status( )
        exp                  = '200' ).

    lr_entity = mr_rest_client->get_response_entity( ).
    lv_response = lr_entity->get_string_data( ).
  ENDMETHOD.

Über die Url /header/0050568500ED1EE2A5A7CF2ACDB80AD0 wird der Salesorder Header mit dem angegebenen Schlüssel gelesen. Setz mal einen Breakpoint in die Methode LCL_HEADER_HANDLER, READ und führe den Unit Test aus. Die folgende Abbildung aus dem Debugger zeigt, wie der Kontrollfluss vom Unit Test zur Application Klasse und danach zur Resource Klasse abläuft.

REST Resource Read Debug

Die Methode get_uri_attributes im LCL_BASE_HANDLER liest die Attribute der Url und mappt diese auf die übergebene Struktur. Dies ist notwendig, wenn wir über einen Schlüssel einen eindeutigen Datensatz ansprechen, wie es beim READ, UPDATE und DELETE der Fall ist.

  
  METHOD get_uri_attributes.
    DATA lt_name_value TYPE tihttpnvp.
    FIELD-SYMBOLS <ls_name_value> TYPE ihttpnvp.
    FIELD-SYMBOLS <lv_value> TYPE any.

    lt_name_value = ir_request->get_uri_attributes( ).

    LOOP AT lt_name_value ASSIGNING <ls_name_value>.
      ASSIGN COMPONENT <ls_name_value>-name OF STRUCTURE rs_uri_attributes TO <lv_value>.
      <lv_value> = <ls_name_value>-value.
    ENDLOOP.
  ENDMETHOD.

Implementierung der Query-Methode im Handler

Mit der Implementierung der Read-Methode im Hinterkopf geht die Implementierung von Create, Update und Delete schnell von der Hand. Ein bisschen schwieriger ist die Query Methode. Die Query Methode soll folgende Url verarbeiten können: /header?filter=so_id bt 0500000001:0500000010,gross_amount gt 1000&top=5&orderby=so_id desc, lifecycle_status. Wir wollen also nach einem Wertebereich filtern, pagen und sortieren. Diese Query wird im Unit Test header_query verwendet.

  METHOD query.
    DATA ls_url_param TYPE ts_url_param.
    DATA lt_query_filter TYPE tt_query_filter.
    FIELD-SYMBOLS <ls_query_filter> TYPE ts_query_filter.
    DATA lt_rng_so_id TYPE tt_rng.
    DATA lt_rng_created_at TYPE tt_rng.
    DATA lt_rng_gross_amount TYPE tt_rng.
    DATA lt_rng_lifecycle_status TYPE tt_rng.
    DATA ls_ent_headers TYPE ts_ent_header.
    DATA ls_headers TYPE snwd_so.
    DATA lt_headers TYPE TABLE OF snwd_so.
    DATA lt_ent_headers LIKE TABLE OF ls_ent_headers.
    DATA lv_max_rows TYPE i.
    DATA lt_sql_order_by TYPE string_table.

    ls_url_param = get_url_param( ir_request ).
    "Skip und Top addieren
    lv_max_rows = ls_url_param-top + ls_url_param-skip.

    lt_query_filter = parse_query_filter( ls_url_param-filter ).

    lt_sql_order_by = parse_query_orderby(
      iv_tabname = 'SNWD_SO'
      iv_orderby = ls_url_param-orderby ).

    LOOP AT lt_query_filter ASSIGNING <ls_query_filter>.
      CASE <ls_query_filter>-property.
        WHEN 'SO_ID'.
          lt_rng_so_id = <ls_query_filter>-t_rng.
        WHEN 'CREATED_AT'.
          lt_rng_created_at = <ls_query_filter>-t_rng.
        WHEN 'GROSS_AMOUNT'.
          lt_rng_gross_amount = <ls_query_filter>-t_rng.
        WHEN 'LIFECYCLE_STATUS'.
          lt_rng_lifecycle_status = <ls_query_filter>-t_rng.
        WHEN OTHERS.
          RAISE EXCEPTION TYPE lcx_rest_resource_exception
            EXPORTING
              iv_msg = |Property { <ls_query_filter>-property } ist ungültig|.
      ENDCASE.
    ENDLOOP.

    "selectieren bis skip+top rows
    SELECT * FROM snwd_so INTO TABLE lt_headers
      UP TO  lv_max_rows ROWS
      WHERE so_id IN lt_rng_so_id
        AND created_at IN  lt_rng_created_at      "Filterkriterien
        AND gross_amount IN lt_rng_gross_amount
        AND lifecycle_status IN lt_rng_lifecycle_status
      ORDER BY (lt_sql_order_by).

    "Zeile 1 bis zeile "skip" löschen
    IF ls_url_param-skip > 0.
      DELETE lt_headers FROM 1 TO ls_url_param-skip.
    ENDIF.

    MOVE-CORRESPONDING lt_headers TO lt_ent_headers.

    mr_helper->serialize_json(
        iv_data     = lt_ent_headers "Header-Daten werden an json übergeben
        ir_response = ir_response ).

  ENDMETHOD.

Die Methode get_url_param dient zur Extraktion der Parameter filter, top, skip und oderby aus der Url.

Das Schwierigste

Für die Query der Handler reichen diese Methoden natürlich noch nicht aus. Wir müssen ebenfalls identifizieren ob und wenn ja welche Operatoren einem Filter zugeordnet sind. Z.B. benötigen wir dies, wenn wir alle Salesorder-Header haben wollen, die in einer bestimmten Zeile einen Wert überschreiten oder unterschreiten. Die Methode parse_query_filter parse die im filter Parameter übergebenen Werte und konvertiert diese in einen ABAP Range.

Eine Referent zu den in ABAP nutzbaren RegEx Ausdrücken findest du hier.

Andere  Handler

Die lokalen Klasse LCL_HEADER_HANDLER und LCL_ITEM_HANDLER erben vom LCL_BASE_HANDLER und redefinieren die Datenbanktypischen CRUDQ Methoden für die tatsächliche Anwendung.

Bei den GET-Operationen der Methoden READ und QUERY arbeiten wir mit einfachen SELECT–Anweisung. Die mr_helper-Klasse serialisiert im Folgenden den zurückgegebenen Datensatz. Bei den anderen drei Methoden folgt am Anfang eine Deserialisierung der Response, gefolgt von den Anweisungen INSERT (POST), UPDATE (PUT) und DELETE (DELETE). Am Ende steht wieder die Serialisierung der Daten.

 

Function Import Handler (lif_fctimp_handler, lcl_fctimp_lifecycle_handler)

In einer typischen Anwendung gibt es neben den CRUDQ-Funktionen auf Datenobjekten auch sogenannte Function Imports. Dies sind Funktionalitäten, die nicht in das CRUDQ-Schema passen. Ein Beispiel ist das Setzen des Lifecycle Status Feldes bei einer Sales Order Header. Die Url sieht so aus: /setLifecycleStatus?SO_ID=0500000002&status=N (Unit Test setlifecyclestatus). Auch das kann man einfach im Rahmen dieses Service umsetzen.

Zunächst erstellen wir uns in der Resource-Klasse ein lokales Interface. Dieses Interface beinhaltet lediglich die execute-Methode. Die Implementierung des Function Import nehmen wir in der lokalen Klasse lcl_fctimp_lifecycle_handler, welche dieses Interface implementiert.

In der Application Class definieren wir das Url Template sowie die Handler Klasse. Damit wir in der Resource Klasse ermitteln können, ob ein Function Import aufzurufen ist, sind die Metadaten um das Boolen Feld is_function_import erweitert.

…um sie dann auf Basis des Templates im Constructor() der APPLICATION  Klasse zur passenden Funktion zu leiten die wir für diesen Zweck implementiert haben:

SICF: Publizierung des REST-Service als HTTP-Endpunkt

Um den Service auch von außen per HTTP erreichbar zu machen, musst Du ihn im SAP erst einmal registrieren. Dafür startest du die Transaktion SICF (System Internet Communication Framework) und rufst die Übersicht „Pflege der Services“ auf. Dort legen wir unter default_host/sap/bc/rest unseren Service an, damit dieser als HTTP Endpunkt zur Verfügung steht.

Das sieht dann folgendermaßen aus:

 

Output & Unit Tests

Als Beispiel ist im Folgenden einmal das Resultat unserer GET-Methode aufgeführt.

In der oberen Abbildung siehst du einen Ausschnitt der erwähnten Datenbank Tabelle snwd_so. Der Node_Key ist Bestandteil der DB-Tabelle und eine von SAP erstellte GUID und dient dir zur eindeutigen Identifikation von Datensätzen. Deshalb verwendest du ihn hier auch als Paramter für die URI. In der folgenden Abbildung siehst du das Resultat unserer GET-Methode, zum Aufruf des gewünschten Datensatzes im Browser, in JSON:

Um alle Anwendungsfälle auch richtig und vor allem persistent durchtesten zu können solltest du noch entsprechende Unit Tests erstellen – so stellst du sicher, dass auch potenzielle Änderungen in den Handlern keine bösen Überraschungen bergen. Dafür kannst du in der APPLICATION Klasse eine lokale Testklasse anlegen und in dieser die Testmethoden definieren und implementieren. Am Beispiel der GET Methode oben könnte eine Implementierung etwa so aussehen:

 

Und damit sind wir auch durch! Wir hoffen, dass wir dir einen guten Einblick verschaffen konnten wie du REST Services in ABAP erstellen kannst. Obwohl CRUDQ und Function Imports mit diesem Ansatz maßgeschneidert und recht komfortabel möglich sind, ist native REST Services in ABAP allerdings keine vollständige Alternative zum klassischen OData Ansatz. Eine Serialisierung in atom bzw. xml ist hier nämlich nicht möglich.

Wenn du noch Fragen zum Thema hast schreib uns einfach – wir freuen uns über eure Nachrichten.

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP

Warum auch du die SAP Cloud Platform nutzen solltest

Die SAP Cloud Platform (SCP) schwirrt nun schon seit einiger Zeit im SAP Umfeld herum. Viele fragen sich: Was hat es damit auf sich? Muss ich das nutzen? Was bringt mir das? Oder auch ganz ähnliche Fragen.

In diesem Artikel möchte ich darlegen, warum aus meiner Sicht an der Nutzung der SAP Cloud Platform kein Weg vorbeiführt und warum jedes Unternehmen daran denken sollte, sich schon jetzt damit zu beschäftigen.

Die Kommandozentrale deines Unternehmens

Wir alle kennen das Bild – Ein futuristisches Flugobjekt im Orbit, viele Lichtjahre von der Erde entfernt. Ein Mann auf einer sogenannten Brücke, der dort mit allen relevanten Informationen versorgt wird. Auf Basis dieser Informationen trifft der Mann Entscheidungen, die sofort in Aktionen umgesetzt werden.
Klingt vertraut? Gut möglich, denn ich rede hier von der U.S.S Enterprise und ihrem Kapitän Jean-Luc Picard
(Anmerkung von mir: Als Millenial ist mir Patrick Stewart hier präsenter als William Shatner)

Was hat das ganze jetzt mit der SAP Cloud Platform zu tun? Erst mal natürlich überhaupt nichts. Auf den zweiten, den funktionalen Blick sieht das dann aber gleich ganz anders aus. Denn die Funktion, die die Brücke auf der U.S.S Enterprise wahrnimmt ist mit der Funktion der SCP in einem Unternehmen vergleichbar. Der Ort, an dem alle Informationen verfügbar sind (SCP Integration und Analytics) und der Ort, über den sich alle Befehle steuern lassen (SCP Portal Service, Zugriff auf die angebundenen Systeme, User- und Berechtigungssteuerung über Identity Authentication Service, etc.)

Um euch diesen Vergleich näher zu bringen lasst uns das doch noch ein bisschen genauer betrachten.

Offenheit der Platform – Arbeiten über Anwendungsgrenzen hinweg

Eine Brücke verbindet laut Definition zwei Objekte, die von einem natürlichen oder künstlichen Hindernis getrennt sind. Die Funktion der SAP Cloud Platform in einem Unternehmen geht bzw. kann noch viel weiter gehen als das: Sie verbindet nicht nur zwei Objekte miteinander, sondern bietet die Möglichkeit, Verbindungen zwischen allen Systemen und Anwendungen herzustellen, die man sich vorstellen kann.

Um sich das anschaulich zu machen kann man sich eine netzartige Brücke vorstellen. Ich komme auf dieser Brücke nicht nur überall hin, ich kann auch entscheiden, ob ich Fahrrad fahre, das Auto nutze oder aber eine Wasserrutsche installiere, um an mein Ziel zu kommen. Zugegeben, letzteres ist vielleicht nicht ganz so trickreich, aber die Möglichkeit besteht.

Möglich macht das die Offenheit der Platform. Dies ist ein wiederkehrendes Credo, das von der SAP propagiert wird. Einerseits bedeutet das, dass man alle Anwendungen und Systeme, die bei einem selbst im Unternehmen genutzt werden, an diese Brücke anschließen kann. (Ich komme überall hin) Zum anderen bedeutet das aber auch, dass ich frei bin, wie ich mich auf dieser Brücke bewege. (Thema Wasserrutsche)

Die Offenheit kann ich aber genauso gut einschränken. Ich entscheide, ob ich nur einzelne Funktionen teilen möchte (Stichwort Function-as-a-Service) oder aber, ob ich ganze Anwendungen zur Integration meiner System SCP entwickle und auf der SCP platziere (Stichwort Bring-your-own-language). Ich kann weiterhin sowohl on Premises als auch andere Cloud Infrastrukturen nutzen und diese an die SCP anbinden. (Stichworte SCP Integration und Multi Cloud)

Die Möglichkeiten sind hier so vielfältig, dass ich nur die paar Beispiele kurz erwähnen möchte. Das, was es zu wissen gibt, würde ich so zusammenfassen: „Alles ist möglich! Die Grenzen werden nur durch die eigene Vorstellungskraft erzeugt.“

Analytics, Machine Learning und Artificial Intelligence – Jean-Luc Picard geht von Bord

Man stelle sich vor, die Brücke ist unbesetzt und die U.S.S. Enterprise fliegt trotzdem unfallfrei und unter Lösung aller Probleme weiter. Ein Szenario, das zum Teil möglich wird, wenn wir uns in die Gebiete Analytics, Machine Learning (ML) und Künstliche Intelligenz (KI).

SAP Leonardo ist der Überbegriff für Technologien, die im Kontext des intelligenten Unternehmens auch die intelligent Technologies genannt werden.

Über die SCP können diese in den eigenen Anwendungen genutzt werden. Was kann das bedeuten? Ich würde sagen: Schlauere Anwendungen, intuitivere Benutzung von Anwendungen, die richtige Information zur richtigen Zeit an der richtigen Stelle, weniger banale, manuelle Tätigkeiten, und noch viele, weitere Verbesserungen.

Was bedeutet das nicht? Eine vollständige Automatisierung. Diese kann auch weder wirklich gewollt sein, noch, ist sie technisch möglich. Es braucht weiterhin Menschen, die den Maschinen sagen, was sie zu tun haben. Es braucht weiterhin Menschen, die die Prozesse überwachen müssen. Die selbst denkenden Maschinen sind dann doch eher Hollywood Gespinste als das sie wirklich Realität sind. Ein Glück für uns und auch unseren Captain: Er wird weiterhin gebraucht!

Der Kontakt zum Endnutzer – Näher dran statt weiter weg

Wo vormals der Kunde weit weg von den eigenen Systemen agierte rückt dieser nun näher an das eigene Unternehmen heran. Auf unserer netzartigen Brücke können Wege eingerichtet werden, die unserem Kunden erlauben, näher und direkter mit uns zu arbeiten. Das kommt sowohl uns als auch dem Kunden selber zu Gute.

Geeignete Tools dafür sind z.B. der SCP Portal Service (Die abgespeckte Cloud Variante des SAP Enterprise Portals) oder maßgeschneiderte, nativ (iOS und Apple) ausgerollte Anwendungen für alle vorstellbaren, fachlichen Prozesse. Auch C/4HANA, ehemals SAP Hybris, spielt hier eine Rolle. Zu guter Letzt sei noch die Übernahme von Qualtrics durch die SAP Anfang des Jahres erwähnt, dessen X- und O-Data dabei hilft, den Kunden besser zu verstehen. Das gewonnene Wissen kann dazu dienen, als Unternehmen anders bzw. zielgerichteter und erfolgreicher mit dem Kunden zu kommunizieren.

Das alles ist essenziell für das Thema Kundengewinnung und -bindung, Kundenzentrierung at ist best!

Es sei auch noch erwähnt, dass dies auch gleichermaßen für Mitarbeiter und Mitarbeiterbindung gilt. Vereinfachung der täglichen Arbeit durch einen, zentralen Zugriffspunkt auf alle Anwendungen, Einsatz von SAP Jam (Social Media for Business), und viele weitere Aspekte.

Das neue SAP – Cloud first Development Organization

Die SAP hat in den letzten Jahren einen radikalen Strategiewechsel vollzogen. Die Cloud steht als Treiber von Innovation im Mittelpunkt. Cloud-first also. Dazu passt das Subscription-Modell, das die SAP zum Ausrollen der Services auf der SCP nutzt.
Die Vorteile, die von der SAP mit Nutzung der Cloud versprochen werden (Mehr Innovation, schnellere Time-to-Market, etc.) möchten sie dadurch auch selber erreichen. In diesem Zusammenhang wurden von Hasso Plattner auf der SAPPHIRE 2019 auch deutlich schnellere Release-Zyklen angekündigt. Natürlich mit dem gleichen Ziel: Mehr und schnellere Innovation.

Was kann das für on Premises Kunden bedeuten? Mehr Downtimes der Systeme, mehr Arbeitsaufwand, höhere Wartungskosten auf Grund von Releases oder aber Systeme, die nicht auf aktuellem Stand sind und deshalb Sicherheitsrisiken darstellen.

Was kann bzw. soll die Lösung sein? Laut SAP ist diese Antwort klar: Der Weg in die Cloud.

Fazit

Die SAP hat sich zu einem Cloud First Unternehmen entwickelt. Diese Richtung wurde schon von Bernd Leukert propagiert, als er noch für die SAP tätig war, und ist, so Hasso Plattner, klar und der einzige Weg nach vorne. Für alle, die SAP nutzen, sollte das ebenso klar sein. Dementsprechend denke ich, dass man diesen Weg als SAP Kunde auf jeden Fall mitgehen sollte und auf mittelfristige Sicht auch muss.

Ich denke, je länger man sich dieser Entwicklung verwährt, desto größer werden die Kosten (Stichwort Update-Rate) bzw. desto schneller wird man von Unternehmen in der Cloud abgehängt, die neue Möglichkeiten in der Cloud schneller und kostenärmer wahrnehmen können als Kunden, die weiterhin on Premises unterwegs sind.

On Premises wird weiterhin ein Thema sein und auch weiterhin von der SAP bedient werden. Aber nicht mehr mit oberster Priorität. Sehr gut vorstellen kann ich mir hier hybride Szenarien, wo ein Teil der Infrastruktur on Premises bleibt, ein Teil aber in die Cloud wandert. Guckt euch dazu gerne auch mein Webinar an, das mögliche Startszenarien mit der SCP darstellt, oder aber das Webinar von meinem Kollegen Rüdiger Lühr zum Thema Anwendungsentwicklung auf der SCP.

Habt ihr Fragen oder Meinungen dazu? Kein Problem, meldet euch einfach in der Kommentarfunktion.

Soweit war’s das dann auch von mir. Bleibt mir nur noch zu sagen:
„Kieck mol wedder in!“

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

S/4HANA – der Weg raus ins Grüne oder rauf aufs Braune?

S/4HANA – ein Echtzeit-ERP-System, das für die digitale Wirtschaft bestens geeignet ist. Eine neue Anwenderoberfläche sorgt für eine moderne Benutzererfahrung. Mit Fiori setzt sich SAP ein neues Gesicht auf, das die Vorurteile zur Benutzerfreundlichkeit wegzaubern könnte. Seitdem SAP das Ende des Supports für die älteren ERP-Systeme ankündigte, wächst die Nachfrage nach S/4HANA.

In diesem Beitrag möchten wir uns mit den möglichen Ansätzen und Wegen zu S/4HANA beschäftigen. Wir stellen die verschiedenen Ansätze vor und werden auf entsprechende Beispiele referenzieren.

Raus ins Grüne – die S/4HANA Neueinführung – der Greenfield-Ansatz

S/4HANA Greenfield

Bei der Neueinführung wird das SAP System neu implementiert. Durch die Neuimplementierung sind die aktuellen Geschäftsprozesse nicht beeinträchtigt, sodass das bestehende SAP System parallel produktiv laufen kann. Dieser Ansatz bedeutet auch gleichzeitig, dass ihr die Prozesse im neuen SAP-System neu einstellen müsst. Die Daten aus den alten SAP-Systemen könnt ihr entsprechend migrieren.

Durch die Neueinführung bekommt ihr die Chance die aktuellen Prozesse zu überdenken und zu bereinigen, sodass ihr automatisch euer aktuelles Vorgehen nochmal überprüfen können. Die Migration bereinigt entsprechend noch Daten und lässt auch hier eine genauere Betrachtung des aktuellen Stands zu.

Vorteile der Greenfield-Strategie

  • Die Neuimplementierung sorgt automatisch für ein reines und schlankes System,  das befreit ist von unnötigen Implementierungen
  • Bereinigte Datenbanken, ohne redundante Datensätze
  • Parallele Produktivität und geringeres Risiko, auf Grund des weiterhin bestehenden SAP-Systems
  • Neue Technologien und Funktionen lassen sich einfacher implementieren, da Voraussetzungen, wie bestimmte Versionen, grundlegend implementiert sind

Nachteile der Greenfield-Strategie

  • Größere Umstellung und entsprechendes Change-Management – durch das neue System müssen Mitarbeiter neue Prozesse kennen lernen
  • Das Anpassen und Erstellen der Prozesse auf dem neuen S/4 System birgt auch einen längeren Projektaufwand mit sich
Erfahrungsbericht
  • Balcke-Dürr GmbH
    • Integration statt Silo-Denken, GoLive in 9 Monaten nach dem Rapid Prototyping Modell von DATAGROUP 2 in 2 Stufen

Rauf auf´s Braune – die S/4HANA Konvertierung – der Brownfield-Ansatz

S/4HANA Brownfield

Die System-Konvertierung dagegen bezeichnet man als Brownfield-Ansatz. Hier transformiert man das bestehende SAP-System schrittweise, bis man zu einem migrationsfähigen Stand kommt. Mit diesem Stand konvertiert man anschließend das alte System zu einem S/4-System. Sollte das alte SAP-System nicht all zu komplex sein und sollten sich die Prozesse nah am Standard halten, dann ist der Brownfield-Ansatz sinnvoll.
Aber auch bei diesem Ansatz werden die Prozesse und Daten reevaluiert.

Vorteile der Brownfield-Strategie

  • Geringere Umstellung und wenig Change-Management bzw. Schulungsaufwand
  • Schnellere System-Conversion – das S/4HANA wird im Gegensatz zur Greenfield-Strategie schneller produktiv genutzt werden. Durch den ad-hoc Wechsel ist die produktive Nutzung unabdingbar
  • Kostenersparnis und geringere Downtime

Nachteile der Brownfield-Strategie

  • Längere Projektumsetzung
  • Neue Technologien sind schwieriger zu implementieren
Erfahrungsbericht
  • T.Con führt die Systemkonversation in 7 Monaten bei Perlen Packing durch

Es liegt nun an euch, wie ihr die Integration oder den Wandel gestalten möchtet. Beide Wege haben ihre Vor- und Nachteile, aber beide Wege führen zu einem ähnlichen Ergebnis. Beurteilt eure eigene Systemlandschaft zunächst. Gibt es Gründe für eine Umstrukturierung? Sind eure Datensätze eventuell noch nicht bereit für einen Wandel, aber ihr möchtet schon jetzt auf die Vorteile eines S/4-Systems setzen oder ist es euch wichtig, dass nicht mehrere Systeme parallel laufen und sich eure Mitarbeiter mit einem Rutsch in die S/4-Welt begeben können?

 

Vielleicht ist auch das Adoption Starter Programm interessant für euch. Wir berichten in einem weiteren Beitrag wie ihr mit Hilfe des Adoption Starter Programms den richtigen Weg zu S/4HANA findet.

Nutze gerne unsere Kommentarfunktion oder schreibt mir direkt an tim.schomaker@cgi.com

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Electron – Mit UI5 Desktop-Apps entwickeln

Moin Moin, heute will ich mit dir aus einer bestehenden UI5-Anwendung eine Desktop App erstellen. Das machen wir mit Electron.

Electron ist ein freies Framework für Dektop Apps, dass von GitHub entwickelt wurde und mittlerweile in vielen modernen Anwendungen zum Einsatz kommt. Electron-Anwendungen können auf Linux, macOS und Windows laufen.

[0]: Vorbereitungen

Bevor wir loslegen, müssen wir ein paar Dinge vorbereiten. Du brauchst eine fertige SAP UI5 Applikation. Wenn du gerade keine Applikation zur Hand hast, kannst du die App aus dem UI5 Walkthrough nehmen. Öffne also einmal die URL zu der App und klick oben rechts auf Download.

Dann muss bei dir NodeJS installiert sein. Falls du das noch nicht hast, gehst du einmal auf die Webseite und lädst die LTS Version herunter (bei mir ist es 10.15.x). Folge einfach den Installationsanweisungen.

Wenn du das erledigt hast, lädst du noch die SAP UI5 Runtime herunter, diese brauchen wir damit unsere App offline laufen kann.

[1]: Projekt anlegen

Als nächstes erstellen wir ein neues Verzeichnis für unser Projekt, mein neues Projekt heißt saptron.

Jetzt brauchen wir die Konsole, wechsel einmal in deinen Projektverzeichnis saptron und gib npm init ein. npm wird dich durch die Erstellung deines neuen Projekts leiten, die Default-Werte sind ausreichend.

C:\Users\benjamin\projekte\saptron>npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install ` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (saptron)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to C:\Users\benjamin\saptron\package.json:

{
  "name": "saptron",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes)

Jetzt liegt in deinem saptron-Verzeichnis eine neue Datei. Auf diese kommen wir später noch mal zurück. Als nächstes öffnest du das sap.m.tutorial.walkthrough.25.zip und kopierst das Verzeichnis webapp in das saptron-Verzeichnis.

Unser Projektverzeichnis saptron.
Unser Projekt-Verzeichnis.

Damit sind unsere Vorbereitungen abgeschlossen. Bis jetzt hast du das eigentlich ganz gut gemacht.

[2]: Electron App erstellen

Als nächstes legst du eine neue Datei index.js an und füllst sie mit folgendem Inhalt.

const {app, BrowserWindow} = require('electron');

let mainWindow;

app.on('ready', () => {
            mainWindow = new BrowserWindow({ 
                width: 800, 
                height: 600,
                webPreferences: {
                        nodeIntegration: false   
                } 
        });
        mainWindow.loadURL('file://' + __dirname + '/webapp/index.html');
});

Das ist der Einstiegspunkt für unsere App. Hier wird ein neues Browser Fenster erstellt und die index.html aus unserer webapp geladen.

Jetzt benötigen wir noch Electron, dafür öffnest du wieder die Konsole und gehst in das saptron-Verzeichnis dort führst du npm install electron --save-dev aus. Sobald npm fertig ist sieht dein Verzeichnis schon etwas anders aus.

package-lock.json und node_modules
package-lock.json und node_modules

Öffne einmal die package.json.

{
  "name": "saptron",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "electron ."
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "^5.0.1"
  }
}

Wie du siehst gibt es jetzt eine neue Sektion „devDependencies“ und darin das Paket electron. Die Developer Dependencies werden nur während der Entwicklung benötigt. In der fertigen Applikation landen nur die Teile von Electorn, die wir auch benutzen.

Da wir Datei schon mal offen haben, ersetzt du den Inhalt von „scripts“ so wie oben.

In dem node_modules-Verzeichnis findest du electron und alle seine Abhängigkeiten. In dem package-lock.json sind alle Pakete mit ihrer Version und Abhängigkeiten festgehalten.

[3]: Testlauf

In der Konsole, gibst du jetzt einmal npm start ein. Es öffnet sich ein neues Fenster und siehe da unsere App erscheint auf dem Bildschirm.

Electron erster start
Erster Start unserer neuen Electron SAP UI5 App.

Sieht doch eigentlich schon ganz gut aus, oder? Aber noch ist unsere App nicht so richtig offline-fähig, denn noch lädt sie die SAP UI5 Bibliothek über das Internet nach.

Jetzt kopierst du das resources Verzeichnis aus der SAP UI5 Runtime in unser Projektverzeichnis.

Das Projektverzeichnis mit resources
Das Projektverzeichnis mit den resources

Damit die neuen Ressourcen auch genutzt werden, müssen wir die index.html im webapp Verzeichnis anpassen. In Zeile 9 wird die URL durch den Pfad zur SAP UI5 Runtime ersetzt.

Anpassen der Resources
Die index.html aus der webapp, bereits angepasst.

Wenn du die App jetzt wieder mit npm start aufrufst wird sie die UI5 Bibliothek nicht mehr aus dem Netz nachgeladen.

[4]: Die Electron App bauen

Zum Abschluss wollen wir noch ausführbare Binarys erstellen, dafür fügen wir ein weiteres Paket zu unserem Projekt hinzu.

npm install electron-packager --save-dev

Unserem package.json fügen wir einen neuen Befehl hinzu.

{
  "name": "saptron",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-packager . saptron --overwrite --platform win32 --arch x64 --out dist/"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "^5.0.1",
    "electron-packager": "^13.1.1"
  }
}

Mit npm run build starten wir den den Prozess. Sobald der packager fertig ist, finden wir in dist/saptron-win32-x64 unsere Windows Anwendung.
Um auch noch für Linux und macOS zu kompilieren, ändern wir erneut das package.json.

{
  "name": "saptron",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "electron .",
    "win": "electron-packager . saptron --overwrite --platform win32 --arch x64 --out dist/",
    "mac": "electron-packager . saptron --overwrite --platform darwin --arch x64 --out dist/",
    "lin": "electron-packager . saptron --overwrite --platform linux --arch x64 --out dist/"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "^5.0.1",
    "electron-packager": "^13.1.1"
  }
}

Du ahnst vermutlich bereits, wie du auch für Linux und macOS, ausführbare Anwendungen bauen kannst, probier es einfach mal aus.

Damit beenden wir unseren kleinen Ausflug in die Welt von Electron. Man kann mit diesem Framework natürlich noch viel mehr machen aber für einen Einstieg war das doch schon mal ganz gut oder?

Noch Zeit? Dann guck dir doch mal einen von unseren anderen UI5 Artikeln an.

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

OData ABAP Entwicklung: Lesen Domänenfestwerte

Eine häufige Anforderung bei der SAPUI5 Entwicklung ist der Umgang mit den zahlreichen Schlüsseln, die im ERP Backend verwendet werden. Zu so einem Schlüssel muss durch die SAPUI5 App die Bedeutung angezeigt werden. In diesem Blog-Artikel entwickeln wir einen Function Import, welcher die NameValue-Paare für Dömänenfestwerte an die SAPUI5 App zurückliefert.

Domänenfestwerte im SAP DDIC

Ein Beispiel für Domänenfestwerte ist der Lebenszyklusstatus einer SalesOrder im EPM Model. Dies ist das Tabellenfeld SNWD_SO-LIFECYCLE_STATUS.

SalesOrder Header Tabelle

Die möglichen Werte des Status sind definiert durch die Domäne D_SO_LC.

Sicht auf eine Domäne

Im SAP DDIC sind diese Werte sprachabhängig in Tabelle DD07T gespeichert.

Tabelle DD07T

Starten wir mit der Implementierung. Die Implementierung besteht aus den folgenden Schritten:

  1. Modellierung des komplexen Typen NameValue
  2. Modellierung des Function Imports getDomFixValues
  3. Implementierung des Function Imports getDomFixValues
  4. Test des Function Imports im Gateway Client
  5. Integration in die Fiori App

1. Modellierung des komplexen Typen NameValue

Der komplexe Typ NameValue besteht aus den beiden String-Feldern Name und Value. Diese Struktur enthält den Schlüssel aus DD07T~DOMVALUE_L sowie die Bedeutung des Schlüssels aus DD07T~DDTEXT. Zur Typisierung benutze ich die ABAP-Struktur /IWBEP/S_MGW_NAME_VALUE_PAIR.

Komplexer Typ NameValue

2. Modellierung des Function Imports getDomFixValues

Der Function Import getDomFixValues erhält als Url Parameter den Namen der Domäne und liefert eine Tabelle des komplexen Typ NameValue zurück.

Function Import getDomFixValue

3. Implementierung des Function Imports getDomFixValues

Nach der Generierung der Laufzeitobjekte kann der Function Import implementiert werden. Das Gateway Projekt habe ich in diesem Beispiel ZRL_DOMFIXVALUE genannt. Die generierte Data Provider Class heißt also ZCL_ZRL_DOMFIXVALUE_DPC_EXT. Für die Implementierung des Function Imports reimplementiere ich die Methode /IWBEP/IF_MGW_APPL_SRV_RUNTIME~EXECUTE_ACTION.

Zur besseren Strukturierung des Codes deligiere ich die Ausführung an die private Methode FCT_GET_DOM_FIX_VALUES.

Implementierung der Execute Action

Die Methode FCT_GET_DOM_FIX_VALUES enthält die eigentliche Logik. Mit dem Namen der Domäne lese ich die Tabelle DD07T. Der Parameter des Function Imports ist über die Struktur zcl_zrl_domfixvalue_mpc=>ts_getdomfixvalues benutzbar. Für den Complex Type NameValue wird in der der Model Provider Class der Typ zcl_zrl_domfixvalue_mpc=>NameValue generiert.

* ---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZRL_DOMFIXVALUE_DPC_EXT->FCT_GET_DOM_FIX_VALUES
* +-------------------------------------------------------------------------------------------------+
* | [--->] IR_FUNC_IMPORT                 TYPE REF TO /IWBEP/IF_MGW_REQ_FUNC_IMPORT
* | [<---] ER_DATA                        TYPE REF TO DATA
* | [!CX!] /IWBEP/CX_MGW_BUSI_EXCEPTION
* +--------------------------------------------------------------------------------------
  METHOD fct_get_dom_fix_values.
    DATA lt_keyvalue TYPE TABLE OF zcl_zrl_domfixvalue_mpc=>keyvalue.
    DATA ls_param TYPE zcl_zrl_domfixvalue_mpc=>ts_getdomfixvalues.

    ir_func_import->get_converted_parameters(
      IMPORTING
        es_parameter_values = ls_param ).

    SELECT
       dd07t~domvalue_l AS key
       dd07t~ddtext AS value
      FROM dd07t
      INTO TABLE lt_keyvalue
      WHERE
        dd07t~domname = ls_param-domname AND
        dd07t~ddlanguage = sy-langu AND
        dd07t~as4local = 'A' AND
        dd07t~as4vers = '0000'.

    IF sy-subrc <> 0.
      RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
        EXPORTING
          message = |FCT_GET_DOM_FIX_VALUES: No domain fix values found for { ls_param-domname } |.
    ENDIF.

    copy_data_to_ref(
      EXPORTING
        is_data = lt_keyvalue
      CHANGING
        cr_data = er_data ).

  ENDMETHOD.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_ZRL_DOMFIXVALUE_DPC_EXT->/IWBEP/IF_MGW_APPL_SRV_RUNTIME~EXECUTE_ACTION
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_ACTION_NAME                 TYPE        STRING(optional)
* | [--->] IT_PARAMETER                   TYPE        /IWBEP/T_MGW_NAME_VALUE_PAIR(optional)
* | [--->] IO_TECH_REQUEST_CONTEXT        TYPE REF TO /IWBEP/IF_MGW_REQ_FUNC_IMPORT(optional)
* | [<---] ER_DATA                        TYPE REF TO DATA
* | [!CX!] /IWBEP/CX_MGW_BUSI_EXCEPTION
* | [!CX!] /IWBEP/CX_MGW_TECH_EXCEPTION
* +--------------------------------------------------------------------------------------
  METHOD /iwbep/if_mgw_appl_srv_runtime~execute_action.
    CASE iv_action_name.

      WHEN 'getDomFixValues'.

        fct_get_dom_fix_values(
          EXPORTING
            ir_func_import               = io_tech_request_context
          IMPORTING
            er_data                      =  er_data ).

      WHEN OTHERS.
        RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
          EXPORTING
            message = |EXECUTE_ACTION: Function import { iv_action_name } not implemented|.
    ENDCASE.
  ENDMETHOD.

4. Test Function Import im Gateway Client

Jetzt ein kurzer Test im Gateway Client (Transaktion /IWFND/GW_CLIENT), ob das Ganze auch wie gewünscht funktioniert. Die URL ist /sap/opu/odata/sap/ZRL_DOMFIXVALUE_SRV/getDomFixValues?domname=’D_SO_LC‘

SAP Gateway Client

Voilà! Der Service liefert für die Domäne D_SO_LC die NameValue-Paare zurück.

5. Integration in die Fiori App

Jetzt bauen wir eine SAPUI5, die diesen Service konsumiert. Das gehört inhaltlich eigentlich nicht in einen ABAP Blog. Ich persönlich finde jedoch, dass der Aufruf eines Function Imports in den diversen Forumsbeiträgen und Blogs immer nur partiell beschrieben ist. Deshalb an dieser Stelle einmal eine Schritt für Schritt Anleitung.

5.1 manifest.json

In der manifest.json unter sap.app den OData Service als dataSource registrieren und diese an ein Model binden. Hier wird das Default-Model verwendet.

	"sap.app": {
		"id": "cgi.DomFixValue",
		"dataSources": {
			"mainService": {
				"uri": "/sap/opu/odata/sap/ZRL_DOMFIXVALUE_SRV",
				"type": "OData",
				"settings": {
					"odataVersion": "2.0"
				}
			}
		}		
	}	

	"models": {
			"": {
				"dataSource": "mainService",
				"preload": true,
				"settings": {
					"defaultBindingMode": "TwoWay",
					"defaultOperationMode": "Server"
				}
			}
		}

5.2 neo-app.json

Für die Ausführung in der WebIDE muss folgender Eintrag in der neo-app.json stehen:

	{
		"path": "/sap/opu/odata",
		"target": {
			"type": "destination",
			"name": "cgigwy",
			"entryPath": "/sap/opu/odata"
		},
		"description": "cgigwy"
	}

5.3 Component.js

Das Ausführen des Function Imports ist an die models-Datei ausgelagert.
In der Component.js erfolgt der Aufruf models.createLifecycleStatusModel(this) hinzufügen.

sap.ui.define([
	"sap/ui/core/UIComponent",
	"cgi/DomFixValue/model/models"
], function (UIComponent, models) {
	"use strict";

	return UIComponent.extend("cgi.DomFixValue.Component", {

		metadata: {
			manifest: "json"
		},

		/**
		 * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
		 * @public
		 * @override
		 */
		init: function () {
			// call the base component's init function
			UIComponent.prototype.init.apply(this, arguments);

			// enable routing
			this.getRouter().initialize();
			
			models.createLifecycleStatusModel(this);
		}

	});
});

5.4 models.js

In models.js wird dann die Methode createLifecycleStatusModel implementieren. Sie erzeugt ein JSON Model mit Namen lifecycleStatus, mit dessen Hilfe das Ergebnis des Function Imports in der App verwendet werden kann.

sap.ui.define([
	"sap/ui/model/json/JSONModel",
	"sap/ui/Device"
], function (JSONModel, Device) {
	"use strict";

	return {

		createDeviceModel: function () {
			var oModel = new JSONModel(Device);
			oModel.setDefaultBindingMode("OneWay");
			return oModel;
		},

		createLifecycleStatusModel: function (oComponent) {
			var mOptions = {
				method: "GET",
				urlParameters: {
					"domname": "D_SO_LC"
				},
				success: function (oData) {
					oComponent.setModel(new JSONModel(oData.results), "lifecycleStatus");
				},
				error: function () {
					oComponent.setModel(new JSONModel({}), "lifecycleStatus");
				}
			};

			oComponent.getModel().callFunction("/getDomFixValues", mOptions);
		}

	};
});

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP

OData Model Best Practices

Moin, Moin aus dem UI5 Maschinenraum!
Heute möchte ich dir das harte Entwicklerleben etwas erleichtern und ein paar meiner OData Model Best Practices mit dir teilen.
Ziel ist es, deine SAPUI5 OData Calls zu stabilisieren und deinen Code durch Standardisierung „cleaner“ zu gestalten.

OData Calls

Das SAPUI5 Framework bietet dir die Grundfunktionalitäten um Datensätze zu lesen und zu manipulieren.

Falls du noch nicht weißt, wie genau diese CRUD-Q Methoden funktionieren, dann lies dir bitte zunächst die folgenden zwei Blogs von meinem lieben Kollegen Sebastian Kielhorn durch.
Er erklärt dir schnell und gut wie man OData liest und schreibt. Ich warte hier so lange auf dich…

Schön, nun will ich dein Wissen etwas erweitern und dir zeigen, wie du deine OData Calls standardisieren kannst.

Standardisierung und Best Practices

In einer typischen UI5-Anwendung wirst du OData CRUD-Q Calls und Function Imports an diversen Stellen ausführen. Typischer Weise soll das UI während der Kommunikation mit dem Backend durch einen BusyIndicator gesperrt werden. Außerdem muss die Frontend-Instanz des OData Models synchronisiert werden, falls einer deiner Calls Daten im Backend manipuliert.

Man muss kein Genie sein, um solch redundante Arbeitsschritte in einem eigenen kleinen Hilfscontroller auszulagern. Ich nenne diesen Controller in diesem Blog „Utilities.js“.

Zunächst legst du das Grundgerüst deines Controllers an und definierst dessen Abhängigkeiten.

sap.ui.define([
	"cgi/controller/BusyHelper"
], function(BusyHelper) {
	"use strict";
 	 return {
         
         };
    }
);

Es fällt dir bestimmt direkt auf, dass innerhalb deines Controllers eine Abhängigkeit namens „BusyHelper“ existiert. Dabei handelt es sich um einen zweiten Controller, der den BusyDialog steuert.

BusyHelper.js

Den BusyHelper kannst du Beispielsweise wie folgt implementieren:

sap.ui.define(["sap/m/BusyDialog"], 
    function(BusyDialog) {
	"use strict";
	
	var _oBusyDialog, _bOpen;
	
	return {
		_init: function() {
			_oBusyDialog = new BusyDialog();
			_bOpen = false;
		},
		show: function() {
			if (!_oBusyDialog) {
				this._init();
			}
			if (!_bOpen) {
				_oBusyDialog.open();
				_bOpen = true; 
			}
		},
		hide: function() {
			if (_bOpen) {
				_oBusyDialog.close();
				_bOpen = false;
			}
		}
	};

 

OData Model read()

Nun erweiterst du deinen Utilities.js Controller um eine standardisierte Methode um damit Daten zu laden.

Diese Methode heißt readFromOdata(). Sie sollte unabhängig von jeglichen Modellnamen oder auch Views sein, sodass sie praktisch von überall in deiner Applikation aufgerufen werden kann. Außerdem sollte die Methode über Parameter verfügen, mit denen man Filter und Pfade für ein eventuellen OData Expands verarbeiten kann.

Deine Methode zum Laden könnte zum Beispiel so aussehen:

readFromOData: function(oModel, sPath, sExpand, aFilters, bNoBusyIndicator) {
	//deferred for asynchronous resolving
	var sUrlParam;
	var dfdResult = jQuery.Deferred();

	if (sExpand.length > 0) {
		sUrlParam = "$expand=" + sExpand;
	} else {
		sUrlParam = null;
	}
		
	if(!bNoBusyIndicator){
		BusyHelper.show();
	}
	// Parameter map for odata service call
	var mParameters = {
		context: null,
		urlParameters: sUrlParam,
		filters: aFilters,
		sorters: null,
		success: function(oData, oResponse) {
			//close loading indicator
			if(!bNoBusyIndicator){
				BusyHelper.hide();
			}
			if (oData) {
				if (oData.results && oData.results.length > 0) {
					dfdResult.resolve(oData.results);
				} 
				else if (typeof(oData) === "object" && !oData.results && !$.isEmptyObject(oData)){
					dfdResult.resolve(oData);
				}
				else {
					dfdResult.resolve(null);
				}
			} else {
				dfdResult.reject(oResponse);
			}
		},
		error: function(oResponse) {
			//close loading indicator
			if(!bNoBusyIndicator){
				BusyHelper.hide();
			}
			dfdResult.reject(oResponse);
		}
	};
	// get data from model with parameters
	oModel.read(sPath, mParameters);

	return dfdResult.promise();
}

Wie du siehst, wird die readFromOdata() mit Hilfe eines jQuery Deferred Objects als Promise ausgeprägt. Dadurch kriegen wir das Timing-Problem, das durch die asynchrone Natur eines OData Calls entsteht, in den Griff und machen die aufrufende Methode zusätzlich noch sehr viel lesbarer. Der Aufruf aus einem View-Controller sieht nämlich in etwas so aus:

...
var oModel = this.getView().getModel();
Utilities.readFromOdata(oModel, "/EntitySet('123456')", "/ToSubEntity", aFilters, true)
     .done(
         function(OData){
               //here is what you want to do with you data
         })
     .fail(
         function(oRespone){
              //here is you error handling
         });
...

Schön übersichtlich, oder?

OData Model callFunction()

Die zweite Methode, die ich dir zeigen möchte ist doFunctionImport(). Dabei handelt es sich um einen standardisierter Aufruf eines Function Imports. Sie funktioniert eigentlich nach dem selben Prinzip wie die readFromOdata(), natürlich ohne Filter und Expands.

doFunctionImport: function(oModel, sFunction, mInputData, bNoBusyIndicator) {

	var dfdResult = jQuery.Deferred();

	if(!bNoBusyIndicator){ 
           BusyHelper.show();
        }

	var mParameters = {
		method: "GET",
		urlParameters: mInputData,
		success: function(oData, oResponse) {
			//close Busy indicator
			BusyHelper.hide();
			if (oData) {
				dfdResult.resolve(oData);
			} else {
				dfdResult.resolve(null);
			}
		},
		error: function(oResponse) {
			//close loading indicator
			BusyHelper.hide();
			dfdResult.reject(oResponse);
		}
	};

	oModel.callFunction(sFunction, mParameters);

	return dfdResult.promise();
}

So lassen sich Function Imports bequem ausführen, ohne dass du dir jedes mal aufs neue Gedanken über Asynchronität oder Busy Handling machen musst.

 

Ich hoffe meine Code Snippets helfen dir etwas weiter und verbessern deine UI5 Entwicklung.

In ein paar Tagen findest du auf unserem Blog auch noch Best Practices für die Standardisierung vom create() und update() des OData Models.

Bis dahin!
In Hamburg sagt man Tschüss!

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Performanzoptimierung des ABAP OData $Expand

Ein häufig anzutreffendes Performanzproblem bei der klassischen OData Entwicklung in ABAP ohne Nutzung von SADL ist das Lesen von Entitäten mitsamt assozierter Entitäten mittels $expand.

Ein Beispiel ist eine SAPUI5 Worklist App, welche zu den angezeigten SalesOrders weitere Informationen aus dem zugehörigen Business Partner und den SalesOrderItems anzeigt. Das Performanzproblem kann man gut an dem bekannten GWSAMPLE_BASIC oData Service beobachten, welcher ansonsten für viele Features der oData Entwicklung Lösungsansätze liefert.

Performanzuntersuchung des GWSAMPLE_BASIC-Service

Lass uns einmal die Performanz vom GWSAMPLE_BASIC-Service beim Expand untersuchen. Das Lesen von 50 SalesOrders mitsamt BusinessPartner und SalesOrderItems mit der Url /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/SalesOrderSet?$top=50&$expand=ToBusinessPartner,ToLineItems&sap-ds-debug=true braucht auf meinem (zugegeben nicht besonders schnellen) SAP Gateway System rund 867ms.

GWSAMPLE_BASIC Performanz beim Expand

Die Laufzeit wird dominiert durch die Methode /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET in der DPC_EXT-Klasse /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT. Die  DPC_EXT-Klasse erbt diese Methode aus dem OData Framework. Diese geerbte Methode geht beim Expand wie folgt vor

  1. Lesen der SalesOrders mittels Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, SALESORDERSET_GET_ENTITYSET
  2. Lesen der SalesOrderItems zu jeder SalesOrder ( Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, SALESORDERLINEIT_GET_ENTITYSET )
  3. Lesen des BusinessPartner zu jeder SalesOrder (Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, BUSINESSPARTNERS_GET_ENTITY )

Das führt beim Lesen von 50 SalesOrders also in Summe zu 101 Datenbankzugriffen. Im Debugger kann man das gut nachvollziehen.

Debuggen des OData Expand

Jeder dieser SQL SELECTs ist nicht teuer. Die Summe alle SQL SELECTs ist jedoch teuer. Ein SQL Trace via Transaktion ST05 zeigt dies.

SQL Trace OData Expand

Lösungsansatz Reimplementierung von /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET

Der Lösungsansatz ist die Reduzierung der Datenbankzugriffe von 101 auf 3, indem die assozierten Entitäten in einem Rutsch für alle SalesOrders gelesen werden. Im Folgenden implementieren wir das einmal durch und führen die Messung dann nochmal durch.

Dafür kopiere den Service GWSAMPLE_BASIC mitsamt seiner Implementierung. Wie das geht, steht in diesem Blogbeitrag. Im nächsten Schritt implementieren wir die Methode /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET in der DPC_EXT-Klasse.

/IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET redefinieren

Die Methode werden bei jedem Expand auf einem EntitySet aufgerufen. Wir müssen also hier prüfen, ob der Expand auf dem SalesOrderSet aufgerufen wird. Wenn ja, lesen wir die SalesOrders und nutzen dabei die vorhandene Implementierung in Methode salesorderset_get_entityset. Danach verzweigen in die Methode exp_entset_salesorder. Wenn der Expand auf einemEntitySet ungleich SalesOrder aufgerufen wird, delegieren wir an die Standardlogik des OData Frameworks,

METHOD /iwbep/if_mgw_appl_srv_runtime~get_expanded_entityset.

    DATA lt_ent_salesorder TYPE zcl_zrlgwsample_basic_mpc=>tt_salesorder.


    IF iv_entity_name = zcl_zrlgwsample_basic_mpc=>gc_salesorder AND
       iv_entity_set_name = |{ zcl_zrlgwsample_basic_mpc=>gc_salesorder }Set| AND
       iv_source_name = zcl_zrlgwsample_basic_mpc=>gc_salesorder.

      salesorderset_get_entityset(
        EXPORTING
          iv_entity_name           = iv_entity_name
          iv_entity_set_name       = iv_entity_set_name
          iv_source_name           = iv_source_name
          it_filter_select_options = it_filter_select_options
          is_paging                = is_paging
          it_key_tab               = it_key_tab
          it_navigation_path       = it_navigation_path
          it_order                 = it_order
          iv_filter_string         = iv_filter_string
          iv_search_string         = iv_search_string
          io_tech_request_context = io_tech_request_context
        IMPORTING
          et_entityset             = lt_ent_salesorder
          es_response_context      = es_response_context  ).

      exp_entset_salesorder(
        EXPORTING
          it_ent_salesorder = lt_ent_salesorder
          ir_expand = io_expand
        IMPORTING
          et_expanded_tech_clauses = et_expanded_tech_clauses
          er_entityset = er_entityset )  .

    ELSE.
      CALL METHOD super->/iwbep/if_mgw_appl_srv_runtime~get_expanded_entityset
        EXPORTING
          iv_entity_name           = iv_entity_name
          iv_entity_set_name       = iv_entity_set_name
          iv_source_name           = iv_source_name
          it_filter_select_options = it_filter_select_options
          it_order                 = it_order
          is_paging                = is_paging
          it_navigation_path       = it_navigation_path
          it_key_tab               = it_key_tab
          iv_filter_string         = iv_filter_string
          iv_search_string         = iv_search_string
          io_expand                = io_expand
          io_tech_request_context  = io_tech_request_context
        IMPORTING
          er_entityset             = er_entityset
          et_expanded_clauses      = et_expanded_clauses
          et_expanded_tech_clauses = et_expanded_tech_clauses
          es_response_context      = es_response_context.
    ENDIF.

  ENDMETHOD.

Implementierung der Expand Logik

Die Methode exp_entset_salesorder sammelt die Daten für den Expand bereitet diese auf. Das Coding ist ein wenig länglich, weil die vorhandene Implementierung in den Methoden bp_get_entityset und soli_get_entityset noch ein paar Tabellen mehr selektiert. Die Logik führt folgende Schritte durch

  1. Ermitteln, wohin expandiert werden soll
  2. Expand TOBUSINESSPARTNER: Extraktion der BusinessPartnerIds aus den SalesOrders und Selektion der BusinessPartner
  3. Expand TOLINEITEMS: Extraktion der SalesOrderIds aus den SalesOrders und Selektion der SalesOrderItems
  4. Abmischen der selektieren Daten in die Expand-Datenstruktur
* ---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT->EXP_ENTSET_SALESORDER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_ENT_SALESORDER              TYPE        ZCL_ZRLGWSAMPLE_BASIC_MPC=>TT_SALESORDER
* | [--->] IR_EXPAND                      TYPE REF TO /IWBEP/IF_MGW_ODATA_EXPAND
* | [<---] ER_ENTITYSET                   TYPE REF TO DATA
* | [<---] ET_EXPANDED_TECH_CLAUSES       TYPE        STRING_TABLE
* +--------------------------------------------------------------------------------------
  METHOD exp_entset_salesorder.
    TYPES:
      BEGIN OF ts_exp_salesorder.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorder.
    TYPES:
      tolineitems       TYPE STANDARD TABLE OF zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem WITH DEFAULT KEY,
      tobusinesspartner TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner,
      END OF ts_exp_salesorder.
    TYPES:
      BEGIN OF ts_soli_helper,
        soli_guid          TYPE snwd_node_key,
        note_guid          TYPE snwd_node_key,
        note_orig_language TYPE spras.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem.
    TYPES END OF ts_soli_helper .

    TYPES:
      BEGIN OF ts_bpa_helper.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner.
        INCLUDE TYPE /iwbep/s_gws_basic_address.
    TYPES END OF ts_bpa_helper.

    DATA lt_child TYPE /iwbep/if_mgw_odata_expand=>ty_t_node_children.
    DATA ls_child TYPE /iwbep/if_mgw_odata_expand=>ty_s_node_child.
    DATA lt_rng_bpa TYPE /iwbep/t_cod_select_options.
    DATA lt_rng_so_id TYPE /iwbep/t_cod_select_options.
    DATA ls_exp_salesorder TYPE ts_exp_salesorder.
    DATA lt_exp_salesorder TYPE table of ts_exp_salesorder.
    DATA ls_ent_businesspartner TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner.
    DATA lt_ent_businesspartner TYPE zcl_zrlgwsample_basic_mpc=>tt_businesspartner.
    DATA lt_ent_salesorderlineitem TYPE zcl_zrlgwsample_basic_mpc=>tt_salesorderlineitem.
    DATA ls_ent_salesorderlineitem TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem.
    DATA lt_soli_helper TYPE TABLE OF ts_soli_helper.
    DATA lt_note_texts TYPE STANDARD TABLE OF snwd_texts.
    FIELD-SYMBOLS  TYPE snwd_texts.
    DATA lt_sosl TYPE STANDARD TABLE OF snwd_so_sl WITH NON-UNIQUE SORTED KEY p_key COMPONENTS parent_key.
    DATA lt_bpa_helper TYPE TABLE OF ts_bpa_helper.

    lt_child = ir_expand->get_children( ).

*   Ermitteln, wohin expandiert werden soll
    LOOP AT lt_child INTO ls_child.
      CASE ls_child-tech_nav_prop_name.

*       Expand zum BusinessPartner
        WHEN 'TOBUSINESSPARTNER'.
          APPEND ls_child-tech_nav_prop_name TO et_expanded_tech_clauses.

*         Extraktion der BusinessPartnerIds
          lt_rng_bpa = conv_itab_to_range(
            it_itab = it_ent_salesorder
            iv_fieldname = 'BUYER_ID' ).

          SORT lt_rng_bpa BY low.
          DELETE ADJACENT DUPLICATES FROM lt_rng_bpa COMPARING low.

*         Code adaptiert aus Methode bp_get_entityset: Selektion der BusinessPartner
          SELECT
            b~bp_id
            b~bp_role
            b~company_name
            b~web_address
            b~email_address
            b~phone_number
            b~fax_number
            b~legal_form
            b~currency_code
            b~created_at
            b~changed_at
            a~city
            a~postal_code
            a~street
            a~building
            a~country
            a~address_type
            INTO CORRESPONDING FIELDS OF TABLE lt_bpa_helper
            FROM ( snwd_bpa AS b
                   INNER JOIN snwd_ad AS a ON a~node_key = b~address_guid )
            WHERE bp_id IN lt_rng_bpa.

          SORT lt_ent_businesspartner BY bp_id.

*       Expand zu den SalesOrderItems
        WHEN 'TOLINEITEMS'.
          APPEND ls_child-tech_nav_prop_name TO et_expanded_tech_clauses.

*         Extraktion der SalesOrderIds
          lt_rng_so_id = conv_itab_to_range(
            it_itab = it_ent_salesorder
            iv_fieldname = 'SO_ID' ).

*         Code adaptiert aus Methode SOLI_GET_ENTITYSET: Selektion SalesOrderItems
          SELECT
            s~so_id
            i~node_key AS soli_guid
            i~so_item_pos
            i~note_guid
            i~currency_code
            i~gross_amount
            i~net_amount
            i~tax_amount
            p~product_id
            tk~original_langu AS note_orig_language
            INTO CORRESPONDING FIELDS OF TABLE lt_soli_helper
            FROM ( ( ( snwd_so_i AS i
                       INNER JOIN snwd_so AS s ON s~node_key = i~parent_key )
                       INNER JOIN snwd_pd AS p ON p~node_key = i~product_guid )
                       LEFT OUTER JOIN snwd_text_key AS tk ON tk~node_key = i~note_guid )
            WHERE so_id IN lt_rng_so_id.

          SORT lt_soli_helper BY so_id.

*         Weitere Infos dazu selektieren: Texte und Schedule Lines
          IF lines( lt_soli_helper ) > 0.
            SELECT * FROM snwd_texts INTO TABLE lt_note_texts
              FOR ALL ENTRIES IN lt_soli_helper
                WHERE parent_key EQ lt_soli_helper-note_guid.

            SELECT * FROM snwd_so_sl INTO TABLE lt_sosl
              FOR ALL ENTRIES IN lt_soli_helper
                WHERE parent_key EQ lt_soli_helper-soli_guid.

            SORT lt_note_texts BY parent_key language.
            SORT lt_sosl BY parent_key delivery_date.

            LOOP AT lt_soli_helper ASSIGNING FIELD-SYMBOL().
              CLEAR ls_ent_salesorderlineitem.
              MOVE-CORRESPONDING  TO ls_ent_salesorderlineitem.

*             Text ermitteln
              UNASSIGN .
              READ TABLE lt_note_texts ASSIGNING  WITH KEY
                  parent_key = -note_guid
                  language   = sy-langu BINARY SEARCH.
              IF sy-subrc NE 0.
                READ TABLE lt_note_texts ASSIGNING  WITH KEY
                  parent_key = -note_guid
                  language   = -note_orig_language BINARY SEARCH.
              ENDIF.

              IF  IS ASSIGNED.
                ls_ent_salesorderlineitem-note          = -text.
                ls_ent_salesorderlineitem-note_language = -language.
              ENDIF.

*             Ermittlung schedule line
              LOOP AT lt_sosl ASSIGNING FIELD-SYMBOL() USING KEY p_key WHERE parent_key = -soli_guid.
                IF -delivery_date IS INITIAL.
                  ls_ent_salesorderlineitem-delivery_date = -delivery_date.
                ENDIF.
                ls_ent_salesorderlineitem-quantity      = ls_ent_salesorderlineitem-quantity + -quantity.
                ls_ent_salesorderlineitem-quantity_unit = -quantity_unit.
              ENDLOOP.

              APPEND ls_ent_salesorderlineitem TO lt_ent_salesorderlineitem.

            ENDLOOP.
          ENDIF.
      ENDCASE.
    ENDLOOP.

*   Expanded Struktur aufbauen
    LOOP AT it_ent_salesorder ASSIGNING FIELD-SYMBOL().
      CLEAR ls_exp_salesorder.

      MOVE-CORRESPONDING  TO ls_exp_salesorder.

*     Business Partner dazumischen
      IF lines( lt_bpa_helper ) > 0.
        READ TABLE lt_bpa_helper ASSIGNING FIELD-SYMBOL()
          WITH KEY  bp_id = -buyer_id BINARY SEARCH.
        IF sy-subrc = 0.
          MOVE-CORRESPONDING  TO ls_exp_salesorder-tobusinesspartner.
          MOVE-CORRESPONDING  TO ls_exp_salesorder-tobusinesspartner-address.
        ENDIF.
      ENDIF.

*     SalesorderItems dazumischen
      IF lines( lt_ent_salesorderlineitem ) > 0.
        READ TABLE lt_ent_salesorderlineitem WITH KEY so_id = -so_id
          BINARY SEARCH TRANSPORTING NO FIELDS.
        IF sy-subrc = 0.
          LOOP AT lt_ent_salesorderlineitem ASSIGNING FIELD-SYMBOL() FROM sy-tabix.
            IF -so_id <> -so_id.
              EXIT.
            ENDIF.

            APPEND  TO ls_exp_salesorder-tolineitems.
          ENDLOOP.
        ENDIF.
      ENDIF.

      APPEND ls_exp_salesorder TO lt_exp_salesorder.
    ENDLOOP.

    copy_data_to_ref(
         EXPORTING
           is_data = lt_exp_salesorder
         CHANGING
           cr_data = er_entityset ).

  ENDMETHOD.

* ---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT->CONV_ITAB_TO_RANGE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_ITAB                        TYPE        ANY TABLE
* | [--->] IV_FIELDNAME                   TYPE        FIELDNAME(optional)
* | [<-()] RT_RNG                         TYPE        /IWBEP/T_COD_SELECT_OPTIONS
* +--------------------------------------------------------------------------------------
  METHOD conv_itab_to_range.
    DATA ls_rng TYPE /iwbep/s_cod_select_option.
    FIELD-SYMBOLS  TYPE any.
    FIELD-SYMBOLS  TYPE any.

    ls_rng-sign = 'I'.
    ls_rng-option = 'EQ'.

    LOOP AT it_itab ASSIGNING .
      IF iv_fieldname IS INITIAL.
        ASSIGN  TO .
      ELSE.
        ASSIGN COMPONENT iv_fieldname OF STRUCTURE  TO .
      ENDIF.

      IF  IS NOT INITIAL.
        ls_rng-low = .
        APPEND ls_rng TO rt_rng.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.

Aufbau der Expand-Datenstruktur

Das Coding ist straight forward. Interessant ist noch, wie die Expand-Datenstruktur aussehen muss, damit das OData unsere expandierten Entities auch verarbeiten kann. Die Methode exp_entset_salesorder liefert eine Tabelle mit dieser Datenstruktur zurück. Die Expand-Datenstruktur ist im Code als lokaler Typ ts_exp_salesorder definiert. Man sieht hier gut, das der Gateway Service Builder bei der Generierung für jede Entity einen zugehörigen Typ in der MPC-Klasse erzeugt.

TYPES:
      BEGIN OF ts_exp_salesorder.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorder.
    TYPES:
      tolineitems       TYPE STANDARD TABLE OF zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem WITH DEFAULT KEY,
      tobusinesspartner TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner,
      END OF ts_exp_salesorder.

Die Datenstruktur ist tief. Sie hat neben der Feldern aus der SalesOrder die Felder tolineitems und tobusinesspartner, welche durch eine Struktur bzw. Tabelle typisiert sind. Diese Feldnamen korrespondieren zu den Navigationseigenschaften im Service Builder.

OData Expand Datenstruktur

Performanzmessung der neuen Logik durchführen

Lass uns mal schauen, was die neue Logik auf der Performanzseite bringt. Hierfür starte den Gateway Client und messe die Url /sap/opu/odata/SAP/ZRLGWSAMPLE_BASIC_SRV/SalesOrderSet?$top=50&$expand=ToBusinessPartner,ToLineItems&sap-ds-debug=true durch.

Die Laufzeit ist von 867ms auf 427ms gesunken. Das ist eine Halbierung! In der Realität wird das noch drastischer sein, weil in den SalesOrder Tabellen des EPM-Modell (Tabellen SNWD_*) nur wenige Tausend Datensätze enthalten sind.

Performanz nach der Expand-Optimierung

Hast du noch Fragen zu OData Performanz oder zu anderen Themen?

Nutze gerne unsere Kommentarfunktion oder schreib mir direkt eine eMail

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

SAP build Startseite

SAP build – User Storys und Prototypen mit UI5

Hast du schon mal eine User Story in einer PowerPoint Präsentation gesehen und dich gefragt ob das nicht auch anders geht? Warum nicht gleich einen Prototypen entwickeln, der schon mal die groben Züge der Anwendung darstellt.

Im Kontext von SAP Fiori Anwendung geht das mit build. Auf build.me kannst du einen Prototypen deiner Applikation per Drag&Drop zusammensetzen. Großartige Funktionen kannst du mit build allerdings nicht einbauen, du wirst trotzdem Entwickler brauchen, welche die finale Anwendung programmieren. Neben der Entwicklung von Prototypen gibt es noch ein paar weitere Funktionen die ganz interessant für dich sein könnten.

[0]: Vorbereitungen

Im folgenden gehe ich mit dir durch die einzelnen Schritte eines build-Projekts, bis du einen groben Überblick über die Funktionen von build hast.

Neben einem Account auf build.me benötigst du einen großen Monitor und Chrome oder Chromium als Browser mit anderen Browsern kann es zu Problemen kommen.
Wenn du dir bereits einen Account angelegt hast, kann es direkt losgehen. Wenn du noch keinen hast? Dann aber schnell, ich warte hier solange auf dich ….. fertig?
Alles klar los geht’s:

Als erstes erstellen wir ein neues Projekt. Dafür gehst du auf Workspace und klickst dann auf New Project.

build: Project Workspace
Wie du siehst habe ich bereits zwei Projekte angelegt. Aber wir starten ganz von Vorne.

In dem kleinen Popup-Fenster, dass jetzt erscheint kannst du auch ein bestehendes Projekt aus der Galerie auswählen. Aber wir wollen selbst eines erstellen, keine Angst wir halten es ganz simpel. Du klickst also auf Create New Project und gibst deinem neuen Projekt einen Namen und eine kurze Beschreibung.

build: Projektübersicht
Wir wollen ein Tool zur Zeiterfassung erstellen.

In diesem kleinen Beispiel will ich eine simple Applikation zur Zeiterfassung von Arbeitszeiten erstellen.

[1]: Persona anlegen

Als erstes klicken wir auf Create a Persona und legen unsere virtuelle Person an. Du kennst diese Peronas bestimmt bereits aus klassischen User Storys. In diesem Beispiel verzichten wir auf ein Bild und detaillierte Angaben. Wenn du mehr erfahren möchtest kannst du oben rechts auf den link im Editor klicken (hier gelb hinterlegt), dort kannst du weitere Informationen zu Personas falls du dir nicht sicher bist wofür man diese braucht.

build: Persona-Editor
Hier füllen wir die Felder nur rudimentär aus. Bei deinem echten Projekt solltest du etwas sorgfältiger vorgehen.

Wenn du mit deiner neuen Persona zufrieden bist, klickst du oben rechts auf Done und gelangst in die Übersicht mit allen Personas in diesem Projekt. Wenig überraschend haben wir genau eine Persona. Wenn es nötig wäre könntest du natürlich noch weitere Personas anlegen. Um in unserem kleinen Beispiel könnte es vielleicht der Vorgesetzte sein, der die erfassten Zeiten genehmigen muss. Aber für unser Beispiel reicht uns eine Persona.

build: zurück zum Projekt
Wenn wir die Persona angelegt haben, geht es zurück in die Übersicht.

 

[2]: Prototypen erstellen

Jetzt geht es richtig los. Hol dir noch schnell einen frischen Kaffee und setz dich gerade hin. In dem nächsten Abschnitt haben wir zwei Optionen entweder laden wir Bilder von unserer neuen Applikation hoch und versehen diese mit Funktionen oder wir bauen unseren Prototypen aus UI5-Elementen.

Die erste Option bietet dir natürlich mehr Freiheiten bei der Gestaltung allerdings musst du bedenken, dass je weiter du dich vom Standard  entfernst desto schwieriger wird die Entwicklung. Über deine Bilder könntest du dann Interaktive Hotspots setzen die sehr limitierte Funktionen auslösen, wie zum Beispiel ein kleines Text PopUp oder ein Wechsel zu einer anderen Ansicht.

build: Template Auswahl
Hier kannst du auswählen ob du mit Bildern oder einem Template starten möchtest.

Ich klicke also auf Start With Template, wir werden unseren Prototypen direkt aus UI5-Elementen bauen.

Freestyle als Template
Wir nehmen Freestyle. Es reicht wenn unsere Anwendung auf dem Desktop läuft.

Es gibt verschiedene Templates. Responsive eignet sich, um verschiedene Display-Größen mit einer Anwendung zu unterstützen. Daneben gibt es noch einige vorgefertigte Fiori-Anwendungen, die du an deine Bedürfnisse anpassen kannst. Als Template wählen wir Freestyle und klicken dort auf Select.

Auf der folgenden Seite sehen wir alle Ansichten innerhalb dieses Prototypen, hier könnte zum Beispiel noch eine Ansicht für Einstellungen oder eine Detailansicht angelegt werden. Zwischen diesen Ansichten können wir auch in unserem Prototypen wechseln. Unsere kleine Anwendung wird jedoch nur eine Ansicht haben.

Als nächstes öffnen wir unsere Page 1, klicke einfach auf die leere Miniaturansicht unserer App. Darauf hin öffnet sich der Prototypen Editor.

[3]: Der Prototypen Editor

Header aktivieren und Namen angeben. $UserName soll in der fertigen Anwendung durch den eingeloggten Nutzer ersetzt werden.

Oben rechts aktivierst du den Header und die Navigation. Du gibst noch einen Titel ein und schon ist unsere App nicht mehr ganz so leer. Der $UserName ist hier ein Platzhalter für den Nutzernamen unseres Mitarbeiters hat aber keine weitere Funktion. Genau wie der Navigation Pfeil, er impliziert nur, dass sich unsere Anwendung in einem größeren Kontext befindet.

Als nächstes brauchen wir den Kalender. Den findest du im linken Menü unter Controls schnell geht es wenn du in die Suche Calendar eingibst. Per Drag & Drop ziehst du ihn aus der Liste direkt auf die leere Fläche. Wenn es nicht sofort klappt, nicht verzweifeln, der Editor funktioniert nicht immer so wie man es möchte und manchmal muss man die Elemente erstmal irgendwo platzieren bevor man sie an die richtige Stelle verschiebt.

Kalender
Der Kalender soll es ermöglichen zwischen den Wochen zu wechseln

Den Kalender passen wir noch etwas an, dafür ändern wir links den „First Day of Week“ auf 1 damit unsere Wochen mit Montag beginnen. Als nächstes fügen wir neben dem Kalender ein Formular ein, dafür suchst du nach Form und ziehst das Element neben den Kalender. Im Zweifel musst du ein wenig herumprobieren, bis Formular an der richtigen Stelle sitzt.

Die Applikation wurde um ein Formular und eine Button-Leiste erweitert.
Wir fügen in unsere App ein Formular ein, dass wir anpassen.
Strukturansicht des Formulars
Hier siehst du die Struktur, die mein Formular hat.

In dem Formular fügen wir insgesamt 5 Rows hinzu, die Elemente die bereits in dem Form sind kannst du einfach per Rechtsklick -> Delete löschen oder in dem Outline unten rechts auch per Rechtsklick. Jede Row braucht ein Label für den Tag und drei Textfelder. Einen Formats-Hinweis schreiben wir in die Textfelder. Alle Elemente für unser Form findest du in der Controls-Liste auf der linken Seite. Wir brauchen Row, Input und Label diese ziehst du unten links ins Outline-Fenster. Elemente die du mehrfach brauchst kannst du im Outline duplizieren mach einfach mal einen Rechtsklick auf einen Input

Wenn du das Formular nicht auf Anhieb hin bekommst, ist das nicht schlimm, die Bedienung ist etwas fummelig und teilweise nicht intuitiv. Unter dem Formular fügen wir noch einen Segmented Button ein, den findest du auch über die Suche.

Bevor du dich zu viel über das Formular ärgerst oder zu viel Zeit damit verbringst, kannst du es auch einfach so lassen wie es ist. Schließlich ist das ganze nur ein Test und das Formular erfüllt eh keine Funktion. Meinem Beitrag kannst du auch ohne richtigem Formular folgen.

Jetzt konfigurieren wir die Button-Leiste, dieser soll die drei Funktionen Speichern, Aktualisieren und Abschicken enthalten.

Button Icons und Texte anpassen.
Schau dich einmal in der Libary um, dort gibt es viele fertige Icons für verschiedene Themen.

Wenn du mit deinen Buttons zufrieden bist schließen wir unseren Ausflug in den Protoypen Editor ab. Man kann hier noch viel mehr machen und wenn du Lust hast kannst du deinen Prototypen ja noch etwas erweitern. Als nächstes wollen wir uns noch die Survery-Funktion ansehen.

[4]: Survey

Die Survey Funktion ist sehr praktisch um Feedback von Anwendern zu sammeln. Deine Anwender können die Applikation testen und Feedback hinterlassen. Aber eins nach dem Anderen oben rechts klicken wir auf Create Study. Als Titel kannst du eingeben was du möchtest.

create study
Wir wollen für den aktuellen Prototypen eine Study erstellen.

Du landest nun in einer Übersicht mit allen Fragen innerhalb dieser Study. Hier siehst du jetzt eine Miniaturansicht unseres Prototypen. Wenn du oben auf New Question klickst könntest du eine weitere Frage anlegen uns reicht jedoch eine.

Übersicht der Fragen für diese Study
Einer Study können wir mehre Fragen hinzufügen. Diese Fragen werden dem Anwender Präsentiert.

Wenn du auf die Miniaturansicht unseres Prototypen klickst, öffnet sich ein neuer Editor, in dem du eine Frage formulieren kannst. Es gibt verschiedene Möglichkeiten auf unsere Frage zu antworten. Ich denke die verschieden Optionen erklären sich von selbst, du kannst sie später ja einmal ausprobieren oder mehrere Fragen mit unterschiedlichen Typen anlegen.

Frage für unsere App
Hier stellst du deine Frage und konfigurierst die Antwortmöglichkeit sowie das Display-Format

Gib einfach irgendeine Frage ein und belasse die anderen Optionen so wie sie aktuell sind.

Wenn du soweit bist klickst du oben rechts klicke auf Done. Danach gelangen zurück in die Übersicht und können oben rechts einmal auf Preview klicken, dort können wir uns den Fragebogen anschauen, ohne ihn zu veröffentlichen.

In diesem Modus kann ich Kommentare direkt an den Elementen hinterlassen.
In diesem Modus kann ich Kommentare direkt an den Elementen hinterlassen.

Die Feedback Funktion ist eigentlich sehr intuitiv zu bedienen. Der Anwender kann in der Anwendung alle Elemente anklicken, in meinem Fall passiert nichts, da ich keine Funktionen hinterlegt habe. Mit Drop Comments kann der Tester an einer beliebigen Stelle einen Kommentar hinterlassen und zusätzlich angeben ob der eher unzufrieden oder zufrieden ist.

Wenn der du fertig mit deinem Feedback bist, klickst du oben rechts auf Done und die Antworten werden übermittelt. Im Preview-Modus werden allerdings keine Daten gespeichert. Wir kehren zurück zu unserem Fragebogen und klicken oben rechts auf Publish. Wir bekommen eine URL die wir mit allen Testern teilen können. Die Tester bekommen genau wie in der Preview die App ausprobieren und Kommentare hinterlassen. Kopiere die URL in einen neuen Tab und fülle einmal unsere kurze Survey aus. Wenn du nach dem Absenden zurück in deinen Workspace wechselst sollte es einen ersten Teilnehmer in deiner Survey geben wenn nicht, drück mal F5.

Übersicht der Umfrage-Ergebnisse
Hier landen die Ergebnisse unserer Umfrage

Nimm dir ruhig mal die Zeit um die verschiedenen Tabs und Funktionen anzusehen. So gibt es zum Beispiel eine Heatmap-Funktion, die dir anzeigt wo deine Tester hingeklickt haben.

Heatmap über unsere App, zeigt wo die Tester hinklicken.
Hier siehst du die Heatmap. Du findest sie in dem Tab Questions wenn du auf deine Frage klickst.

Damit beenden wir unseren Ausflug in die Survey-Funktion.

[5]: Zusammenfassung

Diese Punkte solltest du dir Merken:

  • build ist ein Tool zur Erstellung von Prototypen
  • es bietet verschiedene andere Werkzeuge an, die dich in der Entwicklung deiner Anwendung unterstützen
  • die Feedback Funktion kann sehr hilfreich sein um die Anwendung den Wünschen deiner Nutzer anzupassen
  • es ersetzt nicht einen Entwickler
  • ein Export des Prototypen in die Web IDE ist möglich

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

SEGW-Projekt (OData Service) mitsamt Implementierung kopieren

Der Gateway Service Builder (Transaktion SEGW) ist das allseits bekannte Werkzeug zur Implementierung eines OData Service auf einem SAP ABAP-System. Der Service Builder bietet die Funktion ein SEGW-Projekt zu kopieren. Das Kopieren kann notwendig sein, wenn

  • man eine neue Version eines vorhandenen Service parallel zur existierenden Version erstellen will
  • man mit einem von SAP ausgelieferten Service spielen möchte. Spielen meint auf den vorhandenen Entities und deren Logik aufsetzen und diese erweitertern oder anpassen.

Die Kopierfunktion im Service Builder kopiert jedoch nur das Model, aber nicht die Laufzeitartefakte (MPC- und DPC-Klassen). In diesem Blog beschreibe ich, wie man mit ein bisschen Handarbeit die Laufzeitartefakte kopiert, um dann eine vollständig funktionale Kopie des Service zu erhalten. Als  Beispiel nehme ich den Service GWSAMPLE_BASIC, welcher viele Aspekte der OData-Entwicklung aufzeigt… Also auf gehts mit der Kopie eines SEGW-Projekt.

1. Kopieren des Service Builder Projekts /IWBEP/GWSAMPLE_BASIC

Starte den Service Builder und öffne das Projekt /IWBEP/GWSAMPLE_BASIC. Die Laufzeitartefakte für den Service bestehen ABAP-seitig aus globalen Klassen /IWBEP/CL_GWSAMPLE_BAS_DPC, /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, /IWBEP/CL_GWSAMPLE_BAS_MPC und /IWBEP/CL_GWSAMPLE_BAS_MPC_EXT.

SAP Gateway Serive Builder

Die folgende Abbildung zeigt das Kopieren auf das neue Projekt ZRLGWSAMPLE_BASIC.

Projekt Beschreibung

Ein Generieren des neuen Projekts erzeugt die Laufzeitartefakte in den globalen ABAP-Klassen ZCL_ZRLGWSAMPLE_BASIC_DPC, ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT, ZCL_ZRLGWSAMPLE_BASIC_MPC, ZCL_ZRLGWSAMPLE_BASIC_MPC_EXT.

Laufzeitartefakte

2. MPC_EXT-Klasse: Neuanlage und Anpassen der Vererbung

Nun lösche die generierte ZCL_ZRLGWSAMPLE_BASIC_MPC_EXT-Klasse. Lege sie danach als Kopie von Klasse /IWBEP/CL_GWSAMPLE_BAS_MPC_EXT neu an.

Kopie von Klasse /IWBEP/CL_GWSAMPLE_BAS_MPC_EXT

Ändere die Vererbung der Klasse CL_ZRLGWSAMPLE_BASIC_MPC_EXT von /IWBEP/CL_GWSAMPLE_BAS_MPC auf Klasse ZCL_ZRLGWSAMPLE_BASIC_MPC. Es kommt das Popup mit der Frage, ob Methodenredefinitonen erhalten bleiben sollen. Diese Frage mit Ja beantworten.

Methodenredefinitonen

 

3. MPC_EXT-Klasse: Anpassen des Codes

Die Typen in der Klasse ZCL_ZRLGWSAMPLE_BASIC_MPC_EXT referenzieren noch die originäre MPC-Klasse /IWBEP/CL_GWSAMPLE_BAS_MPC. Deshalb in die Quelltextansicht wechseln und /IWBEP/CL_GWSAMPLE_BAS_MPC durch ZCL_ZRLGWSAMPLE_BASIC_MPC ersetzen.

4. DPC_EXT-Klasse: Neuanlage und Anpassen der Vererbung

Das Vorgehen bei der DPC_EXT-Klasse ist analog zur MPC_EXT-Klasse.

  1. Klasse ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT löschen
  2. Klasse ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT neu anlegen als Kopie von /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT
  3. Vererbung von /IWBEP/CL_GWSAMPLE_BAS_DPC auf ZCL_ZRLGWSAMPLE_BASIC_DPC ändern. Im Popup die Frage nach dem Erhalt der Redefinitionen wieder mit Ja beantworten

5. DPC_EXT-Klasse: Anpassen des Codes

Im Code der DPC_EXT-Klasse ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT nun die Referenzen auf /IWBEP/CL_GWSAMPLE_BAS_MPC durch ZCL_ZRLGWSAMPLE_BASIC_MPC ersetzen. Rund 80 Ersetzungen werden damit durchgeführt. Jetzt kannst Du aktivieren und solltest keine Syntaxfehler bekommen.

6. Zusammenfassung

Das war es schon! Wir haben den GWSAMPLE_BASIC mitsamt seiner Implementierung in den neuen Service ZRLGWSAMPLE_BASIC kopiert. Wir mussten hierfür lediglich die DPC_EXT- und MPC_EXT-Klassen ein wenig anpassen.

Hast du noch Fragen zur Kopie von einem SEGW-Projekt oder zu anderen Themen?

Nutze gerne unsere Kommentarfunktion oder schreib mir direkt eine eMail

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Wiederverwendbarer modaler Auswahldialog in ABAP mit CL_SALV_TABLE

In einer typischen ABAP Dynproanwendung benötigt man irgendwann die Funktionalität, Daten dem Benutzer in einem Popup zu präsentieren und die  Auswahl zu prüfen. Häufig wird so ein Dialog manuell implementiert, obwohl es funktional immer das Gleiche ist. Was ist CL_SALV_TABLE?

Mit dem Funktionsbaustein REUSE_ALV_GRID_DISPLAY gibt es eine Standardfunktionalität zur Implementierung eines Dialogs als auch zur Anzeige von Daten im Fullscreen. Nachteil ist, dass der Funktionsbaustein  REUSE_ALV_GRID_DISPLAY nicht gut zur heute üblichen objektorientierten Programmierung passt, da er als Callback FORM-Routinen in einem Programm erwartet.

Die Klasse CL_SALV_TABLE ist eine objektorientierte Verschalung des Funktionsbausteins REUSE_ALV_GRID_DISPLAY. Ein Beispiel zur Implementierung eines spezifischen Dialogs findet sich in Programm SALV_DEMO_TABLE_POPUP.

Aus dieser Ausgangssituation heraus implementieren wir in diesem Blog einen modalen Auswahldialog mit folgenden Eigenschaften.

  • Einfache objektorientierte Schnittstelle aufsetzend auf Klasse CL_SALV_TABLE
  • Automatische Größenanpassung abhängig von der Anzahl der übergeben Datensätze
  • Optionale Anpassung der Gridspalten über den Feldkatalog
  • Optionale Überprüfung der Auswahl durch eigene Logik

Das Coding besteht aus den folgenden Teilen:

Im Folgenden beschreibe ich die Benutzung der Klasse ZCL_RL_DIALOG_MODAL ausgehend von den Unittests.

Unittest Einfachaufruf

Der folgende Code zeigt den Einfachaufruf des Dialogs. Die anzuzeigenden Daten kommen aus der SalesorderHeader-Tabelle SNWD_SO im Enterprise Procurement Model.  Die durch den Benutzer ausgewählten Datensätze werden im Exportparameter et_data_sel zurückgegeben.

  METHOD simple_call.
    TYPES:
      BEGIN OF ts_dlg_epm_so,
        so_id            TYPE snwd_so_id,
        net_amount       TYPE snwd_ttl_net_amount,
        tax_amount       TYPE snwd_ttl_tax_amount,
        currency_code    TYPE snwd_curr_code,
        lifecycle_status TYPE snwd_so_lc_status_code,
      END OF ts_dlg_epm_so.

    DATA lt_dlg_epm_so TYPE TABLE OF ts_dlg_epm_so.
    DATA lt_dlg_epm_so_sel TYPE TABLE OF ts_dlg_epm_so ##NEEDED.

    SELECT * FROM snwd_so INTO CORRESPONDING FIELDS OF TABLE lt_dlg_epm_so UP TO 20 ROWS.

    mr_dialog_modal->open(
      EXPORTING
        it_data = lt_dlg_epm_so
        iv_title = 'Testdialog'
        iv_width = 40
      IMPORTING
        et_data_sel = lt_dlg_epm_so_sel ).

  ENDMETHOD.

Die open-Methode der Klasse ZCL_RL_DIALOG_MODAL ist generisch als STANDARD TABLE typisiert. Die Logik analysiert die übergebene Datenstruktur und erzeugt daraus die Spaltenköpfe des ALV Grids. Die Breite (Parameter iv_width) übergibt der Aufrufer. Die Höhe des Dialogs berechnet die Logik der open-Methode abhängig von der Anzahl der übergebenen Datenzeilen selbst.

Unittest Anpassung Feldkatalog

Das folgende Beispiel zeigt den Aufruf des Dialogs mit Anpassung des Feldkatalogs, um z.B. nicht benötigte Spalten auszublenden.

METHOD fieldcatalog.
    DATA lt_dlg_epm_so TYPE TABLE OF snwd_so.
    DATA lt_dlg_epm_so_sel TYPE TABLE OF snwd_so ##NEEDED.
    DATA lt_fcat TYPE lvc_t_fcat.
    FIELD-SYMBOLS  TYPE lvc_s_fcat.

    SELECT * FROM snwd_so INTO TABLE lt_dlg_epm_so UP TO 4 ROWS.

    lt_fcat = mr_dialog_modal->get_fieldcatalog( lt_dlg_epm_so ).

    LOOP AT lt_fcat ASSIGNING .
      CASE -fieldname.
        WHEN 'SO_ID'.
          -key = abap_true.
        WHEN 'NET_AMOUNT' OR 'TAX_AMOUNT' OR 'LIFECYCLE_STATUS' OR 'BILLING_STATUS' OR 'DELIVERY_STATUS'.
        WHEN OTHERS.
          -no_out = abap_true.
      ENDCASE.
    ENDLOOP.

    mr_dialog_modal->open(
      EXPORTING
        it_data = lt_dlg_epm_so
        iv_title = 'Testdialog'
        iv_width = 62
        it_fcat = lt_fcat
      IMPORTING
        et_data_sel = lt_dlg_epm_so_sel ).
  ENDMETHOD.

Unittest Prüfung Eingaben

Das folgende Beispiel zeigt den Aufruf des Dialogs mit einer Prüfung der Eingaben. Die Prüfung der Eingaben erfolgt durch Übergabe eines Objekts, welches das Interface ZIF_RL_DIALOG_MODAL_CHECK implementiert. In diesem Fall ist dies die lokale Klasse lcl_dialog_modal_check. Die Prüfung in diesem Beispiel stellt sicher, dass der Benutzer beim Klicken des Ok-Button mindestens 1 Zeile ausgewählt hat.

  METHOD check_selection.
    TYPES:
      BEGIN OF ts_dlg_epm_so,
        so_id            TYPE snwd_so_id,
        net_amount       TYPE snwd_ttl_net_amount,
        tax_amount       TYPE snwd_ttl_tax_amount,
        currency_code    TYPE snwd_curr_code,
        lifecycle_status TYPE snwd_so_lc_status_code,
      END OF ts_dlg_epm_so.

    DATA lt_dlg_epm_so TYPE TABLE OF ts_dlg_epm_so.
    DATA lt_dlg_epm_so_sel TYPE TABLE OF ts_dlg_epm_so ##NEEDED.
    DATA lr_dialog_modal_check TYPE REF TO lcl_dialog_modal_check.

    SELECT * FROM snwd_so INTO CORRESPONDING FIELDS OF TABLE lt_dlg_epm_so UP TO 20 ROWS.

    CREATE OBJECT lr_dialog_modal_check.

    mr_dialog_modal->open(
      EXPORTING
        it_data = lt_dlg_epm_so
        iv_title = 'Testdialog'
        iv_width = 40
        ir_dialog_modal_check = lr_dialog_modal_check
      IMPORTING
        et_data_sel = lt_dlg_epm_so_sel ).

  ENDMETHOD.

CLASS lcl_dialog_modal_check DEFINITION FINAL.
  PUBLIC SECTION.
    INTERFACES zif_rl_dialog_modal_check.

  PRIVATE SECTION.
    METHODS conv_sy_to_bapiret2
      RETURNING VALUE(rs_return) TYPE bapiret2.
ENDCLASS.

CLASS lcl_dialog_modal_check IMPLEMENTATION.
  METHOD zif_rl_dialog_modal_check~check.
    IF lines( it_data_sel ) = 0.
      MESSAGE e499(sy) WITH 'Mindestens 1 Zeile auswählen'(001) INTO DATA(lv_msg) ##needed.
      rs_return = conv_sy_to_bapiret2( ).
    ENDIF.
  ENDMETHOD.

  METHOD conv_sy_to_bapiret2.

    MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
      INTO rs_return-message
      WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.

    rs_return-type = sy-msgty.
    rs_return-id = sy-msgid.
    rs_return-number = sy-msgno.
    rs_return-message_v1 = sy-msgv1.
    rs_return-message_v2 = sy-msgv2.
    rs_return-message_v3 = sy-msgv3.
    rs_return-message_v4 = sy-msgv4.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_dialog_modal_check DEFINITION FINAL.
  PUBLIC SECTION.
    INTERFACES zif_rl_dialog_modal_check.

  PRIVATE SECTION.
    METHODS conv_sy_to_bapiret2
      RETURNING VALUE(rs_return) TYPE bapiret2.


ENDCLASS.

Implementierung in Deinem SAP-System

Die folgende Abbildung zeigt den Aufbau der Klasse ZCL_RL_DIALOG_MODAL. Die  Methoden OPEN und GET_FIELDCATALOG haben wir schon im Rahmen der Unittests gesehen. Die private Methode ON_USER_COMMAND wird als Eventhandler für das Drücken der Button registriert.

Die im Screenshot sichtbare lokalen Klasse LCL_TEST implementiert den Unittest. Die lokale LCL_DIALOG_MODAL_CHECK implementiert das Interface ZIF_RL_DIALOG_MODAL_CHECK und dient der Eingabeüberprüfung beim Unittest check_selection.

Um die Funktionalität in Deinem SAP-System zu implementieren, lege das Interface ZIF_RL_DIALOG_MODAL_CHECK und die Klasse ZCL_RL_DIALOG_MODAL über den Class Builder (Transaktion SE24) an, wechsle danach in die Quelltextansicht  und kopiere den Interface-Code und den Klassen-Code hinein. Generiere danach in Klasse ZCL_RL_DIALOG_MODAL das Testklasseninclude und füge den Unittest-Code ein.

Der letzte Schritt ist ein bisschen fummelig. Der Dialog benötigt einen GUI-Status, damit das Dynpro die Buttons für Ok, Abbrechen, Filtern usw. anzeigt. In dem Beispielcode heißt der GUI-Status SALV_TABLE_POPUP und liegt in Programm ZRL_TEST. Du brauchst irgendein Programm oder Funktionsgruppe, wo Du den GUI-Status anlegen kannst. Der Screenshot zeigt den GUI-Status. Die Buttons CONT und CANC müssen genau so benannt werden, da die Logik in Methode ON_USER_COMAND darauf aufsetzt. Buttons, die vom OkCode her mit & beginnen, werden durch das ALV-Grid selber verarbeitet.

Die manuelle Anlage des GUI-Status ist fehleranfällig. Deshalb ist der einfachste Weg, wenn Du den Standard GUI-Status des ALV-Grids kopierst und danach die Buttons anpasst. Der Standard GUI-Status heißt SALV_TABLE_STDPOPUP in Programm SAPLSALV_METADATA_STATUS.

Hast du noch Fragen zur CL_SALV_TABLE  oder anderen Themen?

Nutze gerne unsere Kommentarfunktion oder schreib mir direkt eine eMail

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP

SAP und Apple – „Challenging the Status Quo in Business“

Letzte Woche fand die SAPPHIRE 2019 in Orlando statt. Auf der Keynote am Dienstag (07. Mai 2019) begrüßte SAP CEO Bill McDermott den CEO von Apple, Tim Cook auf der Bühne. Gemeinsam haben sie weitere Ankündigungen für die Zusammenarbeit von Apple mit SAP gemacht.

Konkret wurden von den beiden folgende Punkte angekündigt:

  • Erweiterung des iOS SDKs mit Apples CORE ML Framework
  • Erneuerung von SAP Fiori Anwendungen bei den Produkten SAP SuccessFactors und SAP Concur
  • Neue SAP Fiori Anwendungen für SAP Ariba
  • Native Apps für den Apple Mac

Hier könnt ihr euch den Auftritt von Bill McDermott und Tim Cook auf der SAPPHIRE angucken:

Was das für einen SAP Kunden bedeutet und was das für Möglichkeiten mit sich bringt will ich möchte in diesem Artikel kurz beschreiben.

Was bringt mir als SAP Kunde die Zusammenarbeit mit Apple?

Apple hat schon oft ganze Branchen kolossal verändert. Man denke an den iPod und iTunes, die am Anfang des Jahrtausends die Musikbranche auf links gedreht haben. Man denke an iPhone und iPad, die den Markt für Endbenutzer Geräte revolutioniert hat. Oder aber auch an den gerade frisch angekündigten Service Apple News+, der mit großer Wahrscheinlichkeit eine Veränderung in der Art, wie wir Nachrichten konsumieren, bedeuten wird.

Wie Tim Cook auf der SAPPHIRE angedeutet hat möchte Apple nun die Art, wie wir arbeiten, genauso auf links drehen. Die Zusammenarbeit mit SAP ist ein Schritt in diesem Unterfangen.

Apple and SAP are working together to reinvent business processes and workflows everywhere. By bringing together powerful iOS features and seamless integration with SAP systems, developers can innovate faster than ever.

Zugegeben, diese Entwicklung ist nicht neu. Der Begriff Digitalisierung schwirrt nun schon eine ganze Zeit lang durch die Luft. Was neu ist, ist die verbesserte Möglichkeit, bei der Entwicklung von Business Apps mit SAP auf diverse iOS Features zugreifen zu können, insbesondere dem CORE ML Framework.
Also eher ein Nischen-Thema? Kommt drauf an, würde ich sagen. Wird es bald mehr Anwendungen mit Machine Learning (ML) und Features zu Künstlicher Intelligenz (KI) geben? Wenn ja, dann ist das doch vielleicht keine so kleine Verbesserung.

Aber Schritt für Schritt. Konkret lassen sich meines Erachtens folgende Verbesserungen für Business Anwendungen ableiten:

  • Performance
  • Speicher und Energieverbrauch
  • Offline-Fähigkeit
  • Sicherheit (von Daten)

Diese Punkte kommen bei Anwendungen zum Tragen, die Features besitzen, die sich rund um die Themen ML und KI drehen. Die Möglichkeit auf das Framework CORE ML zuzugreifen spielt hierbei die entscheidende Rolle.

CORE ML – Apples Framework für künstliche Intelligenz

CORE ML ist ein Machine-Learning Framework, mit dem sich Machine-Learning-Modelle in eigene Apps integrieren lassen. Die Modelle werden automatisch auf das iPhone oder das iPad geladen, womit die Anwendungen auch offline voll funktionsfähig sind. Der große Kniff dabei ist, dass die Modelle auf dem Endgerät laufen und nicht in einem über das Internet erreichbaren Datacenter. Dadurch werden die entsprechenden Features nicht nur offline fähig sondern verursachen auch deutlich weniger Traffic.

Zugegeben, ich bin (noch) kein Experte von CORE ML oder iOS Entwicklung. Deshalb möchte ich auf diesen Blog verweisen. Dort wird das CORE ML Framework von Stefan Luber und Nico Litzel sehr gut erklärt.

Anwendungsentwicklung für iOS mit Swift

Die Anwendungsentwicklung für iOS erfolgt in der Programmiersprache Swift. Mit Hilfe des iOS SDKs von SAP lassen sich Fiori ähnliche, native Business Anwendungen entwickeln. Das SDK stellt entsprechende UI-Elemente zur Verfügung, die wir aus der Entwicklung von Business Anwendungen mit SAPUI5 gewohnt sind. Außerdem sind dort von der SAP Cloud Platform bereitgestellt APIs integriert, mit dessen Hilfe die bekannten SAP Services und Funktionen angesprochen und genutzt werden können.

Und was ist mit Android?

Auch für Android gibt es ein SDK, mit dem native Anwendungen entwickelt werden können. Das Android SDK ist seit dem letzten Jahr verfügbar. Das Equivalent für das CORE ML Framework von Apple ist TensorFlow Light, welches sowohl für Android als auch für iOS Entwicklungen verwendet werden kann.

Allerdings, soweit ich meiner eigenen Recherche vertrauen kann, ist TensorFlow Light noch kein Bestandteil des Android SDKs. Das bedeutet, dass die oben genannten Vorteile, insbesondere die offline fähigen Machine Learning Features, nicht in native Android Anwendungen integriert werden können.

Schon irgendwie blöd, was?! Bedeutet im Klartext, für Android Anwendungen mit ML und KI Features wird weiterhin eine stabile Internetverbindung benötigt, um sie benutzen zu können.

Auf Sicht wird sich das mit Sicherheit ändern. Wenn wir einen Blick in die Vergangenheit werfen sehen wir, dass auch das Android SDK später released wurde als das SDK für iOS. Da wissen wir dann wohl auch, welchen Fokus SAP bei den Betriebssystemen setzt.

Fazit

Spannende Möglichkeiten, die sich da bieten. In wie weit diese wann und/oder überhaupt von den Kunden wahrgenommen werden bleibt abzuwarten.
Ist Machine Learning und AI schon im Unternehmensalltag angekommen? Wird das genutzt werden? Schreibt eure Meinungen und Erfahrungen gerne in die Kommentare.

Ein Punkt, dem ich außerdem interessiert zugucken werde, ist, ob durch die Partnerschaft zwischen SAP und Apple tatsächlich mehr Apple Endgeräte den Weg in die Unternehmenswelt finden werden. Tim Cook hätte da bestimmt nichts gegen einzuwenden.

Meiner bescheidenen Meinung nach wird das eher nicht passieren. Selbst wenn SAP eine Priorität auf iOS legt wird in nicht allzu naher Zukunft das Android SDK ebenso erweitert werden. Alles andere macht keinen Sinn. Das SAP ebenso eine Partnerschaft mit Google hat wie auch mit Apple, bestärkt diesen Gedankengang noch weiter.

So weit erst mal! Bleibt nur noch zu sagen: „Kieck mol wedder in!“

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

SAP Geschäftspartner

SAP S/4HANA – Dein Weg zum SAP Geschäftspartner / die Customer Vendor Integration.

S/4HANA ist da! Ein Teil dieser großen Systemumstellung ist die Customer Vendor Integration (CVI) oder in Deutsch: die Einführung des SAP Geschäftspartners. Kreditoren und Debitoren werden zukünftig nicht mehr in der heutigen Form unterschieden. Sie werden zukünftig als zentraler Geschäftspartner betrachtet. Im S/4HANA Umfeld gibt es keinen Ersatz zum Geschäftspartner. So ist es also eine notwendige Maßnahme Debitoren und Kreditoren vor der Systemmigration zu Geschäftspartnern zu überführen.

Hintergrund:

Stellen wir uns vor, wir haben einen Lieferanten. Dieser liefert uns beispielsweise Leder, so dass wir Schuhe produzieren können. Nach einigen Jahren stellt er fest, dass unsere Schuhe genau in sein Konzept für die Ausrüstung seiner Mitarbeiter*innen passt. Da wir auch schon eine längere Zeit „Geschäftspartner“ sind bestellt er diese Schuhe tatsächlich bei uns. Nun legen wir einen Debitor an – mit den gleichen Daten wie denen des Kreditors. Im Laufe der Bestellungen merken wir, dass er auch an weiteren Produkten interessiert ist – also legen wir einen neuen Interessenten an. Nach gewisser Zeit ändert sich die Anschrift, dieses Geschäftspartners… Was ich damit sagen möchte ist, dass wir für selbige Personen/Organisationen/Gruppen mehrere Datensätze haben, die zu einem großen Teil redundanten Daten haben.

Diese Redundanz möchte die SAP mit der Einführung des zentralen SAP Geschäftspartners reduzieren. Und was genau die Einführung des zentralen Geschäftspartner in euren Systemen bedeutet, werde ich in diesem Blog-Beitrag genauer erläutern.

 

Nützliche Informationen vorab:

Einige spannenden Informationen möchte ich an dieser Stelle zusammenfassen:

  • Kein S/4HANA ohne CVI möglich.
  • Die üblichen Tabellen (LFA1, KNA1, …) bleiben erhalten. Programme, die mit Informationen aus diesen Tabellen arbeiten, können weiter betrieben werden.
  • Es kommen zusätzliche Tabellen hinzu.
  • In der mächtigen BP-Transaktion kannst du Stammdaten für Geschäftspartner, Kunden und Lieferanten zentral erstellen, bearbeiten, anzeigen und löschen.

Und wie sieht es für den Anwender aktuell und zukünftig aus? Darauf gehe ich im nächsten Kapitel ein, bevor es dann an die Details der Umstellung geht.

 

Die technischen Änderungen beim SAP Geschäftspartner

Technisch sieht es für den Anwender der klassischen Stammdatenpflege für Kreditoren und Debitoren vor der CVI so aus:

 

SAP Geschäftspartner

Das ist dir sicherlich bekannt. Um die Daten eines Kreditors/Debitors pflegen/anzeigen/erstellen/löschen zu können, muss der Anwender eine bestimmte Transaktion auswählen, welche dann auf die jeweiligen Stammdatentabellen verweist.

Nach der CVI sieht es dann etwas anders aus, und zwar wie folgt:

 

SAP Geschäftspartner

Mit der Transaktion BP lassen sich sämtliche Daten des Geschäftspartners pflegen.Zusätzliche Tabellen, die Beispielsweise die Verlinkungen zwischen den Geschäftspartnern und den Kreditoren/Debitoren darstellen (CVI_CUST_LINK und CVI_VEND_LINK) habe ich aus Gründen der Übersichtlichkeit in den obenstehenden Grafiken ausgelassen.

Nun möchte ich euch zeigen, wie man den Standard des R3-Systems in den S/4HANA Standard überführt.

 

Vorbereitungen zum SAP Geschäftspartner

Die SAP empfiehlt vor der eigentlichen Integration der Stammdaten aller Kreditoren und Debitoren zum SAP Geschäftspartner einige Schritte. Diese sind:

  • Einspielen diverser SAP-Notes durch die SAP-Basis, die den Businesspartner sowie Funktionen und Pre-Checks zur Integration bereitstellen.
  • Archivieren inaktiver und nicht mehr benötigter Kreditoren/Debitoren.
  • Identifizieren der sogenannten „Golden Records“, also diejenigen kreditorischen/debitorischen Objekte, die zur gleichen Person/Organisation/Gruppe gehören und somit redundante Daten enthalten.
  • Stammdaten vorbereiten (Mit der Integration des Geschäftspartners werden zusätzliche Feldüberprüfungen eingeführt, die bei der Integration zu Fehlern führen können).
  • Datenqualität steigern (um eine gute initiale Synchronisation zu ermöglichen).
  • Festlegen von Nummernkreisen und der Nummernvergabe.
  • Generelles Customizing (Überprüfung mittels CVI_FS_CHECK_CUST – SAP Hinweis 1623677).
  • Bestimmung der Feldmodifikation.
  • Erstellen der GP-Rollen (Standard/Kundenspezifisch).
  • Aktivierung der Funktionsbausteine zum Abmischen von Geschäftspartnerdaten und Geschäftspartnerbeziehungen.
  • Erstellung von Nachbearbeitungsaufträgen aktivieren (Um Fehler bei der initialen Synchronisation im Nachgang bearbeiten zu können).
  • Berücksichtigung der NON-SAP-Systeme.
  • Synchronisationsrichtungen bestimmen.

 

Synchronisation

Es gibt zwei Phasen der Synchronisation:

  • Die initiale Massensynchronisation vor der aktiven Nutzung des Geschäftspartners.
  • Die fortlaufende Synchronisation nach der Massensynchronisation. Diese beiden Phasen werde ich im Folgenden genauer beschreiben.

 

SAP Geschäftspartner – Die initiale Massensynchronisation (die eigentliche CVI)

In dieser Phase ist die Synchronisationsrichtung von Kreditoren/Debitoren hin zum Geschäftspartner geöffnet. Damit kann aus eben diesen neue Geschäftspartner entstehen können. Wie der Name schon vermuten lässt, geht es hierbei um die anfängliche Überführung aller Debitoren und Kreditoren zu den Geschäftspartnern.

Diese Synchronisation wird in der Transaktion MDS_LOAD_COCKPIT durchgeführt. Hierbei muss darauf geachtet werden, dass die Parametrisierung richtig eingestellt wird, sodass nur diejenigen Objekte transferiert werden, die auch gewünscht sind (keine Löschvermerk-Objekte, auslassen bestimmter Bereiche, Berücksichtigung der Golden Records, …).

Bei der Massensynchronisation können sich Fehler ergeben, die mangelnder Datenqualität oder unzureichendem Customizing zuzuschreiben sind. Diese führen zu PPO-Aufträgen (Postprocessing Office), wenn das Schreiben hierfür aktiviert wurde. So lassen sich Daten oder Customizing direkt bearbeiten.

Nach der Massensynchronisation muss geprüft werden, ob und wie alle Kreditoren und Debitoren überführt wurden. Hierfür stellt die SAP den PRECHECK_UPGRADATION_REPORT zur Verfügung.

 

SAP Geschäftspartner – Fortlaufende Synchronisation

Nach der erfolgreichen Überführung sämtlicher Kreditoren und Debitoren wird der Geschäftspartner zum führenden Objekt gemacht. Sobald ein Geschäftspartner geändert wird, müssen die Kreditoren und Debitoren auch automatisiert geändert werden. Deshalb ist in dieser Phase die Synchronisationsrichtung umgekehrt. Zusätzlich muss bei der Anlage neuer Geschäftspartner ein entsprechender Debitor/Kreditor angelegt werden. Hierbei ist auf die Auswahl der Nummernkreise sowie auf die richtige Zuordnung der Rollen aus dem Kreditor/Debitor zu denen im Geschäftspartner zu achten.

 

Ich hoffe, ihr seid nun ein Schritt näher an die Customer Vendor Integration gekommen. Falls noch Fragen offen sind, wendet euch gerne an uns. Wir sind für euch da!

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion oder schreibt mir direkt an henning.gerdes@cgi.com

 

Du programmierst, bist SAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Fiori 3 - Quartz

Fiori 3.0 ist da! – Aktuelle Neuigkeiten und Release Dates

Es ist so weit! Die SAP hat in der letzten Woche verkündet, dass die ersten Features zu Fiori 3 von nun an zur Verfügung stehen. Dabei handelt es sich um das neue Theme Quartz, dass das SAP Belize Theme als Standardtheme ablösen wird. Des Weiteren hat die neue, vereinheitlichte Shell Bar Einzug in die Fiori Welt gefunden.

Verfügbar sind beide Features seit dem 25. April 2019 auf der SAP Cloud Platform ab SAPUI5 Version 1.65.

Was bisher geschah

Die SAP hat letztes Jahr auf ihren TechEd Veranstaltungen mit Fiori 3 ihre neue UX-Strategie vorgestellt. Auch auf unserem Blog haben wir darüber geschrieben.

Doch mit einer genauen Zeitachse zum Release tat sich die SAP schwer. Einzig die Aussage: „Round about the time at SAPHIRE 2019“ gab Aufschluss darüber, wann in etwa mit der Einführung von Fiori 3 zu rechnen ist.

Aber was sind denn jetzt die neuen Features genau und wie und wann kann ich sie benutzen?

Das neue Theme – SAP Quartz

Das Theme Quartz soll, wie schon oben erwähnt, SAP Belize als das neue Standard Theme ablösen. Es besticht durch seine Einfachheit. Quartz bietet ein schlichtes, aus Weiß-, Grau und Dunkelblau-Tönen bestehendes Theme, dass meiner Ansicht nach deutlich moderner wirkt als die Vorgänger-Themes SAP Belize und SAP Blue Crystal.
Der Fokus soll dadurch auf die Inhalte gelegt werden. Ich kann mir sehr gut vorstellen, dass, wenn vor allem auch die Cards eingeführt werden, sehr zum Tragen kommt, da dann auch sehr viel mehr Farben auf den Cards vorhanden sein wird.

Aber auch bei den noch ausschließlich verfügbaren Tiles macht das neue Theme eine gute Figur. Aber seht doch einfach selbst, links das SAP Belize Theme, rechts das neue Theme SAP Quartz:

Das neue Theme lässt sich leider noch nicht mit dem Motiv-Editor auswählen. Um trotzdem das eigene Launchpad mit dem neuen Theme anzuzeigen, dafür schafft folgender GET-Parameter Abhilfe:

?sap-theme=sap_fiori_3
http://www.myUrl.com?sap-theme=sap_fiori_3

Shell Header Bar

Auf dem Screenshot ist auch schon die andere, bereits veröffentlichte Neuerung zu sehen: die neue Shell-Bar. Unter dem Stichwort Harmonisierung hat sich die SAP zum Ziel gesetzt, die unterschiedlichen Look & Feels aus den verschiedenen Anwendungen zu einem einzigen Nutzererlebnis zusammenzufassen.

Die neue Shell-Bar ist unabhängig des genutzten Themes ab der SAPUI5 Version 1.65 verfügbar. Bei meinem Beispiellaunchpad sah die Shell-Bar ohne zu viel Schnick Schnack wie folgt aus:

Macht auf den ersten Blick einen sehr soliden Eindruck auf mich!

Weiterer Ausblick zu den Release-Dates – Wann kommt was?

Für weitere Teil-Releases konnte ich leider noch keine genauen Termine ausfindig machen. Aus der SAP Fiori Roadmap geht aber folgendes hervor:

Geplant in Q2/2019 – (SAP Fiori Launchpad on ABAP/SCP)

  • Einführung Theme SAP Quartz
  • Update der Shell-Bar
  • Einführung von Cards

Geplant in Q3/2019 – (SAP Fiori Launchpad on SCP)

  • UI Theme Designer für Fiori 3
  • Einführung zusätzlicher Design Elemente für Fiori 3

Geplant in Q4/2019 – (SAP Fiori Launchpad on SCP)

  • Weiterentwicklung von Fiori 3

Heißt, wir können weiter gespannt sein. Vor allem die Einführung der Cards als auch die Einführung von Fiori 3 auf dem ABAP Stack sollte nicht mehr all zu lange auf sich warten lassen. Q2 ist ja im Juni auch schon wieder vorbei. Sobald es was neues gibt erfährst du es bei uns auf unserem Blog.

Bis dahin erst mal. Bleibt mir nur noch zu sagen: „Kiek mol wedder in!“

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Wolken über Hochhäusern

Webinar „Abheben mit der SAP Cloud Platform – der Weg zum intelligenten Unternehmen“

Am 20. März 2019 fand unser Webinar zum Thema „Abheben mit der SAP Cloud Platform – der Weg zum intelligenten Unternehmen“ statt. Innerhalb einer Stunde haben wir das intelligente Unternehmen beleuchtet, was sich die SAP genauer darunter vorstellt und was das ganze für die Technik und Infrastruktur im eigenen Unternehmen bedeutet.

Als Untermalung dienten drei Anwendungsbeispiele, die sehr anschaulich die Möglichkeiten des intelligenten Unternehmens und die Rolle der SAP Cloud Platform hierbei darstellten.

Falls ihr es verpasst habt oder euch das Webinar nochmal anschauen wollt könnt ihr das ganz einfach auf unserem YouTube-Kanal tun.
Sollten bei euch eventuell noch Fragen aufkommen, dann könnt Ihr einfach die Kommentar-Funktion unter diesem Beitrag nutzen.

Viel Spaß beim Anschauen!

Webinar-Mitschnitt zum Nachschauen

Teaser

In diesem Webinar zeigen wir anhand konkreter Anwendungsfälle die Vorzüge der SAP Cloud Platform (SCP): Herausragende Time-to-Market. Direkte Adaption von State of the Art-Technologien. Geringes Kostenrisiko. Keine Bindung an Entwicklungszyklen. SAP wird agil.

Ist die Umsetzung wirklich so einfach wie von der SAP versprochen?
Welche Hürden müssen noch umschifft werden, bevor ein produktiver Einsatz möglich ist?

Wir geben Antworten und wagen einen Blick in die nahe Zukunft: In einer Demo zeigen wir die Konfiguration und Nutzung des neuen, persönlichen Assistenten der SAP – den SAP CoPilot.

Agenda

  • Vorstellung des intelligenten Unternehmens mit SAP
  • Wettbewerbsfähig mit „Standalone Innovation“
  • „Enhance existing solution“ – Erweiterung der eigenen Kompetenzen
  • Der neue, persönliche Assistent „SAP CoPilot“

SAP‘s Vision vom Intelligenten Unternehmen

SAP Fiori, speziell Fiori 3.0 , ist das zentrale UX-Paradigma in SAP’s Vision des intelligenten Unternehmen, mit dem sich dieser Blogbeitrag auseinandersetzt.

Seit der TechEd 2017 befindet sich SAP nun auf seiner „Journey to the Intelligente Enterprise“. SAP verwendet dabei Begriffe, die vielen SAP Anwendern abstrakt bis esoterisch erscheinen.
Deshalb möchte ich Licht ins Dunkel bringen und einen klaren Bezug zwischen SAP´s Produktportfolio und den drei Bereichen des Intelligenten Unternehmens herstellen.

The Intelligent Enterprise

SAP hat in den vergangenen Jahren Unmengen an Ressourcen investiert um seinen Kunden ein Framework bereitzustellen, das es ihnen ermöglicht Geschäftsprozesse durch intelligente Echtzeitverarbeitung von Daten zu revolutionieren. SAP spricht in diesem Kontext vom „Intelligenten Unternehmen“, das sich grundsätzlich in drei Bereiche aufteilt.
In der Intelligent Suite werden Geschäftsprozesse initialisiert und dabei Daten generiert. Diese Daten können dann direkt auf der Digital Platform mit zusätzlichen externen Ressourcen kombiniert und mit Hilfe von Intelligent Technologies sofort ausgewertet werden.  Auf diesem Weg kann detailliert Verbesserungspotenzial in Echtzeit erkannt werden. Anhand dieser Erkenntnisse lassen sich Geschäftsprozesse, noch während sie im Gange sind, innerhalb der Intelligent Suite optimieren.

Die drei Bereiche, die den Kern des Intelligent-Enterprise-Frameworks bilden, klingen zunächst einmal sehr abstrakt. Doch bei genauerer Betrachtung stellt sich heraus, dass SAP bereits eine klare Vorstellung davon hat, wo seine Lösungen und Produkte zum Einsatz kommen.

Intelligent Suite

SAP ist davon überzeugt, dass in einem intelligenten Unternehmen die Modularisierung von Geschäftsprozessen (CRM, SCM, HR etc.) aufgehoben und Datensilos abgeschafft werden müssen.
Deshalb vereint es alle Geschäftsmodule unter dem Dach der Intelligent Suite.

 

Digital Core

Den Kern der Intelligent Suite bildet S/4HANA. Es ist das Herzstück einer jeden SAP-Systemlandschaft und wird durch die übrigen Komponenten erheblich erweitert und ergänzt.

Customer Experience

Im Zentrum seiner Customer Experience stellt SAP C/4HANA. Dank prognostischer Algorithmen ermöglicht die cloud-basierte Lösung dem Front-Office seine Kunden proaktiv zu bedienen. Darüber hinaus bietet es eine außergewöhnlich gutes User Interface.

Manufacturing & Supply Chain

Dieser Bereich der Intelligent Suite widmet sich der Digitalisierung unternehmenskritischer Wertschöpfungsprozesse. Mit SAP’s Manufacturing & Supply Chain Produktportfolio sind Unternehmen befähigt, ihre Lieferketten in Echtzeit zu managen und dank IoT-Fähigkeiten interaktiv auf Problemstellungen zu reagieren.

People Engagement

Die vierte Säule der Intelligent Suite wird von SAP SuccessFactors, SAP Fieldglass sowie dem Design Paradigma SAP Fiori gebildet. Gemeinsam bilden sie das Kernstück von SAP’s User Experience und katalysieren die Produktivität seiner Anwender.

Network & Spend Management

Die Intelligent Suite verfügt selbstverständlich auch über ausgereifte Werkzeuge für das Kostencontrolling. SAP Ariba, SAP Fieldglass und SAP Concur versetzen SAP Kunden in die Lage, stets den Überblick über ihre Ressourcen zu behalten und diese optimal einzusetzen.

Digital Platform

Die Digital Platform bildet das Fundament und die Infrastruktur für datengesteuerte Intelligenz und Prozessinnovation.

SAP Cloud Platform

Mit Hilfe eines stets wachsenden SaaS-Produktportfolios ermöglicht die SAP Cloud Platform unter Anderem agile, beschleunigte App-Entwicklung, vollumfängliches API-Management sowie ausgereifte Echtzeitanalysen großer Datensätze.

Data Management

Darüber hinaus umfasst die Digital Platform ein Datenbankmanagement-System, das zur Optimierung der Datenspeicherung dient und bei der Gewinnung wichtiger Erkenntnisse hilft sowie zur Verbesserung der Entscheidungsfindung beiträgt.

Intelligent Technologies

SAP Leonardo ist das zentrale Produkt, wenn es um die Anwendung, Entwicklung und Pflege intelligenter Technologien wie Machine Learning, Big Data, Internet of Things, Analytics, Data Intelligence oder sogar Blockchain geht.

Machine Learning

SAP bietet Conversational AI ein sogenannte Natural Language Processing (NLP) Technologie an, die eine intuitive Sprachsteuerung des Systems via Bots ermöglicht und sich selbstständig anhand der gesammelten Daten weiterentwickelt.
Um Applikationen mit ähnlichen Fähigkeiten auszustatten steht SAP Kunden die SAP Leonardo Machine Leanring Foundation zur Verfügung. Sie ermöglicht es selbstlernende Applikationen einfach zu erstellen, auszuführen und zu warten. Dabei sind keine tiefgreifenden Data-Science-Kenntnisse erforderlich.

Internet of Things

SAP möchte seinen Kunden die Möglichkeit bieten, ihre Wertschöpfungsprozesse datengetriebener Intelligenz zu revolutionieren. Die dazu benötigten Daten werden direkt im Kontext der beteiligten Maschinen, Produkte, Lieferketten, Konsumenten und Partner gewonnen. Dabei wandeln beispielweise SAP Edge Services rohe Sensordaten in Erkenntnisse um, die im höchsten Maße relevant für den Wertschöpfungsprozess sind.

Analytics

SAP misst Daten in Zukunft einen unschätzbaren Wert zu. Sie werden gemeinhin als der Treibstoff jeglicher Innovation bezeichnet. Mit SAP Analytics Hub bietet SAP einen zentralen Einstiegspunkt, der es seinen Kunden ermöglicht die richtigen Analysen für ihre individuellen Bedürfnisse durchzuführen und schnelle datengestützte Entscheidungen zu treffen.
SAP’s Digital Boardroom ermöglicht einen umfassenden Überblick abteilungsübergreifender Problemstellungen und liefert KPI’s in Echtzeit.

Big Data

The SAP HANA Data Management Suite ermöglicht es, Daten mit hohem Volumen, in Echtzeit zu verarbeiten und eliminiert jegliche Datensilos.

Blockchain

Blockchain Technologien befinden sich größten Teils noch in der Experimentierphase. Mit den SCP Blockchain Services (Hyperledger Fabric, MultiChain und Quorum) bietet SAP eine Möglichkeit an, ohne größeres Investment und Risiko mit disruptiven Blockchain Technologien zu experimentieren und Know-how zu sammeln.

Data Intelligence

Data Intelligence-Lösungen helfen SAP Kunden, ihre Daten und Erkenntnisse zu monetisieren.
Mit SAP Data Network können Markttendenzen sowie Trends verfolgt werden, oder Best-in-Class-Benchmarks angezeigt werden. Auf Basis dessen können in Echtzeit unterschiedliche Szenarien simuliert, vorhergesagt und geplant werden, sodass auf unterschiedliche Marktsituationen optimal reagiert werden kann.

Was lernen wir daraus?

Bei näherem Hinsehen verbirgt sich hinter SAP’s intelligenten Unternehmen eine klare Strategie um Geschäftsprozesse durch Echtzeitdatenanalyse transparenter und effizienter zu gestalten.
Diese Strategie lässt sich nicht kurzfristig umsetzen, sondern benötigt einen langfristig angelegten Wandel. Dieser Wandel reicht von den IT-Landschaften bis hinein in die Organisationsstrukturen der Unternehmen.

 

Schon gesehen?
Arbeiten als SAPUI5-Entwickler bei Acando

Erweiterung von SAP Fiori Elements Apps – Welche Erweiterungsmöglichkeiten habe ich?

Mit SAP Fiori Elements lassen sich im Handumdrehen neue UI5 Apps generieren, die weniger Entwicklungsarbeiten im Frontend vorsehen, als bislang verwendet.

Frontend-seitig wird von der SAP ein Smarttemplate bereitgestellt, das Annotationen und Metainformationen von OData-Services ausliest und hieraus eine entsprechende UI5 Oberfläche generiert, die den typischen SAP Fiori Look hat. Beeindruckend ist vor allem, dass wenn im Backend definiert wurde, dass auch das Erstellen, Löschen und Updaten von Entitäten im Backend möglich ist. Die entsprechenden Funktionalitäten werden direkt im Frontend durch das Smarttemplate realisiert.

Doch wie kannst du deine Fiori-Elements-App erweitern und schaffst es beispielsweise Buttons zu implementieren, die mit individuellem Coding hinterlegt werden können. Oder wie kannst du dem Smarttemplate aus dem Backend mitteilen, dass von Beginn an die Tabelle mit vordefinierten Suchfeldern ausgestattet ist?

In diesem Blog erkläre ich dir generell in welcher Weise du das Frontend anpassen kannst und inwieweit Einschränkungen vorliegen könnten. Detaillierte Anpassungsmöglichkeiten beschreibe ich dir in weiteren Beiträgen auf die ich verweisen werde. Schau dir den folgenden Blogartikel zu SAP Fiori Elements an, um dir einen generellen Überblick zu verschaffen. Wenn du dir klar werden willst, welche Möglichkeiten es zur Erweiterung existieren, bist du hier genau richtig!

Die Arten der Erweiterungen

Generell hast du die Möglichkeit deine Fiori Applikation aus der Backend-Definition oder direkt über Frontend Anpassungen zu ändern. Die Backend-Definitionen haben entsprechend Einfluss auf diverse Fiori-Applikationen, die den Service nutzen. Wäge daher ab, ob die Anpassungen nur für eine bestimmte Applikation sinnvoll ist oder ob die Anpassungen generell gelten sollen.


Erweitern über das Backend

Der im Backend erstellte OData-Service wird durch sogenannte Annotationen erweitert. Annotationen, bzw. Anmerkungen sind beeindruckende Informationen, die dem Client, also unserem SAP SmartTemplate mitteilen, wie die ankommenden Daten zu interpretieren sind. Man spricht auch von Vokabular-basierenden Anmerkungen. Diese Funktion ist mit OData der Version 4.0 realisierbar. Die OData-Services, die auf der Version 2.0 basieren, mussten jeweils mit einem SAP-spezifischen Attribut ausgestattet werden, damit die Daten im Frontend entsprechend interpretiert werden. Diese Daten können jedoch nur mittels SAP libraries interpretiert werden und daher nicht von jedem Client genutzt werden. Da wir generell in der SAP Umgebung mittels WEBIDE und auch mittels des SmartTemplates entwickeln, hat dies eher weniger Auswirkung auf den Anwendungsfall, aber die Veränderung zeigt sich nochmal im folgenden Beispiel:

OData Version 2.0

OData Version 4.0

Unterschieden werden bei den Annotationen zwischen den drei Standard Vokabularen: Core, Capabilities und Measures.

  • Core – Core Begriffe, um Vokabulare zu schreiben
  • Capabilities – Capabilities Begriffe beschreiben die Möglichkeiten des Services
  • Measures – Measures Definitionen sind Begriffe, die Einheiten und Währungen beschreiben

 

Zudem kannst du über die folgenden Vokabulare die Daten weiter angemerkt, bzw. beschreiben.

  • Common – Allgemeine Begriffe für alle SAP Vokabulare, bezogen auf Datensemantik
  • Communication – Begriffe, die relevant für die Kommunikation sind
  • UI – Begriffe, die die Darstellung und die Interpretation auf der Benutzeroberfläche verändern

Mit diesen Anmerkungsmöglichkeiten kann ich unsere Daten beschreiben und wirddurch unser Smarttemplate im Frontend direkt interpretiert.

In einem weiteren Beitrag kannst du einige Beispiele zur Erweiterung der SAP Fiori Elements Oberfläche vornehmen. 

Erweitern über das Frontend

Lokale Annotationen

Auch im Frontend lassen sich SAP Fiori Elements erweitern. Wenn wir über unsere WebIDE FullStack ein neues Fiori Elements Projekt anlegen, wird in dem Projektordner eine lokale Annotationsdatei angelegt, mit der wir arbeiten können. Dies ist die erste Option zur Erweiterung unserer Fiori Elements App über das Frontend.

Die Erweiterung über diese Annotations-Datei eröffnet uns ähnlichen Spielraum, wie die Änderungen, die wir im Backend tätigen können.

Unsere Entwicklungsumgebung WebIDE FullStack stellt mit dem „Annotation-Modeler“ ein sehr hilfreiches Werkzeug zur Verfügung, mit dem wir unsere Entitätstypen anpassen, oder die Backend Annotationen überschreiben können. Natürlich lässt sich das Ganze auch wie gewohnt über den Code Editor lösen.

Eine detaillierte, ausführliche Beschreibung, wie sich die lokalen Annotationen anpassen lassen, zeige ich in einem weiterem Beitrag.

Extensions

Aber was ist eigentlich mit eigenen Buttons, eigenem JavaScript und Absprüngen zu unseren gewohnten SAP Fiori Apps? Eigenes Coding lässt sich in einer Extension in einem SAP Fiori Elements Projekt einbauen.

Über Extensions können wir die ListReports, also die Smarttemplates, die uns eine Tabelle generieren und auch ObjectPages, also die Detailansicht unserer Entitäten, anpassen. Durch das Anlegen von Extensions wird uns, je nach Art der Extension, beispielsweise eine View angelegt, in der wir Buttons und weitere Controls hinzufügen können, oder auch ein entsprechend zugehöriger Controller, indem wir wie gewohnt Eventhandler und eigenes Coding ausprägen können. 

Auch diese Art der Erweiterung kannst du in einem weiteren Blogartikel näher aufgeführt finden und mit Beispielen verstehen.

Lies dir am Besten zur Vertiefung der einzelnen Erweiterungsmöglichkeiten die detaillierten Blogartikel durch.

Du möchtest noch weitere Informationen zum Thema Fiori erhalten? Schaue dir auch unsere anderen Blog-Artikel an.

Öffnen einer SAP GUI Transaktion aus einer Fiori

Moin Moin, hier kommt mal wieder ein innovativer Lösungsansatz direkt aus dem SAPUI5-Maschinenraum!

Usecase

Viele SAP Fioris werden in bestehende Arbeitsprozesse integriert. Das hat zur Konsequenz, dass SAPUI5-Entwickler häufig mit der Anforderung konfrontiert werden, aus ihrer Fiori in eine spezifische Transaktion der klassischen SAP GUI abzuspringen.
Das Problem dabei liegt auf der Hand: Wie verlasse ich die HTML-Welt meines Browsers und gelange in die SAP-Welt? Auf konventionellem Wege bietet sich hier offensichtlich das öffnen der SAP GUI for HTML an, was via URL mehr oder weniger trivial ist.

Doch was wenn SAP GUI for HTML nicht ausreicht, da ihre Transaktionen beispielsweise nicht den Anforderungen der User entsprechen? In diesem Blog liefere ich euch eine einfallsreiche Lösung, die den allermeisten Ansprüchen genügen wird.

Lösung via .sap Datei

Der Schlüssel zum Öffnen einer Transaktion der klassischen SAP GUI liegt im Download einer .sap Datei.
Angenommen der User möchte via Click eines Buttons in die SAP GUI abspringen. Dazu muss das Frontend einen Function-Import ausführen, der über das Backend ein .sap-File erzeugt und uns dessen Inhalt Base64 codiert zurückliefert. Der Function-Import muss natürlich so parametrisiert sein, dass das Backend alle nötigen Informationen hat um die den Inhalt der Datei korrekt zu berechnen.

Function Import mit SAPUI5 rufen

In meinem Beispiel heißt der Function-Import „getSAPGUIShortcut“ und hat als einzigen Parameter den Transaktionscode („Tcode“ hier einfach mal die gute alte SE80).

...
onSapGuiButtonPress: function(oEvent){
var that = this;
var oModel = this.getView().getModel();
var mInputData = {
Tcode: "SE80"
};
var sFunction = "/getSAPGUIShortcut";
var mParameters = {
method: "GET",
urlParameters: mInputData,
success: function(oData, oResponse) {
var sBase64Data;
if(oData && oData.getSAPGUIShortcut){
sBase64Data = oData.getSAPGUIShortcut.content;
that._openFileFromBase64(sBase64Data);
},
error: function(oError) {
//ERROR HANDLING NOT PART OF THIS BLOG
}
};
oModel.callFunction(sFunction, mParameters);
}

Was passiert im Backend

Was muss im Backend nun geschehen? Schauen wir uns eine mal eine .sap Datei, oder auch SAP Shortcut genannt, etwas näher an. Dabei handelt es sich nämlich um nicht mehr als eine Text-Datei, die der SAP GUI gewisse Parameter zum Öffnen einer neuen Session übergibt.
Dazu klickt man einfach mal auf einen Shortcut mit der rechten Maustaste und öffnet die Datei im Notepad.

Du weißt nicht wie man einen SAP-Shortcut erstellt? Dann schau mal hier.

Und so sieht so eine .sap Datei dann letztendlich aus:

[System]
Name=XD1
Description=400 XD1 ERP Development
Client=100
[User]
Name=USERABC
Language=EN
[Function]
Title=Object Navigator
Command=SE80
[Configuration]
WorkDir=C:\Users\USERABC\Documents\SAP\SAP GUI
[Options]
Reuse=1

Das interessante dabei ist, dass so eine .sap Datei auch ohne das aufgeführte Directory (WorkDir) auskommt und funktioniert.
Das Backend muss und also einen solchen String erstellen und ihn uns Base64-codiert zurückliefern. In etwa so:

Verarbeiten der Base64 Response

Nun müssen wir den Content-String noch irgendwie speichern bzw. öffnen.
Dabei gibt es starke Unterschiede zwischen IE und den anderen Browsern wie Chrome, Firefox und Co. Irgendwie keine wirkliche Überraschung, oder? 🙂

_openFileFromBase64: function(sBase64Data){
var sFileName = "SE80Shortcut.sap";
// IE workaround
if(window.navigator && window.navigator.msSaveOrOpenBlob){
var byteCharacters = atob(sBase64Data);
var byteNumbers = new Array(byteCharacters.length);
for (var i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
var blob = new Blob([byteArray], {
type: "application/x-sapshortcut"
});
window.navigator.msSaveOrOpenBlob(blob, sFileName);
}
// much easier if not IE
else{
var sIFrame =
"<iframe style='diyplay:none;'
src='data:application/x-sapshortcut;base64, " +
encodeURI(sBase64Data) +
"'></iframe>";
var div = document.createElement('div');
div.innerHTML = sIFrame.trim();
window.document.body.appendChild(div.firstChild);
}
}

Für User die IE nutzen, kreieren wir einen Blob. Das ist ziemlich umständlich wie ihr seht, da wir unseren Base64-String mehrfach umformen müssen.
In allen anderen Browsern hängen wir einen unsichtbares iFrame an den Body unseres HTML-Dokuments an. Der Browser interpretiert den Mime-Type „x-sapshortcut“ automatisch und lädt das Dokument in den Default-Ordner runter.

IE lädt das in die sogenannten „Temporary Internet Files“ herunter. Es ist notwendig, dass dieser Ordner in den SAP Security Einstellungen freigegeben wird. Dazu gibt es aber eine SAP Note (1559107) die all unsere Probleme löst 🙂 .

Ich hoffe ich konnte euch das harte Entwicklerleben etwas erleichtern. Ich und meine Kollegen freuen uns, wenn ihr Kontakt zu uns aufnehmt und beantworten gerne eure Fragen via Kommentar oder auch per Mail.

In Hamburch sagt man Tschüüs – und kiek mol wedder in!

SAP Fiori 3.0 – Der intelligente Anstrich für intelligente Anwendungen

Ahoi Matrosen! Wir wollen einen Blick in die Zukunft wagen. Auf den letztjährigen SAP TechEd Veranstaltungen wurde nämlich der nächste Schritt im UX- bzw. Frontend-Bereich vorgestellt – SAP Fiori 3.0

Fiori 3 ist da! Aktuelle Neuigkeiten und Release Dates

SAP Fiori 3.0 löst seinen Vorgänger SAP Fiori 2.0 ab, der seit etwa Anfang 2016 im Einsatz ist. Mit der Ablösung können sich SAP Enduser, aber auch wir als Entwickler, auf eine neue Gestaltung der SAP Oberflächen freuen. Was das genau bedeutet möchte ich kurz umreißen. Also Rettungswesten an und weiterlesen:

Was bleibt?

Nur weil etwas neues kommt muss das alte nicht komplett entsorgt werden. Wie zuvor auch basieren die Frontend Anwendungen weiterhin auf dem SAP Fiori Prinzip und den Fiori Design Guidelines. Diesen liegt auch immer noch die SAPUI5-Bibliothek zu Grunde, die mittlerweile in Version 1.62.0 (Stand 21.02.2019) angekommen ist.

Wie in SAP Fiori 2.0 schon gelebt bleiben die Design Prinzipien erhalten: Role-Based (Rollenbasiert), Delightful (Entzückend, mehr oder weniger), Coherent (Einheitlich), Simple (Einfach) und Adaptive (Anpassungsfähig).

Auch das Launchpad bleibt als zentraler Zugriffspunkt zu den Anwendungen. Aber es wandelt sich. Was das genau bedeutet und was sonst noch neu ist:

Was ist neu?

Harmonisierung ist ein großes Stichwort. Die SAP spricht hier von aufräumen oder Altlasten ausmerzen. Viele unterschiedliche Anwendungen haben zurzeit noch viele unterschiedliche User Experiences. Der Weg in SAP Fiori 3.0 geht dahin, dass diese vereinheitlicht werden. Bisher, das musste die SAP sich selbst eingestehen, ist das „Coherent“ aus den Design Prinzipien noch nicht wirklich gelungen.

Ein Beispiel für die Harmonisierung lässt sich bei der neuen Gestaltung der Shell-Bar aus den verschiedenen Anwendungen erkennen:

Darüber hinaus finden Vereinheitlichungen hinsichtlich weiterer Punkte statt. Dazu gehören: Layouts, Aktions-Platzierungen (in Neu-Deutsch: Button-Platzierung), Terminologie, Farben, Schriftarten, Icon Sets und weitere, kleinere Aspekte.

Aber Harmonisierung ist nicht alles. Vor allem zwei Neuerungen in SAP Fiori 3.0 haben es in sich. Auf diese möchte ich daher auch etwas detaillierter eingehen.

Cards anstatt Tiles

In Zukunft bekommt das Fiori Launchpad ein ganz neues Gesicht: aus Tiles werden Cards. Die zentrale Motivation dahinter ist, dass durch Cards deutlich mehr Informationen direkt im Launchpad angezeigt werden können. Dies ist vor allem aus konkreten, fachlichen Anforderungen der Kunden gewachsen. Vielen Nutzern des Launchpads reichten die Auskunftsmöglichkeiten auf den Tiles nicht aus.

Während man bei Tiles beschränkt war auf 1×1 große bzw. 1×2 große Kästen, auf denen man all seine Informationen unterbringen musste, können Cards beliebig groß und beliebig gestaltet werden. Es ist Platz für Tabellen, Grafiken, weiterhin wichtige KPIs und andere Controls, alles aber völlig frei gestaltbar.

Die Cards werden in zwei Phasen ausgerollt. In Phase 1 wird es möglich sein alles das oben Beschriebene zu realisieren. Phase 2 geht dann sogar noch einen Schritt weiter: hier wird es konkret möglich sein, Workflow-Aktionen direkt über Cards anzustoßen ohne in die eigentliche Anwendung navigieren zu müssen.

SAP CoPilot

Der SAP CoPilot ist in Fiori 3.0 keine wirkliche Neuerung. Schon mit Fiori 2.0 konnte der CoPilot als persönlicher Assistent im Launchpad eingerichtet werden. Dort war bzw. ist er als eine Art Popup (oder PopOver) im Launchpad anzeigbar. Es wirkte fast so wie ein kleines AddOn zum Launchpad, zwar dabei, aber nicht komplett integriert.

Der eigentliche Grund aber, dass er in meinem Artikel trotzdem eine eigene Überschrift bekommt ist der, dass ihm eine sehr viel prominentere Rolle zugedacht wird.

In Fiori 3.0 ist der SAP CoPilot ist nicht länger ein gefühltes Add-On sondern ein zentraler Bestandteil des Launchpads. Dies äußert sich u.A. durch folgende Punkte:

  • Der CoPilot wird per Navigation auf seiner eigenen View und unter voller Nutzung der Fensterbreite angezeigt
  • Der CoPilot kann alternativ zum Launchpad als Einstiegsseite verwendet werden
  • Aus dem Launchpad ist der CoPilot direkt über die Shell-Bar-Mitte zu erreichen

Das höhere Ziel der SAP geht sogar noch einen Schritt weiter. Dort ist der Gedanke, dass der CoPilot das einzig digitale System für den Enduser wird („Make CoPilot that one digital system for enduser“), was so viel bedeutet wie: Alles was du brauchst und benutzt erreichst du in Zukunft über den CoPilot.

Meiner Ansicht nach sehr schön gestaltet ist, dass als Antwort auf einfache Fragen (die im Fließtext und auch per Sprachsteuerung eingegeben werden können) nicht nur textliche Antworten sondern je nach Kontext Teile von Apps, Grafiken, Workflow-Aktionen, o.Ä. als Antwort ausgegeben werden. Der Enduser muss so nicht erst mühselig in die entsprechende Anwendung, den gesuchten Fall erneut heraussuchen und bearbeiten sondern kann diesen direkt über eine Aktion in der Antwort auf seine Frage im CoPilot erledigen.

Wer tieferes Interesse an dem CoPilot hat sollte sich auf jeden Fall den Beitrag meines Kollegen Sebastian Garms zum Thema SAP CoPilot zu Gemüte führen. Dort wird das ganze Thema sehr anschaulich konkretisiert.

Zusammenfassung

Einmal zusammengefasst bietet SAP Fiori 3.0 folgende Kernpunkte:

  • Harmonisierung
  • Vereinheitlichung Shell-Bar und Menü
  • Cards anstatt Tiles
  • SAP CoPilot mit neuer, zentraler Rolle im Launchpad

Bleibt nur noch die Frage: Wann kommt denn jetzt Fiori 3.0?
Hier hält sich die SAP noch etwas bedeckt. Auf den letztjährigen SAP TechEd Veranstaltungen wurde ein Zeitraum rund um die SAPPHIRE 2019 genannt. Das würde bedeuten, dass wir uns auf ein Release im Mai einstellen können (Die SAPPHIRE findet vom 07.05. – 09.05.2019 in Orlando statt)

Anstehendes Webinar am 21.03.2019

Weitere Infos zu Fiori 3.0 und dem SAP CoPilot gibt es zudem bei unserem nächsten Webinar „Abheben mit der SAP Cloud Platform – der Weg zum intelligenten Unternehmen“ am 21.03.2019 ab 13:00 Uhr. Ein zentraler Bestandteil dieser Veranstaltung wird eine Demo des SAP CoPilots mit samt seiner gesamten Konfiguration sein. Schaut auf jeden Fall mal rein und meldet euch ganz unverbindlich an.

Soweit erst mal! Dann bleibt mir nur noch zu sagen: „Kiek mol wedder inn!“

Update (02.05.2019)

Mit SAPUI5 1.65 wurden die ersten Fiori 3 Elemente ausgeliefert.

In diesem Blog-Beitrag lest ihr, wie sich die Controlls und das brandneue Theme „Quartz“ schlagen!

Fiori Client – Fiori Applikationen auf mobilen Endgeräten

Du hast bereits deine ersten Fiori-Apps programmiert, dein Launchpad konfiguriert und eure Mitarbeiter arbeiten bereits effektiv mit den modernen UI5-Applikationen?

Jetzt kommen die ersten Anforderungen, dass die Mitarbeiter eure Fiori-Apps auch gerne über die mobilen Endgeräte aufrufen möchten. Die nahelegende und schnellste Möglichkeit – den Link zur Applikation einfach über die Standardbrowser aufrufen, wie bspw. Safari oder Chrome. Eine andere Möglichkeit für das Aufrufen des Launchpads und Fiori-Apps ist der Fiori Client, der als Applikation auf dem mobilen Endgerät installiert wird.

In diesem Blog wird dir der FIori Client erklärt, es werden die Vor- und Nachteile des Fiori Clients aufgezeigt, sowie die verschiedenen Entwicklungs- und Installationsmöglichkeiten aufgelistet.

Was ist der Fiori Client? – Definition

Fiori Client

Der Fiori Client ist eine native Applikation auf einem mobilen Endgerät, als Laufzeitumgebung für Fiori Apps. Prinzipiell ist der Fiori Client ein Browser für SAP Fiori Applikationen, der auf viele native Funktionen des Smartphones oder Tablets zugreifen kann. Dies wird durch Kapsel und dem Apache Cordova Framework ermöglicht. Das Apache Cordova Framework ist ein Framework, das hybrid anwendbar auf Android, iOS und Windows Plattformen zugreifen, aber dennoch auf die nativen Mobilfunktionalitäten zugreifen kann. Der Fiori Client ist somit für Android, iOS und auch Microsoft kompatibel. Auf folgende nativen Funktionen kann die Applikation beispielsweise zugreifen:

  • Kamera
  • Barcode
  • Kalender
  • Kontakte
  • Ortung (GPS)
  • Push-Benachrichtigung
  • Mikrofon
  • (Offline Verfügbarkeit nur teilweise)

Eine volle Liste aller unterstützen Cordova Plugins im Fiori Client gibt es hier.

Der Fiori Client kann also direkt auf deine Kamera zugreifen, einen Barcode scannen oder Push-Benachrichtigungen empfangen.

Wie erstelle ich den Fiori Client – Entwicklung

Du hast verschiedene Möglichkeiten den Fiori Client zu erstellen und zu customizen. So lässt sich beispielsweise das Logo und der Link zur Applikation customizen, oder auch die Anmeldung zu den SAP-Systemen über Zertifikstanmeldungen realisieren.
Anschließend werden die verschiedenen Entwicklungsmöglichkeiten und ihre Vor- und Nachteile aufgezeigt.

1. Möglichkeit – Fiori Client aus dem Apple-/ Google-/ oder dem Microsoft-/Store customizen

Der Fiori Client lässt sich als Applikation direkt aus dem Apple Store, im Google PlayStore und im Microsoft Store herunterladen und installieren. Hierfür benötigt es keinen besonderen SAP Account oder eine bestimmte Berechtigung. Es sollte lediglich die Befugnis bestehen, Applikationen aus den jeweiligen Stores laden zu dürfen.
Ist der Service für das SAP Launchpad oder die Fiori Applikation aus dem Internet erreichbar, so lässt sich der Link ohne Weiteres in den Fiori Client eintragen und die App wird im Fiori Client Container dargestellt.

Fiori Client aus Store mit simpler Hinterlegung der URL

Anschließend kannst du auf dein gewohntes Fiori Launchpad oder deine hinterlegte Fiori App zugreifen.


Fiori Client aus Store mit simpler Hinterlegung der URL im Launchpad

Diese Implementierungsstrategie eignet sich vor allem für Unternehmen, die geringe Sicherheitsanforderungen und Sicherheitseinrichtungen haben. Die Installation über die eigenständige Installation der App auf die Endgeräte durch die eigenen Mitarbeiter eignet sich vor allem dann, wenn die Endgeräte sowieso schon mit einer VPN-Software ausgestattet sind, um auf das SAP-System zuzugreifen. Zudem ist diese Installation zielführend, um in kürzester Zeit einen ShowCase aufzuzeigen, wie das Launchpad oder die Fiori-Applikation auf den mobilen Endgeräten angezeigt wird.

Leider sind mit dieser Implementierungsmethode kaum Spielräume für weitere Entwicklungsmöglichkeit geboten. Du hast keinen Einfluss auf das App-Icon auf dem Endgerät, das Branding der App oder Anmeldemöglichkeiten über Single-Sign-On oder zertifikatbasierte Logins. Generell besteht auch keine Möglichkeit zur Überprüfung des Benutzerverhaltens.

Im Gegensatz zu den anderen Entwicklungsmöglichkeiten benötigst du für diese Variante jedoch keinen extra Apple-Developer Account und hast daher keine Probleme mit der Zertifikatssignatur von Apps.

2. Möglichkeit – Fiori Client in der Cloud erstellen über den Service „Mobile Services, std“

Die SAP Cloud Platform bietet die Möglichkeit einen eigenen Fiori Client über den Service „Mobile Services, std“ zu erstellen. Der Service findet sich im SAP Cloud Platform Cockpit in einem entsprechenden Subaccount unter „Services“ und kann dort aktiviert werden.

Mobile Services, std.

Dieser Cloud Service ermöglicht schon mehr Möglichkeiten, den Fiori Client anzupassen und zu customizen. Du kannst neben der Fiori URL auch das Application Launch Icon und die Screens beim Starten der App definieren.

App-Customizing im Sap Service „Mobile Services“

Zusätzlich lässt sich über den Service auch bestimmen, welche Plugins in die App implementiert werden und welche ausgeschlossen werden sollen. So kann man aus einer Auswahl an Plugins wählen und beispielsweise einstellen, dass die Applikation Push Nachrichten ermöglichen soll.

Features / Plugins für den Fiori Client

Die Applikation lässt sich dann anschließend über den Cloud Service kompilieren und du hast sogar die Möglichkeit die Projektdatei herunterzuladen. Mit der kannst du dann in einer eigenen Entwicklungsumgebung weiterarbeiten und Anpassungen durchführen.

Nach erfolgreicher Kompilierung des Fiori Clients besteht die Möglichkeit die Installationsdatei herunterzuladen. Diese kannst du gegebenenfalls in den Apple-/ Play-/ oder Microsoft-Store laden und so an deine Kollegen oder Kunden verbreiten. Weiterhin hast du die Möglichkeit über einen Link direkt zur Installation des Fiori Clients zu gelangen. Der SAP Cloud Service bietet somit auch die Speicherung und das Distributieren der App an. Hierdurch wird die Verbeitung des Fiori Clients erheblich vereinfacht.

Installieren der App über den SAP Cloud Service

Die direkte Installation über den Mobile Service bietet die Möglichkeit der Kontrolle des Nutzungsverhaltens. Mit dem Service lässt sich überprüfen, wer die Applikation benutzt und wie sie benutzt wird. Zudem kann man kontrollieren, wie die Push-Nachrichten und Netzwerkstatistiken der Nutzung aussehen.

Nutzerverhalten

Der Fiori Client lässt sich mit diesem Service derzeit nur für Android und Apple entwickeln. Microsoft-Geräte bleiben daher aus.
In jedem Fall müssen vor der Entwicklung Signaturprofile erstellt werden. Ein Android Signing Profile lässt sich einfach über den Service generieren. Dementsprechend ist eine Android-App-Erstellung ohne jegliche Vorbereitung sehr schnell durchzuführen. Für die Erstellung eines iOS basierten Fiori Clients benötigt es zuvor eine Erstellung eines Provisioning Profiles und eines entsprechenden Zertifikats, wodurch sich das Signing Profile auch für Apple erstellen lässt.

3. Möglichkeit – Fiori Client in der eigenen Entwickungsumgebung erstellen

Zudem lässt sich eine eigene Entwicklungsumgebung aufsetzen, in der man die meisten Funktionen zur weiteren Entwicklung und Anpassung hat.
Eine Einrichtung der Entwicklungsumgebung gestaltet sich in den verschiedenen Entwicklungsmöglichkeiten am kompliziertesten. Dies offenbart jedoch auch den größten Spielraum, um an der App eigene Hand anzulegen und auf mögliche Sicherheitsvorkehrungen Einfluss zu nehmen. Die Erstellung und Kompilierung der App, die man relativ mühelos durch wenige Mausklicke in der SAP Cloud Platform durchführen konnte, lässt sich nämlich auch mit einem entsprechendem Mobile Development Kit durchführen.
Die Kompilierung des Projektordners gestaltet sich halbwegs übersichtlich über eine JSON-Datei, die sich konfigurieren lässt. In dieser Datei lassen sich Zertifizierungseinstellungen, Branding und Weiteres einstellen.

Richte deine Entwicklungsumgebung wie hier beschrieben ein. Je nach Betriebssystem gestaltet sich die Einrichtung komplizierter oder einfacher. Als geeignete Entwicklungsumgebung kann ich dir OSX empfehlen. Hier erfolgte die Einrichtung und Installation am Schnellsten, ohne viele Komplikationen.

Individueller Fiori Client auch im Play- und App-Store

Wie schon kurz angeschnitten stellt die SAP Cloud Platform die Möglichkeit bereit, dass der Enduser direkt über einen entsprechenden Downloadlink den Fiori Client herunterladen kann.

Man kann jedoch auch das Installationsmedium auf herkömmlicher Weise in den Play- oder App-Store hochladen und dort entsprechend die Applikation registrieren. Hierbei muss man sich jedoch bei der Veröffentlichung gedulden. Diese kann bis zu einer Woche andauern, je nachdem wie lange der Prüfungsprozess des entsprechenden Stores andauert.

Weiteres zum SAP Fiori Client

Der Fiori Client ist ein nützlicher Browser, um unser Launchpad oder bestimmte Fiori Applikationen direkt auf unser mobiles Endgerät zu holen. In diesem Blog habe ich dir näher gebracht, auf welche Weisen man den Fiori Client erstellen und distributieren kann.

In einem weiteren Blog werde ich dir zeigen, wie sich der Fiori Client mit MobileIron und anderen Multi Device Management Tools verknüpfen lässt, sodass auch du über die MDMs die Applikation auf die Endgeräte der Nutzer bringen kannst.

Du möchtest noch weitere Informationen zum Thema Fiori erhalten? Schaue dir auch unsere anderen Blog-Artikel an.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei. 

Singleton mit Fiori

Zugriff von Models aus anderen Views? Einsatz von Singleton-Klassen im SAPUI5-Kontext

Bist du an einem Punkt angekommen, an dem du ein Model oder ein Control benötigst, das sich aber in einer anderen View befinden?

Wenn ja, dann bist du hier genau richtig! Denn auch ich bin auf dieses Problem gestoßen und möchte dir zeigen wie ich es gelöst habe.

 

Master-Detail App

Mein Ausgangspunkt ist in diesem Beispiel eine Master-Detail App.
Ich befinde mich gerade im Kontext des Master.Controllers und benötige ein Property, dass sich in im sogenannten „detailView“-Model befindet, welches jedoch lediglich auf die Detail-Page gebunden ist.
Mein erster Gedanke ist der Folgende:

Master.controller.js 
...
onSelectionChange: function(oEvent){
var oModel = this.getModel("detailView");
oModel.getProperty("bEdit");
}, ...

Sieht auf den ersten Blick ganz gut aus. Die Web IDE meckert nicht.

Doch in der Konsole kommt folgende Fehlermeldung.

Fehlermeldung in der Konsole

Anhand der Fehlermeldung wurde mir sofort klar, dass ich innerhalb des Master.Controller nicht das gewünschte Model aufrufen kann.
Sinngemäß suche ich auf der Master-Ebene vergebens nach dem Model „detailView“. Da ist es verständlich, dass die Konsole keine Property erkennt… Der Controller findet ja nicht mal das Model!

Um nun dieses Problem zu lösen zeige ich mit Hilfe von simulierten Klassen, wie du dir ganz leicht andere Models holst.

Ich beginne mit dem Erstellen einer Klasse und erweitere diese mit der Klasse „sap.ui.base.Object“. Meine Klasse entspricht der typischen Funktionalität eines sogenannten Singleton. Ein solches Singleton funktioniert genauso wie in der JAVA Entwicklung. Die Klasse wird aufgerufen, wenn ein Objekt der Klasse erstellt wird. Wie in jeder anderen Sprache können auch Methoden in der Klasse erstellt werden. Anstatt einer statischen Klasse, gebe ich die Funktion getInstance() zurück. Die Funktion erstellt nur beim ersten Mal eine neue Instanz der Klasse. Mit der IF-Abfrage stelle ich fest, ob eine Instanz bereits erzeugt wurde. Die „instance“-Variable erstelle ich, um die aktuelle Instanz festzuhalten.
Des Weiteren implementiere ich in der erweiterten Object-Klasse einen Konstruktor, sowie eine Getter- und Setter- Funktion, um so später den Zugriff und die Registrierung des gewünschten Models zu gewährleisten.

...NEW acando/TamsApp/Utils/UIHelper.js...
sap.ui.define([
	"sap/ui/base/Object"
], function (Object) {
	"use strict";
	var instance;
	var services = Object.extend("be.wl.objects.model.services", {
		constructor: function () {
			// this.controllerDetailView = null;
		},
		setControllerDetailView: function (oController) {
			this.controllerDetailView = oController;
		},
		getControllerDetailView: function () {
			return this.controllerDetailView;
		}
	});
	return {
		getInstance: function () {
			if (!instance) {
				instance = new services();
			}
			return instance;
		}
	};
});

Nun kann ich in meinem gewünschten Controller im UI.Helper registrieren.

Detail.controller.js
sap.ui.define([
...
"acando/TamsApp/Utils/UIHelper"
], function (BaseController, JSONModel, formatter, MessageBox, MessageToast, UIHelper) {
	"use strict";

return BaseController.extend("acando.TamsApp.controller.Detail", {

onInit: function () {
UIHelper.getInstance().setControllerDetailView(this);
...
},

Im Master.controller.js kann ich die zuvor registrierte Instanz mit Hilfe der Helper-Singleton-Klasse holen.

Master.controller.js
sap.ui.define([
	"acando/TamsApp/controller/BaseController",
	"sap/ui/model/json/JSONModel",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/m/GroupHeaderListItem",
	"sap/ui/Device",
	"acando/TamsApp/model/formatter",
	"sap/m/MessageBox",
	"acando/TamsApp/Utils/UIHelper"
], function (BaseController, JSONModel, Filter, FilterOperator, GroupHeaderListItem, Device, formatter, MessageBox,UIHelper) {
	"use strict";
	
return BaseController.extend("acando.TamsApp.controller.Master", {
...
var oView = UIHelper.getInstance().getControllerDetailView();
....

Wenn ich in meiner Funktion einen Debugger setze und die Schritte in der Konsole verfolge, sehe ich folgende Ergebnisse:

Mit Hilfe des Singleton konnte ich nun auch auf andere Views zugreifen, die sich an diesem zuvor registriert haben. Im UI5 Kontext ist dies eine saubere Lösung um Models und deren Properties View-übergreifend zu lesen und zu verändern.
Wenn du weitere Informationen benötigst oder dir der Blog gefallen hat, schreibe doch einfach einen kurzen Kommentar. Schau dir auch unsere weiteren Artikel an, wenn du mehr über Fiori und SAPUI5 erfahren möchtest.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei. 

JSON als Bild

JSON Model – Stärken, Schwächen und Anwendung

In der SAPUI5-Entwicklung kommt man nicht um hin, sich an irgendeinem Zeitpunkt mit JSON auseinanderzusetzen. JSON steht kurz für JavaScript Object Notation und bietet eine einfache, standardisierte, textbasierte Darstellung von Objekten.
In der SAPUI5-Entwicklung ist das JSON-Model eins von zwei zentralen Models für die Datenhaltung. (ODataModel ist das andere). Als client-side Model stellt es Daten zur Verfügung, die für die Anwendung zu jeder Zeit komplett verfügbar sind.

Da die Beschreibung des JSON Models in der API des SAPUI5 Demo Kits recht dürftig ist („Model implementation for JSON format“) will ich euch hier einen tieferen Einblick in das Model geben, sowie Stärken, Schwächen, Anwendungsbereiche und No-Gos ans Herz legen.

Das Arbeiten mit einem JSON Model ist deutlich einfacher und unabhängiger von der Backend-Implementierung als das Arbeiten mit einem ODataModel, weshalb es Frontend-Entwickler dazu verleiten kann, dies auch eher zu nutzen. So ist zumindest meine Erfahrung und, mit Einblick auf Entwicklungen von anderen Entwicklern, bin bzw. war ich da nicht allein.
Das ist aber nicht immer sinnvoll.

Im Folgenden werde ich darauf eingehen, wann es für mich Sinn macht ein JSON Model zu nutzen und wann aber auch nicht. Fangen wir aber mit etwas Einfachem an:

Syntax und Aufruf

Das JSON-Model befindet sich in dem Namespace „sap.ui.model.json“ und lässt sich wie folgt initialisieren:

var oModel = new sap.ui.model.json.JSONModel(oData);

In dem Konstruktor-Aufruf gibt es einen Parameter oData. Dieser kann zwei ganz unterschiedliche Sachen darstellen:

  • Eine URL, von der aus das JSON geladen werden soll
  • Ein JSON-Objekt

Bei beiden Aufrufen wird ein JSON-Objekt in das JSON-Model geladen. Dieses kann anschließend wie gewohnt über ein Binding oder mit der Methode oModel.getData() genutzt bzw. ausgegeben werden.
Gleichermaßen können über die Methode oModel.setData(oData) neue Daten in das JSON-Model geschrieben werden. Interessanterweise kann hier einzig ein JSON-Objekt als Parameter übergeben werden. Für die Variante mit einer URL gibt es den Aufruf oModel.loadData(oData)

Stärken und Anwendungsbereiche

In meinen Anwendungen nutze ich JSON Models für genau drei Anwendungsfälle

  • ViewModels
  • Auswahllisten mit einer geringen Anzahl von Einträgen (z.B. für Select-Boxen)
  • Interaktion mit Dialogen

Eingesetzt als ViewModel speichere ich Einstellungen eines Views in einem eigenen Model. Klassisch für meine Anwendungen ist z.B. eine Eigenschaft „mode“, die beschreibt, ob ich mich im Ansichts- oder im Bearbeitungsmodus befinde. Durch das Setzen dieser Eigenschaft in einem ViewModel kann ich die Controls bzw. deren Eigenschaften (enabled, visible, etc.) sehr einfach über Expression Bindings steuern.

Setzen des ViewModels in der onInit
...
onInit: function(oEvent) {
	...
	this.setModel(new sap.ui.model.json.JSONModel({
		mode: "view",
		busy: false,
		filterActive: false
	}), "viewConfig");
	...
}
...
Steuerung über Expression Bindings
...
<Button icon="sap-icon://edit" press="onEdit" text="Bearbeiten"
	enabled="{= ${viewConfig>/mode} === 'view'}">
...

Wenn du dich näher dafür interessiert kannst du dir auch einfach mal das Entwurfsmuster MVVM näher angucken.

Hinweis: Wenn du das ViewModel onInit setzt musst du darauf achten, dass beim Navigieren das Model zurückgesetzt wird. Sonst können unerwünschte Effekte auftreten.

Der zweite Anwendungsbereich sind die Auswahllisten. Diese erzeuge ich meist beim Starten der Anwendung. Hier achte ich darauf, dass die Anzahl der Einträge auf eine kleine Zahl beschränkt bleibt. Als groben (aber keinesfalls fest definierten) Wert nehme ich mir 20 Einträge als Limit. Alles, was darüber hinaus geht, sollte man meiner Meinung nicht initial Laden sondern anders handhaben.
Meist sind dies Listen, die ich über Function Imports lade, die ich oft benutze und nicht an jeder Stelle neu laden möchte oder die aus anderen Gründen immer vorrätig sein müssen.

Laden einer Auswahlliste in der model.js
...
createMatStatusModel: function(oComponent) {
	var dfdResult = jQuery.Deferred();
	var mOptions = {
		method: "GET",
		urlParameters: {
			"tabname": "TVMST",
			"keyField": "VMSTA",
			"valueField": "VMSTB",
			"sqlWhere": ""
		},
		success: function(oData) {
			oComponent.getModel("matStatusModel").setData(oData.results);
			dfdResult.resolve("fine");
		},
		error: function() {
			oComponent.getModel("matStatusModel").setData([]);
			dfdResult.resolve("fine");
		}
	};

	oComponent.getModel("generalService").callFunction("/getKeyValues", mOptions);

	return dfdResult.promise();
}
...
Aufruf in der Component.js
...
models.createMatStatusModel(this).done(function() {
	this.getRouter().initialize();
}.bind(this));
...

Hinweis: Mit den Deferred Objects stelle ich sicher, dass das Laden der Liste vor dem Start der Anwendung abgeschlossen ist. Der asynchrone Call muss an dieser Stelle berücksichtigt werden.

Zu guter Letzt bleibt der Einsatz im Dialog-Handling. Oftmals nutze ich Eingabedialoge, die auf keine vorhandene Struktur passen oder generische Dialoge, die ich an unterschiedlichen Stellen einsetze. Um einfach Daten, Texte o.Ä. an den Dialog zu übergeben bzw. Daten aus Eingabefeldern des Dialogs auszulesen nutze ich JSON Models.
Dieser Einsatz ist ähnlich dem Einsatz des ViewModels. Diesmal ist das Model aber auf den Dialog beschränkt und, viel wichtiger, hält Daten jenseits von Einstellungen oder Zuständen.

Öffnen eines Dialogs mit einem JSON Model
...
onOpenDialog: function() {
	if (!this._dialog) {
		this._dialog = sap.ui.xmlfragment("acando.blog.view.dialogs.infoDialog", this);
		this.getView().addDependent(this._dialog);
	}
	
	this._dialog.setModel(new sap.ui.model.json.JSONModel({
		"titleLabel" : "Dialogtitel",
		"inputValue" : ""
	}), "dialogData");


	this._dialog.open();
}
...
Dialog Fragment
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
	<Dialog title="{dialogData>/titleLabel}">
		<content>
			<Label text="Eingabe Input-Control" />
			<Input value="{dialogData>/inputValue}" />
			...
		</content>
		...							
	</Dialog>
</core:FragmentDefinition>

Schwächen und No-Gos

Wie schon anfangs angedeutet ist das JSON Model ein client-side Model. Es macht somit keine eigenen Backend-Calls und kann aus diesem Grund nur mit den Daten arbeiten, die es im Bauch hat. Aus diesem Grund ist es für die CRUD-Methoden nicht geeignet.
Nun lässt sich ein JSON Model trotzdem zum Daten-Handling umfunktionieren. Es funktioniert auch einigermaßen aber so richtig toll ist es nicht. Meine Empfehlung ist aber ganz eindeutig:

Kein Daten-Handling mit JSON Models

Das Daten-Handling sollte sich immer mit ODataModels umsetzen lassen. Wenn man denkt, dass dies auf Grund der Anforderungen nicht möglich ist sollte man immer überlegen, ob man

  • nicht doch auf irgendeine Art das ODataModel nutzen kann
  • die Anforderungen ändern/vereinfachen kann

Diejenigen, die die Anwendung im Anschluss warten sollen, werden es euch danken.
Darüber hinaus gibt es noch einen Punkt, den ich euch wärmstens ans Herz legen möchte:

Kein create- oder update Deep

create- und update Deep, also Speichern von Strukturen mit mehreren Ebenen (Navigations), ist schlechte Programmierung. Sie ist fehleranfällig, komplex und sehr schlecht wartbar. Mein Rat auch hier: Macht es nicht. Und meine Erfahrung an dieser Stelle ist auch, dass es immer eine bessere und einfachere Möglichkeit gibt als Deep zu speichern.

Fazit

Dies sind meine Erfahrungen aus der SAPUI5 Entwicklung mit JSON Models. Die Beispiele, die ich hier beschreibe, sowohl die negativen als auch die positiven, habe ich alle schon erlebt, sei es, dass ich sie selbst so implementiert habe oder die Wartung einer entsprechenden Anwendung übernommen habe.

Gerne kannst du auch deine eigenen Erfahrungen mit JSON Models schildern. Siehst du es genauso wie ich oder womöglich doch ganz anders? Nutze einfach unten die Kommentar-Funktion.

Bis dahin erstmal und kiek mol wedder in!

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Performanceoptimierung

10 Kniffe um die Performance deiner Anwendung zu verbessern! – Teil 1

Ist die Performance deiner SAPUI5 Anwendung zu langsam? Diese Frage hat sich bestimmt jeder schon mal gestellt. Zu meist kommt einem aber nicht die Frage als erstes in den Sinn sondern die Antwort. Und meist auch nur dann wenn gilt:

Meine Anwendung ist zu langsam!

Im Folgenden möchte ich 10 Gründe auflisten, die die Performance einer Anwendung negativ beeinflussen könnten. Oder, um das ganze positiv zu formulieren, 10 Kniffe, mit denen du deine Anwendung merklich beschleunigen kannst. So könnte aus deinem Trabbi ganz einfach ein Ferrari werden! Naja, vielleicht manchmal auch nur ein Golf, aber vielleicht reicht ja auch schon der Schritt dahin?

Wer von euch ein paar meiner anderen Beiträge gelesen hat, hat sicher festgestellt, dass ich des Öfteren schon die Performance erwähnt habe. Und das auch aus gutem Grund: Die Performance ist eines der entscheidenden Argumente wenn es darum geht die Akzeptanz der Anwendung bei einem Enduser zu steigern bzw. überhaupt zu erreichen.
Aber lange Rede kurzer Sinn. Hier nun die ersten 5 Kniffe um die Performance deiner Anwendung zu verbessern:

Kniff 1 – Ausschalten des count-Mode im ODataModel

Der count-Mode ist eine standardmäßig gesetzte Eigenschaft bei einem ODataModel. Diese führt dazu, dass zusätzlich zu dem Backend-Call, der die Daten an das Frontend liefert, ein weiterer Call gemacht wird, der vorweg die Anzahl der Einträge zählt, die vom Backend geliefert werden.
Mein Kollege Sebastian Garms hat das Thema in seinem Blog-Artikel schon gründlich beschrieben, deshalb lasse ich es an dieser Stelle mit der kurzen Beschreibung gut. Weil es aber so einfach ist und der Effekt so groß empfehle ich euch aber dringlichst, dort mal reinzuschauen. Der Beitrag zeigt nicht nur, was zu tun ist, sondern stellt auch sehr gut den positiven Effekt dar, den das Ausschalten des count-Modes verursacht.

Zu Sebastians Blog-Artikel gelangt ihr hier

Kniff 2 – Expands nur an Stellen, wo sie gebraucht werden

Mit Hilfe von expands können Entitäten mit Zusatzinformationen gefüllt werden. Im Backend werden dazu nach dem Auslesen der eigentlichen Entität die Zusatzeigenschaften aus weiteren Tabellen geladen. Das kann mitunter ganz schnell gehen, so zum Beispiel bei einfachen 1:1 Beziehungen, kann aber auch länger dauern. Für letzteres prädestiniert sind sowohl 1:n Beziehungen als auch Multi-Expands (Beispiel SD-Auftrag -> ToDebitor -> To Address)
Hier gilt es eine relativ einfache Regel zu beachten: Ich nutze expands nur an Stellen, wo ich sie brauche! Der Klassiker ist die Auswahl der expands bei Übersichtsseiten gegenüber der Auswahl auf Detailseiten.
Auf einer Übersichtsseite (mit einem Table- oder List-Control) brauche ich nur Informationen, die in der Tabelle angezeigt werden. Auf der Detailseite kann ich dann dagegen mehr Informationen anzeigen.
Ein einfaches Beispiel mit einer schlechten Umsetzung und einer guten:

Worklist-View
...
<Table items="{path: '/SalesOrderSet, parameters : { 
expand : ToBusinessPartner, ToBusinessPartner/ToContact, 
ToSalesOrderLineItem, ToSalesOrderLineItem/ToContact}}">
...
</Table>
...

In diesem Code-Snippet werden alle expands der SalesOrder sofort mitgeladen. Wir haben hier sowohl 1:n Beziehungen (SalesOrder -> ToSalesOrderLineItem) als auch Multi-Expands.
Besser wäre folgende Lösung:

Worklist-View
...
<Table items="{/SalesOrderSet}">
...
</Table>
...
Object-Controller
_onObjectMatched: function(oEvent) {
        var oId = oEvent.getParameter("arguments").id;			
	this.getView().bindElement({
		path: "/ProductSet('" + oId + "')",
		parameters: {
			expand: " ToBusinessPartner, ToBusinessPartner/ToContact, 
                        ToSalesOrderLineItem"
		},
	});
},

Die Expands werden in dieser Variante erst beim Springen auf die Detailseite geladen.

Kniff 3 – Reduzierung von Expands bei Aggregation-Bindings

Kniff 3 ist eine Erweiterung von Kniff 2, geht aber noch einen ganzen Schritt weiter. Selbst wenn ich expands bei einer Übersichtsseite benötige kann es Sinn machen unter Performance Gesichtspunkten auf sie zu verzichten.
Hier müsste dann entsprechend die fachliche Anforderung geändert werden.
Das gilt wie bei Kniff 2 auch schon vor allem für 1:n Beziehungen und Multi-Expands. An dieser Stelle werden die entsprechenden expands nämlich nicht nur für eine Entität sondern für n-Entitäten geladen, da wir es hier mit einem Aggregation Binding zu tun haben.
Ein einfaches Beispiel wäre wieder SD-Auftrag -> ToDebitor. Die fachliche Anforderung entspräche beispielsweise zu einem SD-Auftrag den Namen des Debitoren mit anzuzeigen. Bei 10.000 Debitoren im ERP und 20 angeforderten SD-Aufträgen würde bei einem einzigen Backend-Call 20 Mal innerhalb dieser 10.000 Debitoren nach dem richtigen Debitor gesucht werden. Dass das nicht performant sein kann, kann sich, glaube ich, jeder von euch recht einfach vorstellen.
Eine bessere Lösung wäre dagegen auf die Debitoren-Daten in der Übersichtsliste zu verzichten und diese erst auf der Detailseite anzuzeigen. Eine weitere Lösung, sofern die Anzeige in der Übersichtsliste gewünscht ist, kann z.B. sein einen Debitor per Popover on demand nachzuladen.
Beide Varianten haben gemeinsam, dass jeweils nur ein Debitor zur Zeit ausgelesen wird. Dadurch wird an dieser Stelle das Laden etwas länger dauern, was aber ein Klacks ist hinsichtlich der Zeitersparnis beim Laden der Übersichtsliste.

Kniff 4 – Keine unnötigen Calls ans Backend

Kniff 4 ist ein Phänomen, dass an unterschiedlichsten Stellen in der Anwendung auftreten kann. Als allgemeine Richtlinie lässt sich sagen, dass man als Entwickler ein Auge auf den Network Trace haben sollte, um unnötige Backend-Calls zu vermeiden.
Ein relativ anschauliches Beispiel kann die Verwendung des liveChange- anstatt des change-Event eines Input-Controls sein, bei dem ein nachgelagerter, asynchroner Call geschieht (bspw. bei einem Suchfeld). Das liveChange-Event (und somit der Backend-Call) wird nach der Eingabe eines jeden Buchstaben gefeuert, wohingegen das change-Event nur nach Bestätigung der kompletten Eingabe erfolgt (Beim Verlassen des Controls, per Suchbutton oder Enter, je nach Implementierung)

Object-View
<input livechange="onSearch">
vs.
<input change="onSearch">
Object-Controller
onSearch: function(oEvent) {
	var oBinding = this.getView().byId(„table“).getBinding(„items“);
	oBinding.filter(new sap.ui.model.Filter(
            {path: "Name", operator: "EQ", value1: oEvent.getSource().getValue()}

Dieses Beispiel zeigt das klassische Filtern einer Tabelle. Die Methode onSearch macht durch den Filter-Aufruf einen Call ans Backend. Obwohl die beiden Varianten relativ gleich aussehen unterscheiden sie sich massiv im Verhalten und somit auch in der Performance.
Ein anderes Beispiel kann die initiale Filterung einer Liste nach dem Laden der Anwendung sein. Hier wird durch das XML-Binding bereits ein Call ans Backend gemacht. Da diese Liste aber eigentlich vorgefiltert sein soll muss ich direkt danach eine Filterung auf diese Liste machen. Zack, da war er wieder, der zweite Backend Call.
Um diesen Backend-Call zu umgehen könnte man die Filterung direkt im XML mit machen (siehe dazu einen anderen Blog-Beitrag von mir) oder aber das Binding nachgelagert im Controller statt im View durchführen.

Kniff 5 – Nutzung von Paging bei Aggregation Bindings

Schon wieder Aggregation Bindings. Warum eigentlich? Kurze Antwort: Weil es an dieser Stelle einfach so wahnsinnig viel ausmacht, wenn Performance eingespart werden kann. Diese Einsparung wird nämlich sofort multipliziert, weshalb sich n-fach darüber freuen lässt.
Kniff 5 besagt so viel wie: Wenn ein ODataModel auf eine Aggregation trifft und gebunden werden soll, dann sollte immer die Paging Funktionalität des Controls genutzt werden.
Einfache Beispiele sind das Table- und das List-Control, welche beide ein Paging anbieten. (Als kleine Hintergrundinformation: Paging setzt die Parameter $top und $skip bei den Backend-Calls. Diese müssen im Backend richtig verarbeitet werden, damit ein Paging den gewünschten Effekt hat)
Sofern ein ODataModel auf die aggregation eines Controls gebunden werden soll, dass kein Paging anbietet, so sollte man sich das mindestens drei Mal überlegen. Gegebenenfalls gibt es ja ein anderes Control, mit dem sich die eigenen Anforderungen umsetzen lassen? In der Vergangenheit habe ich bei meinen Implementierungen für das Filtern von Listen vom ViewSettingsDialog Abstand genommen und nur noch den FacetFilter-Dialog verwendet. Der einfache Grund dafür war, dass der ViewSettingsDialog kein Paging anbietet, der FacetFilter dagegen schon.

...
<Table ... growing="true" growingthreshold="15"></Table>
...

Im Table-Control entscheiden die beiden Eigenschaften growing (Paging ja/nein) und growingThreshold (Anzahl der max. Einträge pro Call/Page) über das Paging.

Fazit

Das soll es für den ersten Teil gewesen sein. Kommentiert und diskutiert gerne hier die gezeigten Möglichkeiten. Sind eure Fiori Anwendungen auch zu langsam? Habt ihr die hier vorgestellten Kniffe schon mal angewendet oder wurdet ihr durch diesen Beitrag ermuntert das zu tun?
Guckt auf jeden Fall wieder vorbei, spätestens beim zweiten Teil der Serie. Dort gibt es weitere fünf Kniffe um deine Fiori Anwendung zu beschleunigen.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

SAP CoPilot – Alexa für SAP?

SAP CoPilot, die unbekannte Intelligenz aus der fernen Zukunft?

Wer 2018 den DSAG Jahreskongress, die SAP TechEd oder auch einfach nur die SAP Community aufmerksam verfolgt hat, der kam nicht umhin zu bemerken, dass der SAP CoPilot einen zentralen Baustein der dritten Version von SAP’s User Interface Designparadigma Fiori darstellt.

Die Idee der SAP, ihren Kunden eine Art künstliche Intelligenz zur Seite zu stellen, die aus dem Verhalten des Anwenders und dem Gesamtkontext des Geschäftsprozesses seine Absichten ableitet, ist genauso futuristisch wie ambitioniert. Viele SAP Kunden reagieren beim Thema SAP CoPilot deshalb zunächst einmal mit Argwohn. Viele von ihnen haben erst vor kurzem S4-HANA eingeführt oder das Fiori Launchpad in ihren Unternehmen etabliert und wähnen sich Lichtjahre entfernt von Lösungen à la CoPilot und Themen wie AI oder Machine Learning. Die vergangenen Monate im Projekt bei einem unserer Leading-Edge-Kunden haben mich und mein Team jedoch eines besseren belehrt und bewiesen, dass die Bedenken weitgehend unbegründet sind und die Zweifler einem weit verbreiteten Trugschluss unterliegen.

Mit diesem Beitrag möchte ich meine Erfahrungen mit dem CoPilot in der Community teilen. Dabei werde ich auf seine grundsätzliche Funktionsweise, die für den Service benötigten Komponenten sowie deren Architektur eingehen. Ich erhoffe mir Berührungsängste abbauen und etwas Licht ins Dunkel bringen zu können. Darüber hinaus werde ich kurz skizzieren, wie sich SAP mittelfristig die Rolle des CoPilots in Fiori 3 vorstellt.

Architektur

Die folgende Grafik soll einen grundsätzlichen Überblick über alle beteiligten Komponente und deren Interaktion miteinander skizzieren:

Der SAP Web Dispatcher und der SAP Cloud Connector spielen bei der Integration des CoPilot Services eine Schlüsselrolle. Sie leiten die OData-Calls des Front-End-Servers zur SAP Cloud Platform und umgekehrt um. Auf dem Front-End-Server muss ein Fiori 2.0 Launchpad konfiguriert sein. Außerdem sollte ein Remote SAML 2.0 Identity Provider in der Systemlandschaft integriert sein.

S4-HANA

Um es gleich vorweg zu nehmen: SAP CoPilot läuft ausschließlich auf S4-HANA Systemen. Da SAP CoPilot ein OnDemand-Service der SAP Cloud Platform ist, herrscht bei einigen Kunden der Glaube vor, sie müssten auch all ihre Daten in der Cloud halten. Diesen Mythos würde ich gerne ein für alle Mal begraben! Ob es sich bei Ihrem S4-HANA System um ein onPremise- oder um ein S4-HANA Cloud System handelt ist nicht von entscheidender Bedeutung.

Enterprise Search

SAP CoPilot ist ein OnDemand-Service der SAP Cloud Platform und bedarf deshalb grundsätzlich keiner individuellen Programmierung. Der Service basiert auf dem etwas älteren Konzept der indizierten Volltextsuche mit SAP Enterprise Search. Deshalb besteht der erste Schritt zur Einführung des CoPilots auch stets aus der Initialisierung der Enterprise Search sowie der Indizierung der Geschäftsobjekte, auf die der CoPilot später Zugriff haben soll. Für die Initialisierung der Enterprise Search gibt es glücklicherweise Standard-Task-Listen. Für die regelmäßige Indizierung der Datenbank existiert ein Cockpit, das den Administratoren ermöglicht jedes sogenannte Suchmodell individuell zu verwalten.

Fiori 2 vs. Fiori 3

Im aktuellen Fiori 2.0 Launchpad verfolgt SAP einen Floating-Window UI-Ansatz. Das bedeutet, dass der CoPilot als gesondertes Tool in einem separaten Fenster über der normalen Fiori-Oberfläche liegt. Der CoPilot fungiert sowohl als digitaler Assistent, der via Sprache oder Chat bedient werden kann, als auch als eine Art WhatsApp im Enterprisekontext, mit dem ich mich mühelos über konkrete Geschäftsobjekte mit Kollegen austauschen kann.

Wie eingangs bereits erwähnt, hat SAP mit dem CoPilot mittelfristig weitaus mehr vor. Mit Fiori 3 übernimmt er eine zentrale Rolle in der User Experience. Der CoPilot wird sich als Ihr persönlicher digitaler Assistent durch sämtliche Systeme und Anwendungen arbeiten um Ihren digitalen Workspace mit den richtigen Daten zur richtigen Zeit zu füttern. Dabei soll die Bedienung des CoPilots möglichst natürlich und intuitiv sein, sodass in Zusammenarbeit mit SAP Inscribe, die Bedienung der gesamten Fiori-Oberfläche gänzlich intendbasiert abläuft.

Einen tollen Ein- und Ausblick in die Pläne und die Zukunftsvision der SAP User Experience bieten die folgenden zwei Videos:

LT144 – SAP Fiori 3, 2018 Barcelona | SAP TechEd Online

CGE203 – SAP CoPilot: An Overview, 2018 Las Vegas | SAP TechEd Online

Ich hoffe dieser Blog hilft Ihnen bei dem Einstieg ins Thema CoPilot. Wenn Sie weitere Informationen benötigen, zögern Sie nicht einen Kommentar zu hinterlassen oder Acando direkt zu kontaktieren.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Digitale Unterschrift

Digitale Unterschrift als Control in SAPUI5

Ausgangssituation

In Zeiten der Digitalisierung und dem papierlosen Büro gehören digitale Unterschriften heutzutage zum modernen Unternehmensalltag. Verträge und Dokumente werden bereits an vielen Stellen nicht mehr auf Papier, sondern auf Tablets oder Handys unterzeichnet und direkt digital abgespeichert. So umgeht man unnötig analoge Papierarbeiten und man kann sich um die wichtigen Dinge im Alltag kümmern.

Wieso also nicht Verträge in modernen Fiori-Oberflächen unterzeichnen? SAP bietet standardmäßig diverse, schick aussehende UI5 Controls an, mit denen man Tabellen, Graphen oder Diagramme darstellen kann, also auch ein Control für digitale Unterschriften?
Derzeit leider nein. Also warum nicht selber ran und mit modernen HTML5 und JavaScript-Optionen ein eigenes Control bauen, das wir jedes mal einsetzen können, wenn wir eine digitale Signatur in unserer App benötigen.

(Wer sich das komplette Coding als Control kopieren möchte, kann auch gerne einfach an das Ende des Blogartikels springen und den Code kopieren.)

Das Ziel

Das Endergbnis soll in etwa wie im Folgenden aussehen. Wir möchten ein eigenständiges Control, mit folgenden Eigenschaften:

  • Attribute/Properties, die man in der XML setzen und verändern kann
  • Responsible, sowohl auf Desktop, als auch auf mobilen Endgeräten anwendbar
  • Signatur als Bild speichern können
  • Signaturbereich wieder löschen können

Eigene Control erstellen – der Grundbaustein, das Fundament

Wie man bestimmte Controls erweitert und auf bestehenden Controls aufbaut, wurde bereits in einem vorherigem Blog beschrieben.

In diesem Blog erstellen wir unser eigenes Control. Zunächst das Grundgerüst. Wir habe uns ein neues Projekt mittels „SAP UI5 Application“-Template erstellt, sodass das Grundgerüst vorhanden ist und wir eine leere App besitzen, in der wir die Digitale Signatur einfügen. In dieses Grundgerüst erweitern wir die Ordnerstruktur unter „webapp“ um den Ordner „custom“ und den Unterordner „controls“. Hier erstellen wir uns eine neue JavaScript-Datei, die den Namen unseres Controls hat, in meinem Fall „DigitalSignature.js“.

 

In dieses neue Control arbeiten wir nun das Coding ein, das letztlich unsere Signatur zeichnet und die Funktionalitäten bereitstellt.

Das Grundgerüst unseres individuellen Controls sieht wie folgt aus und ist letztendlich „einfach nur“ eine Erweiterung eines allgemeinem SAPUI5 Controls. Wir erweitern das Control um unsere eigene Anpassungen, die in dem Ordner „custom/controls/“ in der Datei „DigitalSignature“ wiederfinden zu sind.

sap.ui.define(
	["sap/ui/core/Control"],
	function (Control) {
		return Control.extend("custom.controls.DigitalSignature", {
			metadata: {
				properties: {},
				aggregations: {}
			},
			renderer: function (oRm, oControl) {
				},
			onAfterRendering: function (oEvent) {
				//if I need to do any post render actions, it will happen here
				if (sap.ui.core.Control.prototype.onAfterRendering) {
					sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first

				}
			}
		});
	});

Nun können wir unsere digitale Signatur bereits mit nur kleinen Anpassungen in unsere View einpflegen:

<mvc:View controllerName="de.acando.blog.DigitalSignatureBlog.controller.MainView" xmlns:html="http://www.w3.org/1999/xhtml"
	xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:customControl="de.acando.blog.DigitalSignatureBlog.custom.controls">
	<App id="idAppControl">
		<pages>
			<Page title="{i18n>title}">
				<content>
					<customControl:DigitalSignature id="digitalSignatureId"/>
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

 

Wenn man die App nun startet erkennt man jedoch relativ schnell, dass der Inhalt noch sehr leer ist. Dies liegt natürlich daran, dass wir in der Methode „renderer“ unseres eigenen Controls noch keine Inhalte zeichnen.

Aus dem Nichts entsteht eine Box – Die Wände hochziehen

Die digitale Signatur erstellen wir mit dem HTML5 Element Canvas. Mit Canvas lassen sich Boxen, Linien, Kreise und Rechtecke in den verschiedensten Formen und Farben zeichnen und ist daher sehr gut geeignet für unsere Anwendung.

Zudem müssen wir uns Gedanken darüber machen, welche Eigenschaften des Controls der User letztlich verändern können soll. Wie wäre es mit den folgenden Eigenschaften:

  • Höhe des Bereichs für die Unterschrift – width
  • Breite des Bereichs für die Unterschrift – height
  • Füllfarbe des Bereichs für die Unterschrift – fillColor
  • Farbe der Unterschrift – signatureColor
  • Breite der Linie der Unterschrift – lineWidth
  • Form der Linie der Unterschrift – lineCap
  • Farbe des Rahmen – borderColor
  • Breite des Rahmen – borderSize
  • Stil/Form des Rahmen – borderStyle

Diese Eigenschaften können wir zu den Properties der Metadaten hinzufügen. Das Hinzufügen der Attribute zu den Properties hat in erster Linie zwei positive Nebeneffekte.

  1. Durch das Hinzufügen dieser Attribute zu den Properties werden automatisch die get- und set-Methoden der jeweiligen Attribute erstellt, ohne dass wir die Methoden codieren müssen.
  2. In der XML-View können wir die Attribute direkt dem Control zuweisen, ohne dass wir Attribute im Controller setzen müssen
metadata: {
	properties: {
		width: {
			type: "sap.ui.core.CSSSize",
			defaultValue: "auto"
		},
		height: {
			type: "sap.ui.core.CSSSize",
			defaultValue: "auto"
		},
		borderColor: {
			type: "sap.ui.core.CSSColor",
			defaultValue: "#000000"
		},
		borderSize: {
			type: "sap.ui.core.CSSSize",
			defaultValue: "1px"
		},
		borderStyle: {
			type: "string",
			defaultValue: "none" //none, hidden, dotted, dashed, solid, double, groove, ridge, inset, outset, initial, inherit
		},
		fillColor: {
			type: "sap.ui.core.CSSColor",
			defaultValue: "#FFFFFF"
		},
		signatureColor: {
			type: "sap.ui.core.CSSColor",
			defaultValue: "#000000"
		},
		lineWidth: {
			type: "float",
			defaultValue: 1.5
		},
		lineCap: {
			type: "string",
			defaultValue: "round" //round, butt, square
		}
	},
	aggregations: {}
},

Nun zur renderer-Funktion. Hier „zeichnen“ wir nun das Canvas-Element, also die Fläche, in der wir letztlich unsere Unterschrift zeichnen möchten.
In der dritten Zeile fügen wir die Attribute ein, die unser Control durch die xml-View mitbekommt, bspw. die ID des Controls.

renderer: function (oRm, oControl) {
	oRm.write("<canvas class='signature-pad' ");
	oRm.writeControlData(oControl);
	oRm.addStyle("width", oControl.getWidth());
	oRm.addStyle("height", oControl.getHeight());
	oRm.addStyle("border", oControl.getBorderSize() + " " + oControl.getBorderStyle() + " " + oControl.getBorderColor());
	oRm.writeStyles();
	oRm.write("/>");
},

Ein erstes Ergebnis? Wenn wir die App starten sehen wir leider noch nichts. Das bedeutet jedoch nicht, dass wir einen Fehler verursacht haben. Ein Blick mit den Browser Entwicklungstools zeigt, dass das Feld vorhanden ist.

Im Endeffekt haben wir unserem Bereich lediglich noch keine Farben gegeben. Dies machen wir nun in unserer onAfterRendering-Methode, da wir auf unser HTML5-Element zugreifen wollen, dass hierfür schon „gerendert“ sein muss. Wir lagern die Methode aus, um diese wieder benutzbar machen zu können und um unseren Code sauber zu halten.

onAfterRendering: function (oEvent) {
	//if I need to do any post render actions, it will happen here
	if (sap.ui.core.Control.prototype.onAfterRendering) {
		sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
		this._drawSignatureArea(this);
	}
},

_drawSignatureArea: function (oControl) {
	var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
	var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
        canvas.width = canvas.clientWidth;
        canvas.height = canvas.clientHeight;
	context.fillStyle = oControl.getFillColor(); //Setting the FillColor/FillStyle
	context.strokeStyle = oControl.getSignatureColor(); //Setting the SignaturColor/StrokeStyle
	context.lineWidth = oControl.getLineWidth(); //Setting the SignatureLineWidth
	context.lineCap = oControl.getLineCap(); //Setting the LineCap of the Signature
	context.fillRect(0, 0, canvas.width, canvas.height);
},

 

Siehe da, wir haben ein erstes Ergebnis. Wir sehen nun unsere Fläche und können diese mit verschiedensten Attributen in der XML-View Einstellungen ändern. Im Folgenden zwei Beispiele:

  • Standard
    <mvc:View controllerName="de.acando.blog.DigitalSignatureBlog.controller.MainView" xmlns:html="http://www.w3.org/1999/xhtml"
    	xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:customControl="de.acando.blog.DigitalSignatureBlog.custom.controls">
    	<App id="idAppControl">
    		<pages>
    			<Page title="{i18n>title}">
    				<content>
    					<customControl:DigitalSignature id="digitalSignatureId"/>
    				</content>
    			</Page>
    		</pages>
    	</App>
    </mvc:View>
    

  • Orange Hintergrundfläche, dicker lilaner gepunkteter Rahmen und Weite auf 7 0%
    <mvc:View controllerName="de.acando.blog.DigitalSignatureBlog.controller.MainView" xmlns:html="http://www.w3.org/1999/xhtml"
    	xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:customControl="de.acando.blog.DigitalSignatureBlog.custom.controls">
    	<App id="idAppControl">
    		<pages>
    			<Page title="{i18n>title}">
    				<content>
    					<customControl:DigitalSignature id="digitalSignatureId" 
    					width="70%" borderColor="#eb1af2" borderSize="4px" borderStyle="dotted"
    					fillColor="#ffb630"/>
    				</content>
    			</Page>
    		</pages>
    	</App>
    </mvc:View>
    

 

In die Box die Signatur – Das Dach setzen

Wir haben die Fläche und das Grundgerüst, jetzt fehlt noch das Eigentliche, die Signatur.
Hierfür implementieren wir klassische HTML5-EventListener, mit denen wir darauf „warten“, dass entweder die Maus oder ein Finger unsere Signaturfläche berührt und darauf zeichnen möchte.

Hierfür implementieren wir uns eine Methode, die das Zeichnen der Signatur möglich macht.

_makeAreaDrawable: function (oControl) {
	var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
	var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
	var pixels = [];
	var xyLast = {};
	var xyAddLast = {};
	var calculate = false;

	function getCoords(oEvent) {
		var x, y;
		if (oEvent.changedTouches && oEvent.changedTouches[0]) {
			var offsety = canvas.offsetTop || 0;
			var offsetx = canvas.offsetLeft || 0;
			x = oEvent.changedTouches[0].pageX - offsetx;
			y = oEvent.changedTouches[0].pageY - offsety;
		} else if (oEvent.layerX || oEvent.layerX === 0) {
			x = oEvent.layerX;
			y = oEvent.layerY;
		} else if (oEvent.offsetX || oEvent.offsetX === 0) {
			x = oEvent.offsetX;
			y = oEvent.offsetY;
		}
		return {
			x: x,
			y: y
		};
	}
	/**
	 * Eventhandler for when the mouse moves on the specific area without pushing
	 * */
	function onMouseMove(oEvent) {
		oEvent.preventDefault();
		oEvent.stopPropagation();
		var xy = getCoords(oEvent);
		var xyAdd = {
			x: (xyLast.x + xy.x) / 2,
			y: (xyLast.y + xy.y) / 2
		};
		if (calculate) {
			var xLast = (xyAddLast.x + xyLast.x + xyAdd.x) / 3;
			var yLast = (xyAddLast.y + xyLast.y + xyAdd.y) / 3;
			pixels.push(xLast, yLast);
		} else {
			calculate = true;
		}
		context.quadraticCurveTo(xyLast.x, xyLast.y, xyAdd.x, xyAdd.y);
		pixels.push(xyAdd.x, xyAdd.y);
		context.stroke();
		context.beginPath();
		context.moveTo(xyAdd.x, xyAdd.y);
		xyAddLast = xyAdd;
		xyLast = xy;
	}

	/**
	 * Eventhandler for when the mouse moves is pressed on the specific area
	 * */
	function onMouseDown(oEvent) {
		oEvent.preventDefault();
		oEvent.stopPropagation();
		canvas.addEventListener("mouseup", onMouseUp, false);
		canvas.addEventListener("mousemove", onMouseMove, false);
		canvas.addEventListener("touchend", onMouseUp, false);
		canvas.addEventListener("touchmove", onMouseMove, false);
		$("body").on("mouseup", onMouseUp, false);
		$("body").on("touchend", onMouseUp, false);
		var xy = getCoords(oEvent);
		context.beginPath();
		pixels.push("moveStart");
		context.moveTo(xy.x, xy.y);
		pixels.push(xy.x, xy.y);
		xyLast = xy;
	}

	/**
	 * removes the eventhandlers
	 * */
	function removeEventListeners() {
		canvas.removeEventListener("mousemove", onMouseMove, false);
		canvas.removeEventListener("mouseup", onMouseUp, false);
		canvas.removeEventListener("touchmove", onMouseMove, false);
		canvas.removeEventListener("touchend", onMouseUp, false);
		$("body").off("mouseup", onMouseUp, false);
		$("body").off("touchend", onMouseUp, false);
	}

	/**
	 * Eventhandler for when the mouse stops pushing on the specific area
	 * */
	function onMouseUp(oEvent) {
		removeEventListeners();
		context.stroke();
		pixels.push("e");
		calculate = false;
	}
	canvas.addEventListener("touchstart", onMouseDown, false);
	canvas.addEventListener("mousedown", onMouseDown, false);
}

Den Aufruf dieser Methode ergänzen wir in unserem onAfterRendering Eventhandler:

onAfterRendering: function (oEvent) {
		if (sap.ui.core.Control.prototype.onAfterRendering) {
			sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
			this._drawSignatureArea(this);
			this._makeAreaDrawable(this);
		}
	},

Das Ergebnis sieht wie folgt aus, wenn wir die App neu starten und in unseren Bereich mit der Maus zeichnen:

Voilà, unser Control lässt uns zeichnen.

Wer beim Control mit Prozentzahlen gearbeitet hat, um die Weite und die Höhe zu setzen, der hat sicherlich bemerkt, dass sich die Signatur seltsam beim Zeichnen verhält. Entweder ist die Linie zu dick oder zu dünn oder die Signatur ist versetzt. Dies liegt daran, dass das canvas-Element „zu früh“ rendert und nicht nachrendert, nachdem die komplette View gerendert wurde. In unserer onAfterRendering-Methode müssen wir das rerendern noch dem

onAfterRendering: function (oEvent) {
if (sap.ui.core.Control.prototype.onAfterRendering) {
		sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
		this._drawSignatureArea(this);
		this._makeAreaDrawable(this);
		var that = this; //make the control resizable and redraw when something changed
		sap.ui.core.ResizeHandler.register(this, function () {
			that._drawSignatureArea(that);
		});
	}
},
Die Signatur löschen oder als Bild bekommen/speichern – Das Haus wohnlich machen

Nun haben wir unsere Signatur. Wie schön wäre es, wenn unser Control standardmäßig eine Methode implementiert, mit der man die Signatur löschen, oder als Bild bekommen kann.
Hierfür stellen wir nun öffentliche Methoden im Control bereit, die die Funktionalitäten bieten:

Zum Löschen:

clearArea: function () {
	this._drawSignatureArea(this);
},

Um die Signatur als Bild zu bekommen (einmal als JPEG und einmal als PNG):

getSignatureAsJpeg: function () {
	return this._getCanvasAsPicture("image/jpeg");
},

getSignatureAsPng: function () {
	return this._getCanvasAsPicture("image/png");
},

_getCanvasAsPicture: function (sMimetype) {
	var canvas = $("#" + this.getId())[0];
	var image = canvas.toDataURL(sMimetype);
	return image;
},
Das komplette Coding – Einzugsbereit
sap.ui.define(
	["sap/ui/core/Control"],
	function (Control) {
		return Control.extend("custom.controls.DigitalSignature", {
			metadata: {
				properties: {
					width: {
						type: "sap.ui.core.CSSSize",
						defaultValue: "auto"
					},
					height: {
						type: "sap.ui.core.CSSSize",
						defaultValue: "auto"
					},
					borderColor: {
						type: "sap.ui.core.CSSColor",
						defaultValue: "#000000"
					},
					borderSize: {
						type: "sap.ui.core.CSSSize",
						defaultValue: "1px"
					},
					borderStyle: {
						type: "string",
						defaultValue: "none" //none, hidden, dotted, dashed, solid, double, groove, ridge, inset, outset, initial, inherit
					},
					fillColor: {
						type: "sap.ui.core.CSSColor",
						defaultValue: "#FFFFFF"
					},
					signatureColor: {
						type: "sap.ui.core.CSSColor",
						defaultValue: "#000000"
					},
					lineWidth: {
						type: "float",
						defaultValue: 1.5
					},
					lineCap: {
						type: "string",
						defaultValue: "round" //round, butt, square
					}
				},
				aggregations: {}
			},
                        renderer: function (oRm, oControl) {
	                  oRm.write("<canvas class='signature-pad' ");
	                  oRm.writeControlData(oControl);
	                  oRm.addStyle("width", oControl.getWidth());
	                  oRm.addStyle("height", oControl.getHeight());
	                  oRm.addStyle("border", oControl.getBorderSize() + " " + oControl.getBorderStyle() + " " + oControl.getBorderColor());
	                  oRm.writeStyles();
	                  oRm.write("/>");
                        },

			onAfterRendering: function (oEvent) {
				//if I need to do any post render actions, it will happen here
				if (sap.ui.core.Control.prototype.onAfterRendering) {
					sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
					this._drawSignatureArea(this);
					this._makeAreaDrawable(this);
					var that = this;										//make the control resizable and redraw when something changed
					sap.ui.core.ResizeHandler.register(this, function () {
						that._drawSignatureArea(that);
					});
				}
			},
			clearArea: function () {
				this._drawSignatureArea(this);
			},

			getSignatureAsJpeg: function () {
				return this._getCanvasAsPicture("image/jpeg");
			},

			getSignatureAsPng: function () {
				return this._getCanvasAsPicture("image/png");
			},

			_getCanvasAsPicture: function (sMimetype) {
				var canvas = $("#" + this.getId())[0];
				var image = canvas.toDataURL(sMimetype);
				return image;
			},

			_drawSignatureArea: function (oControl) {
				var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
				var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
				canvas.width = canvas.clientWidth;
				canvas.height = canvas.clientHeight;
				context.fillStyle = oControl.getFillColor(); //Setting the FillColor/FillStyle
				context.strokeStyle = oControl.getSignatureColor(); //Setting the SignaturColor/StrokeStyle
				context.lineWidth = oControl.getLineWidth(); //Setting the SignatureLineWidth
				context.lineCap = oControl.getLineCap(); //Setting the LineCap of the Signature
				context.fillRect(0, 0, canvas.width, canvas.height);
			},

			_makeAreaDrawable: function (oControl) {
				var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
				var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
				var pixels = [];
				var xyLast = {};
				var xyAddLast = {};
				var calculate = false;

				function getCoords(oEvent) {
					var x, y;
					if (oEvent.changedTouches && oEvent.changedTouches[0]) {
						var offsety = canvas.offsetTop || 0;
						var offsetx = canvas.offsetLeft || 0;
						x = oEvent.changedTouches[0].pageX - offsetx;
						y = oEvent.changedTouches[0].pageY - offsety;
					} else if (oEvent.layerX || oEvent.layerX === 0) {
						x = oEvent.layerX;
						y = oEvent.layerY;
					} else if (oEvent.offsetX || oEvent.offsetX === 0) {
						x = oEvent.offsetX;
						y = oEvent.offsetY;
					}
					return {
						x: x,
						y: y
					};
				}
				/**
				 * Eventhandler for when the mouse moves on the specific area without pushing
				 * */
				function onMouseMove(oEvent) {
					oEvent.preventDefault();
					oEvent.stopPropagation();
					var xy = getCoords(oEvent);
					xyAdd = xy;
					var xyAdd = {
						x: (xyLast.x + xy.x) / 2,
						y: (xyLast.y + xy.y) / 2
					};
					if (calculate) {
						var xLast = (xyAddLast.x + xyLast.x + xyAdd.x) / 3;
						var yLast = (xyAddLast.y + xyLast.y + xyAdd.y) / 3;
						pixels.push(xLast, yLast);
					} else {
						calculate = true;
					}
					context.quadraticCurveTo(xyLast.x, xyLast.y, xyAdd.x, xyAdd.y);
					pixels.push(xyAdd.x, xyAdd.y);
					context.stroke();
					context.beginPath();
					context.moveTo(xyAdd.x, xyAdd.y);
					xyAddLast = xyAdd;
					xyLast = xy;
				}

				/**
				 * Eventhandler for when the mouse moves is pressed on the specific area
				 * */
				function onMouseDown(oEvent) {
					oEvent.preventDefault();
					oEvent.stopPropagation();
					canvas.addEventListener("mouseup", onMouseUp, false);
					canvas.addEventListener("mousemove", onMouseMove, false);
					canvas.addEventListener("touchend", onMouseUp, false);
					canvas.addEventListener("touchmove", onMouseMove, false);
					$("body").on("mouseup", onMouseUp, false);
					$("body").on("touchend", onMouseUp, false);
					var xy = getCoords(oEvent);
					context.beginPath();
					pixels.push("moveStart");
					context.moveTo(xy.x, xy.y);
					pixels.push(xy.x, xy.y);
					xyLast = xy;
				}

				/**
				 * removes the eventhandlers
				 * */
				function removeEventListeners() {
					canvas.removeEventListener("mousemove", onMouseMove, false);
					canvas.removeEventListener("mouseup", onMouseUp, false);
					canvas.removeEventListener("touchmove", onMouseMove, false);
					canvas.removeEventListener("touchend", onMouseUp, false);
					$("body").off("mouseup", onMouseUp, false);
					$("body").off("touchend", onMouseUp, false);
				}

				/**
				 * Eventhandler for when the mouse stops pushing on the specific area
				 * */
				function onMouseUp(oEvent) {
					removeEventListeners();
					context.stroke();
					pixels.push("e");
					calculate = false;
				}
				canvas.addEventListener("touchstart", onMouseDown, false);
				canvas.addEventListener("mousedown", onMouseDown, false);
			}
		});
	});
Zusammenfassung – Was haben wir gelernt?

Wir haben unser eigenes Control erstellt, das wir nun jederzeit in andere Views einsetzen können und das auch direkt Eigenschaften und Methoden bereitstellt. Die Eigenschaften und Methoden lassen sich nach belieben erweitern und an spezifische Bedürfnisse anpassen.

Bis zum nächsten Blog! Möchtet ihr noch mehr über eigene Controls erfahren? Dieser Blog könnte hilfreich sein!

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

XML Fragment

Mit XML-Fragments die Übersichtlichkeit deiner Anwendung verbessern

Nach kurzer Sommerpause geht es weiter mit der Expedition durch die SAPUI5 Welt. Heute beschäftigen wir uns mit dem Einsatz von XML-Fragments in den eigenen Anwendungen.

Fangen wir an mit einer kurzen Definition:
Fragments sind ausgelagerte Code-Schnipsel, die in eigenen Dateien gekapselt werden. Sie beinhalten ausschließlich UI-Elemente, besitzen also keinen eigenen Controller. Die Auslagerung dient in der Regel einem der folgenden Zwecke:

  • Der Code-Schnipsel soll an unterschiedlichen Stellen der Anwendung zum Einsatz aber nicht mehrmals gecodet werden(Stichwort Codeverdopplung)
  • Die Auslagerung dient der Übersichtlichkeit eines Views
  • Das UI-Schnipsel ist nicht direkt in einem View verankert (z.B. ein Dialog)

XML-Fragments können beliebig komplex sein. Meiner Meinung nach macht es aber Sinn diese einfach zu halten und im Zweifel lieber ein weiteres Fragment anzulegen als ein einziges zu überladen. In meinen Anwendungen beinhaltet ein Fragment oftmals nur ein einziges Control.

Zusätzliche Info: Es gibt weitere Arten von Fragments (HTML, JS, etc.), auf die ich hier aber nicht eingehen werde. Der Vollständigkeit halber seien sie aber hier kurz erwähnt.

Wie sieht ein Fragment aus?

Ein Fragment hat ein umschließendes Tag „FragmentDefinition“. Anders als bei einem View wird kein Controller-Name mitgegeben, da das Fragment, wie oben schon erwähnt, keinen eigenen Controller besitzt.

<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
  …
</core:FragmentDefinition>

Innerhalb dieses Tags können alle bekannten SAPUI5 Controls verwendet werden. Um zu zeigen, wie einfach ein Fragment gestaltet werden kann gucken wir uns folgendes an:

<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
  <Button text="Drück mich" press="onPressFragmentButton" />
</core:FragmentDefinition>

Es besteht aus einem einzigen Button. An der Stelle hätte wir jetzt weitere Controls einfügen oder komplexe Strukturen aufbauen können. Für unsere Zwecke ist der Button aber vollkommen ausreichend. Die Datei speichern wir mit dem Suffix .fragment.xml, also beispielsweise MyDrueckMichButton.fragment.xml.

An dieser Stelle eine kurze Bemerkung zum Speicherort:
Je mehr wir mit Fragments arbeiten, desto unübersichtlicher wird unsere Ordnerstruktur. Es empfiehlt sich, sich vorher Gedanken zu machen, wie Fragments verortet werden sollen. Das kann ein einfacher Ordner fragments sein oder, je nach Bedarf, auch mit größerem Detaillierungsgrad. Meiner Erfahrung nach werdet ihr dankbar darüber sein, wenn ihr diese Überlegung am Anfang macht und nicht erst, wenn ihr die Übersicht in eurer Ordnerstruktur verloren habt. Bei letzterem spreche ich aus eigener Erfahrung.

So, jetzt haben wir einen Teil unseres Codes gekapselt. Der erste Schritt ist getan. Als nächstes gucken wir uns an, wie wir unser Fragment in unsere Anwendung einbinden können. Dafür stehen uns mehrere Möglichkeiten zur Verfügung.

01 – Einbindung direkt im XML

Wir können unser Fragment direkt in einem XML-View integrieren. Dadurch wird es zu dem Zeitpunkt gerendert wie auch der View gerendert wird. Außerdem wird das Fragment in den „Lifecycle“ unseres Views integriert. Dadurch kann es auf den Controller des Views zugreifen und auch alle verfügbaren Models des Views im Fragment zum Binding verwenden.
Die Syntax ist wie folgt:

…
<core:Fragment 
  fragmentName="myApp.fragments.MyDrueckMichButton" 
  type="XML" />
…

02 – Aufruf über einen Controller

Wir können unser Fragment auch zu jedem beliebigen Zeitpunkt im Controller instanziieren. Der klassische Anwendungsfall dafür ist die Öffnung und Anzeige eines Dialogs. Für dieses Beispiel sei folgendes Fragment gegeben:

<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
  <Dialog title ="Information">
    <content>
      <Label 
        text="Ich bin ein Dialog und wurde gerade geöffnet" />
    </content>
    <buttons>
      <Button 
        text="Abbrechen"
        press="onCloseDialog" />
    </buttons>
  </Dialog>
</core:FragmentDefinition>

Im Controller schreiben wir uns jetzt eine Methode, die diesen Dialog öffnet. In diesem Zuge achten wir auch gleich darauf, dass wir unseren Dialog wiederverwendbar machen. Wir möchten nämlich nicht bei jedem Öffnen des Dialogs einen neuen Dialog rendern sondern, sofern möglich, einen Dialog einmal erstellen und dann wiederverwenden.

…
onOpenDialog: function() {
  // Prüfung, ob unser Dialog schon erstellt wurde
  if (this._dialog != null) { 
    // Speicherung des Dialogs in einer globalen Variablen
    this._dialog = sap.ui.xmlfragment(
      "myApp.fragments.InfoDialog", 
      this
    );
    this.getView().addDependent(this._dialog);
  }
 
  this._dialog.open();
},

onCloseDialog: function() {
  this._dialog.close();
}
…

Auf unserem Dialog befindet sich ein Button mit einer Funktion „onCloseDialog“. Diese wird im Controller des Views implementiert. Damit der Dialog diese Funktion verwenden kann muss beim Erstellen des Dialogs darauf geachtet werden, dass der Controller als Parameter mitgegeben wird. (In unserem Beispiel der zweite Parameter „this“).
An dieser Stelle kann man viel Zeit und Nerven verlieren. Wenn also eine Funktion nicht aufgerufen wird obwohl sie es sollte empfehle ich das zuerst zu prüfen. Wieder spreche ich hier aus eigener Erfahrung.

03 – Einbindung über einen Extension Point

Die Einbindung über Extension Points ist die fortgeschrittenste Variante. Über Extension Points können Anwendungen, die auf der gleichen Anwendung basieren und nur wenig Unterscheidung hinsichtlich Erscheinung und Funktionalität haben, so erweitert werden, dass sie ein neues Gesicht und unterschiedliche Funktionalitäten bekommen.
In der Basis Anwendung wird ein Extension Point an der Stelle im View definiert, an der eine Erweiterung möglich sein soll.

…
<core:ExtensionPoint name="extension1" />
…

Dieser wird dann über ein Extension Project mit Inhalt gefüllt wird. Die Erweiterung im Extension Project wird standardmäßig in der manifest.json vorgenommen.

…
"sap.ui5": {
  "extends": {
    "extensions": {
      "sap.ui.viewExtensions": {
        "myApp.view.Main": {
          "extension1": {
            "className": "sap.ui.core.Fragment",
            "fragmentName": "myExtApp.view.MyDrueckMichButton",
            "type": "XML"
          }
        }
      }
    }
  }
}
…

Diese Methode ist, wie schon gesagt, eher etwas für fortgeschrittene Entwickler. Wenn man aber etwas mit dem Erweiterungsprinzip von SAP Fiori Anwendung vertraut ist, dann sollte man sich hier relativ schnell zurechtfinden.

Fazit

Die Nutzung von Fragments macht Sinn und ist ein absolutes Muss, je größer und komplexer die eigenen Anwendungen werden. Aber auch bei einfachen Anwendungen ist die Nutzung sehr zu empfehlen (Stichwort Dialog). Mit dem klassischen Erweiterungsprinzip in SAP Fiori Anwendungen kommt man dann schlussendlich sowieso nicht mehr umhin Fragments zu nutzen.
Ich hoffe, dass dieser Beitrag helfen kann, bestehende Hürden für die Nutzung von Fragments abzubauen. Sie bieten eine einfache Möglichkeit die Code-Qualität zu erhöhen und die eigenen Anwendung übersichtlicher und besser wartbar zu gestalten und sollten deshalb auch soft wie es Sinn macht wahrgenommen werden.

Bis dahin erstmal und kiek mol wedder in!

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Einführung in die SAP Cloud Platform (Webinar) – innovative Anwendungen entwickeln

Unser Kollege Rüdiger Lühr hat am 20. Juni 2018 ein Webinar mit dem Titel „SAP Cloud Platform – innovative Anwendung entwickeln“ gehalten. In dem Webinar gibt Rüdiger einen exzellenten Überblick darüber, wie die SCP in die eigene Infrastruktur integriert werden kann. Was schafft die SCP für Möglichkeiten? Und welche technischen Skills sind notwendig diese Möglichkeiten auszuschöpfen? All diese Fragen werden in diesem Webinar beantwortet.

Nachdem Ihr dieses Webinar angeschaut habt wird die SAP Cloud Platform für Euch deutlich greifbarer.
Sollten weiterhin Fragen offen sein könnt Ihr einfach die Kommentar-Funktion nutzen.

Viel Spaß beim Anschauen!

Webinar-Mitschnitt zum Nachschauen

Teaser

Mit der SAP Cloud Platform (SCP) werden die neuen spannenden Möglichkeiten, die sich aus der digitalen Transformation eröffnen. Big Data, Internet of Things und Digital Workplace, schnell und einfach umsetzbar.

Die SCP ermöglicht den Betrieb und die Entwicklung innovativer Anwendungen in der Cloud mit geringem finanziellen Aufwand. Die Integration vorhandener On-Premises- und Cloud-Anwendungen befreit Daten und Geschäftsprozesse aus ihren Silos. Vorhandene Cloud-Anwendungen können mit der SCP um einzelne Funktionalitäten erweitert werden. Die User Experience ist im Ergebnis auf jedem Endgerät hervorragend, unabhängig von den darunterliegenden technischen Details .

In diesem Webinar erhalten Sie einen Überblick zur SCP. Der Fokus liegt auf den SCP Services zur Erstellung eines Portals in der Cloud, in dem die Geschäftsvorfälle erfasst und im ERP On-Premises verarbeitet werden.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Testing in SAPUI5 Teil 3 – QUnit Funktionstests

Im zweiten Teil dieser Blogserie habe ich die Bedeutung von OPA5 Tests erläutert und erklärt, wie man diese implementiert und nutzt.
Dieser dritte Beitrag soll nun zeigen, wie mit QUnit Funktionstests geschrieben werden können.

Womit sollte ich beginnen?

Ein Vorteil der QUnit Tests ist, dass diese direkt im Browser ausgeführt werden können und keine zusätzlichen Tools benötigt werden. Dafür erzeugt man zuerst die sogenannte QUnit Test Page mit der Dateiendung .qunit.html (z. B. unitTests.qunit.html). Abgelegt werden sollte die Datei in dem Ordner /test/unit. Wird für die Erstellung der App ein Web IDE App Template genutzt, werden der Ordner und die QUnit Test Page automatisch erzeugt.

Zur besseren Übersicht sollte die Struktur des /test/unit Ordners sich an der Ordnerstruktur der App orientieren.

Die QUnit Test Page bietet einen Überblick über alle ausgeführten Tests. Hier kann man leicht erkennen, welche Tests erfolgreich waren, bzw. fehlgeschlagen sind. Dazu können einzelne Tests manuell wiederholt werden und ich habe die Möglichkeit die Testliste nach verschiedenen Kriterien zu filtern. Bei fehlgeschlagenen Tests wird zudem der erwartete Wert, das Ergebnis sowie die Differenz angezeigt. Zudem ist die betroffene Zeile im Coding angegeben. So lassen sich Fehlerquellen schnell ausmachen und leichter beheben.

Nutzt ihr kein Web IDE App Template, könnt ihr für die Erstellung der Testpage folgendes Template nutzen.

<html>
  <head>
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />    
    <base href="../../../../../../">
    <!--[if lte IE 9]><script type="text/javascript">
         (function() {
                var baseTag = document.getElementsByTagName('base')[0];
                baseTag.href = baseTag.href;
         })();
    </script><![endif]-->
    <script id="sap-ui-bootstrap" 
        type="text/javascript"
        src="resources/sap-ui-core.js"
        data-sap-ui-theme="sap_bluecrystal"
        data-sap-ui-noConflict="true">
    </script> 
    <link rel="stylesheet" href="resources/sap/ui/thirdparty/qunit.css" type="text/css" media="screen" />
    <script type="text/javascript" src="resources/sap/ui/thirdparty/qunit.js"></script>
    <script type="text/javascript" src="resources/sap/ui/qunit/qunit-junit.js"></script>
    <script type="text/javascript" src="resources/sap/ui/qunit/QUnitUtils.js"></script>
    <script language="javascript">
    /* Create and structure your QUnit tests here
    Documentation can be found at http://docs.jquery.com/QUnit
    Example:
      module("Module A");
      test("1. a basic test example", 2, function() {
        ok( true, "this test is fine" );
        var value = "hello1";
        equals( value, "hello1", "We expect value to be 'hello1'" );
      });
    */
    </script>
  </head>
  <body>
    <div id="qunit"></div>
    <!-- [TODO: add additional html content here] -->
  </body>
</html>

Eigene Tests schreiben

Für jeden zu testenden Controller wird auch eine .js Datei im Testordner erstellt, in der die zugehörigen Tests implementiert werden. Möchte ich zum Beispiel die Funktionen in meiner formatter.js testen, lege ich auch im Testordner eine Datei formatter.js an.

Um die einzelnen Tests zu gruppieren, werden sie in sogenannte Module unterteilt. Diese stellen jeweils eine Funktion des Controllers dar. Innerhalb der Module befindet sich die Testfunktion, welche nach dem Arrange-Act-Assert Muster aufgebaut ist. Dieses Muster wurde in Teil zwei dieser Blogserie in Zusammenhang mit den Objekten Given, Then, When erläutert.

Der Arrange-Teil beschreibt die Ausgangssituation für unser Testsystem. Der Act-Teil sollte bestenfalls nur aus dem Methodenaufruf der zu testenden Methode bestehen. Im Assert-Teil wird dann der erwartete Wert mit dem von der Funktion zurückgegebenen Wert verglichen und eine Erfolgsmeldung definiert. Darauf folgen dann die einzelnen Tests. Diese enthalten als Parameter jeweils eine kurze Beschreibung z. B. „Should round down a 3 digit number“ sowie den Aufruf der Testfunktion. Diese bekommt einen zu testenden Wert (1,223) sowie den erwarteten Output (1,22) übergeben.

Führt man nun die QUnit-Tests aus, ruft die Testfunktion z. B. die formatter Funktion auf. Anschließend wird der Rückgabewert mit dem erwarteten Wert verglichen.

Stimmen diese beiden Werte überein, ist der Test erfolgreich und erscheint grün. Gibt es hier Abweichungen ist der Test rot. Durch einen Klick auf die jeweilige Testzeile sind der erwartete Wert, das Ergebnis, die Abweichung und die entsprechende Stelle im Coding zu sehen.

Ich hoffe mit diesem Beitrag konnte ich euch bei der Erstellung von QUnit-Tests in SAPUI5 helfen.

Dies war der vorerst letzte Beitrag meiner Testing in SAPUI5 Serie. Meine nächsten Artikel werden also von anderen Themen handeln, ich hoffe ihr schaut dann wieder rein 😉

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Git in der SAP Web IDE – Versionskontrolle leicht gemacht – Teil 2

Quick recap

In meinem letzten Blogbeitrag zum Thema Git in der SAP Web IDE habt ihr den Unterschied zwischen zentraler und dezentraler Versionskontrolle kennengelernt und dabei folgendes über Git erfahren:

  • Bei Git handelt es sich um eine dezentrale Versionskontrolle
  • Ein Commit ist als neue Version eines Repositories zu verstehen
  • Ein Branch ist ein Zeiger auf einen Commit und somit auf eine bestimmte Version

Git in der SAP Web IDE – Let’s get ready to rumble!

Nachdem ich euch viel zu lange mit theoretischem Wissen gelangweilt habe ist nun der Zeitpunkt gekommen sich die Hände schmutzig zu machen und die grundlegenden Funktionen eines Git-Repositories kennen zulernen.

Saving –> Staging –> Committing

Änderungen im Code eines Projektes innerhalb der SAP Web IDE müssen selbstverständlich gespeichert werden. Entweder manuell, oder indem man Autosave unter
Preferences->Code Editor aktiviert.

Sobald eine Datei über gespeicherte Änderungen verfügt, kann man sie via Stage File Git zur Verfügung stellen. Die Gesamtheit der Dateien, die Git via Staging zur Verfügung gestellt werden nennt man einen Snapshot. Snapshots kann man sich als potenzielle, zukünftige Commits vorstellen.
Wenn ihr alle notwendigen Änderungen an eurem Projekt durchgeführt, gespeichert und via Stage File zu einem Teil des aktuellen Snapshots gemacht habt ist es an der Zeit für den Commit.
Durch den Commit-Befehl weist ihr Git dazu an, den aktuellen Snapshot zu einer neuen Version des ausgewählten Repositories zu machen.

Stashing

Der Stash eines GitRepositories adressiert ein gelegentlich auftretendes Problem, dass man am besten versteht, wenn es einem an einem Beispiel aus der Praxis erläutert wird:
Stellt euch vor ihr seid mitten in der Entwicklung eines neuen Features, wenn euch ein Ticket für einen sehr dringenden Bugfix erreicht. Ihr wollt euren Code und somit die Arbeit an dem neuen Feature selbstverständlich nicht einfach löschen, möchtet aber genauso ungerne eine neue Version erstellen, da das Feature noch längst nicht funktionstüchtig ist. In diesem Szenario ist der Stash die optimale Lösung, da er eine Möglichkeit bietet (lokal oder anderweitig) zu speichern ohne den aktuellen Stand des Repositories zu verändern. Das bedeutet, dass Git alle gespeicherten Änderungen, unabhängig davon ob sie innerhalb der Staging Area ausgewählt wurden oder nicht, in den Stash speichert und gleichzeitig den Code in deinem Web IDE Workspace auf den Stand des letzten Commits zurücksetzt. Ihr könntet den Bug nun entspannt bearbeiten, die neue stabile Version zu einem neuen Commit machen und sie selbstverständlich deployen. Anschließend holt ihr einfach via Apply Stash den Code eures Features aus eurem „virtuellen Versteck“ hervor und könnt  die Arbeit an ihm fortsetzen.

Reverting

Reverting in der SAP Web IDE via Git ist einfach, aber nichtsdestotrotz sehr mächtig. Jeder Commit kann via Revert rückgängig gemacht werden. Git erstellt bei jedem Revert einen neuen Commit, in dem der rückgängig gemachte Code enthalten ist. Das bedeutet, dass man nicht nur den letzten Commit rückgängig machen kann, sondern jeden beliebigen. Ihr seid also dazu in der Lage fehlerhafte oder ungeliebte Änderungen aus der Vergangenheit einfach rückgängig zu machen, ohne die darauf folgenden neueren Commits zu verlieren. Vorausgesetzt ihr habt euch an die Konvention gehalten, jedem neuen Feature ein eigenen Commit zu spendieren ;-).
Das war’s für Heute! Im dritten Teil dieser Serie widmen wir uns Push und Merge.

Ich hoffe Ihr schaut dann wieder rein. Bis bald!

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Testing in SAPUI5 Teil 2 – OPA5 Tests für Benutzerinteraktionen

Im ersten Teil dieser Blogserie haben wir uns einen Mock Server erstellt, mit dem wir unsere App auch ohne einen vollständigen Service testen können.
Dieser Beitrag soll nun zeigen, wie auf Basis dieser Mock Daten OPA5 Tests geschrieben werden können.

OPA ist die Abkürzung für One Page Acceptance. OPA5-Tests sind ein SAPUI5-Feature, das auf QUnit und JavaScript basiert. Getestet werden können unter anderem Benutzerinteraktionen, die Navigation innerhalb der App sowie das Data Binding.

Wie bereits im vorherigen Beitrag erwähnt, vereinfacht diese Herangehensweise das Schreiben von Tests. Da Mock Daten sich in der Regel nicht ändern und auch keine neuen Datensätze hinzukommen sollten, können in der Testimplementierung statische Bezeichnungen oder Zahlen verwendet werden. Dies hat zudem den Vorteil, dass Tests, die nach dem OPA5 Prinzip durchgeführt werden, ausschließlich Benutzerinteraktionen testen und keine Fehler aufgrund des Datenstamms provozieren.

Ein Besonderheit der OPA5-Tests ist, dass die Asynchronität von JavaScript bzw. SAPUI5 Anwendungen durch Polling „versteckt“ wird. Das bedeutet vereinfacht, es wird bei einem Aufruf nicht auf eine Antwort gewartet, sondern nach einem Status gefragt. Es gibt beispielsweise einen Test der überprüfen soll, ob alle Tabelleneinträge geladen worden sind. Es wird ein Polling Intervall gesetzt (z.B. 400ms) nach dem geprüft wird, ob Daten alle geladen worden sind. Dieser Vorgang wird solange wiederholt, bis sämtliche Bedingungen erfüllt sind.

Da OPA5-Tests auch Exceptions erkennen empfiehlt es sich, für jede View einen eigenen Test zu schreiben. Dadurch werden kritische Fehler direkt erfasst.

Für die Gliederung von OPA5-Tests werden sogenannte Journeys verwendet. Sie können genutzt werden, um die Tests zum Beispiel nach Use-Cases oder auch nach Views zu gruppieren.

Die Journeys enthalten jeweils einen oder mehrere Tests mit den Parameterobjekten Given, Then und When. Diese enthalten die arrangements, actions und assertions für den Test, welche wiederum in einem page-object gekapselt werden.

  • Given beschreibt den Ausgangszustand, beispielsweise App ist gestartet oder bestimmte Daten sind vorhanden.
  • When beschreibt Ereignisse, die durch Userinteraktionen eintreten können, wie zum Beispiel das Klicken eines Buttons oder die Eingabe von Text.
  • Then beschreibt die Veränderungen bzw den Zustand, die durch die in When beschriebenen Ereignisse erwartet werden.

Da das Aufbauen eines iFrames und der Start der App einige Sekunden dauern kann, sollte aus Performancegründen die App pro Journey nur ein mal gestartet und beendet werden. Dies sollte logischerweise innerhalb des ersten Tests bzw. das Beenden innerhalb des letzten Tests durchgeführt werden. Aus diesem Grund ist es auch empfehlenswert Tests in Journeys zu gruppieren, da bei eigenständigen Tests die App jedes mal gestartet und beendet werden müsste. Nachfolgend ein Beispiel für eine Journey.

sap.ui.require(
["sap/ui/test/opaQunit"],
function (opaTest) {
"use strict";
QUnit.module("Posts");
opaTest("Should see the table with all posts", 
function (Given, When, Then) {
     // Arrangements
     Given.iStartMyApp();
     //Actions
     When.onTheWorklistPage.iLookAtTheScreen();
     // Assertions
     Then.onTheWorklistPage.theTitleShouldDisplayTotalAmountOfItems();
});
opaTest("Should be able to load more items", 
function (Given, When, Then) {
     //Actions
     When.onTheWorklistPage.iPressOnMoreData();
     // Assertions
     Then.onTheWorklistPage.theTableShouldHaveAllEntries().
     and.iTeardownMyAppFrame();
});
}
);

Wer sich nun wundert, was es mit diesen ominösen onTheWorklistPage Objekt oder zum Beispiel der iPressOnMoreData() Methode auf sich hat, diese werden in den bereits erwähnten page-objects implementiert.
Eine Implementierung für die action iPressOnMoreData und die dazugehörige assertion würde beispielsweise folgendermaßen aussehen.

Opa5.createPageObjects({
onTheWorklistPage: {
baseClass: Common,
actions: {
     iPressOnMoreData: function () {
     	return this.waitFor({
  	     id: sTableId,
	     viewName: sViewName,
	     matchers: function (oTable) {
		return !!oTable.$("trigger").length;
	     },
	     success: function (oTable) {
		oTable.$("trigger").trigger("tap");
	     },
	     errorMessage: "The Table does not have a trigger"
	});
     }
},
assertions: {
     theTableShouldHaveAllEntries: function () {
	return this.waitFor({
	     id: sTableId,
	     viewName: sViewName,
	     matchers:  new AggregationLengthEquals({
		name: "items",
		length: 23
	     }),
	     success: function () {
		Opa5.assert.ok(true, "The table has 23 items");
	     },
	     errorMessage: "Table does not have all entries."
	});
     },

Hier ist zu sehen, dass aufgrund der Asynchronität jede action oder assertion mit einem waitFor Statement beginnt. Hier kommt das anfangs erwähnte Polling ins Spiel, welches die Bedingung überprüft und auf deren Erfüllung wartet. Der Test schlägt fehl, wenn nach einer vorgegebenen Zeit die Bedingung noch nicht erfüllt wurde.
Mit Hilfe von matchers genannten Objekten, die entweder selbst erzeugt werden können oder im Namensraum sap.ui.test.matchers zu finden sind, werden die Testbedingungen überprüft. Der matcher gibt einen booleschen Wert zurück. Je nach Rückgabewert wird entweder die success Funktion aufgerufen oder es wird eine Fehlermeldung ausgegeben.
Im vorliegenden Fall sucht der selbst erzeugte matcher nach einem DOM-Element trigger innerhalb der Tabelle. Wird dieses Element gefunden wird innerhalb der success Funktion mittels jQuery ein tap Event ausgeführt.
Danach wird die assertion ausgeführt. Assertions führen ausschließlich Überprüfungen durch, wobei es ausreichend ist, wenn nur eine Überprüfung erfolgreich ist. Zuerst steht hier wieder das waitFor Statement, gefolgt von einem OPA5 Standard matcher, welcher die Anzahl der Elemente in der Tabelle überprüfen kann.

Bei diesem Test zeigt sich ein bereits erwähnter Vorteil von Mock Daten. Die Anzahl der Elemente in der theTableShouldHaveAllEntries Funktion ist hart kodiert, wodurch Programmierarbeit gespart wird und die Methode weniger fehleranfällig macht. Das wäre bei der Nutzung eines echten Services nicht möglich, da hier die Möglichkeit besteht, dass sich Daten ändern und somit eventuell eine andere Anzahl vorhanden wäre, was den Test fehlschlagen lassen würde.

Ich hoffe dieser Beitrag dient euch als Orientierungshilfe in der OPA5- und SAPUI5-Welt.

In meinem nächsten Beitrag erkläre euch, was es mit QUnit-Tests auf sich hat! Ich hoffe ihr schaut dann wieder rein 😉

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Testing in SAPUI5 Teil 1 – Mock Server zum Testen von Fiori Apps

Wer kennt es nicht, man baut eine App und möchte sie testen, aber es sind keine Daten vorhanden. Das kann mehrere Gründe haben: der OData Service ist nicht erreichbar, es gibt (noch) keinen Zugriff auf den Service oder das Entwicklungssystem ist schlicht leer.

Das Zauberwort für die Lösung dieses Problems heißt Mock Server.

Das Prinzip des Mock Servers ist ganz einfach. Wenn aus der App das Backend aufgerufen wird, wird dieser Call vom Mock Server abgefangen. Dieser simuliert dann ein Backend System und zeigt uns dafür lokale Daten an. Diese Daten können entweder aus unserem bestehenden Service stammen oder von der Web IDE zufällig erzeugt werden. Will man sich ganz spezielle Daten anzeigen lassen, können diese natürlich auch manuell eingeben werden.

Erstellen des Mock Servers mit Hilfe von Web IDE Templates

Wird eine SAPUI5 App auf Basis der Web IDE Templates für Worklist oder MasterDetail erzeugt, ist bereits ein Ordner „localService“ mit den Dateien metadata.xml und mockserver.js vorhanden. Zudem wurde im Ordner „test“ eine Datei flpSandboxMockServer.html erzeugt. Diese ähnelt der index.html und dient dazu, die App im „Testmodus“, also mit den Mock Daten zu starten.

Nun fehlen nur noch die Daten. Diese lassen sich ganz einfach aus dem eigenen Service erstellen, indem wir den Service mit dem URL-Parameter ?$format=json aufrufen.

Anzeigen des Services im JSON-Format

Alternativ kann auch die Web IDE Zufallsdaten auf Basis der metadata.xml erzeugen.

Dazu klickt man mit der rechten Maustaste auf die metadata.xml im localService Ordner seiner App und im sich öffnenden Kontextmenü auf „Edit Mock Data“. Daraufhin öffnet sich ein Dialog mit allen in der metadata.xml beschriebenen Entitäten. Durch einen Klick auf „Generate Random Data“ lassen sich ganz einfach Daten für die ausgewählte Entität erzeugen.

Assistent zum Bearbeiten der Mock Daten

Außerdem haben wir hier die Möglichkeit, einzelne Zeilen manuell hinzuzufügen oder zu löschen.

Datensätze löschen oder hinzufügen

Nach einem Klick auf „OK“ findet sich im localService Ordner ein neuer Ordner „mockdata“, in dem für jede Entität für die man Daten erzeugt hat, eine entsprechende .js Datei vorhanden ist.

Erstellen des Mock Servers ohne Web IDE Templates

Nutzt man die Templates der Web IDE nicht, müssen diese Dateien selbst erzeugt werden. Dazu sind folgende Schritte notwendig:

Die Datei mockServer.html oder auch flpSandboxMockServer.html falls die App in einer Launchpad Sandbox gestartet werden soll, sollte im Ordner test angelegt werden.

In dieser Datei fügen wir den Code der index.html ein und ergänzen die attachInit Funktion folgendermaßen:

sap.ui.getCore().attachInit(function () {
        // load mockserver.js as required file
	sap.ui.require([
                // change the path to your own mockserver.js!
		"my/mockserverBlog/localService/mockserver"   
	], function (mockserver) {
		// set up test service for local testing
		mockserver.init();
                // initialize the shell
                [...]
	});
});

Die metadata.xml und die json-Dateien mit den Fakedaten lassen sich wie anfangs beschrieben am einfachsten aus dem eigenen Service erzeugen.

Für die mockserver.js kann man folgendes Beispielcoding kopieren. Danach müssen nur noch die Pfade (hier my/mockserverBlog/…) angepasst werden.

sap.ui.define([
	"sap/ui/core/util/MockServer"
], function (MockServer) {
	"use strict";
	return {
		init: function () {
			var sManifestUrl = jQuery.sap.getModulePath("my/mockserverBlog/manifest", ".json"),
			sJsonFilesUrl = jQuery.sap.getModulePath("my/mockserverBlog/localService/mockdata"),
			oManifest = jQuery.sap.syncGetJSON(sManifestUrl).data,
			oMainDataSource = oManifest["sap.app"].dataSources.mainService,
			sMetadataUrl = jQuery.sap.getModulePath("my/mockserverBlog/" + oMainDataSource.settings.localUri.replace(".xml", ""), ".xml"),
			// ensure there is a trailing slash
			sMockServerUrl = /.*\/$/.test(oMainDataSource.uri) ? oMainDataSource.uri : oMainDataSource.uri + "/";
			// create
			var oMockServer = new MockServer({
				rootUri: sMockServerUrl
			}); 
			var oUriParameters = jQuery.sap.getUriParameters();
			// configure
			MockServer.config({
				autoRespond: true,
				autoRespondAfter: oUriParameters.get("serverDelay") || 1000
			});
			// simulate
			oMockServer.simulate(sMetadataUrl, sJsonFilesUrl);
			// start
			oMockServer.start();
		}
	};
});

Die von der Web IDE beim erstellen einer App aus einem Template erzeugte mockserver.js ist etwas größer und beinhaltet unter anderem noch Fehlerbehandlung für das Laden der Mockdaten.

Aus Übersichtsgründen habe ich in eine abgespeckte Variante vorgestellt. Wer interessiert ist kann sich ganz einfach eine Testapp mit dem Worklist oder Master-Detail Template erstellen und sich den von der Web IDE generierten Code anschauen bzw. in sein eigenes Projekt kopieren und anpassen.

Am Ende sollte das Projekt folgende Ordner und Dateien enthalten (der Inhalt des Ordners „mockdata“ hängt von dem eigenen Service ab).

Alle angelegten Dateien und Ordner

Viele denken jetzt vielleicht, „das ist ja zusätzliche Arbeit“, „wieso sollte ich mir das antun, mein Service ist sowieso verfügbar und falls nicht, warte ich einfach bis er wieder verfügbar ist“.

Das mag in manchen Situationen vielleicht zutreffen, das Problem eines Entwicklungssystems ohne (Test-)Daten löst es dennoch nicht.

Zudem hat die Nutzung von Mock Daten noch einen weiteren Vorteil, Stichwort Testing.
Wer möchte schon, dass seine Unit-Tests fehlschlagen, weil der Service kurzzeitig nicht erreichbar war? Im schlimmsten Fall sucht man nach einem Fehler den es gar nicht gibt, bevor auffällt, dass der Service die Ursache war.
Auch machen es Mock Daten einfacher, Tests zu schreiben. Da diese Daten sich im allgemeinen nicht ändern, können in der Testimplementierung ohne Bedenken statische IDs, Namen oder Zahlen verwendet werden.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

SAP Cloud Platform – Einrichtung der Web IDE mit dem Northwind Service

Heute wird’s luftig. Wir heben zwar nicht ab, doch wagen wir uns heute mal in die Cloud, genauer gesagt in oder auf die SAP Cloud Platform.
Entwickler, die sich mal an der SAP Cloud Platform probieren wollen, können mit Hilfe eines Trial-Accounts die weite Welt der Platform kennenlernen. Funktional sehr vielfältig ist viel zu entdecken und auszuprobieren. Für den Anfang beschäftigen wir uns aber mit einem simplen doch absolut relevanten Szenario: Es geht um die Einrichtung der SAP Web IDE mit dem Northwind Service.

Wenn du dieses Szenario durchspielst wirst du danach eine Anwendung auf Basis des Northwind Service in der Cloud erstellt haben.

Erstellung eines Trial Accounts

Zu aller erst bedarf es eines Trial Accounts. Über den Link https://account.hanatrial.ondemand.com kannst du deinen eigenen Trial Account erstellen. Klicke einfach auf den Button „Register“.

SAP Cloud Platform - Registrierung

Du wirst zu einem Formular weitergeleitet, auf dem du deine persönlichen Daten eingibst. Der Registrierungsprozess sollte einfach zu folgen sein. Am Ende kannst du dich mit deinen Daten einloggen.
Sofern das erfolgreich gelaufen ist landest du auf der Startseite der SAP Cloud Platform

SAP Cloud Platform - Trial Account

Für unsere Zwecke nutzen wir die Neo Trial Version. Nach einem Klick wirst du weitergeleitet auf das Cockpit der SAP Cloud Platform.
Das war’s auch schon. Willkommen in der SAP Cloud!

SAP Cloud Platform - Dashboard

Der Weg zur Web IDE…

… ist gar nicht so weit. Unter Services einfach die Kachel suchen und zum Service gehen. Drei Klicks sollten genügen.

Erstellung der Destination für den Northwind Service

Der Account erstellt, die SAP Web IDE gefunden, und jetzt? Zwar kann man schon munter und fröhlich Anwendungen erstellen und bearbeiten aber irgendwie fehlt zum gesammelten Glück doch noch was. Richtig!

So ganz ohne Daten machen die Anwendungen nur einen halbgaren Eindruck. Und auch beim Erstellen von Anwendungen mit Hilfe der Templates kommt man noch nicht so recht weiter.
Der Northwind-Service bietet da Abhilfe. Folgende Schritte sind nötig, um erste, vorzeigbare Ergebnisse mit der SAP Web IDE und dem Northwind Service zu erzielen.

1.) Anlegen einer neuen Destination
Die Destination wird außerhalb der SAP Web IDE in der SAP Cloud Platform angelegt. Über den Pfad Connectivity -> Destinations gelangst du an die richtige Stelle.
Hier klickst du auf „New Destination“ und legst die neue Destination mit folgenden Einstellungen an:

Name			Northwind
Type			HTTP
Description		-- Eine beliebige Beschreibung –
URL			http://services.odata.org/V2/Northwind/Northwind.svc
Proxy Type		Internet
Authentication		No Authentication

SAP Cloud Platform - Neue Destination

2.) Pflege von Attributen für die SAP Web IDE
Für den Einsatz in der SAP Web IDE reicht das noch nicht ganz. Auf der rechten Seite musst du folgende Eigenschaften hinzufügen:

WebIDEEnabled		true
WebIDEUsage		odata_gen

SAP Cloud Platform - Destination additional Attributes

Die erste Anwendung erwacht zum Leben

Wie du vielleicht beim Speichern der Destination im Popup gelesen hast dauert es ein bisschen, bis die Änderungen wirksam werden. Meist reicht aber ein Gang zur Kaffeküche, einmal aus dem Fenster geschaut und ein Neustart der SAP Web IDE, damit die Änderungen wirksam werden.

SAP Web IDE - Startseite

Unter der Rubrik „Create a Project“ wählst du jetzt „New Project form Template“ aus. Es öffnet sich ein Wizard. Für unsere Zwecke wählst du „All Categories“ aus und erstellst deine Anwendung auf Basis des Templates „CRUD – Master Detail Application“, einer einfachen Master-Detail-Anwendung, die standardmäßig Funktionalitäten zum Erstellen (Create), Lesen (Read), Aktualisieren (Update) und Löschen (Delete) beinhaltet.

SAP Web IDE - Create Project from Template

Weiter geht’s mit ein paar Angaben zu unserem neuen Projekt. Hier kannst du frei entscheiden, wie du dein Projekt benennst. Eine Möglichkeit wäre z.B.

Project Name		NorthwindAppV1
Title			Northwind 
Namespace		app_northwind

SAP Web IDE - New CRUD Application

Im nächsten Schritt wählen wir die Datenverbindung aus. Hier erfahren wir jetzt, ob wir bei der Anlage der Destination zum Northwind Service alles richtig gemacht haben. Das sollte der Fall sein. Gehe dafür auf „Service Url“ und wähle den Northwind Service aus.

SAP Web IDE - Auswahl Northwind Service

Falls dir der Northwind Service nicht angeboten wird hat das in der Regel zwei mögliche Ursachen:

  • Deine Angaben bei der Destination sind nicht alle richtig. Vergleiche nochmal oben, ob du alle Angaben richtig übernommen hast
  • Du musst noch ein wenig länger warten weil die Änderungen noch nicht aktiv sind.
    Gerne auch nochmal die Web IDE neu starten. (Frei nach dem Motto: „Have you tried turning it off and on again?“)

Nun wird die folgende Meldung in rot kommen

SAP Web IDE - Northwind Service relative URL

Ich empfehle jetzt folgende Vorgehensweise: In das Eingabefeld einen „.“ (Punkt) Eintragen und dann auf Test drücken. Im Anschluss zeigt die Tabelle alle Entitäten, die der Service zur Verfügung hat. In dieser Testanwendung möchten wir alle „Customers“ angezeigt bekommen und bearbeiten können. Deshalb wählen wir diese in der Tabelle aus.

SAP Web IDE - Northwind Service - Auswahl Entität

Im nächsten Schritt noch folgende Eingaben tätigen

SAP Web IDE - CRUD - Template Customization

Für unsere Zwecke reicht das. Klicke also „Finish“.

SAP Web IDE - CRUD - New Project

Testen deiner Anwendung

Voilà! Über das grüne Häkchen kannst du deine neue Anwendung ausführen. Wie du sehen wirst kann diese schon einiges, ohne, dass du überhaupt eine Zeile gecodet hast.
An dieser Stelle haben wir unser Ziel erreicht. Unsere erste Anwendung in der SAP Cloud ist angekommen!

 

So weit erst mal. Ich hoffe, die ersten Schritte in der Cloud waren für dich hilfreich und verständlich. Das ist natürlich erst der Anfang. In der Cloud bzw. auf der SAP Cloud Platform gibt es viel zu entdecken, also schau dich ruhig um!

Bis zum nächsten Mal und „Kiek mol wedder in!“

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Git in der SAP Web IDE – Versionskontrolle leicht gemacht – Teil 1

Versionskontrolle mit Git sollte zum täglich Brot eines jeden Web-Entwicklers und damit natürlich auch eines jeden SAPUI5-Entwicklers gehören. Die Realität im Projektalltag sieht jedoch sehr häufig ganz anders aus.

Besonders in der SAP Web IDE stellt Git Projektteams oft vor mehr Probleme, als dass es welche löst. Doch woran liegt das eigentlich? Zunächst muss man zugeben, dass das Git Pane der SAP Web IDE nicht besonders intuitiv gestaltet wurde und dem Entwickler wenig visuelle Hilfen bietet, um nachzuvollziehen was genau er durch welche Aktion bewirkt. Darüber hinaus existiert bei sehr vielen Entwicklern ein grundsätzliches Verständnisproblem wie genau Git funktioniert, wie es sich von zentralen Versionskontrollen unterscheidet und wo die Vorteile einer dezentralen Versionsverwaltung liegen, obwohl diese gewisse Abläufe auf den ersten Blick komplexer erscheinen lässt.

Ziel dieser Blog-Serie ist es, diese Verständnisprobleme zunächst auszuräumen um danach Schritt für Schritt die Git-Techniken vorzustellen, die SAPUI5-Projekte mittels Git zum Erfolg führen!

Zentrale vs. dezentrale Versionskontrolle

Zentrale Versionsverwaltung

Dezentrale Versionsverwaltung

Die zentrale Versionsverwaltung basiert auf dem Konzept eines einzigen Repositiory’s auf dem die Versionen gespeichert werden und für alle Entwickler zugänglich gemacht sind. Die bekanntesten Beispiele für Versionierungssoftware, die auf diesem Konzept basieren sind CVS, SVN, Clearcase sowie klassische ABAP Entwicklung auf SAP Netweaver.

Git hingegen basiert auf dem Prinzip der dezentralen Versionsverwaltung. Das bedeutet, dass jeder Entwickler ein eigenes, individuelles Repository besitzt. Dadurch ist es ihnen nicht mehr nur möglich Versionen und Branches zusammenzuführen (Merge), sondern sie sind nun sogar dazu in der Lage gesamte Repositories zu fusionieren. Das macht das gesamte Konzept selbstverständlich etwas komplizierter, da es nun sowohl Befehle zum Ändern des lokalen, als auch externer Repositories gibt. Gleichzeitig ermöglicht es den Entwicklern aber eine sehr viel flexiblere und unabhängigere Arbeitsweise, da sie sowohl untereinander, als auch mit dem zentralen Repository synchronisieren können.

Häufig haben Entwickler im SAPUI5-Umfeld einen ABAP-Hintergund. Sie sind dann an die klassisch-zentrale Versionsverwaltung der SAP GUI gewöhnt die nach dem oben skizzierten Muster arbeitet. Solche Entwickler haben erfahrungsgemäß die größten Startschwierigkeiten mit Git in der SAP Web IDE, da sie beispielsweise unter einem Commit in der zentralen Versionsverwaltung etwas grundlegend anderes verstanden wird, als in der dezentralen. Während in der „zentralen Welt“ ein Commit dazu dient jegliche Änderungen mit dem zentralen Repository zu synchronisieren, kann man in Git ein Commit praktisch als ein Synonym für „Version“ sehen. Diese Version kann nun selbstverständlich in dem lokalen oder in dem zentralen Repository liegen. Ähnlich verhält es sich mit Branches. Ein Branch in Git ist nämlich nichts anderes als ein Zeiger auf einen bestimmten Commit, bzw. eine Version. Das mag zunächst verwirrend sein, doch wenn ihr diese Blog-Serie die nächsten Tage aufmerksam verfolgt werden euch all diese Besonderheiten der Entwicklung mit Git in Fleisch und Blut übergehen.

Also, bleibt dabei und bis bald 🙂

Hier geht´s zum zweiten Teil dieses Blogs. Er behandelt Stage, Commit, Stash und Revert.

 

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

$count – OData Models auf die man zählen kann, aber nicht muss

Moin Moin UI5-Entwickler,

eine Frage bekommen meine Kollegen und ich ständig von unseren Kunden gestellt: „Was zeichnet eine gute Fiori eigentlich aus?!“

Die Antwort auf diese zentrale Fragestellung ist verblüffend einfach. Eine gute Fiori zeichnet sich nämlich durch zufriedene Nutzer aus. Und nichts macht einen Nutzer unzufriedener als unnötiges Warten auf Daten. Deshalb sollte ein SAPUI5-Entwickler stehts unter der Prämisse arbeiten, überflüssige oder redundante Backend-Calls zu vermeiden, um so die Wartezeiten für den Anwender so gering wie möglich zu halten.

Vor diesem Hintergrund möchte ich in diesem Beitrag auf eine Standardeinstellung des OData Models ( sap.ui.model.odata.v2.ODataModel ) näher eingehen, die vielen Entwicklern unbekannt ist, da sie meiner Meinung nach nicht gut dokumentiert wurde. Die Rede ist vom sap.ui.model.odata.CountMode.

Standardmäßig sendet das OData Model zu jedem http GET-Call einen weiteren $count-Aufruf, um die Anzahl der vorhandenen Einträge zu ermitteln. Das kann natürlich von Nutzen sein, vorausgesetzt man möchte diese Information dem Benutzer anzeigen oder benötigt sie um weitere clientseitige Berechnungen durchzuführen. Das ist aber sehr häufig nicht der Fall.

GET mit zusätzlichem $count-Aufruf in Standardeinstellung

Um diesen zusätzlichen Datenbankabruf zu unterdrücken muss man den CountMode der Modelinstanz auf sap.ui.model.odata.CountMode.None ändern. Das kann man auf zwei unterschiedliche Arten realisieren:
Entweder man benutzt setDefaultCountMode() in der init() der Component.js,

init: function () {
  ...
  this.getModel().setDefaultCountMode(sap.ui.model.odata.CountMode.None);
}

oder man überschreibt den Standard in der Descriptor-Datei (manifest.json).

"models": {
   "i18n": {
      "type": "sap.ui.model.resource.ResourceModel",
      "settings": {
      "bundleName": "acando.i18n.i18n"
   }
 },
 "": {
      "dataSource": "mainService",
      "settings": {
         "metadataUrlParams": {
         "sap-documentation": "heading"
         },
        "defaultCountMode": "None"
      }
   }
 }

Diese eine unscheinbare Zeile Code erspart dem Nutzer eine ganze Menge Wartezeit und erhöht damit die Gesamtzufriedenheit.

HTTP Timing mit sap.ui.model.odata.CountMode.None

HTTP Timing mit sap.ui.model.odata.CountMode.Request

In diesem Beispiel muss der User ca. 50% kürzer auf die angefragten Daten warten, was ein beträchtlichen Unterschied für das Nutzungserlebnis bedeutet!

Ich hoffe dieser Beitrag hilft euch eure Fiori von unnötigen Backen-Calls zu befreien sodass auch Ihr zufriedene Anwender habt.

Bis dahin erstmal und: „Kiek mol wedder in!“

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

100 Einträge im Model – Datenbegrenzung durch $top ändern

Wer schon einmal versucht hat eine größere Datenmenge in einem Control anzuzeigen wird sicher festgestellt haben, dass es dort eine Begrenzung der Datenmenge auf 100 Einträge gibt. Diese ist die Default Begrenzung eines jeden Models.
Manchmal durchaus sinnvoll (Hallo liebe Performance), manchmal aber auch einfach nur so semi gut. Dann nämlich, wenn mehr Daten geladen werden sollen, aber nicht werden.
Mein Klassiker ist das Laden von Ländern in eine Combo- oder SelectBox. Nach Ländern mit Anfangsbuchstaben L war dann Schluss.

Es gibt natürlich Wege, diese Begrenzung zu umgehen.
Eine standardmäßige ist, wenn das verwendete Control das Growing-Feature unterstützt. Oftmals liegt hier die Obergrenze bei weniger als 100 Einträgen. Durch das Growing-Feature können aber nach und nach alle Einträge geladen werden.
Ein Beispiel ist z.B. das sap.m.List Control, dass standardmäßig bei Master-Detail Anwendungen als Masterlist genutzt wird

Was aber, wenn wir ein Control benutzen, dass dies nicht tut?

Nun, da gibt es meiner Meinung nach zwei ordentliche Möglichkeiten, wobei ich die eine eigentlich immer der anderen vorziehe.
Deshalb sei diese zuerst erwähnt.

Direkt im Binding – Parameter length

Um bei einem Binding die Standardbegrenzung des Models zu umgehen kann direkt im Binding der Parameter length gesetzt werden. length wird als Integer Wert im Binding mitgegeben, analog zu anderen Parametern wie z.B. filter oder sorter.
Ein Binding mit Vergrößerung des $top-Werts kann wie folgt aussehen:

<Select
  items="{
    path: '/Country',
    length: 500
  }" >
  <items>
    <core:ListItem key="{CountryIsoCode}" text="{Text}" />
  </items>
</Select>

Der Vorteil bei dieser Methode ist, dass pro Binding einzeln entschieden werden kann wie hoch der $top-Wert ist. Was das genau bedeutet können wir sehen, wenn wir uns die zweite Möglichkeit angucken.

Global mit .setSizeLimit() am Model

Die Funktion setSizeLimit eines Models verändert den default $top-Wert des Models, was bedeutet, dass jedes Binding des Models die Daten mit diesem neu gesetzten Limit lädt.
Für mich hat sich bewährt, sofern ich diese Einstellung verwendet habe, diese im init-Prozess der Component zu setzen.
Code-Technisch sieht das dann wie folgt aus:

  init: function() {
    ...
      this.getModel().setSizeLimit(500);
    ...
  }

Problematisch kann diese Methode werden, wenn das limit vergrößert wird. Das kann zu fiesen Performanceverlusten führen, je nachdem wie viele Bindings in der Anwendung sind und/oder wie viele Daten dann in die Anwendung geladen werden. Denn, wie schon erwähnt, dieses neue Limit greift bei jedem Binding!

Probiert’s aus, es kann vielleicht ganz nützlich sein.

Bis dahin erstmal und: „Kiek mol wedder in!“

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Sorter, Filter, Expand Parameter im XML

Da Binding mein absolutes Lieblingsthema ist wird sich dieser Beitrag auch wieder mit diesem Thema beschäftigen. Denn, mal Hand auf’s Herz, ohne die aus dem gebundenen Model geladenen Daten ist das schönste Gerüst nur eben das – ein Gerüst.
Im Speziellen geht es um das Binding im XML und wie dort durch unterschiedliche Parameter Einfluss auf die geladenen Daten genommen werden kann.

Konkret geht es um die Möglichkeit, bei Aggregation Bindings direkt sorter, filter und expand Parameter mitzugeben. Dies sind die, wie ich in der Praxis festgestellt habe, für eine produktive Anwendung relevantesten Parameter bei einem Binding.

Ein einfaches Beispiel

Ein einfaches Beispiel für ein Aggregation-Binding ist das Binden der Aggregation items einer Table (sap.m.Table). Ein Template kann in diesem Fall ein ColumnListItem sein.
Das normale Binding sieht wie folgt aus:

<Table
  items="{modelPersons>/ContactSet}">
  <columns>
    <Column />
  </columns>
  <items>
    <ColumnListItem>
      <Text text="{modelPersons>Name1}">
  </items>
</Table>

Kurz zum Verständnis: Wir haben ein benanntes oData-Model namens „modelPersons“ und nutzen für das Aggregation-Binding das EntitySet „ContactSet“.

Das ist doch schon mal ein guter Anfang. Wir bekommen den Namen einer Person angezeigt. Beziehungsweise, da wir es ja mit einem Aggregation-Binding zu tun haben, die Namen ganz vieler Personen.

Und wie geht es jetzt weiter?

Nun, das war natürlich nur der Anfang.
Wir sehen schon was, aber reicht uns das?

Was, wenn ich noch mehr Daten anzeigen möchte?
Was, wenn man nicht alle Personen angezeigt haben möchte?
Was, wenn ich eine andere Reihenfolge haben möchte?

Meiner Erfahrung nach sind das, so oder so ähnlich, die zentralen Anforderungen, vor die man gestellt wird, wenn man es mit Aggregation Bindings zu tun hat.
Konkret möchte ich uns jetzt vor folgende Herausforderungen stellen:
–> Sortiere absteigend nach dem Namen!
–> Zeige nur ganz bestimmte „Meiers“ an!
–> Zeige die Adresse einer Person mit an!

Und los! Äääh… aber wie jetzt genau?
Nun, dafür bedarf es erst mal der Parameter, die wir benutzen wollen.

Parameter

sorter
  Sortierung der Einträge aus dem Model

  path: Feld, nach dem wir sortieren möchten
  descending: Boolean-Angabe, ob wir absteigend sortieren möchten 
    (Aufsteigend wäre dann folgerichtig "descending = false")

filters
  Filterung der Einträge aus dem Model

  path: Feld, nach dem wir Filtern möchten
  operator: Eine Auflistung der Filter-Operatoren findet ihr hier
  value1: Wert, nach dem wir Filtern möchten
  value2: Manche Operatoren brauchen zwei Werte (z.B. BT)

expand
  Erweiterung des Models mit Daten aus zugehörigen Navigations

Sorter

Die erste Anforderung betrifft das Sortieren unserer Tabelle nach dem Namen.

<Table
  items="{
    path: 'modelPersons>/ContactSet',
    sorter: {
      path : 'Name1',
      descending: true
    }
  }" >
  <columns>
    <Column />
  </columns>
  <items>
    <ColumnListItem>
      <Label text="{modelPersons>Name1}" />
    </ColumnListItem>
  </items>
</Table>

Der Sorter ist im Binding des items mit integriert. Wir geben das Feld (Name1) und die Sortier-Richtung (descending = true) an.
Sobald wir Parameter in das Binding integrieren müssen wir unser gebundenes Array mit „{path: “}“ angeben. (In unserem Beispiel also: path:’modelPersons>/ContactSet‘)

Filter

Die zweite Anforderung beinhaltete die Filterung nach ganz bestimmten „Meiers“.
Die Implementierung ist wie folgt:

<Table
  items="{
    path: 'modelPersons>/ContactSet',
    filters : [
      {path : 'Name1', operator : 'Contains', value1 : 'ei'},
      {path : 'Name1', operator : 'NE', value1 : 'Maier'},
      {path : 'Name1', operator : 'NE', value1 : 'Mayer'},
      {path : 'Name1', operator : 'NE', value1 : 'Meyer'}
    ]
  }" >
  <columns>
    <Column />
  </columns>
  <items>
    <ColumnListItem>
      <Label text="{modelPersons>Name1}" />
    </ColumnListItem>
  </items>
</Table>

Was im Gegensatz zum Sorter auffällt ist, dass filters ein Array ist. Heißt also, bei diesem Paramater können wir mehrere Filter mitgeben. Die Default-Verknüpfung der Filter ist hier „or“.
Um auf unser Beispiel zurückzukommen wird hier nach allen Kontakten mit einem „ei“ im Namen gefiltert, schließt aber die Maier, Mayer und Meyer’s aus.
Über die Sinnhaftigkeit dieser Filterung lässt sich streiten, die Syntax sollte aber durch das Beispiel klar werden.

Expand

Beim expand geht es darum seine Entitäten mit zusätzlichen Informationen zu erweitern. Diese gehören nicht direkt zur Entität, haben aber eine direkte Verbindung (Navigation), weshalb sie dazu geladen werden können.
In unserem Beispiel möchten wir einem Kontakt seine Adressdaten mitgeben. Dazu benutzen wir ein expand auf die Verbindung „ToAddress“

<Table
  items="{
    path: 'modelPersons>/ContactSet',
    parameters : {
      expand : 'ToAddress'
    }
  }" >
  <columns>
    <Column />
    <Column />
  </columns>
  <items>
    <ColumnListItem>
      <Label text="{modelPersons>/name1}" />
      <Label text="{modelPersons>/ToAddress/street} 
        {modelPersons>/ToAddress/streetNumber}" />
    </ColumnListItem>
  </items>
</Table>

Kombination der verschiedenen Parameter

Die unterschiedlichen Kombinationen lassen sich natürlich auch kombinieren. Unser komplettes Beispiel sieht wie folgt aus:

<Table
  items="{
    path: 'modelPersons>/ContactSet',
    filters : [
      {path : 'Name1', operator : 'Contains', value1 : 'ei'},
      {path : 'Name1', operator : 'NE', value1 : 'Maier'},
      {path : 'Name1', operator : 'NE', value1 : 'Mayer'},
      {path : 'Name1', operator : 'NE', value1 : 'Meyer'}
    ],
    sorter: {
      path : 'Name1',
      descending: true
    },
    parameters : {
      expand : 'ToAddress'
    }
  }" >
  <columns>
    <Column />
    <Column />
  </columns>
  <items>
    <ColumnListItem>
      <Label text="{modelPersons>/name1}" />
      <Label text="{modelPersons>/ToAddress/street} 
        {modelPersons>/ToAddress/streetNumber}" />
    </ColumnListItem>
  </items>
</Table>

Fazit

Meiner Meinung nach stellen die Parameter im Binding im XML eine elegante, entwicklerische Lösung dar um direkt Einfluss auf das Binding zu nehmen. Der Code ist minimal und auch die Backend-Calls werden so minimiert, da die Parameter beim initialen Laden der Daten mitgegeben werden. Dies lässt sich ganz einfach im Network-Trace nachvollziehen.
Sobald die Syntax einmal geläufig ist, sollte die Implementierung, zumindest im Frontend, auch keine Probleme mehr bereiten.

Bis dahin erstmal und: „Kiek mol wedder in!“

Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

OData Model – Wie kommen die Daten in meine Anwendung?

Das OData Model besitzt laut API-Dokumentation einige Funktionen die suggerieren, dass ich von diesem Daten bekommen könnte. Binding, ja, da sind die Daten dann da, so irgendwie. Und wenn ich mein OData Model im Controller in der Hand habe, dann bekomme ich die Daten auch. Also zumindest sollte ich das in der Theorie

Aber wann kommen eigentlich wo die Daten her? Wann habe ich die Daten auch praktisch in der Hand und nicht nur theoretisch? Während der Entwicklung von meinen Anwendungen war mir das nicht immer klar. Deshalb folgt hier ein kurzer Versuch der Erklärung mit der Hoffnung, einige Unklarheiten zu beseitigen.

Daten Laden über ein Binding im XML

Der Standardfall ist das Laden der Daten über ein Binding direkt im XML-Code. Entscheidend sind hierbei immer zwei Sachen:

  • Das Model, dass ich auf mein Control gesetzt habe
  • Der Path, der die genaue Entität bzw. Entitäten benennt, die aus dem Model gelesen werden sollen

Model und Path zusammen bilden den Binding-Context. Aus diesem kann ich, sofern die Daten geladen worden sind