Quantcast
Channel: ABAP Development
Viewing all 948 articles
Browse latest View live

Enhanced variant configuration tool

$
0
0

Recently we started to develop a tool to be able to react flexible and fast to the changing requirements in product configuration. I will show in this blog the current solution and you are invited to comment, improve or also mention pros and cons of the shown solution. Do not hesitate to share your opinion and questions.

 

The main goals of the desired solution are:

 

           1. Every - also complicated - variant configurations can be done by pure configuration and there is no need for writing any ABAP code.

           2. The whole configuration of one product can be viewed and managed on one screen

           3. The solution is based entirely on the existing SAP ECC variant configuration.

 

Lets assume you have a packaging company for sweet tasty chocolate and you parcel the different bar sizes into different cases and place diffrent label on top of the closed case.

The products have different heights, widths and lenghts and you have lets say 3 different boxes small medium and large.

     How to you assign the products to the boxes?

     Further how you configure the text on the label. Lets say you want to print the first word of the product description on the label?

 

Well, the variant configuration of SAP offers a bunch of features to solve this things. The two above tasks could be solved with Variant functions and some ABAP code behind (you can find a good introduction in Variant Functions with an example.)

But what happens if you want to make some small changes - do you want really want that for every configuration change you have to find a developer who makes the necessary code changes for you?

 

 

So in the current solution the user view on the product configuration looks like this (simplified):

 

characteristicselection criteriavalue expression
TrayTypeLength > 200 or Width > 50 or Heigth > 20BIG
TrayTypeLength < 100 and Width < 20 and Heigth < 10SMALL
TrayTypeDEFAULTMEDIUM
ChocolateTypeDEFAULTFirstWord(MaterialCode)
ChocolateTypeMaterialCode = "chocolate mix"materialCode
VolumeDEFAULTLength * Width * Height
....

 

That mean for example an customer order with the materialCode "white chocolate" and a characteristic Length = 250 should get additional characteristics TrayType=BIG and ChocolateType=White. In another customer order with characteristic Length=100 the TrayType will be MEDIUM and so on.

 

If the BIG box should only be used if the Length > 210 the user can change this object dependency by simply changing the corresponding table entry.

 

So how this can be implemented, what would you suggest?

In my opinion the above mentioned variant functions fits best to get there and the solution is already working quite good with them. With some adaptions the variant functions get rather generic and to apply a change the only thing which remains for the user is to change the configuration.

 

Code Solution drafted

 

1. object dependency with variant function

for each desired additional characteristic an object dependency should be created. For example

 

FUNCTION Z_EvaluateTrayType

(Length = $ROOT.Length,

Width= $ROOT.Width,

Height = $ROOT.Height,

TARGET_CHARACTERISTIC = 'TrayType'

FUNCTION_RESULT = SELF.TrayType)

 

TARGET_CHARACTERISTIC and FUNCTION_RESULT are two additional characteristics to get the interface between variant function and function module more generic. Well this is a bit tricky and I am still searching for a better solution - do you have one - but its working.

 

Target_Characteristic is defined as input variable and Function_result the only output variable which will get the value of function result.


2. adaption to function module of the above mention example

The function module which links to the variant function evaluates the characteristic value for the target characteristic and writes it back to the variant function.


Adapted example code for function module


* GET name of target characteristic

CALL FUNCTION 'CUOV_GET_FUNCTION_ARGUMENT'
EXPORTING
      ARGUMENT           
= 'TARGET_CHARACTERISTIC'
IMPORTING
     
*   VTYPE               =
      SYM_VAL            
= LV_TARGET_CHARACTERISTIC
*    NUM_VAL             =
*   IO_FLAG             =
     
TABLES
      QUERY              
= QUERY
     
EXCEPTIONS
ARG_NOT_FOUND      
= 1
     
OTHERS              = 2

     ...


if LV_TARGET_CHARACTERISTIC = 'TrayType'.

*Select lines of the configuration table which fits the characteristics best and return the value expression.

*Evaluate the value expression and write the result into the function result

LV_FUNCTION_RESULT

endif.

...


CALL FUNCTION 'CUOV_SET_FUNCTION_ARGUMENT'
EXPORTING
ARGUMENT                     
FUNCTION_RESULT
VTYPE                        
= 'CHAR'
SYM_VAL                      
LV_FUNCTION_RESULT
*   NUM_VAL                       =
TABLES
MATCH                        
= MATCH
EXCEPTIONS
EXISTING_VALUE_REPLACED      
= 1
OTHERS                        = 2

 

The evaluation of the selection criteria can be done with some extension to the SAP function EVAL_FORMULA (this function module can beside numeric expressions like the above "Length * Width * Height"  also process boolean formulas. Operations like FirstWord have to be implemented once in ABAP.

 

I am looking forward to your feedback, ratings, comments and especially your ideas to improve the show solution.


ABAP News for Release 7.50 - Environment Information in ABAP CDS

$
0
0

In the ABAP language you are used to system fields like sy-mandt, sy-uname, sy-langu for environment information. For date and time, you use the good old german ones, sy-uzeit and sy-datum or you use time stamps (GET TIME STAMP and co.). Some of that convenience is available in ABAP CDS now too.

 

Session Variables

 

If a SAP HANA Database serves as the central database of an AS ABAP, you have access to the following three global session variables:

 

  • CLIENT
  • APPLICATIONUSER
  • LOCALE_SAP

 

The ABAP runtime environment fills these database variables with values that correspond to the contents of the above mentioned system fields. You can access the session variables natively, that is in EXEC SQL, ADBC and AMDP, using the built-in function SESSION_CONTEXT. Example for AMDP:

 

METHOD get_session_variables_amdp
       BY DATABASE PROCEDURE FOR HDB
       LANGUAGE SQLSCRIPT.
  clnt := session_context('CLIENT');
  unam := session_context('APPLICATIONUSER');
  lang := session_context('LOCALE_SAP');
ENDMETHOD.

 

That's not new for ABAP 7.50.

 

New for ABAP 7.50:

 

You can access these session variables in an ABAP CDS View. And not only for a SAP HANA Database but for all supported databases! The syntax is

 

  • $session.user
  • $session.client
  • $session.system_language

 

Simple example:

 

@AbapCatalog.sqlViewName: 'DEMO_CDS_SESSVAR'

@AccessControl.authorizationCheck: #NOT_REQUIRED

define view demo_cds_session_variables

as

select

   from demo_expressions

     { id,

       $session.user            as system_user,

       $session.client          as system_client,

       $session.system_language as system_language }

 

Please note, that for other databases than SAP HANA the contents of these variables is only defined when you access the CDS view with Open SQL.

 

Implicit Passing of Parameters

 

While session variables are a convenient way to access the information contained within they are - well -  global variables. And you know the bad reputation of global variables. What's the alternative? Passing the information from AS ABAP to appropriate parameters of CDS Views and CDS table functions. In order to facilitate that for you, a new ABAP annotation@Environment.systemField was introduced  for CDS view and function parameters with ABAP 7.50. The possible values are the enumeration values:

 

  • #CLIENT
  • #SYSTEM_DATE
  • #SYSTEM_TIME
  • #SYSTEM_LANGUAGE
  • #USER


If you access a CDS view or CDS table function with parameters annotated as such in Open SQL, you can (and for #CLIENT you even must)  leave away the explicit parameter passing. Open SQL implicitly passes the contents of the respective system fields for you!


Example View


@AbapCatalog.sqlViewName: 'DEMO_CDS_SYST'

@AccessControl.authorizationCheck: #NOT_REQUIRED

define view demo_cds_system_fields

  with parameters

    @Environment.systemField : #CLIENT

    p_mandt : syst_mandt,

    @Environment.systemField : #SYSTEM_DATE

    p_datum : syst_datum,

    @Environment.systemField : #SYSTEM_TIME

    p_uzeit : syst_uzeit,

    p_langu : syst_langu  

    @<Environment.systemField : #SYSTEM_LANGUAGE,

    p_uname : syst_uname  

    @<Environment.systemField : #USER

  as select from demo_expressions          

     { :p_mandt as client,

       :p_datum as datum,

       :p_uzeit as uzeit,

       :p_langu as langu,

       :p_uname as uname  }

      where id = '1';


Example Open SQL Access


SELECT *

   FROM demo_cds_system_fields(  )

   INTO TABLE @DATA(result).


The parameters are passed implicitly. This replaces


SELECT *

  FROM demo_cds_system_fields( p_datum  = @sy-datum,

                               p_uzeit  = @sy-uzeit,

                               p_langu  = @sy-langu,

                               p_uname  = @sy-uname )

  INTO TABLE @DATA(result).


A value for p_mandt cannot be passed explicitly any more.

 

The implicit parameter passing is for your convenience and only available in Open SQL. If you use CDS entities with parameters within CDS entities you have to pass parameters explicitly again. You might pass the above mentioned session variables then.

 

Date and Time

 

Did you notice that you can implicitly pass values for system date and time from AS ABAP to CDS entities but that there are no session variables for those (at least not in release 7.50)?

 

Instead, with ABAP 7.50 a new set of built-in date and time functions is available in ABAP CDS.

 

An important one is TSTMP_CURRENT_UTCTIMESTAMP(), that returns the current time stamp. Others check values and calculate with dates and times, as e.g. dats_days_between.

 

The following example shows how the current date and time can be extracted from a time stamp using string functions:

 

substring( cast( tstmp_current_utctimestamp() as abap.char(17) ), 1, 8 )

substring( cast( tstmp_current_utctimestamp() as abap.char(17) ), 9, 6 )

 

Smells like a workaraound? More functionality for date and time handling in ABAP CDS is still in development!

 

 

 

 

 

 

 

 

 

 

 

 

 





ABAP News for Release 7.50 - ABAP Keyword Documentation

$
0
0

Although ABAP and ABAP CDS are rather self-explaining some of you still tend to use F1 on ABAP or ABAP CDS keywords in the ABAP Workbench and/or ADT. Then, wondrously, the ABAP Keyword Documentation appears, and if you're lucky, you directly find what you searched for (of course that kind of context sensitive help is overrated; in HANA Studio, you boldly program without such fancies). But since it still seems to be used, one or the other improvement is also done for the ABAP Keyword Documentation. (B.t.w.: What are the most commonly looked up ABAP statements? -  SELECT, READ TABLE, DELETE itab).

 

 

Full Text Search in ADT

 

In the SAP GUI version of the ABAP Keyword Documentation you can carry out an explicit full text search since long. When calling the documentation by transaction ABAPHELP or from SE38 you find the respective radio button:

 

adoc1.gif

 

From the documentation display you can also explicitly call the full text seach by selecting Extended Search:

 

adoc2.gif

 

Not so in ADT (Eclipse). In ADT the so called Web Version of the ABAP Keyword Documentation is used. That's not the one you find in the portal but one that can be reached by an ICF service maintained in transaction SICF and that you can also call with program ABAP_DOCU_WEB_VERSION (of course the contents of all versions are the same, only the display and the functionality of the displays differ, with the functionality of the ADT version coming closer and closer to the SAP GUI version).


Up to now, the Web Version used in ADT started with an index search and only switched to full text search if nothing was found. With ABAP 7.50 you can call the full text search in the Web Version and in ADT explicitly. Simply put double quotes around your search term:


adoc3.gif

So simple. Furthermore, you can continue searching in the hit list of an index search:


adoc4.gif

A piece of cake! Wonder why I had waited so long to implement it. The next one was harder.



Special Characters in Syntax Diagrams

 

In good old times, the special characters { }, [ ], |  had no special meaning in  ABAP. Therefore, they were used with special meanings in the syntax diagrams of the ABAP Keyword Documentation, denoting logical groups, optional additions and alternatives. This has changed since string templates. Mesh paths and of course the DDL of ABAP CDS are other examples.


This resulted in syntax diagrams, where you couldn't decide which special characters are part ot the syntax and which are markups, e.g. embedded expressions:


adoc5.gif


After some complaints from colleagues (customers never complain, almost). I changed that and use another format for markup characters from ABAP 750 on. Embedded expressions now:

adoc6.gif


A small step for mankind, but a large step for me. Why that? I had to check and adjust about eighteen thousand{ }, [ ], | characters manually arrrgh! Couldn't figure out a way of replacing them with a program. The results are worth a look.


Maybe it takes a little getting used to, but my colleagues didn't complain any more. Time for customers to complain? Any suggestions regarding the format? Now I have only a few lines of code to change in order to adjust the format of all markup characters in one go ...

 

 


ABAP News for Release 7.50 - ABAP Channels Reloaded

$
0
0

ABAP Channels (ABAP Messaging Channels AMC and ABAP Push Channels APC) were introduced with ABAP 7.40, SP02, SP05. They enable an event based bidirectional communication between ABAP application servers and with the internet. For that, a dedicated Push Channel Protocol (PCP) can be used since 7.40, SP08. Before ABAP 7.50, the communication between ABAP servers and the internet was restricted to the Web Socket Protocol (WS) and the AS ABAP acting as a stateless APC server. The following features are new with ABAP 7.50:

 

 

Stateful APC Servers

 

Before ABAP 7.50, each ABAP Push Channel that you created as a repository object in SE80 or SAPC was automatically stateless. An APC handler class always inherited from CL_APC_WSP_EXT_STATELESS_BASE or CL_APC_WSP_EXT_STATELESS_PCP_B. Now you can select Stateful too.

adoc7.gif

The respective APC handler classes will inherit from CL_APC_WSP_EXT_STATEFUL_BASE or CL_APC_WSP_EXT_STATEFUL_PCP_B. For a stateful APC server its context and especially the attributes of the APC handler class are not deleted between different client accesses.


Stateful APC applications are running in a so called Non Blocking Model, where all blocking statements are forbidden.Those are the usual suspects that call programs or screens, leave programs or screens, or interrupt.

 

 

AS ABAP as APC Client

 

From ABAP 7.50 on, you can define handler classes in ABAP, that implement either IF_APC_WSP_EVENT_HANDLER or IF_APC_WSP_EVENT_HANDLER_PCP (the latter for using the Push Channel Protocol). Methods of such classes can handle messages from an APC server. The actual client is then created with factory methods of the classes CL_APC_WSP_CLIENT_MANAGER or CL_APC_TCP_CLIENT_MANAGER. Using a client object, you can open and close connections to an APC server and you can create and send messages.

 

In order to receive messages in an ABAP APC client, the AS ABAP has to wait for those. Such a wait state can be programmed explicitly with the new ABAP statement WAIT FOR PUSH CHANNELS, that completes the already existing WAIT FOR MESSAGING CHANNELS and WAIT FOR ASYNCHRONOUS TASKS.

 

See the example in the next section.

 

 

TCP Protocol

 

Besides the WebSocket protocol (WSP), the ABAP Push Channels framework now also supports native TCP (Transmission Control Protocol) sockets. This allows communication with Clients and Servers that  do not support WSP. Those can be embedded systems or Progammable Logic Controllers (PLC). And this ultimately connects our good old ABAP directly to - attention, buzz word alarm - the internet of things (IOT)!

 

For an AS ABAP to work as a TCP server, you simply select the respective connection type:

 

adoc8.gif

 

PCP cannot be used as subprotocol here, but stateful TCP servers are possible. The respective APC handler classes will inherit from CL_APC_TCP_EXT_STATELESS_BASE or CL_APC_TCP_EXT_STATEFUL_BASE.

 

For an AS ABAP to work as a TCP client, you basically do the same as for WSP clients (see above) but use CL_APC_TCP_CLIENT_MANAGER in order to create the client object.

 

The following is a complete example for an ABAP program that creates an ABAP TCP client:

 

CLASS apc_handler DEFINITION FINAL .

  PUBLIC SECTION.

    INTERFACES if_apc_wsp_event_handler.

    DATA       message TYPE string.

ENDCLASS.

 

CLASS apc_handler IMPLEMENTATION.

  METHOD if_apc_wsp_event_handler~on_open.

  ENDMETHOD.

 

  METHOD if_apc_wsp_event_handler~on_message.

    TRY.

        message = i_message->get_text( ).

      CATCH cx_apc_error INTO DATA(apc_error).

        message = apc_error->get_text( ).

    ENDTRY.

  ENDMETHOD.

 

  METHOD if_apc_wsp_event_handler~on_close.

    message = 'Connection closed!'.

  ENDMETHOD.

 

  METHOD if_apc_wsp_event_handler~on_error.

  ENDMETHOD.

ENDCLASS.

 

CLASS apc_demo DEFINITION.

  PUBLIC SECTION.

    CLASS-METHODS main.

ENDCLASS.

 

CLASS apc_demo IMPLEMENTATION.

  METHOD main.

    DATA(tcp_server) = `C:\ncat\ncat.exe`.

    DATA(ip_adress)  = cl_gui_frontend_services=>get_ip_address( ).

    DATA(port)       = `12345`.

    DATA(terminator) = `0A`.

    DATA(msg)        = `Hello TCP, answer me!`.

 

    "Server

    IF cl_gui_frontend_services=>file_exist(

         file = tcp_server ) IS INITIAL.

      cl_demo_output=>display( 'TCP Server not found!' ).

      LEAVE PROGRAM.

    ENDIF.

    cl_gui_frontend_services=>execute(

    EXPORTING

      application = `cmd.exe`

      parameter  =  `/c ` && tcp_server &&

                   ` -l ` && ip_adress && ` -p ` && port ).

    WAIT UP TO 1 SECONDS.

 

    TRY.

        DATA(event_handler) = NEW apc_handler( ).

 

        "Client

        DATA(client) = cl_apc_tcp_client_manager=>create(

          i_host   = ip_adress

          i_port  = port

          i_frame = VALUE apc_tcp_frame(

            frame_type =

              if_apc_tcp_frame_types=>co_frame_type_terminator

            terminator =

              terminator )

          i_event_handler = event_handler ).

 

        client->connect( ).

 

        "Send mesasage from client

        DATA(message_manager) = CAST if_apc_wsp_message_manager(

          client->get_message_manager( ) ).

        DATA(message) = CAST if_apc_wsp_message(

          message_manager->create_message( ) ).

        DATA(binary_terminator) = CONV xstring( terminator ).

        DATA(binary_msg) = cl_abap_codepage=>convert_to( msg ).

        CONCATENATE binary_msg binary_terminator

               INTO binary_msg IN BYTE MODE.

        message->set_binary( binary_msg ).

        message_manager->send( message ).

 

        "Wait for a message from server

        CLEAR event_handler->message.

        WAIT FOR PUSH CHANNELS

             UNTIL event_handler->message IS NOT INITIAL

             UP TO 10 SECONDS.

        IF sy-subrc = 4.

          cl_demo_output=>display(

            'No handler for APC messages registered!' ).

        ELSEIF sy-subrc = 8.

          cl_demo_output=>display(

            'Timeout occured!' ).

        ELSE.

          cl_demo_output=>display(

            |TCP client received:\n\n{ event_handler->message }| ).

        ENDIF.

 

        client->close(

          i_reason = 'Application closed connection!' ).

 

      CATCH cx_apc_error INTO DATA(apc_error).

        cl_demo_output=>display( apc_error->get_text( ) ).

    ENDTRY.

 

  ENDMETHOD.

ENDCLASS.

 

START-OF-SELECTION.

  apc_demo=>main( ).

 

To simulate an external TCP server, the program uses the current frontend computer, to which the freely available Ncat can be downloaded. The ABAP program starts Ncat.exe, which waits for a message. The ABAP APC client sends a message and waits itself by using WAIT FOR PUSH CHANNELS. You can enter a message in the Ncat console and will receive it in ABAP. Note, that the terminator character of the TCP frame structure must be defined explicitly.

 

 

Detached APC Client

 

This one is a little bit tricky. The use case is a scenario, where an AS ABAP wants to work as an APC server, but wants to open the connection himself. In order to do so, the AS ABAP first acts like an APC client (see above) and opens a connection. But instead of sending a message, it is immediately detached but the connection remains open. Now the same or any other ABAP application server can be attached to the connection as a so called attached client, and the AS ABAP of the detached client then plays the role of a stateless or stateful server.

 

A deatched client similar to a real client needs handler classes implementing IF_APC_WSP_EVENT_HANDLER or IF_APC_WSP_EVENT_HANDLER_PCP. But only the ON_OPEN event is needed in order to receive a connection handle. The detached clients itself are created with CL_APC_WSP_CLIENT_CONN_MANAGER or CL_APC_TCP_CLIENT_CONN_MANAGER. Such a detached client is used to open the connection and to be detached immediately by its method CONNECT_AND_DETACH.

 

Now, the connection handle can be used to create an attached client with method ATTACH of CL_APC_WSP_CLIENT_CONN_MANAGER or CL_APC_TCP_CLIENT_CONN_MANAGER. The attached client object can send messages to the AS ABAP of the detached client. Oh boy.

 

 

 

APC Access Object

 

With a similar mechanism as for attaching an attached client to a detached client, you can create an access object for any APC connection. If an APC handler class decides to publish a connection handle that it can get itself with method GET_CONNECTION_ATTACH_HANDLE from its context object, you can use the same method ATTACH as above to create an access object, that can send messages to the connection. In this case some restrictions for the connection handle apply. Only the same session, client and user or the same program and client are allowed to use such a connection handle.


 

 

AMC Point-to-Point-Communication

 

ABAP Messaging Channels as you know them up to now are based on a publlish and subscribe mechanism, where the sender does not know the receivers. With ABAP 7.50, also a point-to-point communication is possible, where a sender can send messages to a specific receiver. For that, the sender needs the id of the receiver. A receiver can get its id with method GET_CONSUMER_SESSION_ID of its channel manager and publish it appropriately. A sender can use that id to create a respective sender object with method CREATE_MESSAGE_PRODUCER_BY_ID of its channel manager.The message can be send asynchronously or synchronously. In the latter case, the sender waits for a feedback.


 

 

More Information and Examples

 

See ABAP Channels.

 

Executable examples for all subjects covered here will be available in the ABAP Documentation's example library with 7.50, SP02.

 

 

 

___

ABAP News for Release 7.50 - Dynamic RFC Destinations

$
0
0

Up to now, there was no officially released or documented possibility to create a fully fledged dynamic RFC destination that can be entered behind CALL FUNCTION DESTINATION.

 

If you crawl the Web for "dynamic RFC destination", you find one or the other hint how to achieve that, but the general recommendation is: Don't do that!

 

In order to direct that uncontrolled growth into the right channels, a new class is delivered with ABAP 7.50:

 

DATA(dest) =

 

  cl_dynamic_destination=>create_rfc_destination(

    logon_client   = ...

    logon_user     = ...

    logon_language = ...

    sid            = ...

    server         = ...

    group          = ...

    ... ).

  

CALL FUNCTION 'DEMO_RFM_PARAMETERS'

      DESTINATION dest

      EXPORTING ...

 

From ABAP 7.50 on, CL_DYNAMIC_DESTINATION is the officially released and documented possibility to create a dynamic RFC destination and the only one to be used if SM59 is not sufficient.

ABAP Language News for Release 7.50

$
0
0

This blog summarizes a series of blogs that I have written about the most important ABAP language news for Release 7.50 during the last  month.


ABAP News by Subject



More Information


For the complete information see: Changes in Release 7.50





Best font for ABAP Editor?

$
0
0

Just one small blogpost.

Which font are you - ABAP developers using in code editor (either in SAP GUI SE38/SE80/SE24... or in Eclipse IDE)?

I have done little research in other programming languages and I found that one of the most preffered non-default one is called Consolas.

So I gave it a try and now I can say that it is great

 

It is one of the pre-installed fonts in MS Windows since Vista I think...

 

Here is comparison between default SAP GUI font:

 

Courier New:

courier.jpg

and here is Consolas:

consolas.png

 

Font settings can be found via button in right bottom corner of editor:

settings.jpg

Journey to setup best possible Continuous Integration with ABAP Project

$
0
0

Background

After working for several years in the nowadays so called "DevOps" area, developing test automation tools, simulation frameworks, automated performance & stability tests and rampup and maintaining quite complex build, test and delivery pipelines (CI's/CD's) for both Java and .NET stack projects i joined this year SAP. Into an purely ABAP based project.

 

Idea

Setup CI. Simple. Or ?

Beside normal CI requirements we have additional test activities to do before release

  • Compatibility to all versions between 7.02 and 7.5x, S4H*
  • Performance KPI's, especially from customers with huge (x hundred's Mio contracts like Vodafone)
  • Test with specific customizations (using one config as blueprint for whole industry sector)

 

so, first sketch (translated Dev/Integration/Release branch model to ABAP development landscape)

CI_Vision_1.PNG

ATC

So easy to start, but not all developers consistently run check and remove - at least Prio1&2 - before triggering transport. For *some* machines (which are connected ) it is possible to use the ATC gadget on the Bridge Dashboard.

Follow this guide to setup HTTP IF for ATC Checks.

AUnit

Same. Failing Unit tests do not block any transport. TDD? Yes, most heard about it, but really doing in daily life ? Good practice is to plan as task immediately for each Item from backlog and add to teams DoD a minimum of code coverage for newly created code.

We found it very effective to setup at least nightly runs of our unit tests, each team got an own variant, an own Jenkins Job for the trend charts and results(errors) discussed in each teams daily standup.

Step-By-Step Guide Setup Jenkins Instance wit AUnit Test Jobs

Transport Events

Yeah! After I first heard about it I immediately thought that is the solution to slackers which ignore both ATC and AUnit. But setup is a bit more complicated as thought. First of all - check version of SUT. Write DLM ticket to update to latest version (as of today - 11/2015 - this is 2.12). Use Transport Requests with latest version of SUT Core as reference within the DLM ticket.

Note: It will not work for 7.02.

 

Now you can configure your systems to run both ATC & AUnit triggered by Import/Export Events (which sends EMail to the author of the change).

 

ITF - Instant Test Framework

During research I found some references to the ITF, which was developed within ByD landscape and never ported to NW.

The idea is close to CI - first run a "trial" transport into a shadow system, and only if all tests passed there activate the transport for next stage.

See some more ITF details.

eCATT with START

In our project we have SAPUI & WebDynPro, which means for E2E tests we have to use eCATTs (or manual testing). eCATTs are quite expensive (around 2 days per TC) and generate a heavy maintenance load (test scripts need adaptation, test data might need to be changed, errors itself are hard to find cause for some types of crashes there is no way to get a detailed trace). All these reasons have led to running them as less as possible. And of course no test trend chart available too.

Best possible solution: Trigger eCATT test plans (which so far must be configured in ECA system cause START cannot run test plans from GTP) via START command line from Jenkins. Meanwhile START team extended functionality so that results can be retrieved in JUnit format and displayed quite nicely (but without possibility to directly jump to detailed traces).

 

Code Coverage Trends

Within one of the latest versions of SUT a new feature was introduced to generate Code Coverage Trend Charts.

And again, not availble for NW 7.02.

 

ABAP code duplication check (Simian, conQAT, …)

http://scn.sap.com/community/abap/blog/2015/02/17/static-abap-code-analysis-using-conqat-and-of-course-sci

I tried to configure conQAT in eclipse but gave up after one day without the expected results.

 

ABAP in Eclipse

Shall be better as ABAP workbench, but not available for NW 7.02.

 

Open Points

  • ATC Checks in Jenkins
  • Code Coverage Trends In Jenkins
  • START to run tests from GTP, providing more params like "Start profile"

 

Further Links


Reminder: ABAP Built-in Functions

$
0
0

The ABAP language offers so many features that you easily can lose track of them all. A recent discussion showed that. The task is some operation on a character string and the question was how to achieve that. The first answer was to do it with regular expressions, and I guess I would have answered the same. Regular expression are general and powerful and you can achieve almost anything you want in finding and replacing characters in strings. They can be easily tested with programs DEMO_REGEX and DEMO_REGEX_TOY. Because of that one can easily forget that there are other more simple and performant means for accomplishing many fundamental tasks. Another answer pointed that out: There is a wealth of built-in functions available in ABAP, especially for string processing, that can be used for many requirements. Those functions can be nested and can use expression (especially string and regular expressions) as operands. If you do not want to break a butterfly on a wheel, built-in functions can help you in many cases to achieve your results.

 

As a reminder I copied the overview of the built-in functions from the documentation below. Check also the examples in the respective sections of the documentation.

 

 

 

Built-in Functions - Overview


The following tables show the predefined functions by purpose. Predefined functions are generally processing functions or description functions.

 

  • A processing function performs general processing and returns a return code according to its purpose.
  • A description function determines a property of an argument and usually returns this property as a numeric value.

 

Furthermore, there are logical functions that either are boolean functions that evaluate a logical expression or predicate functions that return a truth value.

 

Logical Functions


FunctionMeaning
boolc, boolx, xsdboolBoolean functions
contains, contains_any_of, contains_any_not_ofPredicate functions for strings
matchesPredicate function for strings
line_existsPredicate function for internal tables

 

More


Numeric Functions


FunctionMeaning
abs, ceil, floor, frac, sign, truncGeneral numeric functions
ipowInteger power function
nmax, nminNumeric extremum functions
acos, asin, atan, cos, sin, tan, cosh, sinh, tanh, exp, log, log10, sqrtFloating point functions
round, rescaleRounding functions

 

More


String Functions


FunctionMeaning
charlen, dbmaxlen, numofchar, strlenLength functions
char_offLength function
cmax, cminCharacter-like extremum value functions
count, count_any_of, count_any_not_ofSearch functions
distanceSimilarity function
condenseCondense function
concat_lines_ofConcatenation function
escapeEscape function
find, find_end, find_any_of, find_any_not_ofSearch functions
insertInsert function
matchSubstring function
repeatRepeat function
replaceReplace function
reverseReverse function
SegmentSegment function
shift_left, shift_rightShift functions
substring, substring_after, substring_from, substring_before, substring_toSubstring functions
to_upper, to_lower, to_mixed, from_mixedCase functions
translateTranslation function

 

More


Byte String Functions


FunctionMeaning
xstrlenLength function
bit-setBit function

 

More


Table Functions


FunctionMeaning
linesRow function
line_indexIndex function

 

More

Enhanced variant configuration tool - Coding thoughts

$
0
0

This is a follow up to the article Enhanced variant configuration tool

In this blog some developement specific points are shown which ensure that the characteristic configuration is evaluated properly. This are the first thoughts and if you have some improvement ideas, do not hesitate to mention them.

 

characteristicselection criteriavalue expression
TrayTypeLength > 200 or Width > 50 or Heigth > 20BIG
TrayTypeLength < 100 and Width < 20 and Heigth < 10SMALL
TrayTypeDEFAULTMEDIUM
ChocolateTypeDEFAULTFirstWord(MaterialCode)
ChocolateTypeMaterialCode = "chocolate mix"materialCode
WEIGHTTrayType = 'BIG'ROUND(Length * Width * Height*2, 2, 'X') + 20
WEIGHTTrayType = 'SMALL'ROUND(Length * Width * Height*2, 2, 'X') + 10

 

1.  object dependency with variant function

Each characteristic which should be calculated gets a variant function assigned in the object dependencies. This is linked to one function module. To avoid the need for separate function module for each characteristic some "help characteristics" can be very useful.

 

FUNCTION Z_EvaluateTrayType

(Length = $ROOT.Length,

Width= $ROOT.Width,

Height = $ROOT.Height,

TARGET_CHARACTERISTIC = 'TrayType'

FUNCTION_RESULT = SELF.TrayType)


TARGET_CHARACTERISTIC and FUNCTION_RESULT are two additional characteristics to get the interface between variant function and function module more generic. Well this is a bit tricky and I am still searching for a better solution - do you have one? - but its working.

 

Target_Characteristic is defined as input variable and Function_result the only output variable which will get the value of function result.

 

Now you can for example calculate the TrayType, Volume, Weight, Groundfloor with the same function module by setting the TARGET_CHARACTERISTIC to the desired value as they use all the same input characteristics.

 

2. replacement of characteristics in formulas

 

To evaluate the selection criteria and the value expression a more or less complex parser is needed. I found in ABAP only the eval_formula, which does fit quite good for arithmetical expressions but not for text expressions, have you got any hints for me?

 

Finally I settled with REGEX replacement but I am still thinking about switching to a conventional top down or bottom up parser. What do you think? 

 

3. replacement of sub characteristics

Subcharacteristics, like used when calculating the WEIGHT characteristic are done by using recursive function calls.

 

4. handling of brackets and boolean expressions

Brackets in numerical expressions are coverd quite good through the SAP standard method EVAL_FORMULA. This method provides also some standard functions as rounding to integers, square functions and so on. 

The function EVAL_FORMULA can also process boolean expressions, well text comparisions have first be transfered by pure code to a number value (0 or 1)

Numeric functions like Length > 200 or Width > 50 or Heigth > 20 (Length, Width, Height) are characteristics which are replaced with their numerical value can be entirely passed to EVAL_FORMULA.


5. user defined functions

User defined functions like FirstWord(MaterialCode) or for example a oracle like decode function have to be coded in ABAP. To spot formulas in the expressions I used onces more regular expressions.

A bit tricky is the combination of user defined functions with characteristics. I read somewhere about the definition of user defined functions together with EVAL_FORMULA, anybody has good experience with them and want to share it?



6. SAP standard functions

Functions, like substring, round are already available in ABAP. Nevertheless some mapping from the expressions to the ABAP function call has to be done.


All together a lot of coding is necessary. But it should only be necessary once and the big advantage of the solution is that for later configuration changes no further programming effort is needed and thus leads to a efficient and time-consuming configurable system -> i hope so :-)


Any feedback, especially to the questions is welcome.


Andreas

Compare transport objects between systems

$
0
0

     This report can help you to compare objects in transport requests across different systems. It allows you to analyze whole system landscape simultaneously and provides the information about relative versions. The program has user-friendly interface and it is easy to install, just copy paste the source code. It can be a useful tool for analyzing "hard" transport requests before importing. The program uses SAP Version Management mechanism for all objects in change requests. You can run it on any system (DEV, QAS, PRD), so you don't have to import this program into other systems.

     After creating the program, run the report and enter the change requests. You can also set the target system and filter the choices by selecting object types.

UQEBZFXuZ.jpg

     After entering the required data click on execute. The program select objects in transports (repository objects such as screens, reports, data elements etc.) and compare them in the development system with that in the quality assurance system and the production system. The report builds the dynamic ALV that will show the list of objects and their versions in the systems of current domain (it depends on your system landscape).

image001.png

     This list helps you to understand the transport order and fix probable errors. Green objects are consistent. Red objects are importing in incorrect sequence. The colour of the objects depends on target system specified at the selection screen. If you don't fill this parameter, the program will check for consistency through all systems of domain.

 

Hope my tool will be useful.

Regards, Sergey Berezkin.

Search Help Exit: Simplify Restiction Handling Using CL_RSDRV_REMOTE_IPROV_SRV Class

$
0
0

    Usually Search Help source data from either Table or View. Sometimes data selection requirements are so complex, that view no mention table can not satisfy them. In this case developer has to leverage SELECT step of Search Help Exit in order to have a full flexibility of data selection. But flexibility of data selection comes with a price to handle Search Help Data Restrictions.

    In my blog I will demonstrate how gracfully apply Search Help Restrictions to data selection using CL_RSDRV_REMOTE_IPROV_SRV Class. For those who has BW background, this class should be familiar.

 

    Suppose that you need to create a Search Help that provides a list of released, not locked, not deleted and not confirmed Production Orders as displayed below

Search Help Restrictions 1 of 3.jpg

Data is sourced from multiple tables. Left Join and WHERE clause subqueries are used. The latter definintely goes beyond of ABAP view capabilities. That is why Search Help Exit with free style SQL is used. The complexity lies in dynamically appling Search Help Restrictions to SQL statement used to retrieve data

Search Help Restrictions 2 of 3.jpg

 

Search Help Restrictions 3 of 3.jpg

CL_RSDRV_REMOTE_IPROV_SRV Class dynamically generating SQL WHERE clause condition from Search Help Restrcions without any hardcoding

Search Help Exit Debugging 1 of 3.jpg

Search Help Exit Debugging 2 of 3.jpg

Search Help Exit Debugging 3 of 3.jpg

I innovatively used of CL_RSDRV_REMOTE_IPROV_SRV to generation of WHERE condition for multiple joined tables, whereas the class was ment to be used with single table only.

 

Below are step by step instructions how to implement the Search Help

1) Define LCL_REMOTE_IPROV_SRV Wrapper Class;

2) Define ZPROD_ORD_SHLP Search Help Exit;

3) Define ZPROD_ORD_SHLP Search Help.

 

 

Define LCL_REMOTE_IPROV_SRV Wrapper Class

 

DEFINE define_mapping_iobj_2_fld.

  wa_iobj_2_fld
-iobjnm = &1.
  wa_iobj_2_fld
-fldnm  = &2.
 
INSERT wa_iobj_2_fld INTO TABLE wt_iobj_2_fld.

END-OF-DEFINITION.

*---------------------------------------------------------------------*
*      CLASS lcl_remote_iprov_srv DEFINITION
*---------------------------------------------------------------------*
CLASS lcl_remote_iprov_srvDEFINITION.

 
PUBLIC SECTION.
   
METHODS:
      constructor,
      get_where_conditions
IMPORTING it_selopsTYPE ddshselops
                         
RETURNINGVALUE(rt_where) TYPE rsdr0_t_abapsource.
 
PRIVATE SECTION.
   
DATA:
      remote_iprov_srv   
TYPE REF TO cl_rsdrv_remote_iprov_srv.

ENDCLASS.

*---------------------------------------------------------------------*
*      CLASS lcl_remote_iprov_srv IMPLEMENTATION
*---------------------------------------------------------------------*
CLASS lcl_remote_iprov_srvIMPLEMENTATION.

*---------------------------------------------------------------------*
* constructor
*---------------------------------------------------------------------*
 
METHOD constructor.
 
DATA: wa_iobj_2_fld TYPE cl_rsdrv_remote_iprov_srv=>tn_s_iobj_fld_mapping,
        wt_iobj_2_fld
TYPE cl_rsdrv_remote_iprov_srv=>tn_th_iobj_fld_mapping.

    define_mapping_iobj_2_fld:
'AUFNR'        'AUFK~AUFNR',
                             
'AUART'        'AUFK~AUART',
                             
'WERKS'        'AUFK~WERKS',
                             
'APRIO'        'AFKO~APRIO',
                             
'MATNR'        'AFPO~MATNR',
                             
'KUNNR'        'VBAK~KUNNR',
                             
'KDAUF'        'AUFK~KDAUF',
                             
'KDPOS'        'AUFK~KDPOS'.
   
CREATE OBJECTremote_iprov_srv
     
EXPORTING i_th_iobj_fld_mapping= wt_iobj_2_fld
                i_tablnm             
= 'DUMMY'.

 
ENDMETHOD.

*---------------------------------------------------------------------*
* get_where_conditions
*---------------------------------------------------------------------*
 
METHOD get_where_conditions.
 
DATA: wa_selopsTYPE ddshselopt.
 
DATA: wa_selection TYPE LINE OF cl_rsdrv_remote_iprov_srv=>tn_t_selection,
        wt_selection
TYPE cl_rsdrv_remote_iprov_srv=>tn_t_selection.

   
LOOP AT it_selopsINTO wa_selops.
     
MOVE-CORRESPONDING wa_selopsTO wa_selection.
      wa_selection-infoobject
= wa_selops-shlpfield.
     
APPEND wa_selectionTO wt_selection.
   
ENDLOOP.
    remote_iprov_srv
->build_where_conditions(
     
EXPORTING i_t_selection= wt_selection
     
IMPORTING e_t_where    = rt_where).

 
ENDMETHOD.

ENDCLASS.

 

Define ZPROD_ORD_SHLP Search Help Exit

 

FUNCTION zprod_ord_shlp.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  TABLES
*"      SHLP_TAB TYPE  SHLP_DESCT
*"      RECORD_TAB STRUCTURE  SEAHLPRES
*"  CHANGING
*"    VALUE(SHLP) TYPE  SHLP_DESCR
*"    VALUE(CALLCONTROL) LIKE  DDSHF4CTRL STRUCTURE  DDSHF4CTRL
*"----------------------------------------------------------------------
 
data: rc      type i.


* EXIT immediately, if you do not want to handle this step
 
IF CALLCONTROL-STEP <> 'SELONE' AND
    CALLCONTROL
-STEP <> 'SELECT' AND
   
" AND SO ON
    CALLCONTROL
-STEP <> 'DISP'.
   
EXIT.
 
ENDIF.

*"----------------------------------------------------------------------
* STEP SELONE  (Select one of the elementary searchhelps)
*"----------------------------------------------------------------------
* This step is only called for collective searchhelps. It may be used
* to reduce the amount of elementary searchhelps given in SHLP_TAB.
* The compound searchhelp is given in SHLP.
* If you do not change CALLCONTROL-STEP, the next step is the
* dialog, to select one of the elementary searchhelps.
* If you want to skip this dialog, you have to return the selected
* elementary searchhelp in SHLP and to change CALLCONTROL-STEP to
* either to 'PRESEL' or to 'SELECT'.
 
IF CALLCONTROL-STEP = 'SELONE'.
*  PERFORM SELONE .........
   
EXIT.
 
ENDIF.

*"----------------------------------------------------------------------
* STEP PRESEL  (Enter selection conditions)
*"----------------------------------------------------------------------
* This step allows you, to influence the selection conditions either
* before they are displayed or in order to skip the dialog completely.
* If you want to skip the dialog, you should change CALLCONTROL-STEP
* to 'SELECT'.
* Normaly only SHLP-SELOPT should be changed in this step.
 
IF CALLCONTROL-STEP = 'PRESEL'.
*  PERFORM PRESEL ..........
   
EXIT.
 
ENDIF.
*"----------------------------------------------------------------------
* STEP SELECT    (Select values)
*"----------------------------------------------------------------------
* This step may be used to overtake the data selection completely.
* To skip the standard seletion, you should return 'DISP' as following
* step in CALLCONTROL-STEP.
* Normally RECORD_TAB should be filled after this step.
* Standard function module F4UT_RESULTS_MAP may be very helpfull in this
* step.
** *********************************
** modified part starts here
** *********************************
 
IF CALLCONTROL-STEP = 'SELECT'.
 
PERFORM STEP_SELECT TABLES RECORD_TAB SHLP_TAB
                     
CHANGING SHLP CALLCONTROL RC.
 
IF RC = 0.
    CALLCONTROL
-STEP = 'DISP'.
 
ELSE.
    CALLCONTROL
-STEP = 'EXIT'.
 
ENDIF.
   
EXIT. "Don't process STEP DISP additionally in this call.
 
ENDIF.
*"----------------------------------------------------------------------
* STEP DISP    (Display values)
*"----------------------------------------------------------------------
* This step is called, before the selected data is displayed.
* You can e.g. modify or reduce the data in RECORD_TAB
* according to the users authority.
* If you want to get the standard display dialog afterwards, you
* should not change CALLCONTROL-STEP.
* If you want to overtake the dialog on you own, you must return
* the following values in CALLCONTROL-STEP:
* - "RETURN" if one line was selected. The selected line must be
*  the only record left in RECORD_TAB. The corresponding fields of
*  this line are entered into the screen.
* - "EXIT" if the values request should be aborted
* - "PRESEL" if you want to return to the selection dialog
* Standard function modules F4UT_PARAMETER_VALUE_GET and
* F4UT_PARAMETER_RESULTS_PUT may be very helpfull in this step.
 
IF CALLCONTROL-STEP = 'DISP'.
*  PERFORM AUTHORITY_CHECK TABLES RECORD_TAB SHLP_TAB
*                          CHANGING SHLP CALLCONTROL.
   
EXIT.
 
ENDIF.
ENDFUNCTION.



** ********************************************************************
** selection of entries with correct text
** ********************************************************************

FORM STEP_SELECT TABLES RECORD_TAB SHLP_TAB TYPE SHLP_DESCT
                     
CHANGING SHLP TYPE SHLP_DESCR CALLCONTROL TYPE  DDSHF4CTRL RC.
DATA: wt_whereTYPE rsdr0_t_abapsource.
DATA: wt_prod_ord_shlpTYPE TABLE OF zprod_ord_shlp.
DATA: remote_iprov_srvTYPE REF TO lcl_remote_iprov_srv.
CONSTANTS: c_dlfl    TYPE j_status  VALUE 'I0076',
          c_lkd     
TYPE j_status  VALUE 'I0043',
          c_rel     
TYPE j_status  VALUE 'I0002',
          c_cnf     
TYPE j_status  VALUE 'I0009',
          c_active 
TYPE abap_boolVALUE abap_false,
          c_inactive
TYPE abap_boolVALUE abap_true.


 
CHECK shlp-shlpname= 'ZPROD_ORDER'.
 
CREATE OBJECTremote_iprov_srv.
  wt_where
= remote_iprov_srv->get_where_conditions( shlp-selopt ).

 
SELECT aufk~aufnr
        aufk~auart
        aufk~werks
        afko~aprio
        afpo~matnr
        vbak~kunnr
        aufk~kdauf
        aufk~kdpos
 
FROM ( ( aufkINNERJOIN afpo
                       
ON aufk~aufnr= afpo~aufnr)
               
INNERJOIN afko
                       
ON aufk~aufnr= afko~aufnr)
               
LEFT  JOIN vbak
                       
ON vbak~vbeln= aufk~kdauf
 
UP TO callcontrol-maxrecordsROWS
 
INTO CORRESPONDINGFIELDS OF TABLE wt_prod_ord_shlp
 
WHERE (wt_where)
*  Released
   
AND aufk~objnrIN ( SELECT objnr
                       
FROM jest
                       
WHERE aufk~objnr= jest~objnr
                         
AND jest~stat  = c_rel
                         
AND jest~inact= c_active)
*  Not Deleted
   
AND aufk~objnrNOT IN ( SELECT objnr
                           
FROM jest
                           
WHERE objnr= aufk~objnr
                             
AND stat  = c_dlfl
                             
AND inact= c_active)
*  Not Locked
   
AND aufk~objnrNOT IN ( SELECT objnr
                           
FROM jest
                           
WHERE objnr= aufk~objnr
                             
AND stat  = c_lkd
                             
AND inact= c_active)
*  Not Confirmed
   
AND aufk~objnrNOT IN ( SELECT objnr
                           
FROM jest
                           
WHERE objnr= aufk~objnr
                             
AND stat  = c_cnf
                             
AND inact= c_active).

 
if sy-subrc<> 0.
    RC
= sy-subrc.
   
exit.
 
endif.

  record_tab[]
= wt_prod_ord_shlp[].


ENDFORM.

 

Define ZPROD_ORD_SHLP Search Help

 

 

Define ZPROD_ORD_SHLP dummy source table

Search Help 1 of 2.jpg

Define ZPROD_ORD_SHLP Search Help refering ZPROD_ORD_SHLP dummy source and ZPROD_ORD_SHLP Seach Help Exit

 

Search Help 2 of 2.jpg

ABAP News for Release 7.50 - ABAP CDS Access Control

$
0
0

Imagine you have written a nice CDS view, e.g. as follows:

 

@AbapCatalog.sqlViewName: 'Z_T100_SABDEMOS'

define view z_t100_sabapdemos

  as select from t100

    { * }  where arbgb = 'SABAPDEMOS'

 

It should select all messages for a distinct message class SABAPDEMOS from database table t100. And of course it does that, as the following code snippet proves:

 

SELECT *

      FROM z_t100_sabapdemos

      INTO TABLE @DATA(result).

 

 

cl_demo_output=>display( result ).

 

access_cntrl1.gif

 

Now your're happy and ship your view, but ....

 

 

 

 

 

... someday you get an error message from a target system that users do not see all languages any more:

 

access_cntrl2.gif

... and some users do not see anything at all (sy-subrc = 4).

 

You logon to the system and examine the database access with SQL Trace (ST05) and find funny things:

 

access_cntrl3.gif

 

"DCL restrictions" what's that now???

 

You look at the properties of your view in that system and find in the Problems tab:

 

access_cntrl4.gif

 

Uh-huh.

 

  • You're view implicitly uses the default annotation @AccessControl.authorizationCheck: #CHECK.

    Documentation says "If Open SQL is used to access the view, an access control is carried out implicitly if a CDS role is assigned to the view."

  • And someone has created a CDS role in a DCL source code for your view!

 

You find it in ADT:

access_cntrl5.gif

 

@MappingRole: true

define role role_name {

  grant select on z_t100_sabapdemos

  where ( arbgb ) =  aspect pfcg_auth ( s_develop, objname,

                                                  objtype = 'MSAG',

                                                  actvt  = '03' )

                    and sprsl= 'E' ; }

 

What does that harmless looking code snippet do?

 

A CDS role adds an additional selection condition, a so called access condition, to a CDS view! If you access a CDS view that is mentioned in a role, Open SQL from ABAP 7.50 (and SADL Queries from ABAP 7.40, SP10) implicitly consider the access conditions defined in each role.

 

In our case:

 

  • A literal condition sprsl='E' restricts access to English only.

  • A so called PFCG condition aspect pfcg_auth ( s_develop ... ) connects the CDS role to a classical authorization object s_develop and from that the CDS access control runtime generates an access condition that evaluates the authorizations of the current user for that object. Here, a predefined aspectpfcg_auth connects the authorization field objname to the view field arbgb. Additionaly, the user's authorization is checked if it complies with fixed values for authorization fields objtype and actvt.

 

Neat!

 

If you write a view, you must be aware that this can happen. If you don't want any access restriction, you must decorate your view with the annotation AccessControl.authorizationCheck: #NOT_ALLOWED. Then and only then CDS roles are ignored.

 

But of course, CDS access control becomes part of your data modeling efforts from ABAP 7.50 on  ...

 

A first documentation is available, an improved one will follow soon.

 

PS: The CDS roles supported by ABAP CDS up to now are implicitly assigned to each user, so to say. User specific CDS roles are principally possible but not supported yet (those would involve selfdefined aspects). Instead, PFCG conditions offer a new implicit access to classical authorizations.

 

 

 

 

_

Create XML Files XSD-compliant exploiting SPROXY functionalities and XSLT transformation

$
0
0

In large enterprise environments, it happens more and more frequently to deal with XML files for a large variety of purposes. I won't list here the benefits of this self-explanatory files, since there are already many blogs around talking about this.

 

Speaking about concrete cases, in recent times SEPA pain formats within EU payment gave a huge boost into the adoption of XML for communication between partners and authorities. For them, the recommended technology to use, often with pre-build templates like CGI_XML_CT, are DMEE trees.

 

But what about other formats that are not related to payments or for which we cannot use the 'new' Payment Medium Workbench? It may happens that what we get is simply an XSD and we'd need to carefully adhere to it to produce a file. XSD normally are complex (to adapt to different situations) and strict (to minimize the errors at the destination), so building a file manually is definitely not recommended.

 

Investigating the possibilities within our ECC 6.05 environment, we targeted three possibilities:

- DOM/SAX technology to build in memory the tree and then render it into an output stream via iXML classes

- passing through SAP PI via an ABAP Proxy

- use SPROXY to automatically load the XSD into the system and generate the ABAP structures, as well as the XSLT transformation to be used in our program

 

In this BLOG, I will focus on the third option, since DOM/SAX technology require lots of custom code and the usage of PI may not be possible for all customers because:

- direct connection to the external partner is not possible/convenient

- we don't want to introduce an additional point of failure (PI is down, I cannot produce my file?)

- or simply we would like to give back the file to the user immediately for manual processing

 

Following are the main steps to achieve the result:

  • load the External Definition of the XSD on SAP PI and create an Outbound Service Interface in PI, using the new External Definition

ExternalDef PI.png

 

  • Generate the proxy in SPROXY on the backend

Service Interface Proxy.png

  • Take the generate XSLT transformation name and use it in your program to pass from the (automatically generated) ABAP structures to the XML

XSLT Transform.png

ABAP Code.png

  • OPTIONAL: Fine tune the parameters of the proxy to be compliant with your external partner specifications

Proxy Parameters.png

In case XSD is changed, you'll just need to re-upload it into PI (or SOAMANAGER), regenerate the proxy and adjust your ABAP code to fill out new fields.

 

That's all Folks

Innovative Method to Print Service Ticket Directly in CRM

$
0
0

Hey guys,


I have already got this articles published on SAPTechnical but I think it's time to get to through SAP Blog. So here it is.. Here is an innovative idea in CRM. Function Module that is being used in this article is not even there on Google. Do give that a try. As the title suggests this article is related to the printing of duplicate service ticket. This approach is somewhat different.


Steps are simple, all you have to do, is use the standard function module.


Steps

  1. We need a selection screen to get service ticket number as an input.
  2. We need to get the GUID of the entered ticket number
  3. Then, we will call a function module (Standard) to get all the table being used in the Smartform
  4. Finally, we will call the Smartform to give our desired output.


Details


We need a selection screen as shown below. Better make this select option as obligatory.

 


Now we need to declare some data which is required for calling Standard function module. Here is how you would do it.

 


Now we need to get the GUID of the service ticket, can be easily accessed from CRMD_ORDERADM_H (service ticket header table)




Now we will use this Function module to extract service ticket information. Only difference betweenCRM_TEMPLATE_SINGLE_READ and CRM_OUTPUT_SINGLE_READ is the name of the Smartform. All you need to give is the GUID of the ticket and you are done. For example:



This will give us all the data residing in service ticket form. Just a note: This function module can also be used to serve reporting purpose but for the time being I am using it as printing purpose.


Finally, I will call my smartform by passing all the tables and will print it.



This will give me output as follows.



This input will generate the following output.
































You are done with printing of service ticket without using standard T-cod i.e. CRMD_ORDER.


Summary


Printing of duplicate service ticket in CRM is requirement for almost every company. Standard transaction or WEBUI can’t serve this purpose. So, we need to find a way around that could fulfill our requirement.


At last: One did you know fact: SAP_CORBU theme is named after Le Corbusier, the Swiss architect?


How to detect conditional page break in Adobe Forms in master page using JS.

$
0
0

We need a global variable to change the value of it to use it as a flag to detect a the conditional break in java script.

While printing the table details we can detect the values of the current page and as well the previous page.

We will compare the field value of the present page with the previous page dynamically(while printing the output) and then we can change the global variable (flag) to detect the change in value.

 

 

Step 1 :  Click on 'EDIT' of adobe form layout as shown in the picture


step1.png


Step 2: Click on form properties and the below popup will be displayed.


step3.png


Step 3: Click on variables and the below screen will be displayed.


step4.png


Step 4: Click on '+' and add your variable and set a initial value as 1. I have taken brea here.

 

Step 5: Take a variable field in master page and open script editor of it. Please follow the below pseudo code to write the code. Write your code in 'Ready Layout' event in java script:

 

               A.          var thispage = xfa.layout.page(this);

                             var fields = xfa.layout.pageContent(thispage-2, "field");

 

               this will give the previous page values of the current page and put the required field value to  another variable 'lv1'.

              

               B.         var fields1 = xfa.layout.pageContent(thispage-1, "field");

              

               this will give the field value of the current page and put the required field value to another variable 'lv2'.

                   

               C.  Write the below code to change the flag value:

                   

                         if ( lv1 == lv2 )    // this will change the flag value as '1' if page break is not found

                              {

                                       var r = brea.value ;   

                                            r = 1 ;

                                       brea.value = String(r) ; // Here you can do your calculations

                               }

 

 

 

                         if ( lv1 != lv2)   // this will change the flag value as '0' if page break is found

                              {

                                  var zero = 0;

                                  brea.value = String(zero) ;

                              }

 

        this.rawValue = brea.value ;  // this will display the flag value in the screen



  • As the table field in the form, one page consists same value in the case of conditional page break. At the changing point of that particular table field value the flag will be 0.
  • You can use it to display subtotals, changing icons based on item category, display or hide according to the main table field value etc.
  • Else condition was not working while writing this java script code. Please like if it is helpful and mention improvement comments on it.

A primer on implementing DSLs in ABAP

$
0
0

I've been interested in programming languages implementation techniques and domain specific languages for quite some time now and also implemented a few DSLs - but not yet in ABAP. Browsing around a bit I did not find much about implementing DSLs in ABAP, one notable exception being Use Macros. Use Them Wisely, which shows a way of writing an internal DSL in ABAP.

 

With this post I'd like to propose a receipt for implementing an external DSL in ABAP. I will provide some helper classes as well, but my main intention is to show how a DSL can be parsed without too much effort in pure ABAP. I think that the main obstacle with DSLs is to get started and find an approach that works.

 

The process of parsing a DSL can be broken down into a sequence of rather small steps. So let's get started...

 

As example we'll create parser for a list of dates and date ranges. For example, our parser shall be able to transform an input string

 

     2015-01-01 - 2015-01-15, 2015-01-20, 2015-10-10 - 2015-10-12

 

into an internal table of (start date, end date) entries. This is not the most practical DSL on earth, for sure, but allows us to explore the stepwise implementation of a DSL parser without getting caught up in too many details.

 

 

Step 1 - Define an EBNF grammar for your DSL

 

The first step is to come up with an EBNF grammar for our DSL.

 

date-list = date-entry { "," date-entry }

date-entry = date [ "-" date]

date = "\d\d\d\d-\d\d?-\d\d?"

 

A date-list is a non-empty, comma-separated list of date-entries.

A date-entry is either a single date or a date range.

A date is described as a regular expression which can be used for matching date strings.

 

If your are not familiar with grammars described in EBNF or a similar form I suggest you google a bit and take a look at different grammars. The exact syntax you choose for describing the grammar is not that important.

 

In the following steps I'll use the following terms:

  • a terminal is a symbol given by a regular expression
    • if the terminal is given by "name = regex" then I call it a named terminal
    • if the terminal is just a regex (like "," and "-" above) then I call it an unnamed terminal
  • a non-terminal is a symbol defined by a production rule which contains the non-terminal on the left-hand side and terminals, non-terminals and EBNF-symbols on the right-hand side

 

 

Step 2 - Check out the DSL toolkit

 

For getting up and running with implementing a DSL, check out the include source code in ZDSLTK_CORE_1.abap in the source folder of my DSL toolkit GitHub repository. Create the include in your test system and create a report for the upcoming implementation. In the report, include ZDSLTK_CORE_1.

 

We'll talk about the local classes in the include in the following steps.

 

 

Step 3 - Define a custom node class for your DSL

 

The include ZDSLTK_CORE_1 contains the definition of a node class lcl_node. Each node will have a token type ID (attribute mv_token_id). The token types depend on the concrete DSL. In our case, we need token type ids for the date-list, date-entry and date (non-)terminals.

 

Therefore I create a sub-class lcl_dates_node of lcl_node and define constants as follows:

 

CLASS lcl_dates_node DEFINITION INHERITING FROM lcl_node.

 

  PUBLIC SECTION.

 

    CONSTANTS: BEGIN OF gc_token_type,

                 date_list  TYPE lif_dsltk_types=>mvt_token_id VALUE 1,

                 date_entry TYPE lif_dsltk_types=>mvt_token_id VALUE 2,

                 date       TYPE lif_dsltk_types=>mvt_token_id VALUE 3,

               END OF gc_token_type.

 

ENDCLASS.

 

The rule is: create an own token type for each non-terminal and for each named terminal of your grammar.

 

 

Step 4 - Create a parser sub-class

 

Our small DSL toolkit contains an abstract parser class lcl_parser. In this and all of the following steps we will create a concrete sub-class, which implements our parsing logic.

 

Create a sub-class lcl_dates_parser of lcl_parser. Redefine the protected methods parse_input and create_node with (for now) empty implementations.

 

Now implement create_node to return a new instance of lcl_dates_node, passing on all parameters to the constructor. create_node serves as a factory method for node objects. Since we neither redefined any method nor added any member attributes in our own sub-class this is somewhat lame in this example.

 

After this step my parser class looks like this:

 

CLASS lcl_dates_parser DEFINITION INHERITING FROM lcl_parser.

  PROTECTED SECTION.

    METHODS: parse_input REDEFINITION,

             create_node REDEFINITION.

ENDCLASS.

 

 

CLASS lcl_dates_parser IMPLEMENTATION.

 

  METHOD parse_input.

 

  ENDMETHOD.

 

  METHOD create_node.

    ro_node = NEW lcl_dates_node(

        iv_token_id = iv_token_id

        iv_token    = iv_token

        is_code_pos = is_code_pos ).

  ENDMETHOD.

 

ENDCLASS.

 

 

Step 5 - Implement read methods for the terminals

 

The parser class lcl_parser provides a protected method read_token which we'll use to create individual read_... methods for our terminal tokens.

 

Step 5.1: For each unnamed terminal X create a new private method read_X without parameters in your parser class, but delcare lcx_parsing as exception. Implement the method by calling read_token with the regular expression that shall be used for matching the unnamed terminal.

 

Step 5.2: For each named terminal Y create a new private method read_Y in your parser class that returns a lcl_node object and may raise lcx_parsing. Implement the method by calling read_token with the following parameters:

- iv_regex = the regular expression to use for matching the token, including parts of the regex enclosed in (...) to extract the token text

- iv_token_id = the token ID that shall be used for the created node object

- iv_token_text = the text that shall be used for instantiating a lcx_parsing in case that the requested token cannot be parsed

 

For our dates parser this looks as follows:

 

CLASS lcl_dates_parser DEFINITION INHERITING FROM lcl_parser.

    ...

  PRIVATE SECTION.

    METHODS:

      read_comma  RAISING lcx_parsing,

      read_dash   RAISING lcx_parsing,

      read_date   RETURNING VALUE(ro_node) TYPE mrt_node RAISING lcx_parsing.

ENDCLASS.

 

 

CLASS lcl_dates_parser IMPLEMENTATION.

   ...

  METHOD read_comma.

    read_token( ',' ).

  ENDMETHOD.

 

  METHOD read_dash.

    read_token( '-' ).

  ENDMETHOD.

 

  METHOD read_date.

    ro_node = read_token(

              iv_regex      = '(\d\d\d\d-\d\d?-\d\d?)'

              iv_token_id   = lcl_dates_node=>gc_token_type-date

              iv_token_text = 'date' ).

  ENDMETHOD.

ENDCLASS.

 

 

Step 6 - Implement parse methods for the non-terminals

 

In the previous step we created methods for reading individual non-terminals. Now we'll put these parts together and create methods for parsing larger parts of input. We use the production rules of the grammar for structuring this: For each non-terminal X we create a new private method parse_X that returns a lcl_node object and may raise a lcx_parsing (i.e. the same signature as we used for the read methods for named terminals):

 

METHODS:

  parse_date_list   RETURNING VALUE(ro_node) TYPE mrt_node RAISING lcx_parsing,

  parse_date_entry  RETURNING VALUE(ro_node) TYPE mrt_node RAISING lcx_parsing.

In the method implementations we do the following steps:

1. Create a new node object

2. Use the left-hand side of the production rule to invoke other parse_... or read_... methods

 

  METHOD parse_date_entry.

    " Production rule:

    " date-entry = date [ "-" date]


    ro_node = create_node( lcl_dates_node=>gc_token_type-date_entry ).


    DATA(lo_date_1) = read_date( ).


    ro_node->add_child( lo_date_1 ).

 

    TRY.

        push_offsets( ).

        read_dash( ).

        DATA(lo_date_2) = read_date( ).

        ro_node->add_child( lo_date_2 ).

        reset_offsets( ).

      CATCH lcx_parsing.

        pop_offsets( ).

    ENDTRY.

 

  ENDMETHOD.

 

Here we see the right-hand side of the production rule disguised as "read_date() ... read_dash()... read_date()." The rest of the code is housekeeping:

- if we read a named terminal we add it as child to our new date-entry node

- the optional part of the production rule is enclosed in a TRY-CATCH block together with saving and resetting the current source code position correctly

 

  METHOD parse_date_list.

 

    " Production rule:

    " date-list = date-entry { "," date-entry }

 

    ro_node = create_node( lcl_dates_node=>gc_token_type-date_list ).

 

    DATA(lo_date_entry) = parse_date_entry( ).

    ro_node->add_child( lo_date_entry ).

 

    DO.

      TRY.

          push_offsets( ).

          read_comma( ).

          lo_date_entry = parse_date_entry( ).

          ro_node->add_child( lo_date_entry ).

          reset_offsets( ).

        CATCH lcx_parsing.

          pop_offsets( ).

          EXIT.

      ENDTRY.

    ENDDO.

 

  ENDMETHOD.

 

Here we see how a repetition can be implemented: we use a DO loop until parsing fails and we remember to update the source code position correcty.

 

Finally we can implement the redefined parse() method by just delegating to the parse_... method of our start symbol:

 

  METHOD parse_input.

    ro_node = parse_date_list( ).

  ENDMETHOD.

 

 

Now we've implemented our parser class fully, so it's time for a first tests.

 

START-OF-SELECTION.

 

  DATA(go_parser) = NEW lcl_dates_parser( ).

 

  BREAK-POINT.

 

  DATA(lt_error_messages) = go_parser->parse( it_input = VALUE #(

      ( ` 2015-01-01 - 2015-01-15, 2015-01-20, 2015-10-10 ` )

      ( `      - 2015-10-12  ` )

  ) ).

 

  LOOP AT lt_error_messages INTO DATA(ls_msg).

    WRITE: /, ls_msg.

  ENDLOOP.

 

  BREAK-POINT.

 

If you take a look at go_parser->mo_root_node in the debugger at the second break-point you'll see that the parser indeed created a parse tree.

 

 

Step 7 - Decide what to do with the parse tree

 

After the last step our parser is indeed finished and we could work with the created parse tree. However, for lots of applications we really don't need the full parse tree, but can do our actions inside the parse methods.

 

In our running example we could e.g. add a sorted table of (start date, end date) entries to our parser class lcl_dates_parser and populate it directly in either the parse_date_list method or in the parse_date_entry_method. If parsing finished without errors we could then retrieve this table and work with this able afterwards instead of passing around the parse tree.

 

 

 

I think we covered a lot of ground in this post, although there is more to be done: we could (and should!) add more syntax checking (2015-13-01 should be recognized as illegal input, for example).

 

I hope you enjoyed our tour into parsing techniques! Let me know your thoughts on DSLs in ABAP. I'm looking forward to some interesting discussions with you.

 

I wish you all merry Christmas and a happy new year!

Merry, Happy, etc.

$
0
0



cl_demo_output=>display(

  |\n\n\n{ REDUCE string( LET t = replace( val =  REDUCE string(

  INIT l = replace( val = replace( val =  replace( val = replace(

  val = REDUCE string( LET x = cl_abap_random_int=>create( seed =

  CONV i( sy-uzeit ) min = 1 max = 1999 ) IN INIT h = repeat( val

  = ` ` occ = 2000 ) FOR k = 1 UNTIL k > 500 NEXT h = replace( val

  = h off = x->get_next( ) len = 1 with = COND string( WHEN k / 2

  = ( k - 1 ) / 2 THEN `*` ELSE `+` ) ) ) sub = `**` with = `* `

  occ = 0 ) sub = `*+` with = `* ` occ = 0 ) sub = `+*` with = `+ `

  occ = 0 ) sub = `++` with = `+ ` occ = 0 ) FOR j = 0 UNTIL j > 2

  NEXT l = replace( val = l off = 800 + j * 100 + 30 len = 40 with

  = repeat( val = ` ` occ = 40 ) ) ) off = 930 len = 40 with = |{

 

 

                      `Seasons Greetings!`

 

 

  WIDTH = 40 ALIGN = CENTER }| ) IN INIT s = `` FOR i = 0 UNTIL i >

  19 NEXT s = s && substring( val = t off = i * 100  len = 100 ) &&

  |\n| ) }| ).

 

 

 

(Statement can be executed on ABAP 7.40, SP08 and higher)

Regex – May the force be with you.

$
0
0

Regular expressions is an often discussed technique. Is it a technique which makes your coding more readable or does it add unnecessary complexity to your software project?
Recently I was again facing the decision to introduce regex functionality in an project or not. Here I want to describe my thougths, my experience with the provided functionality in ABAP and invite you to share your option.

 

So whats the answer: Regular expression is a mandatory key functionality in processing strings or an technique to be avoided as it adds not needed functionality to your projects. Is there a general answer or as in many other cases “it depends on” ...

Lets see.

First I want to define three common requirements for discussing the facets of regular expressions:

 

Requirement 1:  A product code should be checked whether it starts with the country code ‘AT’ or not?

Requirement 2: An user  types his or her surname which should be validated before it is processed.
Requirement  3: A product code should be checked which consists of a two character country code + a 5-digit product number and the product number should be extracted for further processing.

 

 

Requirement 1: :  A product code should be checked whether it starts with the country code ‘AT’ or not?

 

A possible solution with standard ABAP string functions for the input lv_input would be

 

IF lv_input CP ‘AT*’.
MESSAGE ‘String starts with AT’.
ELSE.
MESSAGE ‘String does not start with AT’

 

Rather straightforward. So let’s see what an equal solution with regex looks like.

 

FIND REGEX ‘^AT’ IN lv_input.
IF sy-subr  =  0.
MESSAGE ‘String starts with AT’ TYPE ‘I’.
ELSE.
MESSAGE ‘String does not start with AT’ TYPE ‘E’.

 

A great thing about ABAP is, that it features regular expressions directly in the standard notation, so you have not to include objects or libraries like in other programming languages. Of course there are some already defined classes like CL_ABAP_STRING_UTILITES but in this reading I will keep them for simplicity aside.

But regardless the simplicity of the regex solution, if we compare the two solutions the first one needs one line less and is more readable as the string evaluation can be directly used in the if statement.

Thus we come to finding 1:


Finding 1: For simple text comparison the standard text comparison function are more readable and regex functionality adds unnecessary complexity.

Finding 2: In ABAP regular expressions are included in the standard language features and can be used especially with the FIND and REPLACE functions.


(Sidenote: Of course also in this simple example further checks would be useful as for example AT would be a valid input, but lets leave this aspect beside for the moment.

 

Requirement 2: An user  types his or her surname which should be validated before it is processed.

To check if a given text is a valid surename some text comparisions are neede (unless you find an existing object/function (which I do not know in ABAP) which does the work for you. So with text functions we could for example do the following checks:

 

IF lv_input NA SY-ABCDE.  
     MESSAGE ‘String contains no upper case character’ TYPE ‘E’.
IF lv_input(1) NA SY-ABCDE.  
     MESSAGE ‘String does not start with an upper case character’  TYPE ‘E’.

lv_input_uppercase = lv_input.
translate lv_input_uppercase TO UPPERCASE.
IF lv_input NC SY-ABCDE.  
     MESSAGE ‘String contains invalid characters’ TYPE ‘E’.
... and so on

 

An equal solution with regular expressions would be for example:

 

FIND REGEX ‘^[A-Z]([a-z]*)$’ IN lv_input.
IF sy-subr  =  0.
     MESSAGE ‘String is a valid surname’.
ELSE.
     MESSAGE ‘String is not a valid surname’ TYPE ‘E’.

 

I will not explain in details what the regex ‘^[A-Z]([a-z]*)$’ does. There are a lot of tools like the ABAP Regex Toy (DEMO_REGEX_TOY) or Internet tools like https://regex101.com/ which provide an much more complete explanation than I could give you here.

Comparing the above given solution some interesting things can be seen. First the regex-part is now much shorter. Of course the regular expression is not self explaining and if you are not used to it needs some thinking. But the whole validation keeps on one place. An difference in the output is, that with the regex solution the user does not get specific messages. Lets summarize this in two findings.

 

Finding 3: As the string parsing gets more complex, regular expressions provide a method to keep the validation on one point and avoid a lot of conditional statements.

 

Finding 4: If specific validation messages are needed string functions may be better than a consolidated regular expression.

 

Requirement 3: A product code should be checked which consists of a two character country code + a 5-digit product number +  a color specification (eg. AT12345-red) and the product number should be extracted for further processing.

The validation for this requirement is quite similar to the requirement 1.

You could check the first two characters for uppercase characters  or compare them to predefined company codes. Then check the next 5 characters for a valid number. Ensure that the – is on the correct place and the last characters correspond to a valid color.
Afterwards you could extract all characters beginning with the 3rd until the ‘-’ character.
Straightforward to code, roughly speaking 5 if-statements or equal expressions should do the work.

The regular expression solution could look like the following:

 

FIND REGEX ‘^[A-Z]{2}([1-9]{5})-[a-z]*$’ IN lv_input
SUBMATCHES lv_product_number.
IF sy-subr  =  0.
CONCATENATE ‘Product number found: ’ lv_product_number INTO lv_message_text.
     MESSAGE  lv_message_text  TYPE ‘I’.

 

So the amount of coding remains nearly the same regardingless the requirement is much complex. Well, the regex expression is already complicated and needs some time to understand. Again, for a detailed description of the regex use the above mentioned tools. This I want to summarize in the last finding.

A big advantage is that validating and extracting can be done together with using the round brackets().
This can also be used to replace parts of the string with the ABAP standard function replace REGEX ‘’...

 

Finding 5: if a string should be validated and processed regular expressions it is possible to perform validation of a string and extract or replace one or more substring with one operation.

To summarize my findings:

 

Regular expressions are a powerful tool which can solve a lot of requirements regarding text validation, processing and replacing.
Simple checks are almost always better done with standard text functions. Regular expression functions performed on medium complex requirements can save you a lot of typing effort and make your code more compact but adds also some not easy understandable regular expressions.
When the requirements get more complex a solution where the whole logic is bundled in on regular expression could leave to a quite hard understandable and maintainable code. In this cases a split into more regular expressions or a combination between regex and standard string functions is advisable.
Regular expressions should in my opinion always be considered for medium complex text operations as they combine when desired text validation, extraction and substitution. And especially ABAP supports them very good as they can be used with standard functions FIND and REPLACE but also with predefined classes like CL_ABAP_MATCHER or CL_ABAP_REGEX and so on.

 

I hope you enjoyed my reading and I appreciate feedback, ratings, corrections, your comments, different opinions or additional experience.

 

May the force of regex be with you

Translation of Copyright texts during logon

$
0
0

I came across a problem where the copyright pop up message during logon is appearing as English with logon in Chinese language.

 

After investing lot of time, found below way to translate those texts.

 

1.jpg

 

These entries are stored in Cluster Table "DOKTL". Based on the contents identify the documentation object from table "DOKTL".

 

1.jpg

 

Documentation object in this case is "COPYRIGHT_ORACLE". Now to translate these entries go to transaction SE63.


Go to ABAP Objects --> Long texts (Documentation) --> C6 F1 Help --> TX   General Texts

1.jpg

1.jpg 

Enter object name as below.


1.jpg


Translate the text as shown below --> Save --> Activate.

 

1.jpg

Now Login with ZH language, you will find the pop up message as translated above.


1.jpg

Viewing all 948 articles
Browse latest View live


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