SAP

SAPscript to PDF

I have a requirement to send SAPscript output as a PDF to SAP cFolders. The context is a transaction for which user exits exist in the form of a BAdI, and it’s already implemented for something else, so I can just latch onto that. The code consists mostly of checking for the right conditions under which the document is to be sent, but the actual sending is the interesting part.
In effect, we’re calling the print routine for that SAPscript. The print routine imports the data it needs from memory, so our code exports all necessary data to memory, including a new flag indicating that we’d like to send a PDF instead of whatever other output method is called for. We change the print routine to read this flag, and make changes at two points: first, call the open_form function with the field tdgetotf of structure itcpo set to ‘X’, and second, instead of the normal call to close_form, call it with the otfdata table supplied. At this point, it’s just a simple call to convert_otf to convert the SAPscript-data into PDF format, either as a string or as a table.
Now we have the PDF, and we can do with it what we want. It could be saved locally, or, in this case, the string is converted to a table (it’s a different format than the table that comes out of convert_otf) and a custom function sends the table to cFolders.
|

Rollback is Your Friend

We have an existing custom program that does an insert of an internal table into a custom database table, like this:
  INSERT ztable FROM TABLE it_table.
  COMMIT WORK.

Variable names are changed to protect the innocent. It short-dumped, for the obvious reason that there was a duplicate key somewhere. Now the short dump doesn’t really give any useful info, so I can’t figure out why there’s a duplicate key in the first place. The program’s not intended to update any existing records, just insert new ones, so catching that error and doing an update isn’t the solution. What I did was catch the exception, and then try to insert every line one by one, writing out the key fields for each, and marking the one for which the error occured. Like this:
  TRY.
    INSERT ztable FROM TABLE it_table.
  CATCH cx_sy_open_sql_db.
    FIELD-SYMBOLS: <ztab> TYPE ztable.
    FORMAT COLOR COL_KEY.
    WRITE: / text-006. "Duplicate key error for the following dataset:
    LOOP AT it_table ASSIGNING <ztab>.
      INSERT ztable FROM <ztab>.
      IF sy-subrc IS INITIAL.
        FORMAT COLOR COL_POSITIVE.
      ELSE.
        FORMAT COLOR COL_NEGATIVE.
      ENDIF.
  *   write the key fields to the report
    ENDLOOP.
    FORMAT COLOR OFF.
  ENDTRY.
  COMMIT WORK.

Nice. The program tends to process lots of data and can run a long time, so I want to keep the mass insert. But when it fails, it’ll write out the key fields of every record I’m currently trying to insert, with the good ones in green, and the bad ones in red. And whaddayaknow... it doesn’t work. Nope. In the debugger, I carefully craft an entry with identical key fields as one in the database table. This does trigger the exception, but the line-by-line insert goes over it as if nothing is wrong. It’s not updating the database for that one line, but it’s not reporting an error either.
The solution, after much thought, turned out to be simple: when the exception is raised, roll back the mass insert before doing the individual inserts:
  TRY.
    INSERT ztable FROM TABLE it_table.
  CATCH cx_sy_open_sql_db.
    ROLLBACK WORK.
  ....
Now the offending entry shows up in red. All we need to do now is wait for the error to occur again, and then go digging through the data to see why there’s a duplicate...
|

SAP Lolcat

SAP Lolcat
|

Really Simple ALV Report

I've posted about a generic ALV grid report class that I developed, but now I've found something so much better. There's actually a class from SAP that does the same thing, only better. If your SAP installation has class cl_salv_table, you can reduce a report to something as simple as this:
REPORT YALVTEST.
  DATA: l_alv TYPE REF TO cl_salv_table,
        l_exc TYPE REF TO cx_salv_msg,
        lt_bseg type STANDARD TABLE OF bseg.

  select * into TABLE lt_bseg up to 100 ROWS from bseg.
  TRY.
    cl_salv_table=>factory( IMPORTING r_salv_table = l_alv
                            CHANGING t_table = lt_bseg ).
    l_alv->display( ).
    CATCH cx_salv_msg INTO l_exc.
      MESSAGE l_exc TYPE 'I' DISPLAY LIKE 'E'.
  ENDTRY.
The program reads 100 lines from table bseg into an internal table and displays them on the screen in an ALV grid.
|

Update to Generic ALV Grid Report Class

A while back (seven months, to be precise), I posted an entry about a generic ALV Grid report class. It's proven very useful to me in many situations, even if it's not for a straight report. In on case, I called a BAPI which returned a table of messages. To present these messages to the user, I used the report class. Heck, the structure of the message list is already in the data dictionary, so it's just a cut and paste job of the sample code.

But there's an embarrassing bug in the class and example, for which I have a fix, but I didn't get around to posting it until now.

The bug is that the container never gets destroyed. As a consequence, when you flip back and forth between selection screen and report output, a new container gets added every time you execute the report. It gets very silly very quickly. The solution is to destroy the container whenever we leave the display of the table. Here's how:

First, add a method to the class:

method FREE .
   CALL METHOD g_container->free.
endmethod.

Next, call this method before you leave the screen. Modify module user_command_9000 thusly:

module USER_COMMAND_9000 input.
   case sy-ucomm.
     when 'BACK'.
       set screen 0.
       CALL METHOD g_report->free.
       leave screen.
     when 'EXIT'.
       set screen 0.
       CALL METHOD g_report->free.
       leave screen.
     when 'CANCEL'.
       set screen 0.
       CALL METHOD g_report->free.
       leave screen.
   endcase.
endmodule. " USER_COMMAND_9000 INPUT


Aaaand... that's it. Now every time we move from the grid display to the selection screen, the container is destroyed, and when the report is re-run, a fresh, new container is created. Beautiful!
|

A Generic ALV Grid Report Class

If you’ve ever used ABAP Objects to create ALV Grid reports, you will have noticed how the same code keeps coming up from one report to another. It doesn’t have to be this way. Here’s a report class that reduces your report to simply gathering the data and instantiating an object.
Let’s build the class, and then show a little example report.

Building The Class



In transaction se24 (the Class Builder), create a class zcl_report. Give it the following attributes:
g_title1 type sytitle
g_grid type ref to cl_gui_alv_grid
gt_fieldcat type lvc_t_fcat
g_variant type disvariant
g_layout type lvc_s_layo
g_print type lvc_s_prnt

These are all private instance variables that’ll be used later on. The g_title1 attribute is used in the handler for the top-of-page event. In the example, we’ll use a customer-specific function module to write the top of page, which uses this attribute for the report title. Obviously, in your environment, you should use as many or as few title fields as needed.

Next up, the constructor. It should have the following interface:

Import parameters:
i_title1 type c optional
i_structure type dd02l-tabname
i_repid type syrepid
i_dynnr type sydynnr

Exceptions:
container_error
grid_error
field_catalog_error

The code of the constructor looks like this:
data: l_container type ref to cl_gui_docking_container.

* Store the report title
g_title1 = i_title1.
* Store the program name
g_variant-report = i_repid.
* Set some lay-out and print-related flags
g_layout-smalltitle = 'X'. "Default grid title to small font
g_layout-zebra = 'X'. "Output rows with alternating colors
g_layout-no_author = 'X'. "Allow users to enter global layouts
g_layout-cwidth_opt = 'X'. "Optimize column width
g_print-prnt_title = 'X'. "Do not print out grid title

* Create a docking container covering 95% of the screen.
* This container will hold the grid, and 95% is the maximum
* allowed size.
IF cl_gui_alv_grid=>offline( ) IS INITIAL.
CREATE OBJECT l_container
EXPORTING
repid = i_repid
dynnr = i_dynnr
ratio = 95
EXCEPTIONS
cntl_error = 1
cntl_system_error = 2
create_error = 3
lifetime_error = 4
lifetime_dynpro_dynpro_link = 5
OTHERS = 6
.
IF sy-subrc <> 0.
RAISE container_error.
ENDIF.
ENDIF.

* Create the ALV Grid object
CREATE OBJECT g_grid
EXPORTING
i_parent = l_container
EXCEPTIONS
error_cntl_create = 1
error_cntl_init = 2
error_cntl_link = 3
error_dp_create = 4
OTHERS = 5
.
IF sy-subrc <> 0.
RAISE grid_error.
ENDIF.

* Handle events
SET HANDLER me->handle_print_top_of_page FOR g_grid.

* Retrieve field specifications and descriptions for ALV Grid
CALL FUNCTION 'LVC_FIELDCATALOG_MERGE'
EXPORTING
i_structure_name = i_structure
CHANGING
ct_fieldcat = gt_fieldcat
EXCEPTIONS
inconsistent_interface = 1
program_error = 2
OTHERS = 3
.
IF sy-subrc <> 0.
RAISE field_catalog_error.
ENDIF.

That’s all there is to it. We need another method to send the data table to the grid. Let’s call it display. It’s a public instance method.

Changing parameters:
et_datatable type standard table

Exceptions:
display_error

The code is one simple method call:
call method g_grid->set_table_for_first_display
exporting
i_save = 'A'
is_layout = g_layout
is_variant = g_variant
is_print = g_print
changing
it_outtab = et_datatable
it_fieldcatalog = gt_fieldcat
exceptions
invalid_parameter_combination = 1
program_error = 2
too_many_lines = 3
others = 4
.
if sy-subrc ne 0.
raise display_error.
endif.

The last method we need is the handler for the top-of-page event. It gets called when you print the report. Create a method called handle_print_top_of_page, and in the detail view, set it to be the event handler for event print_top_of_page of class cl_gui_alv_grid. You can make the method a private instance method. The code is simple:
CALL FUNCTION 'Z_STANDARD_HEADING'
EXPORTING
i_title = g_title1
.

As I mentioned before, this is a customer-specific function that does nothing but write a few lines with such gems as user name, date and time stamp, program name, system and client, name of the report, etc. You can leave it out, or replace it with your own.

That concludes the creation of the class. Go ahead and activate it. We’ll do the example next.

An Example of The Class in Action



For the example, we’ll select some fields from table bseg for the first 100 rows, and display those.

The first step is to go to the data dictionary and create a structure with the fields we want to display. In our case, we could just use bseg, but what’s the fun in that? So off we go to transaction se11, and we create a structure called yyexample. It’s generally a good idea to create the structure with the same name as you’ll use for your program, but you can’t have an underscore in the second or third position of a structure. For our purposes, add fields bukrs, belnr, gjahr, buzei, and buzid from table bseg into the structure, and activate it.

Step two is to create the program. Go to transaction se38 (or se80, if you’re so inclined), and create a new program called y_example. I’ll go over the code bit by bit.

First, select your data:
data: gt_datatable type standard table of yyexample.
select bukrs belnr gjahr buzei buzid from bseg
into table gt_datatable up to 100 rows.

Obviously, in a real report, the whole data selection would be more complicated, but this’ll do for our purposes.

Second, create an object:
data: g_report type ref to zcl_report,
g_repid type syrepid.
* We need a helper field, because the value of sy-repid changes
* as you enter the constructor of the class.
g_repid = sy-repid.
CREATE OBJECT g_report
EXPORTING
I_TITLE1 = 'Sample Report'
i_structure = 'YYEXAMPLE' “Name of the DDIC structure
i_repid = g_repid
i_dynnr = '9000'
EXCEPTIONS
CONTAINER_ERROR = 1
GRID_ERROR = 2
FIELD_CATALOG_ERROR = 3
others = 4
.
IF sy-subrc <> 0.
* Do some error handling here
ENDIF.

Now we can tell the object what our data table is, and call a screen to display it:
CALL METHOD g_report->display
CHANGING
et_datatable = gt_datatable
EXCEPTIONS
DISPLAY_ERROR = 1
others = 2
.
IF sy-subrc <> 0.
* Do some error handling here
ENDIF.
call screen 9000.

So we end up calling screen 9000, but we don’t have a screen 9000, do we? Let’s double-click the screen number to create it. In the flow logic of the created screen, un-comment the two modules. The flow logic should look like this:
PROCESS BEFORE OUTPUT.
MODULE STATUS_9000.
*
PROCESS AFTER INPUT.
MODULE USER_COMMAND_9000.

Create the modules by double-clicking on them. Their contents should be:
module STATUS_9000 output.
set pf-status 'STATUS'.
set titlebar 'TITLEBAR'.
endmodule. " STATUS_9000 OUTPUT
module USER_COMMAND_9000 input.
case sy-ucomm.
when 'BACK'.
set screen 0.
leave screen.
when 'EXIT'.
set screen 0.
leave screen.
when 'CANCEL'.
set screen 0.
leave screen.
endcase.
endmodule. " USER_COMMAND_9000 INPUT

Double-click the word status, and enter the three codes back, exit, and cancel for the three navigation buttons (arrow on green circle, arrow on yellow circle, and x on red circle, respectively). Finally, double-click the word titlebar to create a title. This title will appear at the top of the screen in the title bar. Activate the status, the screen, and the program. That’s it. You can now run the example report and admire it in all its glory.

Some Final Thoughts


What we’ve ended up with is a generic ALV Grid report. Programs that use it become much simpler, and need less testing. They also use a common look, with common functionality, and use common headers when printed. Another advantage is that as the class gets refined and further developed, all the programs that use it immediately use the updated version.

What improvements could be made to the class? For one thing, we could add some more event handlers. Events such as print_top_of_list, print_end_of_page, and print_end_of_list could hold common code for displaying program parameters or whatever else is the standard in your company.

There are times when you need a report with functionality that you can’t abstract out so that every report has it. Drill-downs, for example. You want drill-down capability in one report, but not in all. The path for that is to create a subclass of zcl_report. That way, you can have your custom functions, but still reap the benefit of the common framework of the class.

One advantage of creating a structure in the data dictionary is that the presentation is further removed from the data selection. In the data dictionary, you can create data elements with the exact descriptive labels you need. As the user resizes the columns, the right length label is inserted in the column header.

Finally, I know, the sample report has no selection screen. That’s hardly a representative situation. I can’t imagine a report in a production environment where you would not want to restrict your data selection. But the example program isn’t meant to be a production program.

Enjoy.
|