In dem Beitrag CORRESPONDING habe ich gezeigt, wie mit dem neuen Schlüsselwort CORRESPONDING Werte einer Struktur in eine andere übertragen werden können. Interessant dabei ist die Option, einen Defaultwert übergeben zu können, der in der Quellstruktur nicht gefüllt ist.
In diesem Beitrag zeige ich dir einen Codeschnipsel, der eine ganze Tabelle mit Corresponding überträgt.
Beispiel: Baumknoten ändern
Das GUI-Control CL_LIST_TREE_MODEL zur Darstellung eines Trees hat die Methode UPDATE_ITEMS mit der die einzelnen Felder eines Knotens geändert werden können.
Es könnte sein, dass eine Methode die Änderungen bereitstellen möchte, aber die ID des Knotens nicht weiß. Die Methode zur Änderung der ITEMS bekommt dann zum Beispiel folgendes:
Ich habe vor kurzem ein altes Programm von mir wiedergefunden und gemerkt, dass ich noch gar nichts darüber geschrieben habe…
Am besten zeige ich, worum es geht:
Control in ABAP-Liste
Zu sehen ist eine mit WRITE erzeugte Liste in der zwei Controls (Picture-Control und SALV-Table) eingebunden sind.
Wie ist sowas möglich? Keine Ahnung, aber es geht. Was nicht geht: Bei AT LINE-SELECTION kann man zwar die Controls unsichtbar schalten, aber danach gibt es keine Möglichkeit mehr, sie beim Zurückspringen in die Grundliste wieder sichtbar zu machen.
Code
REPORT NO STANDARD PAGE HEADING.
DATA:
go_cont_pic TYPE REF TO cl_gui_custom_container,
go_cont_alv TYPE REF TO cl_gui_custom_container,
go_pic TYPE REF TO cl_gui_picture,
gt_data TYPE spfli_tab,
go_alv TYPE REF TO cl_salv_table.
AT LINE-SELECTION.
go_cont_pic->set_visible( space ).
go_cont_alv->set_visible( space ).
WRITE: / 'Double click on a list line'.
START-OF-SELECTION.
WRITE: /5 'WARNING', at 20 'Check your flights'.
PERFORM pic.
PERFORM grid.
skip to line 9.
WRITE 'additional text'.
FORM pic.
CHECK go_cont_pic IS NOT BOUND.
CREATE OBJECT go_cont_pic
EXPORTING
container_name = ''
repid = 'SAPMSSY0'
dynnr = '0120'.
go_cont_pic->set_top( 10 ).
go_cont_pic->set_left( 10 ).
go_cont_pic->set_width( 100 ).
go_cont_pic->set_height( 52 ).
go_pic = new #( parent = go_cont_pic ).
go_pic->load_picture_from_sap_icons( icon_warning ).
go_pic->set_display_mode( go_pic->display_mode_stretch ).
ENDFORM.
FORM grid.
CHECK go_cont_alv IS NOT BOUND.
CREATE OBJECT go_cont_alv
EXPORTING
container_name = ''
repid = 'SAPMSSY0'
dynnr = '0120'.
go_cont_alv->set_top( 10 ).
go_cont_alv->set_left( 150 ).
go_cont_alv->set_width( 800 ).
go_cont_alv->set_height( 50 ).
SELECT * FROM spfli INTO CORRESPONDING FIELDS OF TABLE gt_data UP TO 2 ROWS.
TRY.
cl_salv_table=>factory(
EXPORTING
r_container = go_cont_alv
IMPORTING
r_salv_table = go_alv
CHANGING
t_table = gt_data
).
go_alv->display( ).
CATCH cx_salv_msg. "
ENDTRY.
ENDFORM.
Übersetzen sind eine der schlimmsten Dinge für Entwickler. In der Regel kann man direkt in der zugehörigen Transaktion das bearbeitete Objekt übersetzen: Menü Springen – Übersetzung.
Bei Nachrichten funktioniert das jedoch immer nur für eine einzelne Nachricht. Es ist nicht möglich, alle Meldungen der Nachrichtenklasse gesammelt zu übersetzen. Jedenfalls nicht aus der Transaktion SE91 heraus.
Übersetzungsumgebung
Mit folgendem Vorgehen ist es möglich, die Nachrichten einer Nachrichtenklasse nacheinander abzuarbeiten:
Starten der Transaktion SE63
Drucktaste Transport Object anklicken.
Transportobjekt auswählen
Gin in der Eingabemaske die Objektkennung für die Nachrichtenklasse ein:
R3TR MSAG <Name der Nachrichtenklasse>
In diesem Beispiel heißt die Nachrichtenklasse ZTT. Mit der Drucktaste „Source/ Target Language“ kannst du Quell- und Zielsprache vertauschen. Merkwürdigerweise erkennt das System die Originalsprache nicht automatisch.
Einstiegsbild Übersetzungsumgebung Objekte
Um die Übersetzung zu beginnen, klicke auf die Drucktaste „Edit“.
Nachrichten übersetzen
Mit einem Doppelklick auf die einzelnen Nachrichten kannst du diese direkt übersetzen. Der Trick besteht jedoch darin, die Drucktaste „Sequentially Process Objects“ zu bemühen.
Nachrichtenübersicht
Nun startest du mit der ersten Nachricht in der Nachrichtenklasse und kannst sie übersetzen.
Einzelne Nachricht übersetzen
Mit F11 sichern und dann mit F8 zur nächsten Meldung springen.
Fehlermeldungen sind eine wichtige Sache in der Programmierung. Sie geben Auskunft darüber, was schief gelaufen ist und im besten Fall noch, was erwartet wurde.
Ich habe gerade gelesen, dass Fehlermeldungen die beste Art der Dokumentation sind, denn diese würden aufmerksam gelesen. Nun ja… Meine Erfahrungen sind andere.
Häufig sind allein stehende Fehlermeldungen nicht aussagekräftig genug.
Fehlermeldung 2.0
Wie wäre es denn, wenn man die Fehlermeldung mit einem Screenshot der aktuellen Transaktion verbinden würde?
Es ist leider der Fehler XYZ aufgetreten. Zu Dokumentationszwecken wird ein Screenshot erstellt, den du an den Verantwortliche:n weiterleiten kannst.
Beispielhafte Fehlerpräsentation
Leider – oder zum Glück(?) – wird die Aktion „Screenshot erstellen“ von einer Sicherheitsabfrage des SAPGUI begleitet. Deswegen ist es sinnvoll den Anwender vor dem Erstellen des Screenshots zu informieren.
Sicherheitsabfrage vor Erstellen des Screenshots
Fehlermeldung 3.0
Die Krönung wäre es natürlich, wenn der Anwender alles zusammen per Mail an den zuständigen Personenkreis schicken könnte…
Generelle Hilfestellung
Eine andere Möglichkeit, das Tool einzubinden, wäre als direkt Funktion in Transaktionen. Der Anwender könnte in einem Dialog vielleicht eine Frage stellen oder einen Fehler melden, automatisch einen aktuellen Screenshot hinzufügen und dies dann an eine zentrale Stelle oder ausgewählte Key-user oder Verantwortliche schicken.
Quelltext
Das folgende Coding ruft die Methode cl_gui_frontend_services=>get_screenshot auf, um einen Screenshot vom aktuellen SAPGUI zu erstellen. Die Daten werden so umgewandelt, dass sie als PNG-Datei gespeichert werden können.
DATA mime_type TYPE string.
DATA image TYPE xstring.
DATA tabimg TYPE STANDARD TABLE OF x.
DATA path TYPE string.
DATA full_path TYPE string.
DATA useraction TYPE i.
DATA name TYPE string.
cl_gui_frontend_services=>get_screenshot(
IMPORTING
mime_type_str = mime_type
image = image
EXCEPTIONS
access_denied = 1
cntl_error = 2
error_no_gui = 3
not_supported_by_gui = 4
OTHERS = 5 ).
IF sy-subrc <> 0.
RETURN.
ENDIF.
cl_gui_frontend_services=>file_save_dialog(
EXPORTING
window_title = 'save screenshot'
default_extension = 'png'
default_file_name = 'screenshot'
prompt_on_overwrite = 'X'
CHANGING
path = path
filename = name
fullpath = full_path
user_action = useraction
EXCEPTIONS
cntl_error = 1
error_no_gui = 2
not_supported_by_gui = 3
invalid_default_file_name = 4
OTHERS = 5 ).
IF sy-subrc <> 0.
RETURN.
ENDIF.
IF useraction = cl_gui_frontend_services=>action_ok.
CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
EXPORTING
buffer = image
TABLES
binary_tab = tabimg.
cl_gui_frontend_services=>gui_download(
EXPORTING
filename = full_path
filetype = 'BIN'
CHANGING
data_tab = tabimg
EXCEPTIONS
OTHERS = 99 ).
ENDIF.
Mühsam ernährt sich das Eichhörnchen. Die heutige Nuss galt dem Befehlszusatz GROUP BY für den LOOP über eine interne Tabelle. Es gibt dankenswerter Weise in der SAP-Doku inzwischen viele Beispiele. Diese sind jedoch sehr abstrakt. Sie zeigen die Syntax, verdeutlichen aber nicht unbedingt, was damit möglich ist.
Ich präsentiere euch heute ein paar Möglichkeiten der Gruppierung, die hoffentlich die Funktionsweise deutlich machen.
Beispieldaten
im Folgenden verwende ich diese Struktur für meine Beispiele:
TYPES: BEGIN OF _doc,
docnr TYPE n length 10,
itmno TYPE n length 6,
category type c length 2,
del_flag type abap_bool,
END OF _doc,
_docs TYPE SORTED TABLE OF _doc WITH UNIQUE KEY docnr itmno.
DATA(documents) = VALUE _docs(
( docnr = 12001 itmno = 10 category = 'A1' del_flag = 'X' )
( docnr = 12001 itmno = 20 category = 'A1' del_flag = ' ' )
( docnr = 12002 itmno = 10 category = 'A2' del_flag = 'X' )
( docnr = 12003 itmno = 10 category = 'B3' del_flag = ' ' )
( docnr = 12003 itmno = 20 category = 'B1' del_flag = ' ' ) ).
Die Tabelle soll generelle Belege mit Positionsnummer, einem Positionstypen und einem Löschkennzeichen simulieren.
Generelle Funktionsweise
Der Befehlszusatz GROUP BY zum LOOP bietet die Möglichkeit der Gruppierung, ähnlich wie die Sprachelemente AT NEW oder AT CHANGE OF innerhalb eines LOOP. Allerdings bietet der GROUP BY Befehl noch einige weitere Möglichkeiten.
In der einfachen Variante kannst du ein Feld der internen Tabelle angeben, nach dem gruppiert werden soll.
LOOP AT documents INTO DATA(document)
GROUP BY document-docnr INTO DATA(docgrp).
WRITE: / docgrp.
ENDLOOP.
Das Ergebnis ist eine Liste aller eindeutigen Belegnummern:
0000012001
0000012002
0000012003
Gruppenelemente
Es ist nun eine Gruppe DOCGRP vorhanden. Auf die Zeilen dieser Gruppe kann mit Hilfe des Befehls LOOP AT GROUP zugegriffen werden:
LOOP AT GROUP docgrp INTO DATA(docline).
WRITE: / doc1line-docnr, docline-itmno.
ENDLOOP.
So weit so gut…
Es gibt zwei Interessante Zusätze in einer Gruppe:
GROUP INDEX
GROUP SIZE
Diese können zusätzlich definiert werden. Im LOOP kann darauf zugegriffen werden:
LOOP AT documents INTO DATA(document)
GROUP BY
( doc = document-docnr
size = GROUP SIZE
index = GROUP INDEX )
INTO DATA(docgrp).
WRITE: / |elements in group {
docgrp-index align = left } "{
docgrp-doc }": {
docgrp-size ALIGN = left } entries|.
LOOP AT GROUP docgrp INTO DATA(doc).
WRITE: / doc-docnr, doc-itmno.
ENDLOOP.
ENDLOOP.
Ergebnis:
number of elements in group 1: 2 entries
0000012001 000010
elements in group 1 "0000012001": 2 entries
0000012001 000010
0000012001 000020
elements in group 2 "0000012002": 1 entries
0000012002 000010
elements in group 3 "0000012003": 2 entries
0000012003 000010
0000012003 000020
In diesem Fall müssen die Gruppenfelder, die nun in Klammern eingefasst werden müssen, selbst definiert werden.
Dynamische Gruppen
Jetzt kommt der spannende Teil, der einen großen Vorteil gegenüber der alten AT NEW Gruppenstufenverarbeitung hat. Die Gruppen können dynamisch anhand der Feldwerte definiert werden. Im Folgenden Beispiel fasse ich alle Einträge mit einem Löschkennzeichen in der Gruppe „deleted“ zusammen. Alle anderen Einträge kommen in die Gruppe „valid“.
LOOP AT documents
INTO DATA(doc)
GROUP BY (
del_group = COND string( WHEN doc-del_flag = space THEN 'valid' ELSE 'deleted' )
size = GROUP SIZE
index = GROUP INDEX )
INTO DATA(docgrp).
WRITE: / 'number of', docgrp-del_group, 'entries:', docgrp-size.
ENDLOOP.
Ergebnis:
number of deleted entries: 2
number of valid entries: 3
Ich füge also je nachdem, ob das Löschhkennzeichen gesetzt ist oder nicht, eine andere Gruppenbezeichnung ein. Das ist besonders dann interessant, wenn man mehrere unterschiedliche Elemente gruppieren möchte. Beispielsweise alle Positionstypen, die mit A oder B beginnen:
LOOP AT documents
INTO DATA(doc)
GROUP BY (
cat = doc-category(1)
size = GROUP SIZE
index = GROUP INDEX )
INTO DATA(docgrp).
WRITE: / 'number of items in category',
docgrp-cat LEFT-JUSTIFIED NO-GAP,
docgrp-size.
ENDLOOP.
Ergebnis:
number of items in category A 3
number of items in category B 2
Ebenso könnte man nach Bestellungen gruppieren, die einen „geringen“ oder einen „höheren“ Bestellwert haben. Oder ich kann Aufträge direkt nach Aufträgen mit A-, B- oder C-Kunden gruppieren, indem ich für die Ermittlung der Kundenklassifizierung eine Methode in der GROUP BY Klausel verwende.
Gruppen gruppieren
Die definierten Gruppen können ebenfalls weiter gruppiert werden. Im folgenden Beispiel gruppiere ich erst nach der Positionstypengruppe (A oder B) aus dem obigen Beispiel. In dieser Gruppe gruppiere ich dann noch einmal nach dem eigentlichem Positionstyp (A1, A2, …). zudem berücksichtige ich in der WHERE-Bedingung nur die Positionen ohne Löschkennzeichen. Für dieses Beispiel habe ich die Datenbasis etwas erweitert…
LOOP AT documents
INTO DATA(doc2)
GROUP BY (
cat = doc2-category(1)
size = GROUP SIZE
index = GROUP INDEX )
INTO DATA(docgrp2).
WRITE: /1 'number of items in category',
docgrp2-cat LEFT-JUSTIFIED NO-GAP,
docgrp2-size.
LOOP AT GROUP docgrp2 INTO DATA(docline2)
WHERE del_flag = space GROUP BY ( category = docline2-category ) INTO DATA(grpcat).
write: /5 'category', grpcat-category.
LOOP AT GROUP grpcat INTO DATA(cat).
WRITE: /9 cat-docnr, cat-itmno, cat-category.
ENDLOOP.
ENDLOOP.
ENDLOOP.
Ergebnis:
number of items in category A 5
category A1
0000012001 000020 A1
0000012003 000030 A1
category A2
0000012004 000010 A2
number of items in category B 5
category B3
0000012003 000010 B3
0000012003 000020 B3
0000012005 000010 B3
0000012005 000020 B3
category B1
0000012006 000010 B1
Einschränkungen
Bei meinem heutigen Ausflug in die Gruppenstufen bin ich über folgende Einschränkungen gestolpert:
Sortierung nur nach Gruppenstufenfeldern möglich, aber nicht nach GROUP SIZE
Kein WHERE über Gruppenstufen möglich
Sortierung
Ich wollte gerne die Gruppen nach der Anzahl der Elemente sortieren. Das ist leider nicht möglich.
WHERE über Gruppenstufen
Es ist anscheinend nicht möglich, die erzeugten Gruppen direkt über eine WHERE-Bedingung einzuschränken. In den meisten Fällen kann man es sicherlich über eine geschickte WHERE-Bedingung über die Tabelle abbilden (zum Beispiel WHERE category(1) = ‚A‘). Allerdings wäre eine Einschränkung über die Gruppen selbst eventuell auch hilfreich. Zum Beispiel könnte die Bedingung für die Gruppe DEL_GROUP, die ich mit valid und deleted definiert hatte, etwas komplizierter und aufgrund einen Methodenaufrufes nicht ersichtlich sein. Ich würde dann trotzdem nur über die Einträge mit valid verarbeiten wollen. Das geht natürlich mit CHECK innerhalb des LOOP, eine WHERE-Bedingung wäre jedoch eleganter.
Dank des devtoberfestes habe ich diese Woche einiges über github gelernt. Ein cooles Feature möchte ich gerne mit euch teilen:
Github actions
Github actions können verwendet werden, um auf github Ereignisse reagieren zu können. Ein wichtiges Ereignis ist der Commit einer Datei (push).
TODO to Issue Action
Mit einer YAML-Definition kann man mit einer Action auf die Ereignisse reagieren. Ich habe im github Marketplace die Action TODO to issue action gefunden und einmal ausprobiert.
Mit dieser Action ist es möglich auf einen bestimmten Marker zu reagieren, der im Quelltext vorkommt. Sinnvoll ist im ABAP-Umfeld das Doppelte Anführungszeichen, dass einen Kommentar einleitet, gefolgt von dem Wort „TODO“. Findet der Workflow beim Einchecken (push) eines Sourcecodes diesen Marker, dann erstellt die Action automatisch ein Issue im github Repository.
Test Repository
Um das Ganze auszuprobieren, habe ich ein Testrepository im github angelegt: TODO to Issue.
Vorbereitungen
Um TODO to Action Issues in deinem Repository zu aktivieren, musst du zwei Dinge tun:
Erstellen der Workflowdatei
Anlegen des TODO Labeles
YAML-Workflowdatei
Um den Workflow einzurichten, brauchst du nur eine YML-Datei in dem Ordner .github/workflows erstellen. Wahrscheinlich existiert der Ordner noch nicht. Dann lege ihn einfach an.
Die wichtige Anpassung für ABAP-Programme ist die Definition des COMMENT_MARKERs mit „. Da das doppelte Anführungszeichen ein Sonderzeichen ist, muss es mit dem Escape-Zeichen ‚\‘ erstellt werden.
Die Definition des * als Kommentarmarker hat bei mir zu einem Syntaxfehler im YAML-File geführt. Das doppelte Anführungszeichen finde ich aber eh sinnvoller.
Label TODO
Wechsele in deinem Repository zu der Registerkarte Issues und lege ein Issue an. Auf der rechten Seite kannst du aus den verfügbaren Labels auswählen. TODO existiert noch nicht und du musst es anlegen.
Anwendung
Um die Anlage eines Issues zu prüfen, habe ich ein einfaches Programm erstellt und einen TODO-Kommentar eingefügt:
LOOP AT langs INTO DATA(lang). "TODO use ALV grid for display WRITE: / lang-sptxt. ENDLOOP.
Beim Einchecken mit abapGit wird auf das Ereignis push reagiert und der Workflow startet:
Registerkarte „Actions“ direkt nach dem push
Die Verarbeitung dauert etwa eine Minute. Danach ist das Issue in der Registerkarte Issues vorhanden:
Die automatisch generierten Aufgaben in der Registerkarte „Issues“
Features
In der Action TODO to Issue Action gibt es noch ein paar zusätzliche Features, die sehr interessant sind:
Closing issues
Wenn das Label CLOSE_ISSUES auf TRUE gesetzt wird, dann erkennt die Action, wenn ein TODO aus dem Quelltext entfernt wurde. Was will man mehr?
Multiline Todos
Mit mehrzeiligen Kommentaren kann ein Issue mit einer Beschreibung angelegt werden:
"TODO Come up with a more imaginative greeting
"Everyone uses hello world and it's boring.
Dynamic Labels
Fügt man dem TODO-Kommentar das Schlüsselwort labels: voran, dann kann man zusätzliche Tags (Labels) mitgeben:
labels: enhancement, help wanted
Fazit
Ich finde, dass das Feature TODO to Action Issue extrem hilfreich ist. Die Verwaltung funktioniert nach ersten Tests sehr gut. Im Issue selbst ist gut erkenntlich, in welchem Zusammenhang das todo steht. Ein direkter Link zum Sourcecode ist ebenfalls vorhanden. Auch das Löschen von Todos funktioniert gut. Änderungen im Quelltext erzeugen keine doppelten Issues. Wenn ein Issue geschlossen wurde, wird es erneut angelegt, sobald ein Issue mit dem gleichen Text wieder auftaucht.
Viel Spaß beim Verwaltungen deiner Projekte mit Issues!
Als SAP-Programmierer hat man häufig mit Datenstrukturen zu tun, die aus einer Belegnummer und einer Positionsnummer bestehen. Hieraus ergibt sich dann häufig die Aufgabenstellung, alle Belegnummern in einer separaten Tabelle zu sammeln, also auf Belegnummer zu aggregieren.
Der folgende Code-Schnipsel erledigt das für eine Tabelle mit Hilfe der VALUE-Anweisung in Verbindung mit FOR und GROUPS.
Zuerst die Datenstruktur der Tabelle, die aus Belegnummer (Document = D) und Position ( Item = IT) besteht.
TYPES: BEGIN OF _dit,
doc TYPE n LENGTH 10,
itm TYPE posnr,
END OF _dit,
_dits TYPE SORTED TABLE OF _dit WITH UNIQUE KEY doc itm.
Zum Testen fülle ich die Tabelle mit ein paar Testdaten:
TYPES: BEGIN OF _doc,
no TYPE n LENGTH 10,
END OF _doc,
_docs TYPE SORTED TABLE OF _doc WITH UNIQUE KEY no.
Das Coding für die Aggregation lautet folgendermaßen:
DATA(docs) = VALUE _docs(
FOR GROUPS d OF line IN dits
GROUP BY line-doc ( no = d ) ).
Es ist so kompakt, dass man es auch noch vertretbar in einer Zeile stehen lassen kann.
DATA(docs) = VALUE _docs(
FOR GROUPS d OF line IN dits
GROUP BY line-doc ( no = d ) ).
DATA vs. FIELD-SYMBOLS
Die Syntax lässt zwei Schreibweisen zu: einmal mit einer Workarea (DATA) und einmal mit Feldsymbolen. In meinem Beispiel unterscheidet sich die Schreibweise also nur für ein Element:
LINE (DATA)
<LINE> (Field-Symbols)
Die Verwendung von Field-Symbols ist generell schneller. Die Performance hängt stark davon ab, wie breit die Tabelle ist. Bei meinem Bespiel, das nur die beiden Felder DOC und ITM enthält, ist der Vorsprung von Field-Symbols nur minimal. Ändere ich die Breite der Struktur jedoch, indem ich z.B. die Tabelle VBAP einbinde (ca. 3700 Zeichen breit), dann verlängert sich die Laufzeit bei Field-Symbols um ca. 1/3. Bei DATA ist die Laufzeit mehr als drei Mal so hoch!
Empfehlung also: Verwende immer die Variante mit Field-Symbols! Einen praktischen Nutzen habe ich bisher noch nicht finden können. Normalerweise kann man bei der Verwendung von Feldsymbolen mit ihrer Hilfe die zugrunde liegenden Daten direkt verändern. Bei dieser Variante mit GROUPS ist das meines Wissens jedoch nicht möglich.
Gruppierung mit LOOP
In diesem Artikel habe ich beschrieben, wie die Gruppierung bei einer LOOP-Schleife funktioniert.
Jobverarbeitung is in vielen Bereichen und für viele Funktionalitäten wichtig. Jobeinplanungen sind eine gängige Technik, die jahrzehntelang erprobt ist. Kürzlich bin ich in einem Projekt auf eine Anforderung gestoßen, die nicht so ohne Weiteres mit den Standardmitteln der Jobeinplanung möglich war.
Anforderung
Es soll ein periodischer Job mit mehreren Steps eingeplant werden, deren Verarbeitungsschritte (Steps) unabhängig voneinander sind. Unabhängig heißt in diesem Fall, dass ein Verarbeitungsschritt, der durch einen Kurzdump abbricht, die Ausführung der anderen Schritte nicht beeinträchtigen soll.
Möglichkeit 1
Die erste auf der Hand liegende Möglichkeit ist, einen Job periodisch einzuplanen, der einzelne Verarbeitungsschritte enthält. Leider bricht der gesamte Job ab, wenn einer der Verarbeitungsschritte durch einen Shortdump abbricht. Diese Möglichkeit kommt also nicht in Betracht.
Möglichkeit 2
Die zweite Möglichkeit wäre, die einzelnen Verarbeitungsschritte als einzelne Jobs zu definieren und jeweils den einen Job als Vorgänger des jeweils nächsten Jobs zu definieren. Aber auch hier gibt es eine Einschränkung: Diese Jobs können nicht periodisch geplant werden.
Lösung
Die Lösung für mich war in diesem Fall natürlich ein ABAP Programm. Das folgende Programm kann man periodisch als Job einplanen und hier bis zu fünf Programme unabhängig voneinander, jeweils mit Vorgänger – Nachfolger-Beziehung, definieren. Wenn das Programm ausgeführt wird, startet es das erste Programm sofort als Job. Das zweite Programm wird gestartet, wenn der Job für das erste Programm beendet wurde und so weiter. Dabei ist es unerheblich, ob der Vorgängerjob regulär beendet wurde oder abgebrochen ist.
Das Programm ist fest auf fünf Verarbeitungsschritte ausgelegt. Es kann einfach auf weitere Steps erweitert werden. Sofern noch sehr viele Verarbeitungsschritte verwaltet werden sollten, sollte man die Verarbeitung dynamisch programmieren. Dafür müsste man jedoch auch die Eingabe für die Definition der Verarbeitungsschritte so anpassen, dass die einzelnen Programme in einer Liste (Grid) eingegeben werden können.
Report mit beispielhaften Reporteinplanungen
Coding
REPORT z_schedule_05_jobs_periodic.
PARAMETERS nam1 TYPE btcjob OBLIGATORY.
PARAMETERS rep1 TYPE syrepid OBLIGATORY.
PARAMETERS var1 TYPE raldb_vari OBLIGATORY.
SELECTION-SCREEN SKIP 1.
PARAMETERS nam2 TYPE btcjob.
PARAMETERS rep2 TYPE syrepid.
PARAMETERS var2 TYPE raldb_vari.
SELECTION-SCREEN SKIP 1.
PARAMETERS nam3 TYPE btcjob.
PARAMETERS rep3 TYPE syrepid.
PARAMETERS var3 TYPE raldb_vari.
SELECTION-SCREEN SKIP 1.
PARAMETERS nam4 TYPE btcjob.
PARAMETERS rep4 TYPE syrepid.
PARAMETERS var4 TYPE raldb_vari.
SELECTION-SCREEN SKIP 1.
PARAMETERS nam5 TYPE btcjob.
PARAMETERS rep5 TYPE syrepid.
PARAMETERS var5 TYPE raldb_vari.
CLASS lcx_job DEFINITION INHERITING FROM cx_static_check.
ENDCLASS.
CLASS lcl_define_job DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING
iv_name TYPE btcjob
iv_report TYPE syrepid
iv_variant TYPE raldb_vari
RAISING
lcx_job.
METHODS start
IMPORTING
iv_pred_jobcount TYPE btcjobcnt OPTIONAL
iv_pred_jobname TYPE btcjob OPTIONAL
RAISING
lcx_job.
DATA mv_jobcount TYPE btcjobcnt.
DATA mv_jobname TYPE btcjob.
DATA mv_released TYPE abap_bool.
ENDCLASS.
CLASS lcl_define_job IMPLEMENTATION.
METHOD constructor.
mv_jobname = iv_name.
CALL FUNCTION 'JOB_OPEN'
EXPORTING
jobname = iv_name
IMPORTING
jobcount = mv_jobcount
EXCEPTIONS
cant_create_job = 1
invalid_job_data = 2
jobname_missing = 3
OTHERS = 4.
IF sy-subrc = 0.
CALL FUNCTION 'JOB_SUBMIT'
EXPORTING
authcknam = sy-uname
jobcount = mv_jobcount
jobname = iv_name
report = iv_report
variant = iv_variant
EXCEPTIONS
bad_priparams = 1
bad_xpgflags = 2
invalid_jobdata = 3
jobname_missing = 4
job_notex = 5
job_submit_failed = 6
lock_failed = 7
program_missing = 8
prog_abap_and_extpg_set = 9
OTHERS = 10.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE lcx_job.
ENDIF.
ENDIF.
ENDMETHOD.
METHOD start.
CALL FUNCTION 'JOB_CLOSE'
EXPORTING
jobcount = mv_jobcount
jobname = mv_jobname
pred_jobcount = iv_pred_jobcount
pred_jobname = iv_pred_jobname
strtimmed = SWITCH #( iv_pred_jobcount WHEN space THEN 'X' ELSE space )
IMPORTING
job_was_released = mv_released
EXCEPTIONS
cant_start_immediate = 1
invalid_startdate = 2
jobname_missing = 3
job_close_failed = 4
job_nosteps = 5
job_notex = 6
lock_failed = 7
invalid_target = 8
OTHERS = 9.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE lcx_job.
ENDIF.
ENDMETHOD.
ENDCLASS.
START-OF-SELECTION.
TRY.
DATA(job1) = NEW lcl_define_job(
iv_name = nam1
iv_report = rep1
iv_variant = var1 ).
job1->start( ).
IF rep2 IS NOT INITIAL.
DATA(job2) = NEW lcl_define_job(
iv_name = nam2
iv_report = rep2
iv_variant = var2 ).
job2->start(
iv_pred_jobname = job1->mv_jobname
iv_pred_jobcount = job1->mv_jobcount ).
ENDIF.
IF rep3 IS NOT INITIAL.
DATA(job3) = NEW lcl_define_job(
iv_name = nam3
iv_report = rep3
iv_variant = var3 ).
job3->start(
iv_pred_jobname = job2->mv_jobname
iv_pred_jobcount = job2->mv_jobcount ).
ENDIF.
IF rep4 IS NOT INITIAL.
DATA(job4) = NEW lcl_define_job(
iv_name = nam4
iv_report = rep4
iv_variant = var4 ).
job4->start(
iv_pred_jobname = job3->mv_jobname
iv_pred_jobcount = job3->mv_jobcount ).
ENDIF.
IF rep5 IS NOT INITIAL.
DATA(job5) = NEW lcl_define_job(
iv_name = nam5
iv_report = rep5
iv_variant = var5 ).
job5->start(
iv_pred_jobname = job4->mv_jobname
iv_pred_jobcount = job4->mv_jobcount ).
ENDIF.
CATCH lcx_job.
MESSAGE 'Error job creation!' TYPE 'I'.
ENDTRY.
Seit einiger Zeit beschäftige ich mich mit den Geschäftspartnern im S/4 und deren programmatischen Anlage, Änderung sowie der Erweiterung der einzelnen Sichten. In meinem aktuellen Projekt führt kein Weg an der Anlage der Business Partner per Programm vorbei, da die Daten über eine Schnittstelle kommen und verarbeitet werden müssen.
Wie sieht es aber aus, wenn Geschäftspartner zwar massenhaft aber nicht automatisch geändert werden müssen, zum Beispiel durch den Fachbereich? In der aktuellen Situation, in der wir uns befinden, müssen Stammdatenänderungen mitunter schnell erfolgen können. Die IT kann dies oftmals nicht leisten, denn der Aufwand ist erheblich und die Vorlaufzeit mitunter katastrophal. Und wenn dann noch regelmäßige, weit auseinander liegende Releasezyklen dazu kommen (also Produktivsetzung nur alle drei Monate), dann kann locker ein dreiviertel Jahr ins Land gehen, bis der Anwender sein Programm hat.
Mit Simdia² können viele Aufgaben vom Anwender selbst erledigt werden. Die Einarbeitungszeit für Simdia² ist gering, die Möglichkeiten sind vielfältig.
Ich möchte heute einmal vergleichen, wie sich meine Programmierung und Simdia² im Vergleich schlagen.
Disziplin: Geschäftszeiten ändern
Die Änderung von Geschäftspartnern ist eine sehr anspruchsvolle Aufgabe. Das Objekt „Business Partner“ ist komplex und sehr variabel. Es gibt für einzelne Funktionen entsprechende Funktionsbausteine (die man jedoch erst einmal finden muss) oder die zentrale Funktion CL_MD_BP_MAINTAIN=>MAINTAIN, die mit einem komplexen Datentypen gefüttert werden muss.
Aktuell und wohl auch noch viele Monate später sind die genauen Öffnungszeiten von Partnern wichtig. Kunden haben eventuell eingeschränkte und häufig wechselnde Warenannahmezeiten und auch Lieferanten sind nicht ständig zu den normalen Geschäftszeiten erreichbar.
Geschäftszeiten eines Geschäftspartners
Aus diesem Grund denke ich, dass die Funktion der Geschäftszeiten eine sehr wichtige Rolle spielt oder spielen kann. Der Fachbereich erfragt die Geschäftszeiten seiner Partner jedoch in der Regel nicht einzeln um sie dann in den Geschäftspartner einzutragen, sondern erfragt diese eventuell über ein Online-Formular. Oder die Daten werden von Kollegen und Kolleginnen gesammelt, die diese dann in eine Excel-Tabelle eintragen. Am Ende wäre es also für den Fachbereich sehr hilfreich, wenn er ein Programm hätte, mit dem die aktuellen Geschäftszeiten automatisiert in die betroffenen Partner eingetragen werden könnte.
Ablauf in der IT
Auch wenn inzwischen viel über Continuous Integration und Continuous Delivery geredet wird, sieht der Alltag in der SAP-IT immer noch anders aus. Natürlich muss der Fachbereich eine Anforderung stellen. Alleine das ist häufig nicht ganz einfach. Die Anforderung muss geprüft und die technische Umsetzung geplant werden. Wenn dann irgendwann ein Programmierer für die Entwicklung eingeplant wurde, müssen weitere Hürden genommen werden:
Es müssen die richtigen Bausteine für die Aufgabe gefunden werden
Selbst wenn die Programmierende Person sich mit der Programmierung der Geschäftspartner auskennt, muss eventuell erst einmal recherchiert werden.
Dann muss die Excel-Datei eingelesen werden.
entweder wird tatsächlich eine recht aufwendige Programmierung zum Einlesen der Excel-Datei entwickelt
oder man macht es sich leicht und liest eine CSV-Datei ein. Dann muss der Fachbereich die Datei erst als CSV speichern, bevor die Datei verarbeitet werden kann
Es müssen eventuell Berechtigungen für die neue Transaktion beantragt und zugeordnet werden
Sobald das Programm fertiggestellt wurde, muss der Fachbereich testen. Meiner Erfahrung nach funktioniert ein Programm selten nach dem ersten Wurf.
Häufig kommen noch Urlaub oder Krankheit entweder aus dem Fachbereich oder in der IT hinzu, was die Fertigstellung des Programms verzögert…
Und wenn dann noch lange Releasezyklen dazu kommen, dann ist in manchen Firmen die Pandemie bereits vorbei, bevor der Fachbereich das Programm zur Arbeitserleichterung erhalten hat.
Änderung der Geschäftszeiten mit der Business-Partner-API
Da ich mich in letzter Zeit öfters mit der API zu den Geschäftspartnern beschäftigt habe (Klasse CL_MD_BP_MAINTAIN), wollte ich wissen, wie lange ich alleine für die Programmierung zur Änderung der Geschäftszeiten benötige.
Es waren etwa vier Stunden nur um herauszufinden, welche Parameter bei den Geschäftszeiten benötigt werden. Viele Parameter gingen häufig nicht aus den definierten Datenelementen hervor und auch nicht aus der Anwendung selbst.
Beispielsweise ist der Typ (Feldname TYPE), der definiert, welche Regel für die Geschäftszeiten gilt (Täglich, Wöchentlich, Monatlich) vom Typ CHAR2. Ich musste also erst herausfinden, was mit TYPE eigentlich gemeint ist. Welche Id für die Art der Geschäftszeiten (Anrufzeiten, Warenannahmezeiten, Besuchszeiten) verwendet werden muss, habe ich eher durch Zufall herausgefunden. Der SCHEDULE_TYPE hat zwar ein eigenes Datenelement, das jedoch weder Festwerte hat noch auf eine Prüftabelle verweist.
Durch meine Arbeit mit den Geschäftspartnern, beziehungsweise mit der Klasse CL_MD_BP_MAINTAIN, hatte ich bereits eine passende Fehlerbehandlung.
Coding
Der folgende Quelltext legt zu einem Business Partner die Warenannahmezeiten für Montag bis Freitag mit den jeweils übergebenen Von- und Bis-Zeiten an.
SELECT SINGLE partner_guid FROM but000 INTO @DATA(guid) WHERE partner = @partner.
CHECK sy-subrc = 0.
"fill data structure with relevant customer data
DATA(bp_data) = VALUE cvis_ei_extern(
partner-header-object_task = 'M'
partner-header-object = 'BUS1006' "Business Partner
partner-header-object_instance = VALUE #(
bpartner = partner
bpartnerguid = guid )
partner-central_data-business_hour = VALUE #( current_state = ' '
business_hours = VALUE #(
task = 'M'
data_key-schedule_type = 'B' "Warenannahme TAB TB049
data-weekly = VALUE #(
( weeks = 1
fcalid = '01' "Deutschland
conflicts = '0' "Keine Ausnahmen
type = 'W'
start_date = sy-datum
end_date = sy-datum + 365
monday = 'X' monda_from = time_from monday_to = time_to mond_tzone = 'CET'
tuesday = 'X' tuesd_from = time_from tuesday_to = time_to tues_tzone = 'CET'
wednesday = 'X' wedne_from = time_from wednesd_to = time_to wedn_tzone = 'CET'
thursday = 'X' thurs_from = time_from thursda_to = time_to thur_tzone = 'CET'
friday = 'X' frida_from = time_from friday_to = time_to frid_tzone = 'CET'
saturday = ' ' satur_from = VALUE #( ) saturda_to = VALUE #( ) satu_tzone = ''
sunday = ' ' sunda_from = VALUE #( ) sunday_to = VALUE #( ) sund_tzone = '' )
)
( ) ) ) ) .
TRY.
SET UPDATE TASK LOCAL.
cl_md_bp_maintain=>maintain(
EXPORTING
i_data = VALUE #( ( bp_data ) )
IMPORTING
e_return = DATA(lt_return) ).
IF lt_return IS INITIAL.
sy-subrc = 1.
ELSE.
READ TABLE lt_return[ 1 ]-object_msg INTO ls_return WITH KEY type = 'A'.
IF sy-subrc > 0.
READ TABLE lt_return[ 1 ]-object_msg INTO ls_return WITH KEY type = 'E'.
ENDIF.
ENDIF.
IF sy-subrc <> 0.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'.
ENDIF.
CATCH zcx_bc_bp.
RETURN.
ENDTRY.
In meinem Beispiel habe ich noch nicht das Einlesen und prüfen der Excel-Tabelle programmiert, keine Darstellung der eingelesenen Daten und keine Fehlerbehandlung für den Anwender.
Deswegen würde ich schätzen, dass eine ordentliche Entwicklung eines Programms, dass die Geschäftszeiten anlegt und entsprechend Anwenderfreundlich ist, mindestens eine Woche Entwicklungszeit benötigt.
Und was ist jetzt mit Simdia²?
Das SAP-Addon Simdia² von Ersasoft wird vom SAP-Anwender bedient. Im Grunde programmiert der Anwender einen Batchinput auf die Transaktion, die er gerade benötigt. In unserem Fall ist es die Transaktion „BP – Geschäftspartner“.
Der Anwender legt eine Excel-Datei mit den Daten an, die für die Transaktion benötigt werden. Dabei ist es wichtig, dass die Feldnamen eindeutig sind, damit Simdia² die Felder eindeutig zuordnen kann.
Eingabedatei
In unserem Fall benötigen wir eine Datei mit Spalten für die Felder Geschäftspartnernummer und jeweils drei Felder für jeden Tag:
Tag auswählen (X/space)
Uhrzeit von
Uhrzeit bis
Nach einer kurzen Einführung in die Funktionen und die Arbeitsweise von Simdia² ist der Anwender in der Lage, die Transaktion mit den Daten aus der Excel-Datei zu befüllen. Der große Vorteil ist, dass der Anwender seine Transaktion genau kennt und genau weiß, welche Daten benötigt werden.
Bei der Aufzeichnung muss man sich nach den Besonderheiten der Transaktion richten. Bei der Transaktion BP ist zum Beispiel eine Besonderheit, dass die jeweils letzte Sicht des Geschäftspartners bei Auswahl eines anderen Geschäftspartners erneut präsentiert wird.
Aufzeichnung
Die Aufzeichnung ist sehr einfach und intuitiv. Simdia² und die gewünschte Transaktion müssen gestartet werden. Danach muss die Excel-Datei mit den Daten ausgewählt werden. Mit dem Aufzeichungsassistenten wird die Aufzeichnung gestartet. Alle Felder der Excel-Tabelle werden im Simdia²-Fenster angezeigt. Der Anwender muss dann die einzelnen Sichten auswählen und die Felder anklicken und mit einem Klick auf die Spalte aus der Excel-Tabelle im Simdia²-Fenster die Daten übertragen. Die Aufzeichnung kann natürlich gespeichert werden.
Felder aus der Excel-Tabelle
Daten importieren
Zum Import der Daten aus einer Excel-Tabelle müssen die Excel-Datei und die passende Aufzeichnung geladen werden. Die passende Transaktion muss gestartet werden und schon können die Daten importiert werden. Am Anfang kann man den Import Schritt für Schritt überwachen um sicher zu gehen, dass die Aufzeichnung wirklich korrekt ist.
Wenn alles passt, dann kann der Turbo eingeschaltet werden und die Daten werden in Windeseile importiert.
Simdia² integriert in den SAPGUI
Die importierten Daten werden protokolliert. Kann ein Datensatz nicht gespeichert werden, dann wird dies im Protokoll festgehalten.
Excel-Datei mit Geschäftszeiten und Protokoll aus Simdia²
Fazit
Bei der ABAP-Programmierung habe ich dir eine ungefähre Zeitangabe gegeben, wie lange die Vorarbeit für ein richtiges Programm gedauert hat. Jetzt bist du sicherlich gespannt, wie lange die Aufzeichnung mit Simdia² gedauert hat? Nun, während ich diesen Artikel schreibe, habe ich die Aufzeichnung erstellt und ein paar Datensätze importiert und ein paar Screenshots gemacht. Ich musste die Aufzeichnung mehrere Male machen, weil ich erst später darüber gestolpert bin, dass ich für das Aktivieren der einzelnen Tage ein separates Feld benötige. Das alles hat circa zwei Stunden gedauert. Mit etwas Übung ist man innerhalb von 10 Minuten mit einer Aufzeichnung fertig.
Es ist also auf jeden Fall eine Alternative zur ABAP-Programmierung. Besonders dann, wenn die IT überlastet ist und der Fachbereich dringend eine Lösung für zeitraubende, wiederkehrende Arbeiten benötigt.
Sicherheit
Sicherheit und Sicherheitslücken sind ein wichtiges Thema in der IT. Simdia² hat diesbezüglich einen enormen Vorteil: Der Anwender kann nur die Daten ändern, für die er auch die Berechtigung hat. Das ist bei SAP-Programmen nicht immer der Fall und wird gerne übersehen.
Seit SAP Release 7.40 gibt es in der F4-Suchhilfe die erweiterte Option „Vorschlagssuche auf Eingabefeldern“. Im englischen wird es auch type-ahead oder proposal search genannt. Sie zeigt dem Anwender sofort nach Eingabe eines Zeichens in das Eingabefeld mögliche Treffer an. Je nach Datenbank kann in der Suchhilfe auch die Spaltenübergreifende Volltextsuche (fuzzy search) aktiviert werden.
Die Vorschlagssuche wird in den gängigsten SAP-Transaktionen verwendet.
Vorschlagssuche in Transaktion SE24
Einstellung im SAPGUI
Man kann die Vorschlagssuche in den SAPGUI-Optionen abschalten, denn die Verwendung ist manchmal problematisch. So wird die Ergebnismenge zu langsam eingeblendet oder das eingegebene Wort kommt als auch die Suche kommen durcheinander, wenn man das Wort zu schnell editiert.
Folgende Einstellung im SAPGUI steuert die Verwendung der Vorschlagssuche:
Aktivierte Vorschlagssuche im SAPGUI
Eigene Vorschlagsliste
In der Regel basieren die Einträge auf der der Suchhilfe zu Grunde liegenden Selektionsmethode. Ich zeige dir eine Methode, wie du die Vorschlagssuche für eine dynamisch zusammengestellte Liste. In meinem Beispiel habe ich die 30 größten deutschen Städte sowie deren Bundesländer ermittelt. Aus diesen Städten soll sich der Anwender eine Stadt auswählen:
Einfaches Eingabefeld
Was musst du tun, um die dynamische Vorschlagssuche zu realisieren? du benötigst im Grunde nur einen Suchhilfe-Exit, der die folgenden Dinge tut:
Ermittlung der gültigen Werte
Einschränkung der Werte
Zurückstellen der in Frage kommenden Werte in die Trefferliste
Die Frage ist natürlich, wie kommen die in Frage kommenden Werte in die Suchhilfe? Der mir einfachste Weg ist der Austausch über das SAP-Memory. Die Daten werden vom Programm per EXPORT TO MEMORY in den Speicher geschrieben und von der Suchhilfe mit IMPORT FROM MEMORY wieder ausgelesen.
Suchhilfe-Exit
Ein Suchhilfe-Exit greift bei den verschiedenen Aktionen der Suchhilfe-Verarbeitung ein. Lege eine neue Funktionsgruppe an und kopiere den Baustein F4IF_SHLP_EXIT_EXAMPLE auf einen Namen deiner Wahl. Ich habe den Baustein Z_SHLP_EXIT_DYNAMIC genannt.
Bei der Ausführung der F4-Wertsuchhilfe werden mehrere Verarbeitungsschritte durchlaufen. Der Funktionsbaustein wird bei jedem dieser Schritte aufgerufen. Was im jeweiligen Schritt getan werden muss, muss über den Eingabeparameter CALLCONTROL-STEP gesteuert werden.
Der Verarbeitungsschritt, der wohl am häufigsten verwendet wird, ist „SELECT“. Auch wir nutzen diesen Schritt, um die Werteliste zu ermitteln und zur Verfügung zu stellen.
Funktionsbaustein Z_SHLP_EXIT_DYNAMIC
Im folgenden Coding importiere ich die Werteliste, die vom Programm per SAP-Memory übergeben wurden. Danach lese ich den Wert, den der Anwender bereits eingegeben hat und prüfe damit die einzelnen Einträge. Die Werte, die nicht passen, lösche ich aus der Tabelle.
Die am Ende verbleibenden Werte werden Spaltenweise mit dem Funktionsbaustein F4UT_PARAMETER_RESULTS_PUT an die Ergebnistabelle übertragen.
FUNCTION Z_SHLP_EXIT_DYNAMIC .
*"----------------------------------------------------------------------
*"*"Local Interface:
*" TABLES
*" SHLP_TAB TYPE SHLP_DESCT
*" RECORD_TAB STRUCTURE SEAHLPRES
*" CHANGING
*" REFERENCE(SHLP) TYPE SHLP_DESCR
*" REFERENCE(CALLCONTROL) LIKE DDSHF4CTRL STRUCTURE DDSHF4CTRL
*"----------------------------------------------------------------------
TYPES: BEGIN OF ts_value,
line TYPE text40,
text TYPE text40,
END OF ts_value.
DATA values TYPE STANDARD TABLE OF ts_value WITH EMPTY KEY.
IF callcontrol-step = 'SELECT'.
IMPORT values TO values FROM MEMORY ID 'Trcktrsr'.
DATA(selval) = to_upper( shlp-interface[ shlpfield = 'VALUE' ]-value ).
LOOP AT values ASSIGNING FIELD-SYMBOL(<val>).
DATA(val) = to_upper( <val>-line ).
IF NOT val CP selval.
DELETE values INDEX sy-tabix.
ENDIF.
ENDLOOP.
CALL FUNCTION 'F4UT_PARAMETER_RESULTS_PUT'
EXPORTING
parameter = 'VALUE'
fieldname = 'LINE' " Name of the source field in SOURCE_TAB
TABLES
shlp_tab = shlp_tab " Table of Elementary Search Helps
record_tab = record_tab " Hit list
source_tab = values
CHANGING
shlp = shlp " Single (Current) Search Help
callcontrol = callcontrol " Control of the F4 process
EXCEPTIONS
parameter_unknown = 1 " No suitable parameter of the search help
OTHERS = 2.
IF sy-subrc > 0.
RETURN.
ELSE.
CALL FUNCTION 'F4UT_PARAMETER_RESULTS_PUT'
EXPORTING
parameter = 'TEXT'
fieldname = 'TEXT' " Name of the source field in SOURCE_TAB
TABLES
shlp_tab = shlp_tab " Table of Elementary Search Helps
record_tab = record_tab " Hit list
source_tab = values
CHANGING
shlp = shlp " Single (Current) Search Help
callcontrol = callcontrol " Control of the F4 process
EXCEPTIONS
parameter_unknown = 1 " No suitable parameter of the search help
OTHERS = 2.
IF sy-subrc > 0.
RETURN.
ENDIF.
callcontrol-step = 'DISPLAY'.
ENDIF.
ENDIF.
ENDFUNCTION.
Definition der Suchhilfe
Nun musst du noch die Suchhilfe definieren, in der du die Parameter der Suchhilfe definierst und die Datenbeschaffung über den Suchhilfe-Exit steuerst.
Den Suchhilfe-Exit legst du in Transaktion SE11 an. Aktiviere die Option Vorschlagssuche auf Eingabefeldern. Trage den Namen des Funktionsbausteins in das Feld Suchhilfe-Exit ein und definiere die Parameter:
Suchhilfe ZDYNFUZZ
Verwendung im Programm
Sobald du die Suchhilfe angelegt hast, kannst du sie im Programm verwenden, indem du diese zur Verwendung im Feld angibst:
PARAMETERS p_val TYPE text40 MATCHCODE OBJECT zdynfuzz.
Bei Initialisierung ermitteln wir die möglichen Werte und schreiben sie ins SAP-Memory. Das war auch schon alles.
REPORT.
PARAMETERS p_val TYPE text40 MATCHCODE OBJECT zdynfuzz.
INITIALIZATION.
TYPES: BEGIN OF ts_value,
line TYPE text40,
text type text40,
END OF ts_value.
DATA gt_values TYPE STANDARD TABLE OF ts_value WITH EMPTY KEY.
gt_values = VALUE #(
( line = 'Berlin' text = 'Berlin' )
( line = 'Hamburg' text = 'Hamburg' )
( line = 'München' text = 'Bayern' )
( line = 'Köln' text = 'Nordrhein-Westfalen' )
( line = 'Frankfurt am Main' text = 'Hessen' )
( line = 'Stuttgart' text = 'Baden-Württemberg' )
( line = 'Düsseldorf' text = 'Nordrhein-Westfalen' )
( line = 'Leipzig' text = 'Sachsen' )
( line = 'Dortmund' text = 'Nordrhein-Westfalen' )
( line = 'Essen' text = 'Nordrhein-Westfalen' )
( line = 'Bremen' text = 'Bremen' )
( line = 'Dresden' text = 'Sachsen' )
( line = 'Hannover' text = 'Niedersachsen' )
( line = 'Nürnberg' text = 'Bayern' )
( line = 'Duisburg' text = 'Nordrhein-Westfalen' )
( line = 'Bochum' text = 'Nordrhein-Westfalen' )
( line = 'Wuppertal' text = 'Nordrhein-Westfalen' )
( line = 'Bielefeld' text = 'Nordrhein-Westfalen' )
( line = 'Bonn' text = 'Nordrhein-Westfalen' )
( line = 'Münster' text = 'Nordrhein-Westfalen' )
( line = 'Karlsruhe' text = 'Baden-Württemberg' )
( line = 'Mannheim' text = 'Baden-Württemberg' )
( line = 'Augsburg' text = 'Bayern' )
( line = 'Wiesbaden' text = 'Hessen' )
( line = 'Mönchengladbach' text = 'Nordrhein-Westfalen' )
( line = 'Gelsenkirchen' text = 'Nordrhein-Westfalen' )
( line = 'Braunschweig' text = 'Niedersachsen' )
( line = 'Aachen' text = 'Nordrhein-Westfalen' )
( line = 'Kiel' text = 'Schleswig-Holstein' )
( line = 'Chemnitz' text = 'Sachsen' ) ).
EXPORT values FROM gt_values TO MEMORY ID 'Trcktrsr'.
Ergebnis
Nun bekommt der Anwender alle Werte direkt angezeigt, die zur bereits getätigten Eingabe passen:
Aktiviert Vorschlagssuche
Die Einträge können mit den Pfeiltasten direkt ausgewählt werden.
Eine Frage, die Thomas Binder aus Berlin nicht nur bezüglich seines Immunsystems beschäftigt hat, sondern auch bei der Ausgabe von Daten im ALV-Grid. Er ist dabei über eine böse Falle bei den Darstellungsmöglichkeiten des ALV-Grid gestolpert, die sich einerseits logisch erklären lässt, anderseits jedoch Fragen aufwirft, warum dieser Grund im ALV-Grid nicht einheitlich behandelt wird.
Darstellung von Zahlen
Thomas hatte einen stinknormalen ALV-Grid programmiert, in dem positive als auch negative Zahlen angezeigt wurden. Alles hat super funktioniert. Das Grid war sicherlich nicht so simpel wie in folgendem Beispiel, aber es illustriert gut, worum es geht:
ALV-Grid mit positiven und negativen Zahlen
Nach einiger Zeit hat er aus der Fachabteilung die Rückmeldung bekommen, dass die Zahlen nicht stimmen würden. Er testete das zur Verfügung gestellte Beispiel und konnte keinen Fehler feststellen. Die Fachabteilung hat ihm dann einen Ausdruck zur gegeben, auf dem folgendes zu sehen war:
ALV Listausgabe
Der Report wurde im Hintergrund ausgeführt und das dargestellte Grid dementsprechend als ALV-Liste ausgegeben. Wie eindeutig zu sehen ist, ist der Wert TWO, der im Grid negativ ist, in der Listausgabe auf einmal positiv.
Ursache
Die Ursache für den Fehler war einigermaßen schnell gefunden: Das verwendete Datenelement BWERT ist mit der Domäne WERT7 definiert. In der Domäne ist festgelegt, dass der Typ keine Vorzeichen hat:
Domäne WERT7
Auch wenn die Ursache eindeutig ist und mit der Verwendung eines anderen Datenelementes das Problem gelöst werden konnte, ergibt sich nicht, warum der ALV-Grid die Werte unterschiedlich ausgibt.
Die Klasse CL_SALV_TABLE erfreut sich seit Jahren großer Beliebtheit. In erster Linie wahrscheinlich deswegen, weil man ohne Ermittlung des Feldkatalogs oder Vorgabe der zugrunde liegenden Datenstruktur eine Tabelle als Grid anzeigen kann. Das SALV verwendet intern jedoch immer noch die Klasse CL_GUI_ALV_GRID. Und es kann durchaus sein, dass man, nachdem man etwas mit dem SALV programmiert hat, an dieses Objekt herankommen möchte.
Wie das geht, zeige ich dir mit der folgenden Klasse.
Coding
CLASS lcl_access_salv DEFINITION INHERITING FROM cl_salv_model_list FINAL.
PUBLIC SECTION.
CLASS-METHODS:
get_cl_gui_alv_grid IMPORTING io_salv TYPE REF TO cl_salv_model_list
RETURNING VALUE(ro_grid) TYPE REF TO cl_gui_alv_grid.
ENDCLASS.
CLASS lcl_access_salv IMPLEMENTATION.
METHOD get_cl_gui_alv_grid.
DATA:lo_salv TYPE REF TO cl_salv_table.
*--------------------------------------------------------------------*
* Must be bound
*--------------------------------------------------------------------*
IF io_salv IS NOT BOUND.
RETURN.
ENDIF.
*--------------------------------------------------------------------*
* Adapter must be bound
*--------------------------------------------------------------------*
IF io_salv->r_controller IS NOT BOUND
OR io_salv->r_controller->r_adapter IS NOT BOUND.
MESSAGE 'Minor programming fault: Call GET_GRID_FROM_SALV after SALV->DISPLAY( )!'(001)
TYPE 'S' DISPLAY LIKE 'W'.
lo_salv ?= io_salv. " Calling method has cl_salv_table typed in interface
lo_salv->display( ).
ENDIF.
IF io_salv->r_controller IS NOT BOUND
OR io_salv->r_controller->r_adapter IS NOT BOUND.
RETURN. " Still not bound --> can't do anything
ENDIF.
*--------------------------------------------------------------------*
* If method not present or wrong return type exception will handle this and we return unbound grid
*--------------------------------------------------------------------*
TRY.
" Works for CL_SALV_FULLSCREEN_ADAPTER as well as for CL_SALV_GRID_ADAPTER
CALL METHOD io_salv->r_controller->r_adapter->('GET_GRID')
RECEIVING
value = ro_grid.
CATCH cx_root ##CATCH_ALL. " don't dump
RETURN.
ENDTRY.
ENDMETHOD.
ENDCLASS.
Obwohl der SAPGUI bereits als „tot“ eingestuft wird, finden sich immer wieder neue Erkenntnisse… In diesem Beitrag zeige ich dir, wie du einen SAPGUI-Modus in den Vordergrund bringen kannst, um die Aufmerksamkeit des Anwenders zurückzuerlangen.
Solange der Anwender nur SAPGUI-Modi anzeigt, wird der gewünschte SAPGUI-Modus in den Vordergrund kommen und die anderen Fenster überlagern. Sollte noch ein anderes Programm (Browser, Outlook, Excel etc.), dann wird der Wunsch nach Aufmerksamkeit durch ein Blinken des SAPGUI-Icons in der Taskleiste angezeigt.
Das folgende Programm kannst du in zwei verschiedenen Modi starten um den Effekt zu sehen. Die beiden Modi sollten möglichst übereinander (nicht nebeneinander) liegen, um den Effekt erkennen zu können.
Coding
REPORT zz_activate_gui_mode.
*--------------------------------------------------------------------*
* Programm in 2 verschiedenen Modi aufrufen und etwa 1 Sekunde versetzt starten!
*--------------------------------------------------------------------*
DO 10 TIMES.
CALL FUNCTION 'SAPGUI_PROGRESS_INDICATOR'
EXPORTING
text = |Modus: { sy-modno } Index: { sy-index }|.
CALL FUNCTION 'SAPGUI_SET_PROPERTY'
DESTINATION 'SAPGUI'
EXPORTING
property = 'ACTIVATE'
value = 'X'
EXCEPTIONS
OTHERS = 0.
WAIT UP TO 3 SECONDS.
ENDDO.
Unit Tests werden immer wichtiger. Je mehr ich sie selber verwende, desto mehr merke ich aber auch, wie schwierig es teilweise ist, vernünftige Unit Tests aufzubauen und sinnvoll zu testen. Seit ABAP Release 7.52 gibt es eine neue Möglichkeit, Daten für nit Tests zu „fälschen“: Das Open SQL Test Environment.
Mit diesem Framework ist es sehr einfach, der Datenbank manipulierte Daten unterzuschieben. Wie das im Detail geht, zeige ich dir hier.
Ausgangslage
Stellen wir uns vor, wir haben eine Anwendung, die zu einem Material die Materialart ermitteln soll und anhand der Materialart prüfen soll, ob eine bestimmte Aktion erlaubt ist oder nicht.
Die Klasse zur Ermittlung der Materialstammdaten könnte folgendermaßen aussehen:
CLASS mat DEFINITION.
PUBLIC SECTION.
METHODS check_usage
IMPORTING
matnr TYPE matnr
RETURNING
VALUE(result) TYPE abap_bool.
PRIVATE SECTION.
METHODS get_type
IMPORTING
matnr TYPE matnr
RETURNING
VALUE(mtart) TYPE mtart.
ENDCLASS.
CLASS mat IMPLEMENTATION.
METHOD check_usage.
IF get_type( matnr ) = 'FERT'.
result = abap_true.
ENDIF.
ENDMETHOD.
METHOD get_type.
SELECT SINGLE mtart FROM mara INTO mtart WHERE matnr = matnr.
ENDMETHOD.
ENDCLASS.
Mit der Methode CHECK_USAGE kann ich ermitteln, ob ein gegebenes Material für irgendeine hier nicht näher definierte Verwendung erlaubt ist oder nicht.
Unit Tests
Wenn ich im Test System zwei Materialien habe, mit denen ich das testen kann, dann könnten die Unit Tests folgendermaßen aussehen:
CLASS test DEFINITION FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA f_cut TYPE REF TO mat.
METHODS: setup.
METHODS: check_allowed FOR TESTING.
METHODS: check_forbidden FOR TESTING.
ENDCLASS.
CLASS test IMPLEMENTATION.
METHOD class.
f_cut = NEW #( ).
ENDMETHOD.
METHOD check_allowed.
cl_abap_unit_assert=>assert_equals(
act = f_cut->check_usage( 'MAT-2277' )
exp = abap_true ).
ENDMETHOD.
METHOD check_forbidden.
cl_abap_unit_assert=>assert_equals(
act = f_cut->check_usage( 'MAT-565' )
exp = abap_false ).
ENDMETHOD.
ENDCLASS.
Demzufolge ist das auf der Datenbank vorhandene Material MAT-2277 ein Material vom Typ „FERT“ und das Material MAT-565 hat einen anderen Typ.
Problem Stammdaten
Auf Stammdaten darf man sich allerdings nicht verlassen. Diese werden – gerade in einem Entwicklungssystem – gerne mal geändert oder gelöscht. Bei einem Unit Test muss ich mich jedoch auf die Daten verlassen können. Das kann ich nur, wenn ich diese fest mitgebe.
oSQL Test Framework
Mit dem oSQL Test Environment kann ich das sehr komfortabel tun. Und zwar folgendermaßen:
Es muss mit der Methode CL_OSQL_TEST_ENVIRONMENT=>CREATE eine Instanz erstellt werden. Dieser Instanz gebe ich die Tabellennamen mit, die ich beeinflussen möchte. Zusätzlich gebe ich genau die Daten mit, die der Datenbank vorgegaukelt werden sollen. In diesem Beispiel verwende ich das Material FAKEMAT1 als Testmaterial für FERT und FAKEMAT2 als Testmaterial für ein Material ungleich FERT.
Die Anwendung ist wirklich denkbar einfach. Keine Ahnung, was da im Hintergrund passiert, aber das ist mir (zur Zeit) auch egal. Hauptsache, es funktioniert.
Das Gute ist, dass es auch mit Funktionsbausteinen funktioniert, die innerhalb der Testumgebung aufgerufen werden.
Wenn du ermitteln möchtest, welche Tabellen angesprochen werden, dann kannst du dies einigermaßen bequem mit der Transaktion SAT machen. Dort werden alle Datenbankzugriffe aufgeführt.
Ausnahmeklasse sind in vielerlei Hinsicht Ausnahme-Klassen. Es fängt mit der Generierung des CONSTRUCTORs an, geht über die unterschiedliche Behandlung in SE24 und Eclipse und endet bei der Übersetzung der Exception-ID’s der Ausnahmeklasse.
Man könnte sich nun darüber streiten, ob die Ausnahme-ID’s der Ausnahmeklassen überhaupt übersetzt werden sollten oder ob sie eh nur technischen Charakter haben und nicht an die Oberfläche gelangen sollten. Das können wir allerdings an anderer Stelle tun. Ich bin der Meinung, dass die Ausnahme-IDs‘, die nicht mit einer SAP-Meldung erzeugt werden, regulär nutzbare Elemente sind.
Ausnahme-ID’s
Die Texte, bzw. Ausnahme-IDs von Ausnahmeklassen werden über einen sogenannten OTR-Text definiert. Für jeden Text, den du in der Ausnahmeklasse hinzufügst, wird eine Konstante vom Typ SOTR_CONC mit einer eindeutigen GUID angelegt. OTR steht für Online Text Repository. OTR-Texte können mit Transaktion SOTR_EDIT gepflegt werden. Allerdings können Sie auch aus dieser Transaktion heraus nicht übersetzt werden.
Reguläre Übersetzung
In der Regel kann man ein Objekt in der SAPGUI – bis auf wenige Ausnahmen – über das Menü Springen •Übersetzung übersetzen. Ausnahmeklassen sind leider so eine Ausnahme.
Übersetzung von Ausnahme-IDs
Um eine Ausnahme-ID zu übersetzen, musst du wie folgt vorgehen:
Finde heraus, wie die ID des OTR-Objektes lautet. In der SE24 findest du die ID im Reiter „Attribute“. Es sind alle Attribute vom Typ SOTR_CONC. In Eclipse findest du die generierten Attribute in der PUBLIC SECTION. Auch hier sind es die Konstanten vom Typ SOTR_CONC.
Kopiere die OTR-ID
Starte Transaktion SE63 und klicke die Drucktaste „Kurztexte“ an
Suche nach OTR oder öffne den Ordner 00 Meta Objects.
Wähle „OTRS – OTR Kurztext Meta“ aus.
Gib die kopierte GUID in das Feld GUID ein. Achte darauf, dass Quell- und Zielsprache korrekt eingestellt sind.
Klicke auf die Drucktaste „Bearbeiten“
Auswahl Objekttyp OTRSOTR bearbeiten
Du befindest dich nun im bekannten Dynpro zum Übersetzen der Texte. Leider müssen die Ausnahme-ID’s einzeln kopiert und bearbeitet werden. Es gibt meines Wissens nach keine Sammelfunktion zum Bearbeiten so wie bei den Nachrichtenklassen.
Ich arbeite immer noch viel mit Dynpros. Für mich und die meisten meiner Kunden ist das nach wie vor das Frontend, mit dem gearbeitet wird. Bei der GUI-Programmierung habe ich mir angewöhnt, einzelne Teile in separaten Controls zu programmieren. Das hat den Vorteil, dass diese Module austauschbar und einzeln testbar sind. Um mehrere Module unterzubekommen, bieten sich Splittercontainer an. Diese haben jedoch den Nachteil, dass die Konfiguration (Größe, Breite, Höhe) feste definiert werden muss. Verwendet man nur einen Splitter, dann kann man dessen Größe einfach auslesen, speichern und bei Bedarf wieder setzen. So kann sich der/die Anwender:in die Größe so einstellen, wie es für das aktuelle Setup aus Monitorauflösung, Skalierung und SAPGUI-Schriftgröße am besten passt.
Verwendet man jedoch mehrere Splitter, dann und befindet sich zudem noch in der Findungsphase wo sich die Aufteilung der Container noch ändert, dann ist ein gezieltes Auslesen und Setzen der Größenparameter doch sehr aufwendig.
Idee
Da ich nicht nur ein Freund von Controls bin, sondern auch von generischen Lösungen, war meine erste Idee, die aktuelle Konfiguration der Controls automatisch zu ermitteln. Die Routine dafür war auch schnell geschrieben:
Das Programm erzeugt einen einigermaßen komplexen Bildschirmaufbau mit Splittern und liest diesen aus.
Dynpro mit Splittercontainern
Die Ausgabe der Konfiguration sieht wie folgt aus:
Allerdings gibt es hier ein Problem: Ich habe keine Möglichkeit herauszufinden, wie die Container bzw. die Splitter inhaltlich einzuordnen sind. Wenn ich mich, wie beschrieben, noch in der Entwicklungsphase befinde, dann kommt eventuell mal ein Container dazu, ein Splitter verschwindet oder bekommt mehr oder weniger Controls. Dementsprechend würde sich das Wiederherstellen ungewollt ändern.
Man kann zwar jedem Control einen Namen mitgeben, allerdings funktioniert dieses Feature nicht so, wie ich es mir vorgestellt habe. Wenn man die Namen, die übrigens nur aus Großbuchstaben und ohne Space bestehen müssen, vergibt, dann müssen alle Container und Controls einen Namen bekommen. Ansonsten gibt es einen Kurzdump.
Vorgehen
die Lösung hierfür besteht darin, eine Ableitung des Splittercontainers zu erstellen und diesen zu verwenden. In der Ableitung kann man den Parameter NAME des CONSTRUCTORs in einem eigenen Attribut speichern. Die Methode GET_NAME ist bereits vorhanden und ich redefiniere diese.
CLASS zcl_gui_splitter DEFINITION INHERITING FROM cl_gui_splitter_container.
PUBLIC SECTION.
METHODS constructor
IMPORTING
link_dynnr TYPE sy-dynnr
link_repid TYPE sy-repid
shellstyle TYPE i
left TYPE i
top TYPE i
width TYPE i
height TYPE i
metric TYPE cntl_metric
align TYPE i
parent TYPE REF TO cl_gui_container
rows TYPE i
columns TYPE i
no_autodef_progid_dynnr TYPE c
name TYPE string.
METHODS get_name REDEFINITION.
PRIVATE SECTION.
DATA my_name TYPE string.
ENDCLASS.
CLASS zcl_gui_splitter IMPLEMENTATION.
METHOD constructor.
super->constructor(
link_dynnr = link_dynnr
link_repid = link_repid
shellstyle = shellstyle
left = left
top = top
width = width
height = height
metric = metric
align = align
parent = parent
rows = rows
columns = columns
no_autodef_progid_dynnr = no_autodef_progid_dynnr ).
my_name = name.
ENDMETHOD.
METHOD get_name.
name = my_name.
ENDMETHOD.
ENDCLASS.
Merkwürdigerweise funktioniert diese Variante nur, wenn man die Klasse global anlegt. mit einer lokalen Vererbung erhalte ich zwar keinen Fehler, aber leider auch keine Splittercontainer.
Container mit Namen
Dadurch, dass ich dem Splittercontainer nun einen Namen mitgeben kann, kann ich diesen auch wieder auslesen. Die Funktion ist genau so, wie bisher auch, nur dass ich nun eben auch den Namen ermitteln und speichern kann.
Die Konfiguration enthält nun alle Splitter mit Namen und deren Containergrößen
Splitter mit Namen
Lösung
Die aktuelle Konfiguration kann nun mit Namen ausgelesen werden. Dementsprechend kann ich die Konfiguration auch speichern. Ich habe es mir hier einfach gemacht und speichere sie per EXPORT TO DATABASE in INDX(Z2).
Beim Auslesen muss ich zusätzlich die aktuelle Konfiguration ermitteln, um mithilfe der Namen den aktiven Splitter zuordnen zu können. Zu diesem setze ich dann mit SET_ROW_HEIGHT die Höhe und mit SET_COLUMN_WIDHT die Breite der Splittercontainer.
Bevor man damit beginnt, eine vorhandene Applikation zu erweitern oder umfangreich anzupassen, könnte man auf die Idee kommen, prüfen zu wollen, ob die beteiligten Objekte eventuell gerade gesperrt sind. In diesem Fall könnte man die Entwicklung nämlich nicht reibungslos durchführen. Die Änderung des gesperrten Objektes wird dann nämlich dem bereits vorhandenen Transportauftrag zugeordnet. Das wiederum bedeutet, dass die Transportaufträge gemeinsam transportiert werden müssen.
Um prüfen zu können, welche Objekte gesperrt sind, habe ich einen kleinen Report geschrieben. Er gibt eine Liste der selektierten Objekte aus und zeigt an, ob diese gesperrt sind oder nicht. Sind sie gesperrt, dann wird auch der Transportauftrag angezeigt, in dem das Objekt gesperrt ist.
Mit einem Klick auf den Transportauftrag wird dieser angezeigt.
Code
REPORT.
DATA gs_tadir TYPE tadir.
DATA gv_trkorr TYPE trkorr.
SELECT-OPTIONS so_objt FOR gs_tadir-object DEFAULT 'PROG'.
SELECT-OPTIONS so_objn FOR gs_tadir-obj_name DEFAULT 'ZVTEST*' OPTION CP.
SELECT-OPTIONS so_devc FOR gs_tadir-devclass.
PARAMETERS pa_onlyl AS CHECKBOX DEFAULT space.
AT LINE-SELECTION.
CHECK gv_trkorr IS NOT INITIAL.
CALL FUNCTION 'TR_DISPLAY_REQUEST'
EXPORTING
i_trkorr = gv_trkorr.
START-OF-SELECTION.
PERFORM start.
FORM start.
DATA ls_lockkey TYPE tlock_int.
DATA lv_locked TYPE abap_bool.
DATA lt_locks TYPE STANDARD TABLE OF tlock WITH DEFAULT KEY.
SELECT * FROM tadir INTO TABLE @DATA(lt_tadir)
WHERE object IN @so_objt
AND obj_name IN @so_objn
AND devclass IN @so_devc.
LOOP AT lt_tadir INTO gs_tadir.
ls_lockkey-obj = gs_tadir-object.
ls_lockkey-low = gs_tadir-obj_name.
ls_lockkey-hi = gs_tadir-obj_name.
ls_lockkey-len = 40.
ls_lockkey-pat_len = 40.
CALL FUNCTION 'TRINT_CHECK_LOCKS'
EXPORTING
wi_lock_key = ls_lockkey
IMPORTING
we_lockflag = lv_locked
TABLES
wt_tlock = lt_locks
EXCEPTIONS
empty_key = 1.
IF lv_locked = abap_false AND pa_onlyl = abap_true.
CONTINUE.
ENDIF.
WRITE: /
gs_tadir-object,
gs_tadir-obj_name,
gs_tadir-devclass.
HIDE gs_tadir.
IF lv_locked = abap_true.
WRITE lv_locked COLOR COL_GROUP.
gv_trkorr = lt_locks[ 1 ]-trkorr.
HIDE gv_trkorr.
SELECT * FROM e07t
INTO TABLE @DATA(lt_e07t)
WHERE trkorr = @gv_trkorr.
WRITE: gv_trkorr HOTSPOT ON.
IF sy-subrc = 0.
DATA(lv_trtxt) = lt_e07t[ 1 ]-as4text.
WRITE: lv_trtxt.
ENDIF.
ENDIF.
ENDLOOP.
CLEAR gs_tadir.
CLEAR gv_trkorr.
ENDFORM.
In einem Projekt stand ich erneut vor der Aufgabe, eine Hierarchie von ausgelösten Exceptions anzeigen zu müssen, um einem Fehler auf die Spur zu kommen. Natürlich kann man sich im Debugger das Ausnahmeobjekt anzeigen lassen und jeweils die PREVIOUS-Ausnahmeobjekte durchklicken. Allerdings funktioniert das nur, wenn man selbst den Fehler nachstellen kann. Einfacher wäre es unter Umständen, wenn der Anwender bereits die Aufrufhierarchie sehen könnte. Das könnten zwar durchaus zu viele und vor allen Dingen zu technische Informationen sein, aber in jedem Fall besser, wenn man dem Entwickler diesen Screenshot schicken kann, als nur die lapidare Meldung „Es ist eine Ausnahme aufgetreten“.
Da es im Standard anscheinend keinen Baustein gibt, um diese Aufrufhierarchie anzuzeigen – jedenfalls habe ich keinen gefunden –, habe ich kurzerhand selbst eine kleine Funktion geschrieben.
Worum geht es genau?
Klassenbasierte Ausnahmen können nach oben, also an den Aufrufer, propagiert werden. Dabei kann der Ausnahme immer der Parameter PREVIOUS übergeben werden. In diesem Parameter kann ein Ausnahmeobjekt eines vorherigen Fehlers übergeben werden. Das bietet die Möglichkeit, die komplette Fehlerhistorie nachvollziehen zu können.
Beispielprogramm
Das folgende Beispielprogramm soll das Vorgehen kurz und knapp demonstrieren. Ich habe hier lokal drei Ausnahmeklassen definiert:
ERR_ST – erbt von CX_STATIC_CHECK und hat ein IF_T100-Interface
ERR_DYN – erbt von CX_DYNAMIC_CHECK und hat ein IF_T100-Interface
ERR_PURE – erbt von CX_STATIC_CHECK und hat kein Interface für MESSAGES
Die Klasse DEMO besteht aus den vier Methoden ONE, TWO, THREE und GO. GO ist die Startmethode und ruft ONE auf, die TWO aufruft, die THREE aufruft. Jede Methode benutzt dabei eine andere Ausnahmeklassen. Die vorher ausgelöste Ausnahme wird als PREVIOUS übergeben. Am Ende hat man eine Hierarchie von drei Ausnahmeinstanzen.
Code
REPORT.
CLASS err_st DEFINITION INHERITING FROM cx_static_check CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_t100_message.
INTERFACES if_t100_dyn_msg.
ENDCLASS.
CLASS err_dyn DEFINITION INHERITING FROM cx_dynamic_check.
PUBLIC SECTION.
INTERFACES if_t100_message.
INTERFACES if_t100_dyn_msg.
ENDCLASS.
CLASS err_pure DEFINITION INHERITING FROM cx_static_check.
PUBLIC SECTION.
DATA text TYPE string.
METHODS constructor
IMPORTING
text TYPE clike OPTIONAL.
methods get_text REDEFINITION.
ENDCLASS.
CLASS err_pure IMPLEMENTATION.
METHOD constructor.
super->constructor( ).
me->text = text.
ENDMETHOD.
method get_text.
result = me->text.
ENDMETHOD.
ENDCLASS.
CLASS demo DEFINITION.
PUBLIC SECTION.
METHODS one RAISING err_st.
METHODS two.
METHODS three RAISING err_pure.
METHODS go.
ENDCLASS.
CLASS demo IMPLEMENTATION.
METHOD one.
TRY.
two( ).
CATCH err_dyn INTO DATA(err).
RAISE EXCEPTION TYPE err_st
MESSAGE ID 'OO'
TYPE 'E'
NUMBER '000'
WITH 'Method one failed'
EXPORTING
previous = err.
ENDTRY.
ENDMETHOD.
METHOD two.
TRY.
three( ).
CATCH cx_root INTO DATA(err).
RAISE EXCEPTION TYPE err_dyn
MESSAGE ID 'OO'
TYPE 'E'
NUMBER '000'
WITH 'Failure method two'
EXPORTING
previous = err.
ENDTRY.
ENDMETHOD.
METHOD three.
data(err) = new err_pure( text = 'pure exception w/o T100 interface' ).
RAISE EXCEPTION err.
ENDMETHOD.
METHOD go.
TRY.
one( ).
CATCH cx_root INTO DATA(error).
excp=>show( error ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
START-OF-SELECTION.
NEW demo( )->go( ).
Anzeige der Ausnahmen
Im oben gezeigten Coding wird die Methode EXCP=>SHOW aufgerufen. Diese stelle ich dir jetzt vor. Sie nimmt ein beliebiges Ausnahmeobjekt entgegen (vom Typ CX_ROOT oder Unterklassen) und sammelt Informationen über die Ausnahmehierarchie.
Mit Hilfe der Klasse CL_ABAP_CLASSDESCR wird der Name der Ausnahmeklasse ermittelt. Klasse CL_MESSAGE_HELPER hilft dabei, die MESSAGE-Variablen in die entsprechenden SY-Felder zu stellen. Die Ermittelten Daten werden mit Hilfe des CL_SALV_TABLE im Popup angezeigt.
Verbesserungen
Das hier vorgestellte Popup sollte einem Endanwender nicht direkt angezeigt werden, denn es werden sehr viele technische Details gezeigt. Sinnvoller wäre sicherlich eine Popup, dass eine entsprechende Nachricht oder die Meldung der letzten Ausnahme mit der Möglichkeit, die Aufrufhierarchie anzeigen zu lassen.
Meiner Meinung nach müsste diese Funktion bereits direkt in der SAPGUI beim Anzeigen einer Ausnahme-MESSAGE möglich sein, wenn die Meldung mittels MESSAGE exception_object TYPE ‚I‘ ausgegeben wird.
Code
CLASS excp DEFINITION.
PUBLIC SECTION.
CLASS-METHODS show IMPORTING obj TYPE REF TO cx_root.
PRIVATE SECTION.
TYPES: BEGIN OF _exception,
message TYPE string,
msgno TYPE symsgno,
msgid TYPE symsgid,
msgv1 TYPE symsgv,
msgv2 TYPE symsgv,
msgv3 TYPE symsgv,
msgv4 TYPE symsgv,
relative_name TYPE string,
absolute_name TYPE string,
prog_name TYPE syrepid,
incl_name TYPE inclname,
source_line TYPE i,
END OF _exception,
_exceptions TYPE STANDARD TABLE OF _exception WITH DEFAULT KEY.
CLASS-DATA exceptions TYPE _exceptions.
CLASS-METHODS fill_table IMPORTING obj TYPE REF TO cx_root.
CLASS-METHODS display_popup.
ENDCLASS.
CLASS excp IMPLEMENTATION.
METHOD show.
fill_table( obj ).
display_popup( ).
ENDMETHOD.
METHOD fill_table.
DATA(error) = CAST cx_root( obj ).
WHILE error IS BOUND.
APPEND INITIAL LINE TO exceptions ASSIGNING FIELD-SYMBOL(<excp>).
DATA(descr) = cl_abap_classdescr=>describe_by_object_ref( error ).
<excp>-absolute_name = descr->absolute_name.
<excp>-relative_name = descr->get_relative_name( ).
<excp>-message = error->get_text( ).
error->get_source_position(
IMPORTING
program_name = <excp>-prog_name
include_name = <excp>-incl_name
source_line = <excp>-source_line ).
IF error IS INSTANCE OF if_t100_message.
DATA(msg) = CAST if_t100_message( error ).
cl_message_helper=>set_msg_vars_for_if_t100_msg( msg ).
<excp>-msgno = sy-msgno.
<excp>-msgid = sy-msgid.
<excp>-msgv1 = sy-msgv1.
<excp>-msgv2 = sy-msgv2.
<excp>-msgv3 = sy-msgv3.
<excp>-msgv4 = sy-msgv4.
ENDIF.
error ?= error->previous.
ENDWHILE.
ENDMETHOD.
METHOD display_popup.
TRY.
cl_salv_table=>factory(
IMPORTING
r_salv_table = DATA(popup)
CHANGING
t_table = exceptions ).
popup->set_screen_popup(
start_column = 10
end_column = 170
start_line = 2
end_line = 10 ).
popup->get_columns( )->set_optimize( abap_true ).
popup->get_columns( )->get_column( 'MESSAGE' )->set_medium_text( `Message` ) ##no_text.
popup->get_columns( )->get_column( 'ABSOLUTE_NAME' )->set_medium_text( `Absolute class name` ) ##no_text.
popup->get_columns( )->get_column( 'RELATIVE_NAME' )->set_medium_text( `Relative class name` ) ##no_text.
popup->display( ).
CATCH cx_salv_msg cx_salv_not_found.
ENDTRY.
ENDMETHOD.
ENDCLASS.
I’d rather write code that writes code than write code
mir unbekannter Autor
Dieses Zitat passt sehr gut zu dem hier vorgestellten Code-Schnipsel. Mit diesem kleinen Programm zeige ich dir exemplarisch, wie man per Programm eine Klasse im Repository anlegen kann. Es wird die folgende Klasse angelegt:
CLASS zcl_test_007 DEFINITION
PUBLIC
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_test .
METHODS demo .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_test_007 IMPLEMENTATION.
METHOD demo.
* demo implementation
ENDMETHOD.
METHOD zif_test~if_test.
* demo interface implementation
ENDMETHOD.
ENDCLASS.
Anlegen einer Klasse
Aber nun zu dem spannenden Teil: Der Anlage der Klasse. Es werden alle notwendigen Bestandteile mitgegeben. Bei den meisten verwendeten Tabellen des Bausteins SEO_CLASS_CREATE_COMPLETE beziehen sich auf Datenbankviews, so dass man einfach bei vorhandenen Klassen nachsehen kann, welche Attribute welche Werte erhalten sollten. In der Regel kann man dies auch über die Festwerte der jeweiligen Domäne erfahren.
Code
REPORT.
TYPE-POOLS seoc.
PARAMETERS pa_num TYPE num03 DEFAULT 1.
START-OF-SELECTION.
DATA t_attributes TYPE seoo_attributes_r.
DATA t_methods TYPE seoo_methods_r.
DATA t_method_sources TYPE seo_method_source_table.
DATA t_interfaces TYPE seor_implementings_r.
DATA s_class TYPE vseoclass.
s_class = VALUE #(
clsname = |ZCL_TEST_{ pa_num }|
descript = |Test class number { pa_num }|
langu = sy-langu
exposure = 2
state = 1 ). "implemented
t_attributes = VALUE #(
( clsname = s_class-clsname
cmpname = 'DEMO'
descript = 'Demo attribute'
exposure = 2 "public
attdecltyp = 0 "instance
state = 1
) ).
t_methods = VALUE #(
( clsname = s_class-clsname
cmpname = 'DEMO'
descript = 'demo method'
langu = sy-langu
exposure = 2 "public
mtddecltyp = 0 "instance
state = 1
) ).
t_method_sources = VALUE #(
( cpdname = 'DEMO' source = VALUE #( ( `* demo implementation` ) ) )
( cpdname = 'ZIF_TEST~IF_TEST' source = VALUE #( ( `* demo interface implementation` ) ) ) ).
t_interfaces = VALUE #(
( clsname = s_class-clsname
refclsname = 'ZIF_TEST'
exposure = 2 "public
state = 1
reltype = 1 ) ).
"Create class
CALL FUNCTION 'SEO_CLASS_CREATE_COMPLETE'
EXPORTING
devclass = '$TMP'
overwrite = space
version = seoc_version_active
suppress_dialog = abap_true " Parameter missing in 702
method_sources = t_method_sources
CHANGING
implementings = t_interfaces
class = s_class
attributes = t_attributes
methods = t_methods
EXCEPTIONS
existing = 1
is_interface = 2
db_error = 3
component_error = 4
no_access = 5
other = 6
OTHERS = 7.
WRITE: / sy-subrc.