Friday, 14 June 2024

Upload and modify OpenXML documents via RAP App in SAP BTP ABAP Environment

Introduction


In this blog post, I would like to share some insights for generating OpenXML documents by use of the RESTful Application Programming Model with Cloud-released development objects. With this app, you will be able to upload .docx templates and fill them with information from you CDS view (could be used for generating invoices, business documents and so on...).

Prerequisite


  • SAP BTP ABAP environment or an S/4 system to your disposal.  
  • Eclipse IDE installed on your local machine with the ABAP Development Tools.

Shortcut with abapGit:


For those who are using abapGit, feel free to check out the code from my GitHub repo!

Step 1:

Create a database table with the following config (hint: do not forget to generate custom domains for mime type and attachment type.

@EndUserText.label : 'Invoice for document generation'
@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zmri_invoice {

  key client            : abap.clnt not null;
  key invoice           : ebeln not null;
  comments              : abap.char(30);
  attachment            : zmriattachment;
  mimetype              : zmimetype;
  filename              : zfilename;
  purchaseorder         : abap.char(30);
  price                 : abap.dec(8,2);
  local_created_by      : abp_creation_user;
  local_created_at      : abp_creation_tstmpl;
  local_last_changed_by : abp_locinst_lastchange_user;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
  last_changed_at       : abp_lastchange_tstmpl;

}
 
Step 2: 

Generate ABAP repository objects by right-clicking on the previously created database table. 
Choose the ABAP RESTful Application Programming Model (OData UI Service) variant.

Upload and modify OpenXML documents via RAP App in SAP BTP ABAP Environment

Step 3

Create a custom action in your Behavior Definition: 

managed implementation in class ZBP_R_MRI_INVOICE unique;
strict ( 2 );
with draft;
define behavior for ZR_MRI_INVOICE alias ZrMriInvoice
persistent table ZMRI_INVOICE
draft table ZMRI_INVOIC000_D
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master( global )

{
  field ( mandatory : create )
   Invoice;

  field ( readonly )
   LocalCreatedBy,
   LocalCreatedAt,
   LocalLastChangedBy,
   LocalLastChangedAt,
   LastChangedAt;

  field ( readonly : update )
   Invoice;


  create;
  update;
  delete;

  action ( features : global ) createMSWordInvoice ;

  draft action Activate optimized;
  draft action Discard;
  draft action Edit;
  draft action Resume;
  draft determine action Prepare;

  mapping for ZMRI_INVOICE
  {
    Invoice = invoice;
    Comments = comments;
    Attachment = attachment;
    Mimetype = mimetype;
    Filename = filename;
    PurchaseOrder = purchaseOrder;
    Price = price;
    LocalCreatedBy = local_created_by;
    LocalCreatedAt = local_created_at;
    LocalLastChangedBy = local_last_changed_by;
    LocalLastChangedAt = local_last_changed_at;
    LastChangedAt = last_changed_at;
  }
}
 
Step 4:

Implement the custom action in your behaviour implementation class:

CLASS lhc_zr_mri_invoice DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.

    CLASS-DATA: mt_data TYPE zmri_invoice.

    DATA:
          lv_content   TYPE xstring,
          lo_zip       TYPE REF TO cl_abap_zip.

    METHODS:
      get_global_authorizations FOR GLOBAL AUTHORIZATION
        IMPORTING
        REQUEST requested_authorizations FOR ZrMriInvoice
        RESULT result,

      get_global_features FOR GLOBAL FEATURES
        IMPORTING
        REQUEST requested_features FOR ZrMriInvoice
        RESULT result,

      createMSWordInvoice FOR MODIFY
        IMPORTING keys FOR ACTION ZrMriInvoice~createMSWordInvoice.

ENDCLASS.

CLASS lhc_zr_mri_invoice IMPLEMENTATION.

  METHOD get_global_authorizations.
*  This method does not need an implementation
  ENDMETHOD.

  METHOD createMSWordInvoice.

*     Select document to be filled
    SELECT * FROM zc_mri_invoice
     FOR ALL ENTRIES IN @keys
     WHERE invoice      = @keys-invoice
     INTO CORRESPONDING FIELDS OF @MT_data.
    ENDSELECT.

*     Create zip class instance
    lo_zip = NEW cl_abap_zip( ).

*     Search for main document part
    DATA lv_index TYPE string VALUE 'word/document.xml'.

*     Load attachment into zip class object
    lo_zip->load( zip = mt_data-attachment check_header = abap_false ).

*     Fetch the binaries of the XML part in the attachment
    lo_zip->get(
    EXPORTING
      name   = lv_index
    IMPORTING
      content = lv_content
    ).


* Convert the binaries of the xml into a string
    DATA(lv_string) = xco_cp=>xstring( lv_content
      )->as_string( xco_cp_character=>code_page->utf_8
      )->value.

* Search for the text to be replaced and fill with the information in your data set
    REPLACE FIRST OCCURRENCE OF '<InvoiceNumber>' IN lv_string
    WITH mt_data-Invoice.

    REPLACE FIRST OCCURRENCE OF '<Purchase Order>' IN lv_string
    WITH mt_data-PurchaseOrder.

    REPLACE FIRST OCCURRENCE OF '<Comments>' IN lv_string
    WITH mt_data-Comments.

    DATA lv_price TYPE string.
    lv_price = mt_data-Price.

    REPLACE FIRST OCCURRENCE OF '<Price>' IN lv_string
    WITH lv_price.

* Convert the changed XML string into binaries
    DATA(lv_new_content) = xco_cp=>string( lv_string )->as_xstring( xco_cp_character=>code_page->utf_8
    )->value.

* Delete "old" main document part from the zip file
    lo_zip->delete(
    EXPORTING
      name   = lv_index
    ).

* Add "new" main document part to the zip file
    lo_zip->add(
    EXPORTING
      name   = lv_index
      content = lv_new_content
    ).

* Save the new zip file
    DATA(lv_new_file) = lo_zip->save( ).

* Upload changed docx file
    MODIFY ENTITIES OF zr_mri_invoice IN LOCAL MODE ENTITY ZrMriInvoice
      UPDATE SET FIELDS WITH
      VALUE #( (
          Invoice = mt_data-Invoice
          Attachment = lv_new_file
      )  )
    FAILED failed.

    APPEND VALUE #( %msg = new_message_with_text( severity = if_abap_behv_message=>severity-success text = 'Template successfully filled...' ) ) TO reported-ZrMriInvoice.
  ENDMETHOD.

  METHOD get_global_features.
* This method does not need to be implemented
  ENDMETHOD.


ENDCLASS.

Explanation: In this implementation class, the cl_abap_zip class (which is released for cloud development) is used to load the .docx template. If you didn't already know, a .docx file is basically a ZIP-file. In order to get the main document part, we need to fetch the "word/document.xml" file within the zip object by applying the lo_zip->get() method. This method returns an XSTRING which has to be converted into an UTF-8 encoded string, for us to modify the content of the main document part. For the binary-to-string conversions the xco_cp class is used, which is part of the XCO library (I can only recommend using this library as it has some great features). Afterwards the converted XML-string has to be modified with the invoice details from your CDS view. The modified XML-string has to be converted back into binaries, by use of the same class. The last step is to delete the "old" main document part from the zip object and add the new document content to the zip object. Now update the attachment field in your CDS entity and that's it. Your app should now be able to populate a .docx-file with information from your CDS view. 

Step 5:

Create a metadata extension and a service binding for your Fiori Frontend and test the application: 

@Metadata.layer: #CORE
@UI: { headerInfo: {
typeName: 'Invoice',
typeNamePlural: 'Invoices',
title: { type: #STANDARD, value: 'Invoice' },
         description: { type: #STANDARD, value: 'Invoice' } },
         presentationVariant: [{
         sortOrder: [{ by: 'Invoice', direction: #ASC }],
         visualizations: [{type: #AS_LINEITEM}] }] }
annotate view ZC_MRI_INVOICE with
{
  .facet: [    {
                label: 'General Information',
                id: 'GeneralInfo',
                type: #COLLECTION,
                position: 10
                },
                     { id:            'Invoicedet',
                    purpose:       #STANDARD,
                    type:          #IDENTIFICATION_REFERENCE,
                    label:         'Invoice Details',
                    parentId: 'GeneralInfo',
                    position:      10 } ]

  : { lineItem:       [ { position: 10, importance: #HIGH , label: 'Invoice Number'} ] ,
          identification: [ { position: 10 , label: 'Invoice Number' } ] }
  Invoice;
  : { lineItem:       [ { position: 20, importance: #HIGH , label: 'Purchase Order'} ] ,
           identification: [ { position: 20 , label: 'Purchase Order Number' } ] }
  PurchaseOrder;
  : { lineItem:       [ { position: 20, importance: #HIGH , label: 'Price'} ] ,
           identification: [ { position: 20 , label: 'Price' } ] }
  Price;
  : { lineItem:       [ { position: 20, importance: #HIGH , label: 'Comments'} ] ,
           identification: [ { position: 20 , label: 'Comments' } ] }
  Comments;
  :
  { lineItem:       [ { position: 30, importance: #HIGH , label: 'Attachment'}, { type: #FOR_ACTION, dataAction: 'createMSWordInvoice' , label: 'Create Invoice' } ], 
    identification: [ { position: 20 , label: 'Attachment' }] 
  }
  Attachment;

  .hidden: true
  MimeType;

  .hidden: true
  Filename;

}

Test the application by creating an entry and uploading a docx-template.

The template could look something like this: 

Upload and modify OpenXML documents via RAP App in SAP BTP ABAP Environment

Now select the entry and use the custom action to fill the template with your data: 

Upload and modify OpenXML documents via RAP App in SAP BTP ABAP Environment

After the success message, click on the attachment (template.docx) and verify that the placeholders have been filled: 

Upload and modify OpenXML documents via RAP App in SAP BTP ABAP Environment

Addition:

For those of you who want to populate tables in MS word, you can have a look at this blog post, which uses text and row fragments for placeholders in the docx-template.

Conclusion:

In this blog post, you learned how to upload and modify OpenXML documents by use of the cl_abap_zip class and the XCO library. This code is consists of cloud-released code only and should give you a good starting point to start handling your documents with a clean core approach.

No comments:

Post a Comment