Quantcast
Channel: Programmierung – Tricktresor
Viewing all 166 articles
Browse latest View live

Controls stapeln

$
0
0

Heute ist mir wieder eine kleine Spielerei unter die Finger gekommen, die einerseits wichtige Grundlagen zeigt und andererseits eine nette Spielerei ist, die durchaus einen Nutzwert hat. Es geht um die Anzeige und Steuerung von Controls.

Um ein Control anzeigen zu können, benötigt man einen Container. In diesem Beispiel verwende ich einen Docking-Container.

go_dock = NEW #( side = cl_gui_docking_container=>dock_at_right ratio = 80 ).

In diesen Container hänge ich ein Text-Control.

go_text = NEW #( parent = go_dock ).

Allerdings ist das noch nichts Besonderes.

Controls stapeln

Besonders wird es, wenn ich noch ein ALV-Grid und noch ein Picture-Control in den gleichen Container packe.

" ALV-Grid
go_grid = NEW #( i_parent = go_dock ).
go_grid->set_table_for_first_display(
  EXPORTING i_structure_name = 'T000'
  CHANGING it_outtab = gt_data_alv ).

" Picture
go_pic = NEW cl_gui_picture( parent = go_dock ).

Die Controls sind nun gewissermaßen gestapelt. Sie liegen übereinander in dem Container. Das zuletzt instantiierte Control wird angezeigt. Die anderen Controls sind jedoch noch da! Und sie sind auch nutzbar. Man muss lediglich die jeweils darüber liegenden Controls auf „nicht sichtbar“ stellen.

Jedes Control hat die Methode SET_VISIBLE mit der man die Sichtbarkeit eines Controls steuern kann (Vererbung von CL_GUI_CONTROL). Jeder Container hat übrigens ebenfalls diese Eigenschaft (denn auch diese erben von CL_GUI_CONTROL)! Das heißt, es kann auch ein Docking-Container komplett ausgeblendet werden, ohne dass er wirklich „weg“ ist.

In seltenen Fällen kann man sich diesen Umstand zu Nutze machen. Bei diesem Trick verwende ich ein ähnliches Verfahren: Werte aus Excel per DOI (unsichtbar). Hier wird nur nicht der Container unsichtbar geschaltet, sondern das Control wird an ein Standard-Dynpro gehängt, dass nicht angezeigt wird.

Man kann also dadurch, dass man nur das gewünschte Control auf „sichtbar“ und alle anderen auf „unsichtbar“ stellt, zwischen den einzelnen Control hin- und her schalten. Es ist dementsprechend nicht notwendig, das im Container befindliche Control zu zerstören und das neue aufzubauen um einen Wechsel zu realisieren.

Beliebter Fehler

Den Zustand, den ich hier bewusst und mit voller Absicht herbei führe, ist wahrscheinlich schon häufig die Ursache vieler verzweifelter Stunden im Debugger und einiger grauer Haare gewesen. Häufig passiert es nämlich (nicht nur Anfängern, sondern auch Profis!), dass man ein und dasselbe Control mehrfach instantiiert und dem gleichen Container zuordnet. Das macht SAP auch klaglos mit und stellt die neuen Control-Instanzen immer wieder in den Container hinein. Sichtbar ist einzig und allein das zuerst erzeugte Control.

Das ist genau das tückische daran, denn durch diesen Umstand ergeben sich eine Vielzahl von Symptomen, die man sich auch nach stundenlangem Debugging häufig nicht erklären kann:

  • Geänderte Daten werden nicht im ALV-Grid angezeigt
  • Datenänderungen werden vom ALV nicht in die interne Tabelle übernommen
  • Doppelklick funktioniert nicht mehr
  • Änderungen am Control werden nicht sichtbar (geändertes Bild, Icon, aktualisierte Website, …)

Sofern man daran denkt, dass der Fehler einer Mehrfach-Instantiierung vielleicht vorliegen könnte, kann man sehr leicht prüfen, ob das wirklich der Fall ist. Jeder Container hat das Attribut CHILDREN. In dieser Tabelle werden die dem Container zugeordneten Controls verwaltet:

Wenn man in seinem Programm zwar den Container nur einmal erzeugt, aber bei jedem Tastendruck (PAI) eventuell ein neues Control, dann könnte es so aussehen, wie hier:

TIPP
In den meisten Fällen ist es sinnvoll und ausreichend, wenn man abfragt, ob der Container bereits erzeugt wurde. Falls er noch nicht erzeugt wurde (Programmstart etc.), dann erzeugt man den Container und auch gleich das Control.

Häufig treten solche Fehler auf, wenn man Codezeilen von einer Routine oder Methode in ein Programm-Ereignis kopiert oder umgekehrt. Auf einmal befindet sich der Codeabschnitt, der vorher nur einmal aufgerufen wurde, an einer Stelle im Programm, die mehrmals durchlaufen wird.

Screenshots

Die Anzeige wird über die entsprechenden Radiobuttons gesteuert.

Code

REPORT zz_switch_controls.


*== Data
DATA gt_data_alv TYPE STANDARD TABLE OF t000 WITH NON-UNIQUE DEFAULT KEY.
DATA go_dock TYPE REF TO cl_gui_docking_container.
DATA go_text TYPE REF TO cl_gui_textedit.
DATA go_grid TYPE REF TO cl_gui_alv_grid.
DATA go_pic TYPE REF TO cl_gui_picture.

*== Selektionsbild
PARAMETERS: rb_text RADIOBUTTON GROUP rb1 DEFAULT 'X' USER-COMMAND space,
 rb_grid RADIOBUTTON GROUP rb1,
 rb_pic RADIOBUTTON GROUP rb1.

AT SELECTION-SCREEN.
 "steuern der controls
  CASE 'X'.
    WHEN rb_grid. 
      go_text->set_visible( space ).
      go_pic->set_visible( space ).
      go_grid->set_visible( 'X' ).
    WHEN rb_text.
      go_text->set_visible( 'X' ).
      go_pic->set_visible( space ).
      go_grid->set_visible( space ).
    WHEN rb_pic.
      go_text->set_visible( space ).
      go_pic->set_visible( 'X' ).
      go_grid->set_visible( space ).
  ENDCASE.

INITIALIZATION.

*== Docker
 go_dock = NEW #( side = cl_gui_docking_container=>dock_at_right ratio = 80 ).

*== Textedit
 go_text = NEW #( parent = go_dock ).

*== ALV-Grid
 SELECT *
 INTO TABLE gt_data_alv
 FROM t000.

 go_grid = NEW #( i_parent = go_dock ).
 go_grid->set_table_for_first_display(
     EXPORTING i_structure_name = 'T000'
     CHANGING it_outtab = gt_data_alv ).

*== Picture
 go_pic = NEW cl_gui_picture( parent = go_dock ).
 go_pic->load_picture_from_sap_icons( icon_booking_ok ).
 go_pic->set_display_mode( cl_gui_picture=>display_mode_fit ).
 

Der Beitrag Controls stapeln erschien zuerst auf Tricktresor.


Preisfindung im Kundenauftrag von außen anstossen

$
0
0

In diesem Beitrag zeige ich dir, wie du die Preisfindung eines Kundenauftrags neu ausführen lassen kannst. Das grundsätzliche Verfahren sieht so aus:

  1. Userexit in SAPMV45A anpassen
  2. Parameter setzen
  3. BAPI aufrufen
  4. Parameter zurücknehmen

Um die Preisfindung von außen triggern zu können, musst du Änderungen im Programm SAPMV45A durchführen. Zuerst benötigst du jedoch die Möglichkeit, einen Parameter zur Laufzeit zu setzen, der dann im SAPMV45A abgefragt werden kann. Das kann gut über eine der beiden Methoden erfolgen:

  1. EXPORT TO MEMORY und IMPORT FROM MEMORY
  2. Öffentliches Attribut der eigenen globalen Klasse

Anlage der globalen Klasse

Als erstes musst du eine Klasse anlegen mit der die neue Preisfindung durchgeführt werden soll. In meinem Beispiel heißt sie ZCL_SD_NP (New Pricing).

Lege das öffentliche Klassenattribut KNPRS vom Typ KNPRS an (static). Das ist die Preisfindungsart, mit der die Art der neuen Preisfindung gesteuert werden kann.

Quelltext (relevanter Teil ) in MV45AFZB:

FORM userexit_new_pricing_vbap CHANGING new_pricing.
  IF zcl_sd_np=>knprs IS NOT INITIAL.  
     new_pricing = zcl_sd_np=>knprs.
   ENDIF.
ENDFORM.                    "USEREXIT_NEW_PRICING_VBAP
FORM userexit_new_pricing_vbkd CHANGING new_pricing.
  IF zcl_sd_np=>knprs IS NOT INITIAL. 
     new_pricing = zcl_sd_np=>knprs.
   ENDIF.
ENDFORM.                    "USEREXIT_NEW_PRICING_VBKD

Quelltext Klasse

Nun brauchen wir noch die Methode TRIGGER_NEW_PRICING

DATA: 
  ls_bapisdh1x  TYPE bapisdh1x,
  lt_pos        TYPE STANDARD TABLE OF bapisditm WITH NON-UNIQUE DEFAULT KEY,
  lt_posx       TYPE STANDARD TABLE OF bapisditmx WITH NON-UNIQUE DEFAULT KEY,
  lt_return     TYPE bapiret2_t.
FIELD-SYMBOLS: 
  <ls_pos>      LIKE LINE OF lt_pos,
  <ls_posx>     LIKE LINE OF lt_posx.


CLEAR ct_bapiret2.
knprs = iv_knprs.
*--------------------------------------------------------------------*
* Get all positions to be redermined
*--------------------------------------------------------------------*
SELECT posnr AS itm_number werks AS plant
INTO CORRESPONDING FIELDS OF TABLE lt_pos
FROM vbap
WHERE vbeln = iv_vbeln_va.

LOOP AT lt_pos ASSIGNING <ls_pos>.
APPEND INITIAL LINE TO lt_posx ASSIGNING <ls_posx>.
<ls_posx>-itm_number = <ls_pos>-itm_number.
<ls_posx>-updateflag = 'U'.
ENDLOOP.

ls_bapisdh1x-updateflag = 'U'.
CALL FUNCTION 'BAPI_SALESORDER_CHANGE'
  EXPORTING
    salesdocument    = iv_vbeln_va
    order_header_inx = ls_bapisdh1x
  TABLES
    return           = ct_bapiret2
    order_item_in    = lt_pos
    order_item_inx   = lt_posx
  EXCEPTIONS
    ERROR_MESSAGE = 1.

CLEAR knprs.  " Only once

TRY.
    DATA(ls_return) = ct_bapiret2[ type = 'E' ].
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    RAISE EXCEPTION TYPE zcx_my_exception.
  CATCH CX_SY_ITAB_LINE_NOT_FOUND.
    CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'.
ENDTRY.

Durch Aufruf der Methode ZCL_SD_NP=>TRIGGER_NEW_PRICING( … ) kannst du nun einen Beleg dazu bewegen, eine neue Preisfindung durchzuführen.

Der Beitrag Preisfindung im Kundenauftrag von außen anstossen erschien zuerst auf Tricktresor.

Komponenten einem Fertigungsauftrag hinzufügen

$
0
0

Für den Fertigungsaufträge sind die BAPIs leider sehr rar gesät und man muss auf andere Bausteine ausweichen. Um einem Auftrag Komponenten hinzuzufügen, habe ich nur den Baustein CO_XT_COMPONENT_ADD gefunden. Die CO_XT-Funktionsbausteine sind zwar prinzipiell „extern“ und in der Funktionsgruppe „APIs Fertigungsauftrag“, jedoch sind die Bausteine allesamt sehr mit Vorsicht zu genießen.

Das folgende Coding fügt einem Fertigungsauftrag eine Komponente (Materialnummer) hinzu.

Code

"Lokale Daten
DATA ls_return TYPE coxt_bapireturn.
DATA lt_return TYPE coxt_t_bapireturn.
DATA lv_error_occurred TYPE boolean.
DATA ls_resbd_created TYPE resbd.
DATA lt_resbt_exp TYPE STANDARD TABLE OF resbb.
DATA lv_posnr TYPE positionno.

DATA ls_quan TYPE coxt_s_quantity.
DATA ls_stor_loc TYPE coxt_s_storage_location.
DATA ls_stor_loc_x TYPE coxt_s_storage_locationx.

ls_quan-quantity    = menge.
ls_quan-uom         = meins.

ls_stor_loc-werks   = werks.
ls_stor_loc-lgort   = lgort.
ls_stor_loc_x-werks = abap_true.
ls_stor_loc_x-lgort = abap_true.

"Komponente hinzufügen
CALL FUNCTION 'CO_XT_COMPONENT_ADD'
  EXPORTING
    is_order_key         = aufnr
    i_material           = matnr
    is_requ_quan         = ls_quan
    i_operation          = 1
    is_storage_location  = ls_stor_loc
    is_storage_locationx = ls_stor_loc_x
    i_postp              = 'L' "Lagerposition
    i_posno              = lv_posnr
  IMPORTING
    es_bapireturn        = ls_return
    e_error_occurred     = lv_error_occurred
    es_resbd_created     = ls_resbd_created
  TABLES
    resbt_exp            = lt_resbt_exp.

IF lv_error_occurred = abap_false.
*== PRE-Commit
  CALL FUNCTION 'CO_XT_ORDER_PREPARE_COMMIT'
    TABLES
      et_bapireturn = lt_return.

"Keine Fehler!
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'.
ELSE.
"Fehlerbehandlung
ENDIF.

Fehlende Positionsnummer

Der Baustein hat leider einen kleinen Schönheitsfehler: Die Positionsnummer (RSB-POSNR) wird leider nicht gesetzt und kann auch nicht ohne weiteres geändert werden. Um die Positionsnummer trotzdem ändern zu können, habe ich zwei Lösungen gefunden:

  1. Die Änderung der internen Tabelle RESB_BT  über einen Dirty-Assign
  2. Änderung über die Standardbausteine
    • CO_BT_RESB_READ_WITH_KEY
    • CO_BT_RESB_GET_LAST_POSNR
    • CO_BT_RESB_UPDATE

Die Änderung muss nach dem CO_XT_COMPONENT_ADD und vor dem Commit erfolgen.

Positionsnummer ändern über Dirty-Assign

*--------------------------------------------------------------------*
* set item number
*--------------------------------------------------------------------*
  TYPES: BEGIN OF ts_resb_bt.
    INCLUDE TYPE resbb.
    TYPES: indold LIKE sy-tabix,
    no_req_upd LIKE sy-datar,
  END OF ts_resb_bt.

  TYPES tt_resb_bt TYPE TABLE OF ts_resb_bt.
  FIELD-SYMBOLS <lt_resb_bt> TYPE tt_resb_bt.
  FIELD-SYMBOLS <ls_resb_bt> TYPE ts_resb_bt.

  ASSIGN ('(SAPLCOBC)RESB_BT[]') TO <lt_resb_bt>.
  LOOP AT <lt_resb_bt> ASSIGNING <ls_resb_bt>.
    IF <ls_resb_bt>-posnr IS INITIAL.
      <ls_resb_bt>-posnr = CONV numc04( <ls_resb_bt>-rspos * 10 ).
    ENDIF.
  ENDLOOP.

Änderung der Positionsnummer über Funktionsbausteine

DATA resbd_exp TYPE resbd.
 DATA posnr_max TYPE tcn41-posnr_mat.
 DATA index_exp TYPE sy-tabix.
 DATA vbkz_exp  TYPE resbb-vbkz.
 DATA nfgrp_exp TYPE resbd-nfgrp.
 DATA posnr_exp TYPE resbd-posnr.

 CALL FUNCTION 'CO_BT_RESB_READ_WITH_KEY'
   EXPORTING
     rsart_imp = ls_resbd-rsart
     rsnum_imp = ls_resbd-rsnum
     rspos_imp = ls_resbd-rspos
   IMPORTING
     index_exp = index_exp " Index interner Tabellen
     posnr_exp = posnr_exp " Nummer der Stücklistenposition
     resbd_exp = resbd_exp " Reservierung/Sekundärbedarf
     nfgrp_exp = nfgrp_exp " Ein-/Auslaufdaten: Nachfolgegruppe
     vbkz_exp  = vbkz_exp " Verbuchungskennzeichen
   EXCEPTIONS
     not_found = 1
     OTHERS    = 2.

 CALL FUNCTION 'CO_BT_RESB_GET_LAST_POSNR'
   EXPORTING
     aufpl     = aufpl
     aplzl     = aplzl
   IMPORTING
     posnr_max = posnr_max. 

 resbd_exp-posnr = posnr_max + 10.

 CALL FUNCTION 'CO_BT_RESB_UPDATE'
   EXPORTING
     resb_new  = resbd_exp
     tabix_old = index_exp.

Dialog oder nicht Dialog?

Ein weiteres Problem könnte der Baustein CO_XT_ORDER_PREPARE_COMMIT machen, denn bei der Änderung des Fertigungsauftrags können Popups erscheinen, die vom Anwender bestätigt werden müssen.

Wenn man Dialoge verhindern möchte, dann muss der Update-Baustein CO_ZV_ORDER_POST direkt aufgerufen werden. Dieser hat einen Parameter NO_DIALOG, den man entsprechend mit X besetzen kann.

 

 

Der Beitrag Komponenten einem Fertigungsauftrag hinzufügen erschien zuerst auf Tricktresor.

Fertigungsauftrag rückmelden

$
0
0

Ein kurzes und knappes Code-Beispiel um eine Rückmeldung zu einem Fertigungsauftrag mit Hilfe des Bausteins BAPI_PRODORDCONF_CREATE_TT zu erfassen. Die Rückmeldedaten werden erfasst und dann dem Baustein übergeben.

Um das ganze etwas interessanter zu machen, verwende ich die neuen ABAP-740 Sprachfeatures VALUE und SWITCH…

Time Ticket füllen

append value #(
   "Rückmeldedaten füllen
    orderid        = aufnr
    operation      = vornr
    work_cntr      = arbpl
    plant          = werks
    postg_date     = sy-datum
    conf_text      = |Rückmeldung Hugo|
    yield          = gutmenge
    conf_quan_unit = 'ST'
    recordtype     = COND #( WHEN gutmenge >= gesamtmenge THEN 'L40' ELSE 'L20' )
  ) TO tickets.

Rückmeldung buchen

DATA return     TYPE bapiret1.
 DATA return_det TYPE STANDARD TABLE OF bapi_coru_return.
 DATA return_det TYPE bapi_coru_return.

 "Rückmeldung buchen
 CALL FUNCTION 'BAPI_PRODORDCONF_CREATE_TT'
   EXPORTING
     post_wrong_entries = '0'
     testrun       = abap_false
   IMPORTING
     return        = return
   TABLES
     timetickets   = tickets
     detail_return = return_det.
Wie bei allen BAPIs muss die Buchung durch ein BAPI_TRANSACTION_COMMIT bestätigt werden

Der Beitrag Fertigungsauftrag rückmelden erschien zuerst auf Tricktresor.

Moderne UI mit altem SAPGUI und ALV-Grid

$
0
0

Ich habe eine kleine Spielerei gebaut, weil ich eine Möglichkeit brauchte um Parameter ein- und auszuschalten. Die normale Methode mit „X“ und „Space“ oder Checkbox fand ich langweilig und mir kam die Idee, dass es möglich sein müsste, eine etwas modernere Art der Darstellung möglich sein müsste.

Sowas in dieser Art:

Bild: http://pixabay.com

Neue UI

Natürlich sind die grafischen Möglichkeiten etwas beschränkt, aber die Funktionalität, wie man sie von jedem aktuellen Smartphone kennt, müsste machbar sein. In Frage kam nur der ALV-Grid (CL_GUI_ALV_GRID). Ich habe mit dem CL_SALV_TABLE angefangen, aber hier lassen sich die Rahmenlinien nicht ausblenden; das geht leider nur im ALV-Grid.

Und tatsächlich:

Ich habe noch mit ein paar anderen Varianten herumgespielt, aber das Prinzip ist immer das gleiche:

Beschreibung

Das Programm baut aus der Parametertabelle eine neue Parametertabelle, die jeweils für ON und OFF ein eigenes Feld für ein Icon hat. Für die Felder wurde die Hotspot-Funktionalität gesetzt, damit man per Klick den Zustand des Schalters ändern kann.

Um die Funktion „wasserdicht“ zu machen, müsste noch verhindert werden, dass die Spaltenbreite verändert werden kann (passiert schnell beim Klicken auf die ausgeblendete Rahmenlinie in der Mitte):

Geschützte Methoden nutzen für die Methode  SET_RESIZE_COLS.

Code

REPORT zz_swwwwwwitch.

"Dummy parameter do display docker
PARAMETERS p.

CLASS main DEFINITION.
 PUBLIC SECTION.
   INCLUDE <cl_alv_control>.
   TYPES: BEGIN OF ty_param,
            name TYPE string,
            text TYPE string,
            status TYPE boolean,
          END OF ty_param,
          ty_params TYPE STANDARD TABLE OF ty_param WITH NON-UNIQUE DEFAULT KEY.

   METHODS init_grid IMPORTING parent TYPE REF TO cl_gui_container.
   METHODS add_parameter
     IMPORTING name TYPE clike
       text TYPE clike OPTIONAL
       status TYPE boolean OPTIONAL.
   METHODS get_params
     RETURNING VALUE(parameters) TYPE ty_params.

   "Settings color
   CONSTANTS color_on TYPE i VALUE col_positive.
   CONSTANTS color_off TYPE i VALUE col_negative.

   "Settings icons
* CONSTANTS status_icon_on TYPE icon_text VALUE icon_businav_szenario.
* CONSTANTS status_icon_off TYPE icon_text VALUE icon_businav_szenario.

* CONSTANTS status_icon_on TYPE icon_text VALUE icon_led_green.
* CONSTANTS status_icon_off TYPE icon_text VALUE icon_led_red.

* CONSTANTS status_icon_on TYPE icon_text VALUE ICON_oo_class.
* CONSTANTS status_icon_off TYPE icon_text VALUE ICON_oo_class.

* CONSTANTS status_icon_on TYPE icon_text VALUE ICON_oo_object.
* CONSTANTS status_icon_off TYPE icon_text VALUE ICON_oo_class.

 CONSTANTS status_icon_on TYPE icon_text VALUE icon_ps_network_activity.
 CONSTANTS status_icon_off TYPE icon_text VALUE icon_ps_network_activity.

* CONSTANTS status_icon_on TYPE icon_text VALUE ICON_add_row.
* CONSTANTS status_icon_off TYPE icon_text VALUE ICON_remove_row.

* CONSTANTS status_icon_on TYPE icon_text VALUE icon_org_unit.
* CONSTANTS status_icon_off TYPE icon_text VALUE icon_org_unit.

   TYPES: BEGIN OF ty_ui_param,
     name TYPE string,
     text TYPE string,
     status_on TYPE icon_text,
     status_off TYPE icon_text,
     t_color TYPE lvc_t_scol,
     t_style TYPE lvc_t_styl,
   END OF ty_ui_param,
   ty_ui_params TYPE STANDARD TABLE OF ty_ui_param.

 PROTECTED SECTION.
   DATA grid TYPE REF TO cl_gui_alv_grid.
   DATA params TYPE ty_params.
   DATA ui_params TYPE ty_ui_params.

   METHODS set_ui.
   METHODS set_color
     IMPORTING status TYPE boolean
     RETURNING VALUE(color) TYPE lvc_t_scol.
   METHODS handle_click FOR EVENT hotspot_click OF cl_gui_alv_grid
     IMPORTING e_row_id.

ENDCLASS.

CLASS main IMPLEMENTATION.

 METHOD init_grid.

 "Local data
 DATA fieldcat TYPE lvc_t_fcat.
 DATA field TYPE lvc_s_fcat.
 DATA layout TYPE lvc_s_layo.

 "Transform parameter data to display UI
 set_ui( ).

 "Create grid in given container
 CREATE OBJECT grid
   EXPORTING
     i_parent = parent.

 "Set fields
 CLEAR field.
 field-fieldname = 'NAME'.
 field-outputlen = 20.
 field-colddictxt = 'Parameter'.
 field-style = alv_style_font_bold.
 APPEND field TO fieldcat.

 CLEAR field.
 field-fieldname = 'TEXT'.
 field-outputlen = 40.
 field-colddictxt = 'Description'.
 APPEND field TO fieldcat.

 CLEAR field.
 field-fieldname = 'STATUS_ON'.
 field-outputlen = 4.
 field-colddictxt = 'On'.
 field-hotspot = abap_true.
 field-icon = abap_true.
 field-fix_column = abap_true.
 APPEND field TO fieldcat.

 CLEAR field.
 field-fieldname = 'STATUS_OFF'.
 field-outputlen = 4.
 field-colddictxt = 'Off'.
 field-hotspot = abap_true.
 field-icon = abap_true.
 field-fix_column = abap_true.
 APPEND field TO fieldcat.

 "Layout
 layout-stylefname = 'T_STYLE'.
 layout-ctab_fname = 'T_COLOR'.
 layout-no_toolbar = abap_true.
 layout-no_headers = abap_true.

 "Display Grid
 grid->set_table_for_first_display(
   EXPORTING
     is_layout = layout
   CHANGING
     it_outtab = ui_params
     it_fieldcatalog = fieldcat
   EXCEPTIONS
     OTHERS = 4 ).

   "Set handler
   SET HANDLER handle_click FOR grid.

 ENDMETHOD.

 METHOD get_params.
   "return current settings
   parameters = params.
 ENDMETHOD.

 METHOD add_parameter.
   "add parameter to parameter table
   APPEND VALUE #( name   = name
                   text   = text
                   status = status ) TO params.
 ENDMETHOD.

 METHOD set_color.

   CASE status.
     WHEN abap_true.
       "set color for switched on
       color = VALUE #( ( fname = 'STATUS_OFF' color-col = color_on )
                        ( fname = 'STATUS_ON' color-col = color_on ) ).
     WHEN abap_false.
       "set color for switched off
       color = VALUE #( ( fname = 'STATUS_OFF' color-col = color_off )
                        ( fname = 'STATUS_ON' color-col = color_off ) ).
   ENDCASE.

 ENDMETHOD.


 METHOD set_ui.

 "for each parameter
 LOOP AT params INTO DATA(param).

   "check if ui entry exists
   READ TABLE ui_params ASSIGNING FIELD-SYMBOL(<ui_param>) WITH KEY name = param-name.
   IF sy-subrc > 0.

   CASE param-status.
     WHEN abap_true.
       "set parameter switched on
       APPEND VALUE #( name = param-name
                       text = param-text
                       status_on = status_icon_on
                       status_off = 'ON'
                       t_color = set_color( abap_true )
                       t_style = VALUE #( ( fieldname = 'STATUS_ON' style2 = alv_style2_no_border_right )
                                          ( fieldname = 'STATUS_OFF' style2 = alv_style2_no_border_left ) )
                     ) TO ui_params ASSIGNING <ui_param>.

     WHEN abap_false.
     "set parameter switched off
       APPEND VALUE #( name = param-name
                       text = param-text
                       status_on = 'OFF'
                       status_off = status_icon_off
                       t_color = set_color( abap_false )
                       t_style = VALUE #( ( fieldname = 'STATUS_ON' style2 = alv_style2_no_border_right )
                                          ( fieldname = 'STATUS_OFF' style2 = alv_style2_no_border_left ) )
                     ) TO ui_params ASSIGNING <ui_param>.

   WHEN abap_undefined.
     "Set parameter not yet defined
     APPEND VALUE #( name = param-name
                     text = param-text
                     status_on = space
                     status_off = space
                     t_style = VALUE #( ( fieldname = 'STATUS_ON' style2 = alv_style2_no_border_right )
                                        ( fieldname = 'STATUS_OFF' style2 = alv_style2_no_border_left ) )
                   ) TO ui_params ASSIGNING <ui_param>.

       ENDCASE.
     ENDIF.
   ENDLOOP.
 ENDMETHOD.

 METHOD handle_click.
 "read parameter entries
 READ TABLE ui_params ASSIGNING FIELD-SYMBOL(<ui_param>) INDEX e_row_id-index.
 READ TABLE params ASSIGNING FIELD-SYMBOL(<param>) WITH KEY name = <ui_param>-name.

 IF <ui_param>-status_on = status_icon_on OR
   <ui_param>-status_on = status_icon_off.
   "set switch to OFF
   <ui_param>-status_on = 'OFF'.
   <ui_param>-status_off = status_icon_off.
   <ui_param>-t_color = set_color( abap_false ).
   <param>-status = abap_false.
 ELSE.
   "Set switch to ON
   <ui_param>-status_on = status_icon_on.
   <ui_param>-status_off = 'ON'.
   <ui_param>-t_color = set_color( abap_true ).
   <param>-status = abap_true.
 ENDIF.
 "Make changes visible
 grid->refresh_table_display( i_soft_refresh = abap_true ).
 ENDMETHOD.

ENDCLASS.

INITIALIZATION.

 DATA(main) = NEW main( ).

 main->add_parameter( name = 'DISPLAY_TECH_DESCR' text = 'Display technical description' status = abap_true ).
 main->add_parameter( name = 'DISPLAY_VALUES' text = 'Display values' status = abap_false ).
 main->add_parameter( name = 'AUTOSAVE' text = 'Autosave' status = abap_true ).
 main->add_parameter( name = 'INST_CALC' text = 'Instant calculation' status = abap_undefined ).

 main->init_grid( NEW cl_gui_docking_container( ratio = 60 side = cl_gui_docking_container=>dock_at_bottom ) ).

AT SELECTION-SCREEN.
 "Enter on selection screen displays current parameters
 DATA(params) = main->get_params( ).
 cl_demo_output=>display_data( params ).
 

Der Beitrag Moderne UI mit altem SAPGUI und ALV-Grid erschien zuerst auf Tricktresor.

ABAP 740-Features unter der Lupe

$
0
0

Aus einer einfachen Anfängerfrage im abapforum.com hat sich eine recht spannende Antwortserie entwickelt, die auf die neuen Sprachfeatures von ABAP740 eingeht. Ich habe diese einmal zusammen gefasst und auch Laufzeitmessungen durchgeführt.

Die Frage

Die Frage von debianfan lautete: Wie ermittele ich die Anzahl von Datensätzen bestimmter Ausprägung in einer internen Tabelle?

Die interne Tabelle NAMES besteht nur aus den Feldern

  • NAME (string)
  • TF (boolean)

Die folgenden Lösungen sind teilweise vereinfacht und ohne DATA-Definitionen. Die einzelnen lauffähigen Lösungen sind unten im Beispielprogramm ersichtlich.

Lösung 1 – 2xLOOP+WHERE(DATA)

Die einfachste und auf der Hand liegende Antwort von Tron war:

LOOP AT names INTO name WHERE tf = abap_true.
  ADD 1 TO zaehler_true.
ENDLOOP.

LOOP AT names INTO name WHERE tf = abap_false.
  ADD 1 TO zaehler_false.
ENDLOOP.

Die Lösung ist einfach und verständlich.

Der Einwand von Ralf war, dass bei WHERE die gesamte Tabelle durchlaufen werden muss, wenn kein Index verwendet wird. Das kann sich bei großen Tabellen negativ auf die Laufzeit auswirken.

Mein Gedanke war, dass ich zwei LOOPs nicht schön finde und außerdem ein LOOP mit einer Case-Anweisung noch einen Tacken einfacher und deutlich sein müsste. Dazu später mehr.

Lösung 2 – FILTER

Haubi hat dann den Vorschlag gemacht, die einzelnen Einträge mittels FILTER zu zählen:

DATA(lv_true)  = lines( FILTER #( names WHERE tf = abap_true ) ).
 DATA(lv_false) = lines( FILTER #( names WHERE tf = abap_false ) ).

Diese Lösung finde ich sehr schlank und gut lesbar. Was mich hier stört, ist, dass durch FILTER alle verarbeiteten Tabelleneinträge kopiert werden. Es werden alle Datensätze die der WHERE-Anweisung entsprechen in eine neue Tabelle kopiert. Die Tabelle ist zwar temporär und wird nur für die Zeit der Verarbeitung des FILTER-Befehls verwendet, aber bei großen Tabellen kann sich die zusätzliche Speicherlast negativ auswirken.

Lösung 3 – REDUCE

Ich wollte dann unbedingt noch eins drauf setzen und eine Lösung haben, die auch bei vielen Ausprägungen von TF funktioniert und die Werte von TF nicht bekannt sind. Zudem wollte ich komplett die neuen Sprachfeatures verwenden.

Bei beiden vorhergehenden Lösungen fand ich es nicht gut, dass gezielt im Programm auf ABAP_TRUE und ABAP_FALSE abgefragt wurde. In diesem Beispiel ist es in Ordnung, weil das die Vorgabe war. Der häufigere Fall ist jedoch, dass eine Gruppe viele und gegebenenfalls nicht bekannte Ausprägungen hat (Verkaufsorganisation, Datum, Materialnummer, etc.).

Meine Lösung bestand dann aus einer Kombination aus VALUE und REDUCE:

DATA(sum) = VALUE ttf( FOR GROUPS grp OF <name> IN names
                        WHERE ( name IS NOT INITIAL )
                        GROUP BY ( tf = <name>-tf )
                          ( tf    = grp
                            count = REDUCE #( INIT i = 0
                                       FOR name IN names
                                       WHERE ( tf = grp )
                                       NEXT i = i + 1 ) ) ).

Diese Lösung baut eine Tabelle auf aus TF und COUNT, so dass alle Gruppenwerte mit der entsprechenden Anzahl Einträge in der Tabelle SUM landen.

Eigentlich müsste diese Lösung die langsamste sein, denn es werden zuerst die Gruppen gebildet. Dafür muss die gesamte Tabelle durchlaufen werden. Dann werden zu jedem Gruppeneintrag erneut die zugehörigen Einträge gelesen und gezählt. Deswegen wollte ich zuerst gar keine Laufzeitmessung machen. Die Herausforderung für mich war in erster Linie, die Problemstellung mit den neuen Sprachfeatures abzubilden, da ich mich mit der Syntax eher schwer tue.

Lösung 4 – 1xLOOP+WHERE(DATA)

Ich habe mit den vorhandenen drei Lösungen ein Testprogramm geschrieben um die Laufzeit mit der Transaktion SAT analysieren zu können.

Allerdings habe ich gemerkt, dass ich die Lösung von Tron falsch übernommen hatte, nämlich folgendermaßen:

LOOP AT names INTO name.
  CASE name-tf.
    WHEN abap_true.
      ADD 1 TO zaehler_true.
    WHEN abap_false.
      ADD 1 TO zaehler_false.
  ENDCASE.
ENDLOOP.

Anstatt zweier LOOPs hatte ich nur einen LOOP und eine CASE-Abfrage.

Da ich die schon dabei war zu testen, wollte ich Trons Code genau so übernehmen, da ich davon ausging, dass meine Variante mit CASE schneller sein würde. Allerdings war dem nicht so…

Update

Zusätzlich zu den LOOP-Lösungen, die mit dem Zusatz INTO workarea arbeiten, habe ich noch die Varianten mit ASSIGNING (Feldsymbol) und TRANSPORTING NO FIELDS aufgenommen.

Lösung 5 – 1xLOOP+CASE(Fieldsymbol)

Die Lösung mit einem LOOP und CASE-Anweisung jedoch mit LOOP-ASSIGNING.

Lösung 6 – 2xLOOP+WHERE(Fieldsymbol)

Die Lösung mit zwei LOOPs und entsprechender WHERE-Bedingung jedoch mit LOOP-ASSIGNING.

Lösung 7 – 2xLOOP+WHERE(ohne Feldtransport)

Die Lösung mit zwei LOOPs und entsprechender WHERE-Bedingung jedoch mit dem Zusatz TRANSPORTING NO FIELDS.

Laufzeitanalyse

 

Der Vollständigkeit halber habe ich die Messung auch noch einmal mit der Variante „SORTED TABLE“ durchgeführt. Und wieder war ich überrascht: Die Variante mit Sorted Table ist deutlich langsamer als die Variante mit Standard Table…

Hier das Ergebnis der Laufzeitmessungen mit 100.000 Datensätzen und STANDARD TABLE:

Variante          Laufzeit
P01_REDUCE         76.602
P02_FILTER         36.755
P03_LOOP_CASE      33.891
P04_LOOP_WHERE     27.282
P05_LOOP_CASE_FS   25.097
P06_LOOP_WHERE_FS  18.805
P07_LOOP_WHERE_NO  17.774

Code

Methode rnd_name baut aus zufälligen Buchstaben Fantasienamen auf.

Methode rnd_bool liefert per Zufall den Wert TRUE oder FALSE zurück.

Die Methoden p01 – p07 enthalten die jeweils erwähnten Lösungsvarianten.

REPORT.
" http://www.abapforum.com/forum/viewtopic.php?f=1&t=21900&p=82017#p82017

PARAMETERS p TYPE i DEFAULT 100000.

CLASS help DEFINITION.
 PUBLIC SECTION.
 CLASS-METHODS rnd_name RETURNING VALUE(name) TYPE string.
 CLASS-METHODS rnd_bool RETURNING VALUE(tf) TYPE boolean.
 CLASS-METHODS class_constructor.
 CLASS-METHODS p01_reduce.
 CLASS-METHODS p02_filter.
 CLASS-METHODS p03_loop_case.
 CLASS-METHODS p04_loop_where.
 CLASS-METHODS p05_loop_case_fs.
 CLASS-METHODS p06_loop_where_fs.
 CLASS-METHODS p07_loop_where_no.
 PROTECTED SECTION.
 CLASS-DATA rnd TYPE REF TO cl_abap_random.
 TYPES:
 BEGIN OF lst_names,
 name TYPE string,
 tf TYPE abap_bool,
 END OF lst_names,
 ltt_names TYPE STANDARD TABLE OF lst_names
 WITH NON-UNIQUE KEY name
 WITH NON-UNIQUE SORTED KEY key_tf COMPONENTS tf.

* ltt_names TYPE SORTED TABLE OF lst_names
* WITH NON-UNIQUE KEY name
* WITH NON-UNIQUE SORTED KEY key_tf COMPONENTS tf.
 CLASS-DATA names TYPE ltt_names.
ENDCLASS.

CLASS help IMPLEMENTATION.
 METHOD class_constructor.
 rnd = cl_abap_random=>create( ).
 names = VALUE ltt_names( FOR i = 1 THEN i + 1 WHILE i <= p
 ( name = help=>rnd_name( ) tf = help=>rnd_bool( ) ) ).

 ENDMETHOD.

 METHOD rnd_name.
 DATA(len) = rnd->intinrange( low = 5 high = 40 ).
 DO len TIMES.
 DATA(pos) = rnd->intinrange( low = 0 high = 25 ).
 name = name && sy-abcde+pos(1).
 ENDDO.
 ENDMETHOD.

 METHOD rnd_bool.
 CASE rnd->intinrange( low = 0 high = 1 ).
 WHEN 0.
 tf = abap_false.
 WHEN 1.
 tf = abap_true.
 ENDCASE.
 ENDMETHOD.

 METHOD p01_reduce.
 TYPES:
 BEGIN OF stf,
 tf TYPE abap_bool,
 count TYPE i,
 END OF stf,
 ttf TYPE SORTED TABLE OF stf WITH UNIQUE KEY tf.

 DATA(sum) = VALUE ttf( FOR GROUPS grp OF <name> IN names
 WHERE ( name IS NOT INITIAL )
 GROUP BY ( tf = <name>-tf )
 ( tf = grp
 count = REDUCE #( INIT i = 0
 FOR name IN names
 WHERE ( tf = grp )
 NEXT i = i + 1 ) ) ).
* cl_demo_output=>display_data( sum ).
 ENDMETHOD.

 METHOD p02_filter.
 DATA(lv_true) = lines( FILTER #( names USING KEY key_tf WHERE tf = abap_true ) ).
 DATA(lv_false) = lines( FILTER #( names USING KEY key_tf WHERE tf = abap_false ) ).

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p03_loop_case.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names INTO DATA(name).
 CASE name-tf.
 WHEN abap_true. ADD 1 TO lv_true.
 WHEN abap_false. ADD 1 TO lv_false.
 ENDCASE.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p04_loop_where.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names INTO DATA(name) WHERE tf = abap_true.
 ADD 1 TO lv_true.
 ENDLOOP.
 LOOP AT names INTO name WHERE tf = abap_false.
 ADD 1 TO lv_false.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p05_loop_case_fs.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names ASSIGNING FIELD-SYMBOL(<name>).
 CASE <name>-tf.
 WHEN abap_true. ADD 1 TO lv_true.
 WHEN abap_false. ADD 1 TO lv_false.
 ENDCASE.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p06_loop_where_fs.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names ASSIGNING FIELD-SYMBOL(<name>) WHERE tf = abap_true.
 ADD 1 TO lv_true.
 ENDLOOP.
 LOOP AT names ASSIGNING <name> WHERE tf = abap_false.
 ADD 1 TO lv_false.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.

 METHOD p07_loop_where_no.

 DATA lv_true TYPE i.
 DATA lv_false TYPE i.

 LOOP AT names TRANSPORTING NO FIELDS WHERE tf = abap_true.
 ADD 1 TO lv_true.
 ENDLOOP.
 LOOP AT names TRANSPORTING NO FIELDS WHERE tf = abap_false.
 ADD 1 TO lv_false.
 ENDLOOP.

* DATA(out) = cl_demo_output=>new( ).
* out->write( lv_true )->write( lv_false )->display( ).
 ENDMETHOD.


ENDCLASS.

START-OF-SELECTION.


 help=>p01_reduce( ).
 help=>p02_filter( ).
 help=>p03_loop_case( ).
 help=>p04_loop_where( ).
 help=>p05_loop_case_fs( ).
 help=>p06_loop_where_fs( ).
 help=>p07_loop_where_no( ).

Der Beitrag ABAP 740-Features unter der Lupe erschien zuerst auf Tricktresor.

REDUCE + SWITCH + COND [ABAP740]

$
0
0

Heute im Code-Dojo hatte ich die Aufgabe gestellt, eine Funktion zu schreiben, die einen String mit variabel zu bestimmender Länge und zufälligen Zeichenfolgen aus Zahlen und Buchstaben zurück liefert. Zum Beispiel „I71B7HJ4BG“ oder „6EE17ICBF54IE486EHD8“.

Idee

Mit VALUE und FOR sollte ein String Zeichen für Zeichen zusammengesetzt werden. Mit einer Zufallsfunktion sollte ermittelt werden, ob ein Buchstabe oder eine Zahl eingesetzt werden soll. Per SWITCH sollte ebenfalls eine Zufallsfunktion aufgerufen werden, die eine Zahl bzw. einen Buchstaben zurück liefert.. Per String-Konkatenation sollten die zufälligen Zeichen zusammengesetzt werden.

Abweichung

die Aufgabe lässt sich mit VALUE nicht lösen. Stattdessen muss REDUCE genommen werden.

Code

Für jede Stelle des zu generierenden Strings (FOR – UNTIL – NEXT) wird eine Funktion RND_TYPE aufgerufen. Diese gibt zufällig den Wert TRUE oder FALSE zurück. Per SWITCH-Anweisung wird entschieden, ob eine Zahl (FALSE) oder ein Buchstabe (TRUE) generiert werden soll. Das generierte Zeichen wird per String-Konkatenation Zeichen für Zeichen zusammengebaut.

 

REPORT.

CLASS main DEFINITION.
   PUBLIC SECTION.
     DATA rnd_num TYPE REF TO cl_abap_random_int.
     DATA rnd_chr TYPE REF TO cl_abap_random_int.
     METHODS constructor.
     METHODS rnd_type
       RETURNING VALUE(type) TYPE boolean.
     METHODS create_random_string
       IMPORTING max           TYPE i
       RETURNING VALUE(string) TYPE string.
     METHODS get_random_char
       RETURNING VALUE(char) TYPE char01.
     METHODS get_random_number
       RETURNING VALUE(number) TYPE numc01.
 ENDCLASS.

CLASS main IMPLEMENTATION.
   METHOD constructor.
     rnd_chr = cl_abap_random_int=>create( seed = CONV #( sy-uzeit ) min = 0 max = 25 ).
     rnd_num = cl_abap_random_int=>create( seed = CONV #( sy-uzeit ) min = 0 max = 9 ).
   ENDMETHOD.
   METHOD rnd_type.

    type = COND #( LET random = get_random_number( ) IN
                    WHEN random <= 5 THEN abap_true
                    ELSE abap_false ).
   ENDMETHOD.

  METHOD get_random_char.
     DATA(offset) = rnd_num->get_next( ).
     char = sy-abcde+offset(1).
   ENDMETHOD.

  METHOD get_random_number.
     number = rnd_num->get_next( ).
   ENDMETHOD.

  METHOD create_random_string.

    string = REDUCE #( INIT text = ``
                        FOR i = 1
                        UNTIL i > max
                        NEXT text = text && SWITCH #( rnd_type( )
                                              WHEN abap_true  THEN get_random_char( )
                                              WHEN abap_false THEN get_random_number( ) ) ).

  ENDMETHOD.

ENDCLASS.

PARAMETERS p_len type i DEFAULT 10.
PARAMETERS p_str TYPE char20 MODIF ID a.

AT SELECTION-SCREEN OUTPUT.
   LOOP AT SCREEN.
     CASE screen-group1.
       WHEN 'A'.
         screen-input = '0'.
         MODIFY SCREEN.
     ENDCASE.
   ENDLOOP.

AT SELECTION-SCREEN.
   p_str = NEW main( )->create_random_string( p_len ).

Lessons Learned

Mit REDUCE können Operationen auf einen Datentyp „reduziert“ werden. Mit VALUE funktioniert das nicht.

string = REDUCE #( INIT text = ``
                   FOR i = 1
                   UNTIL i > max
                   NEXT text = text && SWITCH #( rnd_type( )
                                         WHEN abap_true  THEN get_random_char( )
                                         WHEN abap_false THEN get_random_number( ) ) ).

Mit SWITCH können nur Exakte Werte abgefragt werden (wie bei CASE auch mit OR verknüpft). Es sind jedoch keine „Größer-/ Kleiner-Vergleiche“ möglich.

[...] SWITCH #( rnd_type( )
        WHEN abap_true  THEN get_random_char( )
        WHEN abap_false THEN get_random_number( ) ) ).

Mit COND können beliebige Bedingungen geprüft werden. Allerdings muss hier jede Bedingung separat angegeben werden. Wenn der abzufragende Wert das Ergebnis einer Funktion ist, so sollte mit LET gearbeitet werden, um nicht für jede Bedingung die Funktion aufrufen zu müssen.

var = COND #( LET random = get_random_number( ) IN
              WHEN random <= 5 THEN abap_true
              ELSE abap_false ).

Die implizite Typ-Definition mit INIT (bei der REDUCE-Anweisung) ist mit Vorsicht zu genießen! Ich hatte aus Gewohnheit einen leeren „String“ mit Hochkomma-Space-Hochkomma definiert. In Wirklichkeit hatte ich damit aber einen CHAR(1)-Feld definiert und die Funktion hat immer nur ein Zeichen zurück geliefert. Die String-Konkatenation hat diesen fest definierten Typ also nicht automatisch erweitert, so wie es beim String der Fall ist. Erst die Verwendung eines echten Strings durch die Backticks liefert das gewünschte Ergebnis.

Es kann auch der Typ direkt angegeben werden (INIT text TYPE string) aber dann ist keine Vorbelegung mehr möglich. Eine implizite Definition durch Vorbelegung ist dann jedoch wieder durch die Verwendung von CONV möglich: INIT text = CONV string( ‚hallo‘ )

Bei der FOR-Funktion (FOR i = 1) muss das Hochzählen der Variable (THEN i + 1) nicht zwingend definiert werden! Wird THEN nicht angegeben, so wird implizit die Inkrementierung um Eins vorgenommen:

[...] FOR i = 1 UNTIL i > 10 [...]

 

Der Beitrag REDUCE + SWITCH + COND [ABAP740] erschien zuerst auf Tricktresor.

Tricktresorsche Glockenkurve

$
0
0

Eine kleine Fingerübung im old-style, die aus einem Denkansatz heraus abgefallen ist: Eine kleine „grafische“ Spielerei zur Darstellung der Gauß-Funktion/ Glockenkurve/ Normalverteilung.

Da dies für mich bereits höhere Mathematik ist, bin ich besonders stolz auf diese kleine Spielerei. Eine schönere Möglichkeit wäre sicherlich die Darstellung im GFW-Framework, aber ich wollte nur schnell sehen, ob die berechneten Werte irgendwie stimmig sind.

GFW-Framework

Bei Interesse schau dir die Demoprogramme GFW_PROG* und GFW_DEMO* einmal an:

GFW_DEMO_HIER3 GFW: Demonstration einer Hierarchie-/Präsentationsgrafik (Drag&Drop)
GFW_DEMO_PRES GFW: Demonstration von GFW mit sichtbarem Datencontainer
GFW_DEMO_PRES1 GFW: Demonstration von Präsentationsgrafiken mit GFW
GFW_DEMO_PRES_MAIN  class with application logic of example report „GFW_DEMO_PRES“

GFW_PROG_BAR GFW: Programmierbeispiel für ein Balkendiagramm
GFW_PROG_COLUMNS_AND_TIME_AXIS GFW: Programmierbeispiel für ein Balkendiagramm mit Zeitachse
GFW_PROG_CREATE_CUSTOMIZING GFW: Programmierbeisp. für einfachen Gebrauch von Customizing-Bündeln
GFW_PROG_DC_PERFORMANCE GFW: Programmierbeispiel für die schnelle Datencontainerverwendung
GFW_PROG_GET_CU_BUNDLE GFW: Programmierbeispiel mit Methode if_graphic_proxy~get_cu_bundle
GFW_PROG_HISTOGRAM GFW: Programmierbeispiel für ein Histogramm
GFW_PROG_LABELS GFW: Programmierbeispiel für dieselben Beschriftungen, lange Beschr.
GFW_PROG_MTA GFW: Programmierbeispiel für eine Meilensteintrendanalyse
GFW_PROG_PIE GFW: Programmierbeispiel für ein Kreisdiagramm
GFW_PROG_POINT_WITH_LABEL GFW: Programmierbeispiel für Diagramm mit gekennzeichnetem Punkt
GFW_PROG_PORTFOLIO GFW: Programmierbeispiel für ein Balkendiagramm
GFW_PROG_SPEEDOMETER GFW: Programmierbeispiel für ein Balkendiagramm
GFW_PROG_TIME_AXIS GFW: Programmierbeispiel für ein Punktdiagramm mit Zeitachse
GFW_PROG_TUTORIAL GFW: Programmierbeispiel für eine einfache PräsGrafik = GFW-Tutorial

Code

REPORT zz_gauss_timer_demo NO STANDARD PAGE HEADING LINE-SIZE 1000.


CLASS main DEFINITION.
  PUBLIC SECTION.
    METHODS start.
  PROTECTED SECTION.
    DATA sigma TYPE f VALUE '0.1'.
    DATA my TYPE f VALUE 0.
    DATA count TYPE i.
    METHODS gauss.
    DATA timer TYPE REF TO cl_gui_timer.
    METHODS finished FOR EVENT finished OF cl_gui_timer.
    METHODS clear_screen.
ENDCLASS.

CLASS main IMPLEMENTATION.
  METHOD start.
    gauss( ).
    timer = NEW #( ).
    SET HANDLER finished FOR timer.
    timer->interval = 1.
    timer->run( ).
  ENDMETHOD.

  METHOD finished.
    gauss( ).
    ADD 1 TO count.
    IF count < 10.
      timer->run( ).
    ELSE.
      SKIP TO LINE 28.
      POSITION 120.
      WRITE 'FINISHED'.
    ENDIF.
  ENDMETHOD.

  METHOD gauss.

    DATA e   TYPE f VALUE '2.718281828459'.
    DATA pi  TYPE f VALUE '3.14159265359'.
    DATA x   TYPE f.
    DATA erg TYPE f.
    DATA anz TYPE i VALUE 51.
    DATA l   TYPE i. "value for result (scaled)

    clear_screen( ).

    SKIP TO LINE 3.

    ADD '0.02' TO sigma.

    x = -1.


    DO anz TIMES.

      "calculate gauss
      erg = ( 1 / sigma * sqrt( 2 * pi ) ) * e **
            ( '0.5-' * (  ( x - my ) / (  sigma  ) ) ** 2 ) .

      "write result
      WRITE: / x EXPONENT 0 DECIMALS 2, erg EXPONENT 0 DECIMALS 5.

      "scale result
      l = erg * 4.

      "write graph
      DO l TIMES.
        WRITE sym_checkbox as SYMBOL NO-GAP.
*        WRITE icon_bw_apd_source as icon NO-GAP.
      ENDDO.

      "add step
      x = x + 1 / ( ( anz - 1 ) / 2 ) .

    ENDDO.
  ENDMETHOD.

  METHOD clear_screen.
    SKIP TO LINE 1.

    write: /20 'x-value                 result;   sigma=', sigma EXPONENT 0 DECIMALS 3 LEFT-JUSTIFIED,
               'my=', my EXPONENT 0 DECIMALS 3 LEFT-JUSTIFIED.

    SET BLANK LINES ON.
    DO 50 TIMES.
      DO sy-linsz TIMES.
        WRITE space NO-GAP.
      ENDDO.
    ENDDO.
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  NEW main( )->start( ).
 

Der Beitrag Tricktresorsche Glockenkurve erschien zuerst auf Tricktresor.


Falle beim Left Outer Join

$
0
0

Häufig sind es die Kleinigkeiten, die einem das Leben schwer machen. Die berühmten letzten 20%. Häufig bemerkt man jedoch gar nicht, dass man überhaupt ein Problem hat, weil alles scheinbar so funktioniert, wie man es sich vorstellt.

Aufgabe: SELECT

Die Aufgabe sollte sein: Selektiere aus Tabelle ZZT1 alle Einträge anhand der gegebenen Selektionskriterien (AREA). Lies zusätzlich mit einem LEFT OUTER JOIN alle Einträge aus Tabelle ZZT2, die über das Feld LINK verknüpft sind hinzu. Einträge aus Tabelle ZZT2 mit gesetztem Löschkennzeichen dürfen nicht berücksichtigt werden.

Demodaten

Die folgenden Demodaten stehen zur Verfügung:

  • Tabelle ZZT1
  • Tabelle ZZT2

Selektionskriterium soll sein: AREA = TEST

Tabelle ZZT1 (links)

KEY1 TEXT LINK AREA
1 Eins A TEST
2 Zwei B TEST
3 Drei C TEST
4 Vier TEST
5 Fünf A NEU

Tabelle ZTT2 (rechts)

LINK TEXT LOEVM
A Info A
B Info B X
C Info C

Überlegungen

Wenn aus Tabelle ZZT1 alle Einträge selektiert werden, bei denen das Feld AREA mit „TEST“ gefüllt ist, dann erwarte ich, dass die folgenden Einträge selektiert werden: 1, 2, 3 und 4.

Zusätzlich muss bei den Einträgen 1 und 3 der LINK_TEXT gefüllt sein, denn diese Einträge verweisen auf Einträge in Tabelle ZZT2, die keine Löschvormerkung haben.

Erster Versuch

Der erste Wurf sieht folgendermaßen aus:

SELECT z1~key1,
       z1~text,
       z1~link,
       z1~area,
       z2~text AS link_text
  FROM zzt1 AS z1
  LEFT OUTER JOIN zzt2 AS z2 ON z1~link = z2~link
  INTO TABLE @DATA(t_data)
 WHERE z1~area  = 'TEST'.
   AND z2~loevm = @space.

Der SELECT ist syntaktisch (!) fehlerfrei, liefert jedoch leider ein falsches Ergebnis:

KEY1 TEXT LINK AREA LINK_TEXT
1 Eins A TEST Info A
3 Drei C TEST Info C

Zweiter Versuch

Ohne die Einschränkung über die WHERE-Bedingung auf der rechten Seite (AND z2~loevm = @space) funktioniert die Selektion wie erwartet.

Das ist nicht ganz das, was ich erwartet habe… Ich habe mich dann damit beschäftigt, warum das Ergebnis so aussieht und warum Ergebniszeilen fehlen. Beziehungsweise habe ich zuerst versucht, das richtige Ergebnis zu bekommen. Der richtige Select lautet so:

SELECT z1~key1,
       z1~text,
       z1~link,
       z1~area,
       z2~text AS link_text
  FROM zzt1 AS z1
  LEFT OUTER JOIN zzt2 AS z2 ON z1~link = z2~link
                            AND z2~loevm = @space
  INTO TABLE @DATA(t_data)
 WHERE z1~area = 'TEST'.

Ergebnis:

KEY1 TEXT LINK AREA LINK_TEXT
1 Eins A TEST Info A
2 Zwei B TEST
3 Drei C TEST Info C
4 Vier TEST

Lessons learned

Bei einem LEFT OUTER JOIN darf die WHERE-Bedingung keine Einschränkung auf die rechte Tabelle haben. In der Hilfe zum JOIN steht:

Eine WHERE-Bedingung für eine SELECT-Anweisung mit Joins wirkt auf die durch die Joins gebildete Ergebnismenge.

Ich deute das so, dass die WHERE-Bedingung sozusagen erst nachträglich angewendet wird. Wobei das für mich ein Zirkelschluss ist, den ich nicht verstehe. Zudem enthält die Ergebnismenge ja gar kein Feld „LOEVM“.

Auf jeden Fall sollte man die Verwendung beziehungsweise die gesammelten Daten eines LEFT OUTER JOINS sehr genau prüfen. Schnell schleicht sich hier ein Fehler ein, mit dem man nicht gerechnet hat.

Der Beitrag Falle beim Left Outer Join erschien zuerst auf Tricktresor.

Datenstrukturen EXPORT TO MEMORY

$
0
0

Die Befehle EXPORT TO MEMORY und IMPORT FROM MEMORY sind einfach zu benutzen und sind eine große Hilfe bei der Speicherung von Daten jeder Art. Mit dem Medium DATABASE werden die Daten in einer Clustertabelle gespeichert. Die bekannteste ist INDX. Zusätzlich muss ein Gebiet angegeben werden (zweistelliges Kürzel) und eine ID.

Mit folgendem kleinen Testprogramm zeige ich dir, wie du einen strukturierten Datensatz ablegen und wieder laden kannst.

Coding

REPORT.

*== Typisierung der Datenstruktur
TYPES: BEGIN OF ty_data,
         key    TYPE matnr,
         mara   TYPE mara,
         t_mard TYPE STANDARD TABLE OF mard WITH DEFAULT KEY,
         t_makt TYPE STANDARD TABLE OF makt WITH DEFAULT KEY,
       END OF ty_data.

*== Datenstruktur
DATA s_data TYPE ty_data.

*== Auswahl: Import oder Export der Daten
PARAMETERS p_import RADIOBUTTON GROUP mode DEFAULT 'X'.
PARAMETERS p_export RADIOBUTTON GROUP mode.
*== Zu lesendes Element (MaterialnummeR)
PARAMETERS p_matnr TYPE matnr DEFAULT '1000002'.

START-OF-SELECTION.

  CASE 'X'.
    WHEN p_export.
      "MARA-Daten lesen
      SELECT SINGLE * FROM mara INTO @s_data-mara WHERE matnr = @p_matnr.
      IF sy-subrc = 0.
        "Schlüssel setzen
        s_data-key = p_matnr.
        "Zusätzliche Tabellen lesen
        SELECT * FROM mard INTO TABLE @s_data-t_mard WHERE matnr = @p_matnr.
        SELECT * FROM makt INTO TABLE @s_data-t_makt WHERE matnr = @p_matnr.
        "Daten in INDX exportieren
        EXPORT data FROM s_data TO DATABASE indx(z1) ID p_matnr.
      ENDIF.

    WHEN p_import.
      "Exportierte Daten zum Schlüssel wieder einlesen
      IMPORT data TO s_data FROM DATABASE indx(z1) ID p_matnr.
  ENDCASE.

  "Daten im Debugger überprüfen
  BREAK-POINT.
Im Debugger sehen die Daten wie folgt aus:

Vorteile

Die Handhabung der beiden Befehle zum Exportieren und Einlesen der Daten ist extrem einfach. Auf diese Weise können einfache Feldleisten, interne Tabellen oder auch komplexe Datenstrukturen unkompliziert in der Datenbank abgelegt werden.

Die Testdaten zu Funktionsbausteinen werden übrigens auf diese Weise verwaltet (Clustertabelle EUFUNC und Gebiet FL).

Die Technik gibt es bereits sehr lange und dürfte ausreichend schnell und sicher in der Anwendung sein.

Nachteile

Änderbarkeit

Der große Nachteil bei der Verwendung: Die Datenstrukturen müssen immer gleich bleiben. Sobald sich die Datenstrukturen ändern, kann es sein, dass der Import alter Daten nicht mehr funktioniert. Die größte Chance auf alte Daten zuzugreifen hat man noch, wenn neue Felder ans Ende der Struktur gesetzt werden. Werden Objekte aus der Mitte der Struktur gelöscht oder eingefügt, kann die Zuordnung der Daten nicht mehr erfolgen. Es erfolgt der Laufzeitfehler CONNE_IMPORT_WRONG_COMP_TYPE. Die gute Nachricht: Du kannst den Laufzeitfehler mit der Ausnahme CX_SY_IMPORT_MISMATCH_ERROR abfangen. Die zweite gute Nachricht: es gibt eine ebenfalls recht komfortable Alternative: Daten dynamisch verwalten

Lesbarkeit

Die Daten werden in einem Datencluster abgespeichert. Dieser Cluster sieht in etwa folgendermaßen aus:

In der gespeicherten Tabelle kannst du also noch sehen, dass Einträge zu einer ID vorhanden sind, aber sie sind nicht mehr lesbar.

Typsisierung

Um die Daten aus der Clustertabelle wieder lesen zu können, musst du zwingend die Struktur der Daten kennen. Ansonsten wird es s o gut wie unmöglich, diese wieder sichtbar zu machen. Aber auch hier hilft der Tipp Daten dynamisch verwalten.

Aufruf der Befehle

Die Methode mit IMPORT TO DATABASE wird häufig in verschiedenen Programmen verwendet, um auf Daten zugreifen zu können, die eigentlich an anderer Stelle nicht mehr zur Verfügung stehen. Dadurch ist es häufig sehr schwer zu erkennen, wo das jeweilige Gegenstück des Befehls verwendet wird. Wenn du an einer Stelle über den IMPORT gestolpert bist, dann ist es eventuell sehr schwer, die Stelle zu finden, die den EXPORT macht und umgekehrt.

Aber auch hier gibt es eine einfache Lösung. Sie bedeutet einen kleinen Aufwand, sollte aber in jedem Fall gemacht werden: Der EXPORT und IMPORT der Daten wird in eine eigene Klasse ausgelagert. Export und Import erfolgen jeweils über die gleichnamige Methode der Klasse. So sind EXPORT und IMPORT für ein Objekt an einer Stelle vorhanden. Zusätzlich kann über einen Verwendungsnachweis der Aufruf ermittelt werden.

Analyse

Der Grund für diesen Artikel ist allerdings, dass ich heute erst – nachdem ich den Befehl schon mehrere Jahre lang verwendet habe – erfahren habe, dass es einen Report gibt, der die Struktur der Daten ausgibt: RSINDX00

Der Ausgabe des Reports sieht man sein Alter an… Erstellt wurde er vor über 15 Jahren. Aber, wenn alle Stricke reißen, so kann er eine gute Hilfe sein, um die gespeicherten Daten zu analysieren:

Der Beitrag Datenstrukturen EXPORT TO MEMORY erschien zuerst auf Tricktresor.

Fibonacci

$
0
0

Ein kleines Beispielprogramm um die verschiedenen Arten der Berechnung von Fibonacci-Zahlen und deren Geschwindigkeit zu demonstrieren.

Ursprünglich habe ich das Programm auf Grund eines Beitrags im ABAP-Forum geschrieben (der aber leider gelöscht wurde) und um die verschiedenen Arten der Berechnung zu vergleichen. Hauptsächlich sollte es ein Vergleich sein zwischen der rekursiven und der iterativen Variante. Dazu gekommen ist dann noch eine Lösung, die mit einer internen Tabelle arbeitet. Ausschlaggebend für die Veröffentlichung war dann ein Beispiel von Lars Hvam dafür, wie man nicht programmieren sollte. Wie man an den Ergebnissen sieht, auch im Sinne der schlechten Performance…

Code

REPORT zz_fibonacci.

DATA result_f TYPE f.
DATA start    TYPE i.
DATA stopp    TYPE i.
DATA i        TYPE i.

PARAMETERS p_n       TYPE i.
PARAMETERS p_reku RADIOBUTTON GROUP b USER-COMMAND space.
PARAMETERS p_iter RADIOBUTTON GROUP b.
PARAMETERS p_tabl RADIOBUTTON GROUP b.
PARAMETERS p_hvam RADIOBUTTON GROUP b.
PARAMETERS p_res  TYPE text50 MODIF ID x.
PARAMETERS p_time TYPE i      MODIF ID x.

CLASS lcl_fibonacci DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS calc_rekursiv IMPORTING n TYPE i RETURNING VALUE(result) TYPE f.
    CLASS-METHODS calc_iterativ IMPORTING n TYPE i RETURNING VALUE(result) TYPE f.
    CLASS-METHODS calc_read_table IMPORTING x TYPE i RETURNING VALUE(result) TYPE f.
    CLASS-METHODS calc_hvam IMPORTING n TYPE i RETURNING VALUE(r) TYPE f.
  PRIVATE SECTION.
    CLASS-METHODS f IMPORTING i TYPE f RETURNING VALUE(f) TYPE f.
ENDCLASS.

CLASS lcl_fibonacci IMPLEMENTATION.

  METHOD calc_iterativ.

    DATA f1 TYPE f VALUE 0.
    DATA f2 TYPE f VALUE 1.
    DATA x  TYPE f VALUE 0.

    IF n <= 0.
      result = 0.
    ELSEIF n = 1.
      result = 1.
    ELSE.
      x = n - 1.
      DO x TIMES.
        result = f1 + f2.
        f1 = f2.
        f2 = result.

      ENDDO.
    ENDIF.

  ENDMETHOD.

  METHOD calc_rekursiv.
    DATA f TYPE f.
    f = n.
    result = f( f ).
  ENDMETHOD.

  METHOD calc_read_table.
    "http://www.abapforum.com/forum/viewtopic.php?f=1&t=21045

    TYPES BEGIN OF ts_fibonacci.        "Strukturtyp
    TYPES n         TYPE i.             "Zählvariable    (Spalte)
    TYPES fib_n     TYPE i.             "Fibonacci-Zahl  (Spalte)
    TYPES rechnung  TYPE string.        "Rechenweg       (Spalte)
    TYPES END OF ts_fibonacci.

    DATA gf_zahl1 TYPE i.
    DATA gf_zahl1_s TYPE string.
    DATA gf_zahl2 TYPE i.
    DATA gf_zahl2_s TYPE string.
    DATA gt_fibzahl TYPE TABLE OF ts_fibonacci.     "Tabelle
    DATA gs_fib TYPE ts_fibonacci.

    DO x TIMES.

      IF sy-index = 1 OR sy-index = 2.
        gs_fib-n = sy-index.
        gs_fib-fib_n = 1.
        gs_fib-rechnung = '-'.

      ELSE.
        READ TABLE gt_fibzahl
        INTO gs_fib
        INDEX sy-index - 1.
        gf_zahl1 = gs_fib-fib_n.

        READ TABLE gt_fibzahl
        INTO gs_fib
        INDEX sy-index - 2.
        gf_zahl2 = gs_fib-fib_n.

        gs_fib-fib_n = gf_zahl1 + gf_zahl2.
        gs_fib-n = sy-index.
        gf_zahl1_s = gf_zahl1.
        gf_zahl2_s = gf_zahl2.
        CONCATENATE gf_zahl1_s '+' gf_zahl2_s INTO gs_fib-rechnung SEPARATED BY space.
      ENDIF.

      APPEND gs_fib TO gt_fibzahl.
      CLEAR gs_fib.

    ENDDO.

    READ TABLE gt_fibzahl INDEX lines( gt_fibzahl ) INTO gs_fib.
    result = gs_fib-fib_n.

  ENDMETHOD.

  METHOD f.
    DATA x TYPE f.
    DATA y TYPE f.

    IF i <= 0.
      f = 0.
    ELSEIF i = 1.
      f = 1.
    ELSE.
      x = i - 2.
      y = i - 1.
      f = f( x ) + f( y ).
    ENDIF.
  ENDMETHOD.

  METHOD calc_hvam.

    "negative example of Lars Hvam for how _NOT_ to code!
    "https://gist.github.com/larshp/cc5326dec8fe413bdc29e4d6b8c64b4f
    DATA n1 TYPE i.
    DATA n2 TYPE i.
    DATA r1 TYPE p.
    DATA r2 TYPE f.

    n2 = n - 1.
    n1 = n2 - 1.
    IF n = 1.
      r = n.
    ELSEIF n := 2.
      r = n - 1.
    ELSE.
      r2 = calc_hvam( n1 ).
      r1 = calc_hvam( n2 ).
    ENDIF.
    r = r + r1 + r2.

  ENDMETHOD.

ENDCLASS.



AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN.
    IF screen-group1 = 'X'.
      screen-input = '0'.
      MODIFY SCREEN.
    ENDIF.
  ENDLOOP.

  GET RUN TIME FIELD start.
  CASE abap_true.
    WHEN p_iter.
      result_f = lcl_fibonacci=>calc_iterativ( p_n ).
    WHEN p_reku.
      result_f = lcl_fibonacci=>calc_rekursiv( p_n ).
    WHEN p_tabl.
      result_f = lcl_fibonacci=>calc_read_table( p_n ).
    WHEN p_hvam.
      result_f = lcl_fibonacci=>calc_hvam( p_n ).
  ENDCASE.

  WRITE result_f TO p_res EXPONENT 0 DECIMALS 0 LEFT-JUSTIFIED.
  GET RUN TIME FIELD stopp.
  p_time = stopp - start.


START-OF-SELECTION.

  DO p_n TIMES.
    i = sy-index.

    GET RUN TIME FIELD start.
    CASE abap_true.
      WHEN p_iter.
        result_f = lcl_fibonacci=>calc_iterativ( i ).
      WHEN p_reku.
        result_f = lcl_fibonacci=>calc_rekursiv( i ).
      WHEN p_tabl.
        result_f = lcl_fibonacci=>calc_read_table( i ).
    ENDCASE.

    WRITE result_f TO p_res EXPONENT 0 DECIMALS 0 LEFT-JUSTIFIED.
    WRITE: / i, p_res.

  ENDDO.

  GET RUN TIME FIELD stopp.
  p_time = stopp - start.

  WRITE: / 'Time:', p_time.
 

Der Beitrag Fibonacci erschien zuerst auf Tricktresor.

Abhängige Suchhilfe

$
0
0

Immer wieder ein Thema in Selektionsbildschirmen oder Dynpros: Abhängig vom Feldwert eines anderen Feldes die Suchhilfe für die angeforderte Suchhilfe einschränken.

Die wirklich einfachste Möglichkeit ist die über die Verknüpfung der Felder in einer Struktur: Feldabhängige Selektion

Manchmal reicht das jedoch nicht aus oder man möchte anhand anderer Feldwerte unterschiedliche Suchhilfen aufrufen. Das folgende Coding zeigt die Möglichkeit mit Hilfe der Funktionsbausteine DYNP_VALUES_READ und DYNP_VALUES_UPDATE.

Feldübertragung / PAI – PBO

Eine kurze Info, warum mit den genannten Bausteinen gearbeitet werden muss: Ein Dynpro ist ein eigenes Objekt. Es können zwar Feldnamen (Variablen) aus dem ABAP-Programm verwendet werden, aber diese sind erst einmal unabhängig vom Dynpro. Ebenso können im Dynpro Felder definiert werden, die im ABAP-Programm nicht bekannt sind.

Die Übertragung der Feldwerte erfolgt über Namensgleichheit.

Ein Dynpro wird im PBO – Process Before Output initialisiert. Hier können Feldattribute (Sichtbar, eingabebereit, …) gesetzt werden. Der GUI-Status und GUI-Titel können gesetzt bzw. geändert werden. Felder können befüllt werden bzw. werden aus dem ABAP-Programm übernommen.

Danach wird das Dynpro mit den Feldern, Feldwerten und Feldattributen angezeigt.

Bei Tastendruck wird das PAI – Process After Input ausgeführt. Hier werden geänderte Feldwerte in die ABAP-Variablen, nach Prüfung auf Richtigkeit (Datum, Festwerte etc), übernommen. Der OK-Code kann ausgewertet werden.

Es gibt drei Ausnahmen von dieser Logik:

  • F1-Hilfe: Die Taste F1 wird ausgeführt, ohne dass der PAI angestoßen wird.
  • F4-Wertehilfe: Auch die F4-Hilfe bewirkt keinen PAI. Das bedeutet, dass ein soeben eingegebener Wert in ein Eingabe bereites Feld nicht in die entsprechende ABAP-Variable übernommen wird!
  • Exit-Kommandos: Funktionen, die im GUI-Status als „Exit-Funktion“ gekennzeichnet sind bewirken zwar einen PAI, jedoch ohne dass Feldprüfungen statt finden oder Feldwerte übernommen werden

F4-Falle

Diese Besonderheiten muss man wissen, um zu verstehen, warum bei einer F4-Hilfe die Datenübertragung zwischen Dynpro und ABAP-Programm nachprogrammiert werden muss. Wenn du im Dynpro in FELD1 einen Wert eingibst und dann, ohne eine Funktionstaste zu drücken (ENTER, F2, F3, etc.) und mit dem Cursor in das FELD2 springst, dann ist der Wert aus FELD1 noch nicht im ABAP-Programm bekannt! Wenn du nun in FELD2 die F4-Werthilfe betätigst, weiß das ABAP-Programm nichts von dem soeben eingegebenen Wert in FELD1. Der Wert muss erst mit DYNP_VALUES_READ ermittelt werden.

Änderst du den Wert für FELD2, also das Feld für das du die Werthilfe aufgerufen hast, dann kannst du im ABAP-Programm dieses Feld einfach füllen. Der Transport zum Dynpro erfolgt automatisch. Wenn du allerdings ein anderes Feld ändern möchtest, dann musst du DYNP_VALUES_UPDATE verwenden.

Beispielprogramm

Das Beispielprogramm demonstriert die Verwendung von DYNP_VALUES_GET und DYNP_VALUES_UPDATE. Bei F4 im Feld P_EINS werden Werte gesetzt. Bei F4 im Feld P_ZWEI wird abhängig von P_EINS ein anderer Wert gesetzt.

Code

REPORT.

PARAMETERS p_eins TYPE char10.
PARAMETERS p_zwei TYPE char10.


AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_eins.
  PERFORM eins.


AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_zwei.
  PERFORM zwei.

FORM eins.

  DATA lt_fields TYPE STANDARD TABLE OF dynpread.
  DATA ls_field  TYPE dynpread.

  p_eins = 'Z'.
  p_zwei = '999'.

  ls_field-fieldname  = 'P_ZWEI'.
  ls_field-fieldvalue = p_zwei.
  APPEND ls_field TO lt_fields.

  CALL FUNCTION 'DYNP_VALUES_UPDATE'
    EXPORTING
      dyname     = sy-cprog
      dynumb     = sy-dynnr
    TABLES
      dynpfields = lt_fields
    EXCEPTIONS
      OTHERS     = 8.
  IF sy-subrc = 0.
    MESSAGE 'Feldwert gesetzt' TYPE 'S'.
  ENDIF.
ENDFORM.

FORM zwei.

  DATA lt_fields TYPE STANDARD TABLE OF dynpread.
  DATA ls_field  TYPE dynpread.

  CALL FUNCTION 'DYNP_VALUES_READ'
    EXPORTING
      dyname                   = sy-repid
      dynumb                   = sy-dynnr
      translate_to_upper       = 'X'
      request                  = 'A'
      perform_conversion_exits = 'X'
    TABLES
      dynpfields               = lt_fields
    EXCEPTIONS
      OTHERS                   = 11.

  READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'P_EINS'.
  IF sy-subrc = 0.
    p_eins = ls_field-fieldvalue.
  ENDIF.

  CASE p_eins.
    WHEN 'A'.
      p_zwei = '1'.
    WHEN 'B'.
      p_zwei = '2'.
    WHEN 'C'.
      p_zwei = '3'.
  ENDCASE.

ENDFORM.
 

Der Beitrag Abhängige Suchhilfe erschien zuerst auf Tricktresor.

ALV-Grid um Sortierfunktion erweitern (Vererbung)

$
0
0

Objektorientierte Programmierung ist häufig immer noch ein rotes Tuch für viele. Man weiß zwar, wie Methoden aufgerufen werden und dass ein Objekt mit CREATE OBJECT oder NEW erzeugt werden muss aber die Designprinzipien sind irgendwie unklar. Und SAP-Klassen sind eh unantastbar.

In diesem Artikel möchte ich dir eine Möglichkeit vorstellen, wie du den SAP-Standard mit Standardmitteln, nämlich mit Hilfe der Vererbung, erweitern kannst.

Enjoy und Bedienung

Trotz der GUI-Elemente, die unter dem Schlagwort ENJOY eingeführt wurden, sind viele Elemente immer noch nicht wirklich benutzerfreundlich. Einiges kann man ändern, anderes nicht. Eine Möglichkeit um mit Hilfe der SAP-Standardcontrols ein neues Look & Feel zu erzeugen, habe ich in diesem Beitrag gezeigt: Moderne UI mit altem SAPGUI und ALV-Grid

Hier habe ich das Standard-ALV-Grid verwendet, um eine neue Funktionalität zu erzeugen. Dies ist allerdings ein eigenständiges Objekt und erweitert nicht die Standardfunktionalität des ALV-Grids.

Wie das Überschreiben von geschützten Methoden generell funktioniert, habe ich hier beschrieben: Geschützte Methoden nutzen

Dieser Artikel soll zeigen, dass es sich eventuell lohnt, auch über andere Erweiterungen von SAP-Standardfunktionalitäten nachzudenken. Es gibt Funktionalitäten, die eventuell in jedem ALV-Grid hilfreich wären. Zum Beispiel das einfache Umsortieren von Einträgen.

Umsortierung mittels Drag & Drop

Eine Möglichkeit ist die Sortierung mittels Drag&Drop im ALV-Feldkatalog:

Die Bedienung ist hier zwar auch gewöhnungsbedürftig, denn ein Eintrag, der umsortiert werden soll, muss erst mit einem Klick markiert und kann dann erst mittels Drag & Drop an eine andere Stelle verschoben werden, aber immerhin.

Umsortierung mit Funktionstasten

Eine andere Möglichkeit wäre das Verschieben von Einträgen mit Funktionstasten. Folgender Screenshot ist aus dem unten stehenden Demo-Programm. Das ALV-Grid wurde um die Funktionstasten „Sort Up“ und „Sort Down“ erweitert.

Wie das im Einzelnen geht, erkläre ich gleich.

Redefinition

eine wirklich starke Waffe des objektorientierten Sprachumfangs ist die Vererbung. Sofern die anzupassende Klasse nicht als „Final“ definiert wurde, können geschützte und öffentliche Methoden redefiniert werden. Leider wird bei Anlage einer Klasse das Kennzeichen „Final“ vorbelegt, so dass der Programmierer dieses aktiv entfernen muss. Ist dieses Kennzeichen gesetzt, funktioniert die Ableitung bzw. Vererbung nicht.

Die Klasse CL_GUI_ALV_GRID allerdings darf vererbt werden, denn sie ist nicht als final gekennzeichnet. Nichts desto Trotz muss bei Klassen sehr genau definiert werden, welche Methoden vererbt werden dürfen und welche nicht.

Funktion „Umsortieren“

Die neue Funktion nenne ich „Umsortieren“, denn es ist keine Sortieren-Funktion, die man aus dem Standard kennt. Um Einträge umsortieren zu können, wird in der Regel im SAP mit einem Sortierfeld gearbeitet, das der Anwender manuell pflegen muss. Dazu wird häufig in Zehnerschritten gearbeitet, um später Einträge einfügen zu können. Einträge in dieser Form in eine andere Reihenfolge zu bringen ist in der Regel sehr mühselig.

Die Idee ist, dass der Anwender einen Eintrag markieren kann und diesem mit den Funktionstasten „Sort Up“ und Sort Down“ in der Liste hoch und runter verschieben kann. Ein Feld, in der die aktuelle Reihenfolge festgehalten wird, brauchen wir natürlich trotzdem.

Vererbung

Ich möchte die Klasse CL_GUI_ALV_GRID also für meine Zwecke missbrauchen und muss sie deswegen ableiten bzw. vererben. Dazu lege ich in der Transaktion SE80 oder SE24 eine neue Klasse an: ZCL_GUI_ALV_GRID_SORT und gebe als Oberklasse zu zu beerbende Klasse CL_GUI_ALV_GRID an:

Sortierfeld

Der Programmierer muss angeben können, welches Feld für die Sortierung der Einträge verwendet werden soll. In diesem Feld wird dann die automatische Nummerierung anhand der Reihenfolge gesetzt. Ich lege dafür die Methode SET_SORT_FIELD an mit dem Übergabeparameter FIELDNAME.

Diese Methode muss vor SET_TABLE_FOR_FIRST_DISPLAY aufgerufen werden, damit die Umsortierfunktionalität von Anfang an zur Verfügung steht.

Das Sortierfeld merke ich mir im Attribut MV_SORT_FIELD.

Ereignis TOOLBAR

Da ich das Ereignis TOOLBAR nutzen möchte, um die neuen Funktionstasten einzubauen, muss ich es für meine abgeleitete Klasse registrieren:

SET HANDLER on_toolbar FOR me.

Zusätzlich benötige ich eine Methode, die beim Auslösen des Ereignisses angesprungen wird: ON_TOOLBAR. Diese Methode muss als Ereignisbehandler für das Ereignis TOOLBAR definiert werden:

Der Methode stehen nun theoretisch alle Parameter des Ereignisses zur Verfügung. Allerdings müssen diese manuell übernommen werden. Die Drucktaste „Ereignisparameter“ in der Sicht „Parameter“ erledigt das für mich:

In der Methode füge ich die Drucktasten SORT_UP und SORT_DOWN der Toolbar hinzu.

METHOD on_toolbar.

    check mv_sort_field is NOT INITIAL.

    APPEND VALUE #(   function  = 'Sort_down'
                      icon      = icon_next_page
                      quickinfo = space
                      butn_type = if_sat_ui_button_types=>normal
                      disabled  = space
                      text      = 'Sort down'
                      checked   = space ) TO e_object->mt_toolbar.
    APPEND VALUE #(   function  = 'Sort_up'
                      icon      = icon_previous_page
                      quickinfo = space
                      butn_type = if_sat_ui_button_types=>normal
                      disabled  = space
                      text      = 'Sort up'
                      checked   = space ) TO e_object->mt_toolbar.

  ENDMETHOD.

Redefinition DISPATCH

Um intern auf die Drucktasten reagieren zu können, muss ich die Methode DISPATCH redefinieren und meine Drucktasten SORT_UP und SORT_DOWN für das Ereignis TOOLBAR_BUTTON_CLICK abfangen.

In allen anderen Fällen muss die Methode DISPATCH der abgeleiteten Klasse aufgerufen werden (SUPER->DISPATCH).

Im Falle des Ereignisses TOOLBAR_BUTTON_CLICK muss ich mir noch die Ereignisparameter besorgen in denen der Funktionscode der Drucktaste steht (Methode GET_EVENT_PARAMETER).

Nun gilt es noch, folgendes zu tun:

  • Abfrage auf die Funktionscodes SORT_UP und SORT_DOWN
  • Zugriff auf die Datentabelle erhalten
  • Ermitteln der aktuellen Cursorposition
  • Umsortieren des Eintrags
  • Neunummerierung
  • Cursor auf die umsortierte Zeile setzen
  • Anzeige aktualisieren
METHOD dispatch.

    DATA action TYPE string.
    CASE eventid.
      WHEN evt_toolbar_button_click.
        CALL METHOD get_event_parameter
          EXPORTING
            parameter_id = 0
            queue_only   = space
          IMPORTING
            parameter    = action.
        CALL METHOD cl_gui_cfw=>flush.
        CASE action.
          WHEN 'Sort_up'.
            CALL METHOD get_current_cell
              IMPORTING
                es_row_id = DATA(ls_row)
                es_row_no = DATA(ls_row_no).
            FIELD-SYMBOLS <outtab> TYPE table.
            ASSIGN mt_outtab->* TO <outtab>.
            IF ls_row-index > 1.
              READ TABLE <outtab> ASSIGNING FIELD-SYMBOL(<outline>) INDEX ls_row-index.
              DATA(indx) = ls_row-index - 1.
              ASSIGN COMPONENT 2 OF STRUCTURE <outline> TO FIELD-SYMBOL(<value>).
              IF sy-subrc = 0.
                INSERT <outline> INTO <outtab> INDEX indx.
                indx = indx + 2.
                DELETE <outtab> INDEX indx.
                indx = indx - 2.

                LOOP AT <outtab> ASSIGNING <outline>.
                  ASSIGN COMPONENT mv_sort_field OF STRUCTURE <outline> TO <value>.
                  <value> = sy-tabix.
                ENDLOOP.
                refresh_table_display( is_stable = VALUE #( col = abap_true row = abap_true ) i_soft_refresh = abap_true ).
                set_selected_rows( it_row_no = VALUE #( ( row_id = indx ) ) ).
              ENDIF.
            ENDIF.
            EXIT.
          WHEN 'Sort_down'.
            CALL METHOD get_current_cell
              IMPORTING
                es_row_id = ls_row
                es_row_no = ls_row_no.
            ASSIGN mt_outtab->* TO <outtab>.
            IF ls_row-index < lines( <outtab> ).
              READ TABLE <outtab> ASSIGNING <outline> INDEX ls_row-index.
              indx = ls_row-index + 2.
              ASSIGN COMPONENT 2 OF STRUCTURE <outline> TO <value>.
              IF sy-subrc = 0.
                INSERT <outline> INTO <outtab> INDEX indx.
                indx = indx - 2.
                DELETE <outtab> INDEX indx.
                indx = indx + 1.
                LOOP AT <outtab> ASSIGNING <outline>.
                  ASSIGN COMPONENT mv_sort_field OF STRUCTURE <outline> TO <value>.
                  <value> = sy-tabix.
                ENDLOOP.

                refresh_table_display( is_stable = VALUE #( col = abap_true row = abap_true ) i_soft_refresh = abap_true ).
                set_selected_rows( it_row_no = VALUE #( ( row_id = indx ) ) ).
              ENDIF.
            ENDIF.
            EXIT.
        ENDCASE.
    ENDCASE.


    super->dispatch(
      EXPORTING
        cargo             = cargo
        eventid           = eventid
        is_shellevent     = is_shellevent
        is_systemdispatch = is_systemdispatch
      EXCEPTIONS
        cntl_error        = 1
        OTHERS            = 2 ).

  ENDMETHOD.

Testprogramm

In folgendem Testprogramm kannst du die Verwendung des neuen Standards sehen. Du siehst, dass trotz meines Eingriffs in die Toolbar weiterhin Drucktasten hinzugefügt werden können:

REPORT zz_alv_grid_sort.

DATA gs_data TYPE vbak.

SELECT-OPTIONS s_vbeln FOR gs_data-vbeln.


CLASS main DEFINITION.
  PUBLIC SECTION.
    TYPES ty_data       TYPE vbak.

    TYPES ty_data_t     TYPE STANDARD TABLE OF ty_data
                             WITH DEFAULT KEY.

    DATA ms_data        TYPE ty_data.
    DATA mt_data        TYPE ty_data_t.

    DATA mr_grid        TYPE REF TO zcl_gui_alv_grid_sort.
    METHODS start.
  PROTECTED SECTION.
    METHODS selection.
    METHODS display.
    METHODS handle_toolbar      FOR EVENT toolbar
                  OF cl_gui_alv_grid
      IMPORTING e_object.
    METHODS handle_user_command FOR EVENT user_command
                  OF cl_gui_alv_grid
      IMPORTING e_ucomm sender.

ENDCLASS.

CLASS main IMPLEMENTATION.

  METHOD handle_user_command.

    DATA lt_rows TYPE lvc_t_row.
    DATA ls_row   TYPE lvc_s_row.
    DATA ls_data  TYPE ty_data.

    CASE e_ucomm.
      WHEN 'USER01'.
        sender->get_selected_rows( IMPORTING et_index_rows = lt_rows ).
        LOOP AT lt_rows INTO ls_row.
          READ TABLE mt_data INTO ls_data INDEX ls_row-index.
          IF sy-subrc = 0.
            MESSAGE i000(oo) WITH 'Usercommand 01: Beleg' ls_data-vbeln.
          ENDIF.
        ENDLOOP.
    ENDCASE.
  ENDMETHOD.

  METHOD handle_toolbar.

    DATA: ls_toolbar  TYPE stb_button.

*** Trenner
    CLEAR ls_toolbar.
    MOVE 3 TO ls_toolbar-butn_type.
    APPEND ls_toolbar TO e_object->mt_toolbar.

*** Icon “Test”
    CLEAR ls_toolbar.
    MOVE icon_generate              TO ls_toolbar-icon.
    MOVE 'USER01'                   TO ls_toolbar-function.
    MOVE 'User 01'                  TO ls_toolbar-quickinfo.
    MOVE 'Userbutton 01'            TO ls_toolbar-text.
    APPEND ls_toolbar TO e_object->mt_toolbar.

  ENDMETHOD.


  METHOD start.
    selection( ).
    display( ).
  ENDMETHOD.

  METHOD selection.
    SELECT * FROM vbak INTO TABLE mt_data UP TO 10 ROWS.
  ENDMETHOD.

  METHOD display.

    WRITE 'DUMMY'.

    CREATE OBJECT mr_grid
      EXPORTING
        i_parent      = cl_gui_container=>screen0
        i_appl_events = space.

    mr_grid->set_sort_field( 'ERNAM' ).

    SET HANDLER handle_toolbar      FOR mr_grid.
    SET HANDLER handle_user_command FOR mr_grid.


    DATA lv_structure_name    TYPE dd02l-tabname VALUE 'VBAK'.
    DATA ls_variant           TYPE disvariant.
    DATA lv_save              TYPE char01 VALUE 'U'.
    DATA lv_default           TYPE char01 VALUE abap_true.
    DATA ls_layout            TYPE lvc_s_layo.

    ls_layout-sel_mode       = 'A'.
    ls_layout-grid_title     = 'Titel'.

    mr_grid->set_table_for_first_display(
      EXPORTING
        i_structure_name              = lv_structure_name
        is_variant                    = ls_variant
        i_save                        = lv_save
        i_default                     = lv_default
        is_layout                     = ls_layout
      CHANGING
        it_outtab                     = mt_data ).

  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  NEW main( )->start( ).

 

Ergebnis

Du kannst nun den Cursor auf einen Eintrag stellen und durch Klicken auf „Sort Up“ oder „Sort Down“ den Eintrag umsortieren. Die Sortierung wird in dem Feld „ERNAM – Angelegt von“ vorgehalten.

Fazit

Die Änderung von SAP-Standardfunktionen ist möglich. Allerdings ist das erstens nicht immer so einfach, wie in diesem Artikel beschrieben. In der Regel muss man genau und langwierig debuggen und prüfen, wo welche Methoden verwendet werden können. Zudem müssen die Funktionen natürlich ausgiebig getestet werden. Immerhin sollen sie genau wie die Standardfunktionalität zuverlässig funktionieren.

Des Weiteren sollten Funktionen, die wirklich in einer Vielzahl von eigenen Programmierungen eingesetzt werden sauber ausprogrammiert werden. In dem hier vorgestellten Beispiel sollte zum Beispiel sichergestellt werden, dass das Feld mit der Sortierung auch wirklich im Feldkatalog vorhanden ist. Die Benutzereigene Sortierung muss irgendwie berücksichtigt werden.

Zudem sollte es natürlich möglich sein, auch mehrere Zeilen zu markieren und diese en bloc zu verschieben. Das hängt aber wiederum von der Programmierung ab, ob wirklich mehrere Zeilen markiert werden dürfen oder nicht.

Ebenso wäre die Eingangs erwähnte Sortierung mittels Drag & Drop sinnvoll. Diese könnte dann allerdings einer anderen vom Programmierer erstellten Drag & Drop Funktionalität in die Quere kommen.

Allerdings lohnt es sich, hier Aufwand zu investieren, denn die erweiterte Funktionalität kann eventuell viele separate Programmierungen überflüssig machen oder vorhandene Programmierungen auf einfache Weise benutzerfreundlicher machen.

Der Beitrag ALV-Grid um Sortierfunktion erweitern (Vererbung) erschien zuerst auf Tricktresor.

Bildschirmauflösung ermitteln mit VBScript

$
0
0

In diesem Artikel zeige ich dir, wie du die Bildschirmauflösung eines Monitors ermitteln kannst. Verwendet wird dazu VBScript eingebettet in ABAP. Um an die gewünschten Informationen zu gelangen, wird WMI – Windows Management Instrumentation bemüht.

Die Ermittlung der Standardauflösung mit ABAP-Mitteln ist hier beschrieben: Bildschirmauflösung des Computers ermitteln

Vielen Dank an Stefan Schnell, der wieder mal beweist, dass der SAPGUI nicht die Grenze ist…

Win32_VideoController

Das unten stehende Programm erzeugt ein VBScript mit dessen Hilfe Informationen des Windows-Videocontrollers ausgelesen werden. Für jedes gefundene Element wird ein Popup erzeugt:

Info

Leider berücksichtigt der Code nicht mehrere angeschlossene Monitore. Für weiterführende Hinweise diesbezüglich bin ich dankbar.

Allerdings: Selbst wenn man weiß, wie viele Monitore, mit welcher Auflösung angeschlossen sind, weiß man immer noch nicht, auf welchem Monitor gerade der SAPGUI-Modus angezeigt wird.

Weiterführende Links

How to use Windows Management Instrumentation (WMI) Inside ABAP

How to use Windows PowerShell Script inside ABAP

WMI – Windows Management Instrumentation

Win32_VideoController

Code

CONSTANTS crlf(2) TYPE c VALUE cl_abap_char_utilities=>cr_lf.
DATA scriptctrl   TYPE ole2_object.
DATA cmd          TYPE string.

CREATE OBJECT scriptctrl 'MSScriptControl.ScriptControl'.
IF sy-subrc = 0.
  SET PROPERTY OF scriptctrl 'AllowUI' = 1.
  SET PROPERTY OF scriptctrl 'Language' = 'VBScript'.
  cmd = 'Set oWMI = GetObject("Winmgmts:\\.\root\cimv2")'                          && crlf &&
        'Set colItems = oWMI.ExecQuery("Select * from Win32_VideoController",,48)' && crlf &&
        'For Each oItem in colItems '                                              && crlf &&
        ' intHorizontal = oItem.CurrentHorizontalResolution'                       && crlf &&
        ' intVertical = oItem.CurrentVerticalResolution'                           && crlf &&
        ' chrCaption  = oItem.Caption'                                             && crlf &&
        ' chrDeviceID = oItem.DeviceID'                                            && crlf &&
        ' MsgBox chrDeviceID & "/" & chrCaption & ": " & intHorizontal & ":" & intVertical'            && crlf &&
        'Next'.
  CALL METHOD OF scriptctrl 'ExecuteStatement' EXPORTING #1 = cmd.
  FREE OBJECT scriptctrl.
ENDIF.
 

Der Beitrag Bildschirmauflösung ermitteln mit VBScript erschien zuerst auf Tricktresor.

ECATT Datencontainer nutzen

$
0
0

ECATT (Extended Computer Aided Test Tool) ist ein mächtiges Tool zur Testautomatisierung und Testverwaltung. In der Transaktion SECATT können Skripte aufgezeichnet und abgespielt werden. Es können auch Testdaten verwaltet werden. Um diese Testdaten möchte ich mich in diesem Artikel einmal kümmern.

Testdatencontainer

In einem Testdatencontainer werden Testdaten vorgehalten. Die Datentypen können beinahe beliebig komplex sein. Nur Objektreferenzen können nicht verwaltet werden. Ansonsten kannst du folgende Datentypen einfach im Container erfassen und ändern:

  • Felder
  • Strukturen
  • Tabellen
  • komplexe Datenobjekte: Struktur mit Tabelle etc.

Testdatencontainer anlegen

Mit Transaktion SECATT kannst du einen Container anlegen. Ich lege einen Container mit Namen „Z1“ an:

Als erstes musst du die Attribute pflegen:

Parameter

Jetzt kommt der interessante Teil: Die Definition der Parameter. Hier kannst du (fast) beliebige Strukturen definieren und mit Daten füllen. Ich habe einmal ein paar Parameter definiert:

Einfache Variablen wie im obigen Beispiel „ID“, werden direkt in dem ALV-Grid gepflegt.

Alles andere wird in einem separaten  Container gepflegt. Durch einen Doppelklick auf die entsprechende Zelle in der Spalte „Parameterwert“ öffnet sich dieser:

Bei einer komplexeren Struktur muss man sich durch die einzelnen Objekte durch hangeln, kann aber alles pflegen:

Varianten

Ein Testdatencontainer kann mehrere Varianten verwalten. Für jeden definierten Parameter gibt es nun eine Spalte in der Variantentabelle. Auch hier kann mit einem Doppelklick auf die entsprechende Zelle der Pflegedialog für diesen Parameter geöffnet werden:

Ausblick

So. Das war nur ein kleiner Ausflug damit du weißt, was ein Datencontainer ist. Wie du ihn als Datenspeicher nutzen kannst, erfährst du in diesem Artikel.

Der Beitrag ECATT Datencontainer nutzen erschien zuerst auf Tricktresor.


Auf ECATT Datencontainer zugreifen

$
0
0

In dem letzten Artikel – ECATT Datencontainer nutzen – habe ich dir gezeigt, was ein Testdatencontainer ist. Den Artikel brauchte ich als Vorbereitung für diesen, viel interessanteren Artikel: Der Zugriff auf die Daten des Testdatencontainers.

Ein Testdatencontainer kann wunderbar für eigene Zwecke „missbraucht“ werden um mehrere verschiedenartige Daten an einer Stelle zu verwalten.

Zugriff!

Der Zugriff auf den Datencontainer des ECATT ist durch die zur Verfügung gestellte API sehr simpel:

"Objekte für Zugriff auf Testdatencontainer
DATA tdc_ref TYPE REF TO cl_apl_ecatt_tdc_api.
DATA par_ref TYPE REF TO etpar_gui.

"Datenstrukturen
DATA ls_mara TYPE mara.
DATA demo    TYPE zecatt_heads.

"Objekt für Testdatencontainer erzeugen
tdc_ref = cl_apl_ecatt_tdc_api=>get_instance( 'Z1' ).

"Datenselektion MARA-MATNR für Default-Variante
tdc_ref->get_value( EXPORTING i_param_name   = 'MARA'
                              i_path         = 'MATNR'
                              i_variant_name = 'ECATTDEFAULT'
                    CHANGING  e_param_value  = ls_mara-matnr ).

"Datenselektion gesamte Struktur MARA für Variante A1
tdc_ref->get_value( EXPORTING i_param_name   = 'MARA'
                              i_variant_name = 'A1'
                    CHANGING  e_param_value  = ls_mara ).

"Datenermittlung des komplexen DEMO-Objektes aus der Default-Variante
tdc_ref->get_value( EXPORTING i_param_name   = 'DEMO'
                              i_variant_name = 'ECATTDEFAULT'
                    CHANGING  e_param_value  = demo ).

Über die Klasse CL_APL_ECATT_TDC_API kann der gesamte Datencontainer per Programm verwaltet werden. Es können eigene Container erstellt werden, Attribute ausgelesen und geändert werden und die Daten können ebenfalls gelesen und manipuliert werden.

Im Gegensatz zu meinem Tipp, komplexe Daten in einer INDEX-Tabelle per XML zu speichern, haben die Testdatencontainer den großen Vorteil, dass die Daten strukturiert angezeigt und manipuliert werden können.

Der Beitrag Auf ECATT Datencontainer zugreifen erschien zuerst auf Tricktresor.

Windowsdrucker mit VBScript ermitteln

$
0
0

VBScript ist ein starkes Instrument, um dem Windows-System Informationen zu entlocken. Stefan Schnell hat bereits einige Beispiele für die Verwendung von VBScript in ABAP geliefert (Siehe unten). Nachdem ich mit Stefans Hilfe kurz gezeigt habe, wie man die Bildschirmauflösung mittels VBScript ermitteln kann (Bildschirmauflösung ermitteln mit VBScript), hier noch ein Bespiel, das demonstriert, wie man das Ergebnis einer VBScript-Funktion zurück ins ABAP bekommt und wie man das Ergebnis als „Tabelle“ übergeben kann.

Code

Das folgende Beispielprogramm liefert alle im Windows installierten Drucker als Tabelle zurück.

REPORT.


CLASS lcl_wmi DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS get_printers RETURNING VALUE(result) TYPE string_table.
ENDCLASS.

CLASS lcl_wmi IMPLEMENTATION.

  METHOD get_printers.

    "-Macros--------------------------------------------------------------
    DEFINE _.
      vbscode = vbscode && &1 && cl_abap_char_utilities=>cr_lf.
    END-OF-DEFINITION.

    "-Variables-----------------------------------------------------------
    DATA scriptctrl TYPE obj_record.
    DATA vbscode TYPE string.
    DATA result_string TYPE string.


    "-Main----------------------------------------------------------------
    _ 'Function GetPrinters()'.
    _ '  Set oObj = GetObject("winmgmts:\\.\root\cimv2", "")'.
    _ '  If IsObject(oObj) Then'.
    _ '    Set colItems = oObj.ExecQuery("Select * from Win32_PrinterConfiguration",,48)'.
    _ '    For Each oItem in colItems'.
    _ '      Res = Res & vbCrLf & oItem.Description'.
    _ '    Next'.
    _ '    GetPrinters = Res'.
    _ '  End If'.
    _ 'End Function'.
    CREATE OBJECT scriptctrl 'MSScriptControl.ScriptControl'.
    CHECK sy-subrc = 0 AND scriptctrl-handle <> 0 AND scriptctrl-type = 'OLE2'.

    DATA result_table TYPE STANDARD TABLE OF string.
    "Set Property Of ScriptCtrl 'AllowUI' = 1.
    SET PROPERTY OF scriptctrl 'Language' = 'VBScript'.
    CALL METHOD OF scriptctrl 'AddCode' EXPORTING #1 = vbscode.
    CALL FUNCTION 'AC_SYSTEM_FLUSH' EXCEPTIONS OTHERS = 1.
    CALL METHOD OF scriptctrl 'Eval' = result_string EXPORTING #1 = 'GetPrinters()'.
    CALL FUNCTION 'AC_SYSTEM_FLUSH' EXCEPTIONS OTHERS = 1.
    FREE OBJECT scriptctrl.

    SPLIT result_string AT cl_abap_char_utilities=>cr_lf INTO TABLE result.

  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA(result) = lcl_wmi=>get_printers( ).
  Write: / 'Installierte Drucker:'.
  LOOP AT result INTO DATA(line).
    WRITE: / line.
  ENDLOOP.

 

Neue Welten

Welche anderen Informationen noch auf Entdeckung im ABAP warten, kannst du auf dieser Microsoft-Seite herausfinden:

https://msdn.microsoft.com/en-us/library/aa389273(v=vs.85).aspx

Es ist eventuell eine ganz neue Benutzererfahrung möglich, wenn ein Programm mit langer Laufzeit, dass häufig auf mobilen Notebooks ausgeführt wird (zum Beispiel von Außendienstlern oder von Lagerarbeitern), vor Benutzung den Batteriestatus prüft. Wenn der Status „kritisch“ ist, erfolgt die Meldung, dass das Notebook vor Ausführung evtl. lieber an Strom angeschlossen werden sollte…

Weiterführende Links

https://blogs.sap.com/2014/05/14/how-to-use-vbscript-inside-abap-and-store-vbscript-source-as-include/

https://wiki.scn.sap.com/wiki/display/Snippets/How+to+use+VBScript+in+ABAP

 

Der Beitrag Windowsdrucker mit VBScript ermitteln erschien zuerst auf Tricktresor.

Logik vs. Daten // SAP vs. SimDia² // Programmierer vs. Anwender

$
0
0

Ich bin Programmierer aus Leidenschaft. Ich liebe es, Daten so zu malträtieren, umzuformen und anzureichern, dass eine für den Menschen sinnvolle Darstellung dabei herauskommt. Ebenso liebe ich es, Anforderungen so umzusetzen, dass am Ende ein paar zufriedene Anwender vor dem PC und wohlgeformte Daten auf der Datenbank sind. Zudem bin ich bestrebt, die Programmierungen so universell und wiederverwendbar wie möglich zu gestalten.

Voraussetzung dafür ist, dass der Anwender oder Kunde sich exzellent ausdrücken kann und sehr genau weiß, was er möchte und seine Prozesse und Daten selbst gut kennt. Dabei ist nicht nur der aktuelle Zustand wichtig, sondern auch, wie es mit den Daten weiter gehen soll. Wie müssen sie weiter verarbeitet werden? Inwieweit müssen sie wie lange änderbar sein? Häufig müssen im Nachhinein wilde Verrenkungen gemacht werden, um Dinge zu prüfen oder sicherzustellen, die bei sorgfältiger Planung – ich glaube so etwas nennt man 360°-Sicht – nicht nötig gewesen wäre.

Zugegebener Maßen ist das nicht immer möglich, denn sehr oft entwickeln sich Dinge einfach. Aus einem ehemals kleinen Auswertungsreport entsteht nach und nach die Schaltzentrale für eine ganze Abteilung.

Daten

Die Daten sind tatsächlich der wichtigste Bestandteil bei der Programmierung in einem ERP-System. Dadurch, dass die Daten in Strukturen abgelegt und organisiert werden, ist es recht einfach, zusammengehörende Daten zu ermitteln. Es ist allerdings sehr schwer, diese Strukturen erst einmal zu erkennen und dann zu erzeugen. Zudem ist es schwer, wenn nicht gar unmöglich, anhand der Daten die Zugehörigkeit zu Prozessen zu ermitteln. Um die zu einem Prozess notwendigen Daten zu wissen, müsste ein detailliertes Datenflussdiagramm erstellt werden. Das wird jedoch kaum gemacht, denn es ist unendlich viel Arbeit und starken Änderungen unterworfen.

Logik

Für einen Programmierer ist es sehr wichtig zu wissen, wo welche Daten stehen und wie sie verarbeitet werden müssen. Der Programmierer muss die Daten so verarbeiten, dass am Ende das gewünschte Ergebnis heraus kommt. Dafür ist es notwendig, dass er bestimmte Logiken anwendet. Einige Logiken ergeben sich aus den Einstellungen im Customizing, andere Logiken müssen vom Fachbereich bzw. dem Kunden vorgegeben werden. Sofern es genug „Beweismaterial“ gibt, können aus speziellen Anforderungen allgemein gültige Logiken abgeleitet werden. Diese wiederum können im Kundeneigenen Customizing abgebildet werden. Dies hat den Vorteil, dass die Lösung flexibel und vom Kunden steuerbar ist.

Programmierer

Ja, es mag ein Klischee sein, aber Programmierer sind ein wundersames Völkchen. Ich spreche aus eigener Erfahrung… 😉  Programmierer sind in der Regel sehr Technik-affin und sind mehr daran interessiert, eine möglichst figgelinsche Lösung zu präsentieren oder eine besonders komplexe Aufgabenstellung zu meistern. In der Regel haben Programmierer wenig Sinn für Design und Layout. Ja, es gibt Ausnahmen…! Aber meistens bilden Programmierer genau das ab, was in der Vorgabe steht – egal wie es aussieht und ob es den SAP-Design-Vorgaben gleich in mehreren Ebenen widerspricht. Der einfachste Weg, einen Programmierer zur Arbeit zu bewegen ist, ihm zu sagen: „Das geht nicht. Haben schon andere versucht“.

Anwender

Auch Anwender sind häufig speziell. Sie müssen mit ihrer Arbeit fertig werden und sie wollen ihre Arbeit in der Regel gut machen. Anwender verstehen ihre Daten und ihre Prozesse. Aber sie können häufig nicht einschätzen welche Auswirkungen Änderungen in ihrem Bereich auf andere Teile der Firma haben. Zudem schätzen sie häufig die Komplexität von Programmieraufgaben falsch ein. Dinge, die einfach zu programmieren sind, weil bereits ähnliche Programmierungen vorhanden sind oder es eine SAP-Funktion dafür gibt, werden eventuell als zu komplex abgetan. Andererseits werden Dinge, die sich verbal einfach ausdrücken lassen und offensichtlich sind, als einfach eingeschätzt. Dabei sind gerade diese Dinge schwer und aufwändig zu programmieren.

Programmierer vs. Anwender

Im allgemeinen mehr oder weniger chaotischen Durcheinander, was in einigen Firmen Alltag genannt wird, müssen Programmierer und Anwender (Fachbereiche) miteinander auskommen und sich gegenseitig soweit verstehen, dass eindeutige Ergebnisse erzielt werden. Ein guter Programmierer wird versuchen, ein Schema, eine Logik, eine allgemein gültige Regel zu finden oder scheinbar allgemein gültige Regeln zu hinterfragen. Ein guter Anwender kennt seine Daten und Prozesse und erkennt Daten, die „nicht passen“ oder „unsinnig“ sind. Dahingehend unterscheiden sich in der Regel Programmierer und Anwender. Denn genau das, was der eine kann, kann der andere mit ziemlicher Sicherheit nicht. Ein Programmierer kann nicht erkennen, ob Daten unsinnig sind und ein Anwender erkennt selten die technische Logik hinter bestimmten Prozessen oder Funktionen.

Logik vs. Daten

Für einen Anwender ist es eventuell einfacher „seine“ Daten so zu ordnen, wie sie für ihn aktuell sinnvoll und wichtig sind. Um Daten aus einem Fremdsystem oder manuell erarbeiteten Prozessen in ein SAP-System zu übernehmen gibt es im Grunde zwei maschinelle Möglichkeiten

  1. Anwender und Programmierer setzen sich zusammen und definieren eine Datenstruktur. Sie besprechen, welche Daten wann unter welchen Bedingungen wo hin müssen. Der Programmierer arbeitet in der Regel mit einer (je nach Anforderung natürlich auch mit mehreren) allgemeinen Datenstruktur. Felder, die für einen Datensatz nicht relevant sind, bleiben leer. Der Programmierer erstellt dann ein Programm mit den entsprechenden Regeln, um die Daten ins SAP-System zu schreiben.
  2. Der Anwender baut sich seine Daten in Gruppen so zusammen, wie sie für ihn logisch sind. Dabei ist es egal, dass er 12 Excel-Blätter hat, die alle zu 90% die gleiche Struktur haben. Für ihn ist wichtig, dass er die komplexen Daten möglichst einfach gruppieren und verwalten kann. Der Anwender kann dann einen einfachen Prozess starten, um die Daten zu übernehmen (zum Beispiel mit SimDia²).

Die erste Lösung würde ich als Programmierer natürlich immer bevorzugen. Immerhin verdiene ich damit mein Geld. Es ist jedoch nicht von der Hand zu weisen, dass eine Programmierung fast immer recht kompliziert ist. Es sind Absprachen mit dem Auftraggeber notwendig, es muss getestet werden, es müssen Programme transportiert werden usw. Zudem erfordert die Arbeit in der Ausführung häufig zwei Leute: Einen aus der IT (Programmierer), der Daten in ein Verzeichnis schiebt, Daten hochlädt, Daten konvertiert und so weiter. Der Anwender ist hilflos, wenn etwas nicht so funktioniert, wie er es erwartet.

Die zweite Lösung mag auf den ersten Blick nicht professionell erscheinen. Im Hinblick darauf, dass Daten meistens regelmäßig und von beliebigen Personen übernommen werden sollen, wird eine „Frickellösung“ häufig nicht in Betracht gezogen.

Dass der Anwender meistens gar nichts machen kann, liegt häufig an diesen zwei Umständen:

  1. Es gibt häufig keine guten Möglichkeiten für einen Anwender, Daten massenhaft in ein SAP-System zu importieren.
  2. Die IT-Abteilung hat für kleinere Aufgaben häufig keine Zeit und keine Kapazitäten.

Die cleverste Möglichkeit, von der ich gehört habe ist die folgende: Ein Poweruser (also ein Benutzer mit hinreichend guten Berechtigungen zum Ausführen von Programmen) hat sich die Batchinputstruktur zu einem SAP-Standard-Übernahmeprogramm genommen, diese in Word bearbeitet und mit Hilfe der Serienbrieffunktion eine Batchinputdatei für seine Daten generiert. Die so erzeugten Batchinputmappen hat der Anwender dann eingespielt. Ich weiß leider nicht mehr genau, um welche Daten es sich handelte.

Diese Lösung erfordert jedoch umfangreiche Berechtigungen, sehr gutes Word- und Excelwissen sowie natürlich ein vorhandenes SAP-Übernahmeprogramm.

Alternative SimDia²

Das Addon SimDia² von Ersasoft ist eine – gemessen am Nutzen – kostengünstige Alternative für den Fachbereich um alltäglich wiederkehrende Datenübernahmen einfach und effizient zu erledigen. Der Anwender hat zumeist hinreichende Excel-Kenntnisse und er kennt seine SAP-Anwendungen. Die Bedienung von SimDia² ist sehr leicht und darauf ausgelegt, wiederkehrende Datenimporte von Excel nach SAP auszuführen.

Beispiele für die Anwendung sind:

  1. Übernahme von Vertriebsstücklisten
  2. Ausführung von Buchungen
  3. Anlage von Fertigungsaufträgen
  4. Gezieltes Ändern von Materialstämmen

SimDia² kann sogar dazu genutzt werden, um Daten aus einem SAP-System für Auswertungszwecke zu sammeln, da SimDia² Felder einer SAP-Transaktion auslesen kann. Ein Anwender kann sich so also gezielt selber Listen erstellen, ohne dass er die entsprechenden SAP-Tabellen kennen muss. Besonders gut funktioniert das bei Daten, die für einen Programmierer nur sehr umständlich zu ermitteln sind, da die Daten auf viele untereinander verknüpfte Tabellen verteilt sind.

Der Beitrag Logik vs. Daten // SAP vs. SimDia² // Programmierer vs. Anwender erschien zuerst auf Tricktresor.

Simple Tree Model mit User-Object

$
0
0

Bäume sind immer interessant, finde ich. Sie sind nicht so eintönig gleichmäßig wie die meisten Listen. Der CL_SIMPLE_TREE_MODEL hat jedoch zudem auch in der Programmierung eine schöne Besonderheit: Zu jedem Knoten kann die Instanz einer beliebigen Klasse übergeben werden. Mit der Methode NODE_GET_USER_OBJECT kann man sich dann die Instanz geben lassen und hiermit weiter arbeiten.

Tree-Ausgabe

Als Beispielaufgabe habe ich mir die folgende gestellt: Zeige alle Materialien zu einer Selektion nach Materialart unterteilt an. Also: Hauptknoten – Materialart – Materialien. Die Selektion der Daten erfolgt im Selektionsbild des Reports bei AT SELECTION-SCREEN. Auf unserem IDES-System haben ich nur ein paar Dutzend Materialien, da geht die Selektion zügig.

Mit Doppelklick auf einen Eintrag soll – je nach Knotenart „Materialart“ oder „Materialnummer“ – die jeweilige Information ausgegeben werden. Es muss also bei dem Knoten „Materialart“ eine andere Klasse verwendet werden, als bei der Knotenart „Materialnummer“.

Zusätzlich zur Demonstration des CL_SIMPLE_TREE_MODEL habe ich versucht, möglichst viele neue Sprachbefehle zu verwenden. Gerade bei der Verwendung des User-Objects macht der Befehl NEW zur Instantiierung einer Klasse die Programmierung wirklich elegant.

User-Object

für das User-Object habe ich eine Hauptklasse LCL_USER_OBJECT_MARA definiert:

CLASS lcl_user_object_mara DEFINITION ABSTRACT.
  PUBLIC SECTION.
    DATA mara TYPE mara.
    METHODS constructor IMPORTING i_mara TYPE mara.
    METHODS get_text.
ENDCLASS.

CLASS lcl_user_object_mara IMPLEMENTATION.
  METHOD constructor.
    mara = i_mara.
  ENDMETHOD.
  METHOD get_text.
    SELECT SINGLE maktx FROM makt INTO @DATA(text)
     WHERE matnr = @mara-matnr
       AND spras = @sy-langu.
    IF sy-subrc = 0.
      MESSAGE text TYPE 'I'.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

Die Klasse hat nur das Attribut MARA, das bei der Erzeugung mitgegeben werden muss und die Methode GET_TEXT mit der ein Text zum Objekt ermittelt wird.

Da ich die Klasse als ABSTRAKT definiert habe, kann ich diese Klasse nicht instantiieren. Das geht nur bei den von dieser Klasse abgeleiteten Klassen:

CLASS lcl_user_object_mtart DEFINITION INHERITING FROM lcl_user_object_mara.
  PUBLIC SECTION.
    METHODS get_text REDEFINITION.
ENDCLASS.
CLASS lcl_user_object_mtart IMPLEMENTATION.
  METHOD get_text.
    MESSAGE |Materialart { mara-mtart }|  TYPE 'I'.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_user_object_matnr DEFINITION INHERITING FROM lcl_user_object_mara.
  PUBLIC SECTION.
ENDCLASS.

In der Klasse MTART redefiniere ich die Methode „GET_TEXT“ um einen eigenen Text für Materialart zu bekommen.

Die Klasse MATNR ist nur eine leere Hülle, da sie alles andere von der Hauptklasse erbt.

Natürlich hätte ich auch zwei komplett unterschiedliche und voneinander unabhängige Klassen definieren können.

Hauptprogramm

Das Hauptprogramm besteht nur den Selektionsparametern und aus zwei Ereignissen:

  1. dem Ereignis INITIALIZATION, in dem ich den Docking-Container erzeuge und
  2. dem Ereignis AT SELECTION-SCREEN, in dem ich die Daten selektiere und den Baum erzeuge
REPORT z_simple_tree_model.

DATA s_mara TYPE mara.

SELECT-OPTIONS s_matnr FOR s_mara-matnr.
SELECT-OPTIONS s_mtart FOR s_mara-mtart.

[...Klassendefinition...]

INITIALIZATION.
  lcl_main=>create_docker( ).

AT SELECTION-SCREEN.
  TRY.
      lcl_main=>get_data( ).
      lcl_main=>create_tree( ).
      lcl_main=>add_nodes( ).
    CATCH lcx_error.
      MESSAGE 'Fehler bei Selektion' TYPE 'I'.
  ENDTRY.

Hauptklasse

Die Klasse LCL_MAIN, in der die Logik des Programms vorhanden ist, besteht aus diesen vier Methoden:

  1. Create_Docker
  2. Create_Tree
  3. Get_Data
  4. Add_Nodes

Zusätzlich gibt es noch die Methode zur Ereignisbehandlung des Doppelklicks auf einen Knoten: Handle_Node_Double_Click.

Create_Docker

So simple:

IF docker IS INITIAL.
      docker = NEW #( side = cl_gui_docking_container=>dock_at_bottom ratio = 50 ).
    ENDIF.

 

Get_Data

Ebenfalls nicht spektakulär:

SELECT * FROM mara INTO TABLE t_mara
     WHERE matnr IN s_matnr
       AND mtart IN s_mtart.
    IF sy-subrc > 0.
      RAISE EXCEPTION TYPE lcx_error.
    ENDIF.

Zur Fehlerbehandlung habe ich eine eigene Exception-class erstellt:

CLASS lcx_error DEFINITION INHERITING FROM cx_no_check.
ENDCLASS.

Create_Tree

Bei der Erzeugung des CL_SIMPLE_TREE_MODEL bin ich auf die erste Hürde gestoßen, denn die Erzeugung des Control läuft etwas anders, als bei den meisten anderen GUI-Controls. Normalerweise instantiiert man das GUI-Control unter Angabe des Containers in das das Control eingefügt werden soll (Parameter PARENT). Nicht so bei dieser Klasse. Hier wird erst das Tree-Objekt erzeugt und danach mit der Methode CREATE_TREE_CONTROL an den PARENT-Container gehängt:

IF tree IS BOUND.
      tree->delete_all_nodes( ).
    ELSE.
      tree   = NEW #( node_selection_mode = cl_simple_tree_model=>node_sel_mode_single ).
      tree->create_tree_control( EXPORTING parent = docker ).

      tree->set_registered_events(
              EXPORTING events = VALUE #(
                 ( eventid = cl_simple_tree_model=>eventid_node_double_click appl_event = space ) ) ).

      SET HANDLER handle_node_double_click FOR tree.
    ENDIF.

    tree->add_node(
             node_key = 'Root'                              "#EC NOTEXT
             isfolder = 'X'
             text     = 'Materialien zur Selektion' ).

Das Event Doppelklick wird hier ebenfalls registriert und der event handler dafür installiert. Zusätzlich wird der Hauptknoten ROOT eingefügt.

 

 

Add_Nodes

Mit der Methode ADD_NODE des Tree-Controls werden einzelne Knoten in den Baum eingehängt. Immer unter Angabe des Knoten-ID, des übergeordneten Knotens, Text und ein paar anderen. An dieser Stelle kann das User-Object übergeben werden, dass dann zur Knoten-ID zur Verfügung steht:

DATA l_mtart TYPE mtart.

    SORT t_mara BY mtart matnr.

    LOOP AT t_mara INTO DATA(mara).

      IF l_mtart <> mara-mtart.
        l_mtart = mara-mtart.
        "Knoten MATERIALART
        tree->add_node(
            node_key = CONV #( mara-mtart )
            relative_node_key = 'Root'
            relationship = cl_simple_tree_model=>relat_last_child
            isfolder = 'X'
            text     = |Materialart { mara-mtart } |
            user_object = NEW lcl_user_object_mtart( i_mara = mara ) ).
      ENDIF.

      "Knoten MATERIALNUMMER
      tree->add_node(
          node_key          = |{ mara-matnr ALPHA = OUT }|
          relative_node_key = CONV #( mara-mtart )
          relationship      = cl_simple_tree_model=>relat_last_child
          isfolder          = space
          text              = |{ mara-matnr ALPHA = OUT }|
          user_object       = NEW lcl_user_object_matnr( i_mara = mara ) ).

    ENDLOOP.

    tree->expand_root_nodes( ).

Ereignisbehandler

Im Ereignisbehandler prüfen wir, welchen Objekttyp das User-Object hat, um gegebenenfalls anders reagieren zu können:

DATA o_user_object_matnr TYPE REF TO lcl_user_object_matnr.
    DATA o_user_object_mtart TYPE REF TO lcl_user_object_mtart.
    DATA o_object TYPE REF TO object.

    tree->node_get_user_object( EXPORTING node_key    = node_key
                                IMPORTING user_object = o_object ).

    IF o_object IS INSTANCE OF lcl_user_object_mtart.
      o_user_object_mtart ?= o_object.
      o_user_object_mtart->get_text( ).
      EXIT.
    ENDIF.

    IF o_object IS INSTANCE OF lcl_user_object_matnr.
      o_user_object_matnr ?= o_object.
      o_user_object_matnr->get_text( ).
      EXIT.
    ENDIF.

In diesem Fall verwenden wir zwar für MTART und MATNR die gleiche Methode GET_TEXT, aber hier könnte man je Objekt eine andere Funktion ausführen. Falls im ABAP Release die Syntax IS INSTANCE OF noch nicht verfügbar ist, muss mit TRY – CATCH geprüft werden, ob der Cast zwischen OBJECT und User-Object erfolgreich war oder nicht:

TRY.
        o_user_object_matnr ?= o_object.
        o_user_object_matnr->get_text( ).
      CATCH cx_sy_move_cast_error.
    ENDTRY.

Das komplette Programm

REPORT zdemo_simple_tree_model.


DATA s_mara TYPE mara.

SELECT-OPTIONS s_matnr FOR s_mara-matnr.
SELECT-OPTIONS s_mtart FOR s_mara-mtart.

CLASS lcx_error DEFINITION INHERITING FROM cx_no_check.
ENDCLASS.

CLASS lcl_user_object_mara DEFINITION ABSTRACT.
  PUBLIC SECTION.
    DATA mara TYPE mara.
    METHODS constructor IMPORTING i_mara TYPE mara.
    METHODS get_text.
ENDCLASS.

CLASS lcl_user_object_mara IMPLEMENTATION.
  METHOD constructor.
    mara = i_mara.
  ENDMETHOD.
  METHOD get_text.
    SELECT SINGLE maktx FROM makt INTO @DATA(text)
     WHERE matnr = @mara-matnr
       AND spras = @sy-langu.
    IF sy-subrc = 0.
      MESSAGE text TYPE 'I'.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_user_object_mtart DEFINITION INHERITING FROM lcl_user_object_mara.
  PUBLIC SECTION.
    METHODS get_text REDEFINITION.
ENDCLASS.

CLASS lcl_user_object_mtart IMPLEMENTATION.
  METHOD get_text.
    MESSAGE |Materialart { mara-mtart }|  TYPE 'I'.
  ENDMETHOD.
ENDCLASS.
CLASS lcl_user_object_matnr DEFINITION INHERITING FROM lcl_user_object_mara.
  PUBLIC SECTION.
ENDCLASS.



CLASS lcl_main DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS get_data.
    CLASS-METHODS create_tree.
    CLASS-METHODS add_nodes.
    CLASS-METHODS create_docker.
    CLASS-METHODS handle_node_double_click
      FOR EVENT node_double_click
                  OF cl_simple_tree_model
      IMPORTING node_key.

    CLASS-DATA docker TYPE REF TO cl_gui_docking_container.
    CLASS-DATA tree   TYPE REF TO cl_simple_tree_model.
    CLASS-DATA t_mara TYPE STANDARD TABLE OF mara.
    CLASS-DATA s_mara TYPE mara.

ENDCLASS.

CLASS lcl_main IMPLEMENTATION.
  METHOD create_docker.
    IF docker IS INITIAL.
      docker = NEW #( side = cl_gui_docking_container=>dock_at_bottom ratio = 50 ).
    ENDIF.
  ENDMETHOD.

  METHOD handle_node_double_click.

    DATA o_user_object_matnr TYPE REF TO lcl_user_object_matnr.
    DATA o_user_object_mtart TYPE REF TO lcl_user_object_mtart.
    DATA o_object TYPE REF TO object.

    tree->node_get_user_object( EXPORTING node_key    = node_key
                                IMPORTING user_object = o_object ).

    IF o_object IS INSTANCE OF lcl_user_object_mtart.
      o_user_object_mtart ?= o_object.
      o_user_object_mtart->get_text( ).
      EXIT.
    ENDIF.

    IF o_object IS INSTANCE OF lcl_user_object_matnr.
      o_user_object_matnr ?= o_object.
      o_user_object_matnr->get_text( ).
      EXIT.
    ENDIF.

  ENDMETHOD.


  METHOD get_data.

    SELECT * FROM mara INTO TABLE t_mara
     WHERE matnr IN s_matnr
       AND mtart IN s_mtart.
    IF sy-subrc > 0.
      RAISE EXCEPTION TYPE lcx_error.
    ENDIF.
  ENDMETHOD.

  METHOD create_tree.

    IF tree IS BOUND.
      tree->delete_all_nodes( ).
    ELSE.
      tree   = NEW #( node_selection_mode = cl_simple_tree_model=>node_sel_mode_single ).
      tree->create_tree_control( EXPORTING parent = docker ).

      tree->set_registered_events(
              EXPORTING events = VALUE #(
                 ( eventid = cl_simple_tree_model=>eventid_node_double_click appl_event = space ) ) ).

      SET HANDLER handle_node_double_click FOR tree.
    ENDIF.

    tree->add_node(
             node_key = 'Root'                              "#EC NOTEXT
             isfolder = 'X'
             text     = 'Materialien zur Selektion' ).

  ENDMETHOD.

  METHOD add_nodes.

    DATA l_mtart TYPE mtart.

    SORT t_mara BY mtart matnr.

    LOOP AT t_mara INTO DATA(mara).

      IF l_mtart <> mara-mtart.
        l_mtart = mara-mtart.
        "Knoten MATERIALART
        tree->add_node(
            node_key = CONV #( mara-mtart )
            relative_node_key = 'Root'
            relationship = cl_simple_tree_model=>relat_last_child
            isfolder = 'X'
            text     = |Materialart { mara-mtart } |
            user_object = NEW lcl_user_object_mtart( i_mara = mara ) ).
      ENDIF.

      "Knoten MATERIALNUMMER
      tree->add_node(
          node_key          = |{ mara-matnr ALPHA = OUT }|
          relative_node_key = CONV #( mara-mtart )
          relationship      = cl_simple_tree_model=>relat_last_child
          isfolder          = space
          text              = |{ mara-matnr ALPHA = OUT }|
          user_object       = NEW lcl_user_object_matnr( i_mara = mara ) ).
      .

    ENDLOOP.

    tree->expand_root_nodes( ).


  ENDMETHOD.

ENDCLASS.



INITIALIZATION.
  lcl_main=>create_docker( ).

AT SELECTION-SCREEN.
  TRY.
      lcl_main=>get_data( ).
      lcl_main=>create_tree( ).
      lcl_main=>add_nodes( ).
    CATCH lcx_error.
      MESSAGE 'Fehler bei Selektion' TYPE 'I'.
  ENDTRY.

 

Der Beitrag Simple Tree Model mit User-Object erschien zuerst auf Tricktresor.

Data-Aging in S4/HANA

$
0
0

Daten mit einem Verfallsdatum sind keine neue Erfindung. Seit Google und Co. alles speichern, was jemals irgendwo auf einem Webserver verfügbar war, ist der Wunsch da, Daten nach einer bestimmten Zeit automatisch wieder zu löschen. Was im World-Wide-Web eher persönliche Gründe hat, hat bei HANA einen sehr praktischen und handfesten Hintergrund: Speicherplatz.

Datenbankkonzept HANA

Der große Geschwindigkeitsdurchbruch gelingt HANA durch die InMemory-Technik bei der alle notwendigen Daten im Hauptspeicher verwaltet werden. Für die Verarbeitung im Hauptspeicher werden die Spalten von der Festplatte komplett in den Hauptspeicher geladen. Es ist nämlich ein Irrglaube, dass HANA stets und ständig alle Daten im Hauptspeicher vorhält. Die Datenspeicherung erfolgt auch weiterhin auf der Festplatte. Um so wichtiger ist es bei einer HANA-Datenbank, dass nur die Felder selektiert werden, die auch wirklich für die Verarbeitung von Bedeutung sind.

Hauptspeicher

Auch bei großen Datenmengen hat HANA kaum Probleme, diese zu verarbeiten. Aber mit wachsenden Datenbeständen wird auch eine HANA-Datenbank an ihre Grenzen kommen. Da bei der Verarbeitung die komplette Spalte einer Tabelle geladen wird, kann man sich leicht vorstellen, dass auch HANA einmal Probleme bekommen wird, alle Daten vorzuhalten.

Wer eine HANA-Datenbank einsetzt oder einsetzen möchte, muss sich mittelfristig mit dem Thema Data-Aging auseinander setzen.

Datenalterung

Um diesem Trend entgegen zu wirken, gibt es das Konzept der Datenalterung. Die Daten werden hierbei in Partitionen verteilt, die nicht standardmäßig selektiert werden. Es gibt dann für eine Datenbanktabelle mehrere Partitionen. Eine davon enthält nur die aktuellen Daten. Bei einem SELECT werden nur die Daten aus dieser Partition gelesen. Alle anderen Daten sind für den Anwender – und auch für den Entwickler! – unsichtbar.

Data-Aging-Objekte

Für die Datenalterung werden zusammengehörige Tabellen unter einem Data-Aging-Object zusammengefasst. Unter diesem DA-Objekt werden alle notwendigen Einstellungen verwaltet. Für dieses Objekt wird das Analyseprogramm gestartet um den Alterungsprozess anzustoßen.

Datentemperatur

Die aktuellen Daten sind heiß. Daten, die normalerweise nicht mehr benötigt werden, sind kalt. Auch wenn sich der Begriff Temperatur eingebürgert hat, gibt es keine lauwarmen Daten oder  Datensätze mit einer Temperatur von 12° Celsius. Die Temperatur wird in Tagen gemessen.

Technik

Alle Tabellen, die gealtert werden sollen, müssen ein Feld _DATAAGING besitzen. Ist dieses Feld leer, so gehört der Datensatz zum heißen, also aktuellen, Datenbestand. Die Analyseprogramme setzen für die Datensätze, die nicht mehr aktuell sind, das entsprechende Datum.

Die Datenbanktabelle selbst besteht aus verschiedenen Partitionen. Wie diese Partitionen aufgeteilt werden sollen, kann je Data-Aging-Objekt definiert werden. Sinnvoll sind zum Beispiel Zwei-Jahres-Partitionen. Das bedeutet, dass Daten innerhalb dieses Zeitraums in diese Partition verlegt werden. Bei der Definition der Partitionszeiträume werden alle Partitionen bereits angelegt. Es bedeutet jedoch nicht, dass die Daten auch schon darin laden. Wenn für ein Objekt definiert wird, dass es nach 5 Jahren alt ist, dann landet es auch erst nach 5 Jahren in der entsprechenden Partition.

Archivierung

Welche Daten nicht mehr benötigt werden, wird durch eine Funktionalität abgebildet, die auch bei der Archivierung verwendet wird. Da ein Datensatz für sich nicht einfach als alt (oder kalt) definiert werden kann, sondern nur im Kontext, müssen bestimmte Logiken dafür sorgen, dass die Daten richtig interpretiert werden.

Es gibt Daten, die gar nicht altern. Offen Posten zum Beispiel sind immer aktuell. Bei anderen Datenobjekten muss definiert werden, anhand welchen Datums das Objekt als alt klassifiziert werden kann und welche abhängigen Datensätze dazu gehören.

Der Vorteil hierbei ist, dass die grundsätzlichen Logiken durch die Archivierungsprogramme bereits vorhanden sind. Der Nachteil ist, dass es nach wie vor Beziehungen zwischen Datenobjekten gibt, die sehr schwer zu fassen sind. Der komplette Belegfluss eines Dokumentes muss dafür analysiert und bewertet werden.

Konsequenzen für die Anwender

Der Anwender soll von dem Alterungsprozess möglichst nichts mitbekommen. Er soll seine Transaktionen wie gewohnt aufrufen und damit arbeiten. Nur sieht er eben nicht mehr die Daten, die von einem Analyseprogramm als alt eingestuft wurden. In der Praxis wird sich zeigen, wie gut das Data-Aging funktioniert bzw. wie zufrieden der Anwender damit ist. Wenn es gut läuft, sieht der Anwender weniger Daten, was in der Regel gut ist, sofern alle relevanten Daten auf dem Bildschirm erscheinen.

Konsequenzen für die Entwicklung

Im ersten Moment hört es sich etwas strange an, dass man als Entwickler keinen vollen Zugriff mehr auf die Daten in der Datenbank hat. Ich habe im Hands-On-Workshop ein mulmiges Gefühl gehabt, als nach dem Alterungsprozess die Daten aus der SE16n einfach weg waren. In der SE16h gibt es unter S4/HANA einen neuen Menüpunkt in dem man die Temperatur (also das die Anzahl der Tage) einstellen kann, mit der die Daten gelesen werden sollen.

In der Programmierung hat man die Möglichkeit zu einem Data-Aging-Objekt die Selektion zu beeinflussen. Vor dem Select-Befehl muss das DA-Objekt angesprochen werden und das gewünschte Alter definiert werden.

Die letzten Jahre ist der Umgang mit der Datenbank leichter geworden. Joins funktionieren besser und es ist über ein Jahrzehnt her, dass ich einen Oracle-Hint verwenden musste. Die Performance ist insgesamt besser geworden; SELECT INTO CORRESPONDING FIELDS ist nur unwesentlich langsamer als der direkte Select in eine Tabelle.

Zuerst dachte ich, dass nun nach diesen Vereinfachungen wieder mehr Arbeit auf den Entwickler zukommt. Für den Großteil der Entwickler wird sich jedoch nichts ändern. Der Zugriff auf die Datenbank erfolgt weiterhin über die üblichen Open-SQL-Befehle.

Es kommt nun lediglich ein neues, sehr spezielles Aufgabengebiet hinzu.

Eine sehr gute und detaillierte Übersicht zeigt Bartosz Jarkowki in seinem SAP-Blog How To Perform Data Aging in S4/HANA. Weitere technische Details kannst du in dem Blogbeitrag von Jens Gleichmann nachlesen: Technical Details About Data Aging.

Der Beitrag Data-Aging in S4/HANA erschien zuerst auf Tricktresor.

Viewing all 166 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>