Generation de PDF dans ABAP Cloud

Catégorie
Integration
Publié
Auteur
Johannes

La generation de PDF dans ABAP Cloud differe fondamentalement de l’environnement On-Premise. Les Adobe Forms classiques (SFP) et SmartForms ne sont pas disponibles sur SAP BTP. A la place, vous utilisez des alternatives Cloud-native comme XSL-FO, le SAP Forms Service by Adobe ou des services PDF externes.

Apercu des options disponibles

Dans ABAP Cloud, il existe plusieurs methodes pour generer des PDF :

OptionDescriptionCas d’utilisation
XSL-FOTransformation XML vers PDFDocuments simples a moyens
SAP Forms Service by AdobeService Adobe base sur le CloudFormulaires complexes avec templates Adobe
Services PDF externesAPIs PDF basees sur HTTPFlexibilite, exigences speciales
SAP Document ManagementIntegration avec DMSWorkflows documentaires

Vue d’ensemble de l’architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ Generation de PDF dans ABAP Cloud │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ RAP Business Object / Service │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ RAP Action │ │ │
│ │ │ "Generate PDF" │ │ │
│ │ └──────────┬──────────┘ │ │
│ └─────────────┼────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Options de generation PDF │ │
│ │ │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │
│ │ │ XSL-FO │ │ SAP Forms │ │ APIs externes │ │ │
│ │ │ │ │ Service │ │ (Services PDF) │ │ │
│ │ │ - XML + XSLT │ │ - Adobe Templates│ │ - REST APIs │ │ │
│ │ │ - cl_xslt_proc │ │ - BTP Service │ │ - HTTP Client │ │ │
│ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Sortie PDF │ │
│ │ - Telechargement via RAP Action Result │ │
│ │ - Piece jointe au Business Object │ │
│ │ - Envoi par email │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

XSL-FO : Generation PDF basee sur XML

XSL-FO (Extensible Stylesheet Language Formatting Objects) est une technique standard W3C pour la transformation XML vers des documents pagines comme les PDF.

Principe de base

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Donnees │ + │ Feuille de │ = │ PDF │
│ XML │ │ style XSL-FO│ │ │
└──────────────┘ └──────────────┘ └──────────────┘

Structure de donnees XML

D’abord, vous definissez une structure XML pour vos donnees de document :

" Generer les donnees XML pour la facture
CLASS zcl_invoice_xml_generator DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_invoice_item,
position TYPE i,
description TYPE string,
quantity TYPE i,
unit_price TYPE p LENGTH 10 DECIMALS 2,
total TYPE p LENGTH 10 DECIMALS 2,
END OF ty_invoice_item,
ty_invoice_items TYPE STANDARD TABLE OF ty_invoice_item WITH KEY position.
TYPES:
BEGIN OF ty_invoice,
invoice_number TYPE string,
invoice_date TYPE d,
customer_name TYPE string,
customer_addr TYPE string,
items TYPE ty_invoice_items,
subtotal TYPE p LENGTH 10 DECIMALS 2,
tax TYPE p LENGTH 10 DECIMALS 2,
total TYPE p LENGTH 10 DECIMALS 2,
END OF ty_invoice.
METHODS:
generate_xml
IMPORTING is_invoice TYPE ty_invoice
RETURNING VALUE(rv_xml) TYPE xstring.
ENDCLASS.
CLASS zcl_invoice_xml_generator IMPLEMENTATION.
METHOD generate_xml.
" Construire le document XML
DATA(lo_doc) = cl_ixml=>create( )->create_document( ).
" Element racine
DATA(lo_root) = lo_doc->create_element( name = 'Invoice' ).
lo_doc->append_child( lo_root ).
" Elements d'en-tete
DATA(lo_header) = lo_doc->create_element( name = 'Header' ).
lo_root->append_child( lo_header ).
DATA(lo_inv_no) = lo_doc->create_element( name = 'InvoiceNumber' ).
lo_inv_no->set_value( is_invoice-invoice_number ).
lo_header->append_child( lo_inv_no ).
DATA(lo_date) = lo_doc->create_element( name = 'InvoiceDate' ).
lo_date->set_value( |{ is_invoice-invoice_date DATE = USER }| ).
lo_header->append_child( lo_date ).
" Donnees client
DATA(lo_customer) = lo_doc->create_element( name = 'Customer' ).
lo_root->append_child( lo_customer ).
DATA(lo_cust_name) = lo_doc->create_element( name = 'Name' ).
lo_cust_name->set_value( is_invoice-customer_name ).
lo_customer->append_child( lo_cust_name ).
DATA(lo_cust_addr) = lo_doc->create_element( name = 'Address' ).
lo_cust_addr->set_value( is_invoice-customer_addr ).
lo_customer->append_child( lo_cust_addr ).
" Lignes
DATA(lo_items) = lo_doc->create_element( name = 'Items' ).
lo_root->append_child( lo_items ).
LOOP AT is_invoice-items INTO DATA(ls_item).
DATA(lo_item) = lo_doc->create_element( name = 'Item' ).
lo_items->append_child( lo_item ).
DATA(lo_pos) = lo_doc->create_element( name = 'Position' ).
lo_pos->set_value( |{ ls_item-position }| ).
lo_item->append_child( lo_pos ).
DATA(lo_desc) = lo_doc->create_element( name = 'Description' ).
lo_desc->set_value( ls_item-description ).
lo_item->append_child( lo_desc ).
DATA(lo_qty) = lo_doc->create_element( name = 'Quantity' ).
lo_qty->set_value( |{ ls_item-quantity }| ).
lo_item->append_child( lo_qty ).
DATA(lo_price) = lo_doc->create_element( name = 'UnitPrice' ).
lo_price->set_value( |{ ls_item-unit_price }| ).
lo_item->append_child( lo_price ).
DATA(lo_total) = lo_doc->create_element( name = 'Total' ).
lo_total->set_value( |{ ls_item-total }| ).
lo_item->append_child( lo_total ).
ENDLOOP.
" Totaux
DATA(lo_totals) = lo_doc->create_element( name = 'Totals' ).
lo_root->append_child( lo_totals ).
DATA(lo_subtotal) = lo_doc->create_element( name = 'Subtotal' ).
lo_subtotal->set_value( |{ is_invoice-subtotal }| ).
lo_totals->append_child( lo_subtotal ).
DATA(lo_tax) = lo_doc->create_element( name = 'Tax' ).
lo_tax->set_value( |{ is_invoice-tax }| ).
lo_totals->append_child( lo_tax ).
DATA(lo_grand_total) = lo_doc->create_element( name = 'GrandTotal' ).
lo_grand_total->set_value( |{ is_invoice-total }| ).
lo_totals->append_child( lo_grand_total ).
" XML en chaine
DATA(lo_stream) = cl_ixml=>create( )->create_stream_factory( )->create_ostream_xstring( rv_xml ).
cl_ixml=>create( )->create_renderer( document = lo_doc ostream = lo_stream )->render( ).
ENDMETHOD.
ENDCLASS.

Feuille de style XSL-FO

La feuille de style XSL-FO definit la mise en page de votre PDF :

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:template match="/Invoice">
<fo:root>
<!-- Definir la mise en page -->
<fo:layout-master-set>
<fo:simple-page-master master-name="A4"
page-height="297mm" page-width="210mm"
margin-top="20mm" margin-bottom="20mm"
margin-left="25mm" margin-right="25mm">
<fo:region-body margin-top="30mm" margin-bottom="20mm"/>
<fo:region-before extent="25mm"/>
<fo:region-after extent="15mm"/>
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="A4">
<!-- En-tete -->
<fo:static-content flow-name="xsl-region-before">
<fo:block font-size="18pt" font-weight="bold" text-align="center">
FACTURE
</fo:block>
</fo:static-content>
<!-- Pied de page -->
<fo:static-content flow-name="xsl-region-after">
<fo:block font-size="8pt" text-align="center">
Page <fo:page-number/> sur <fo:page-number-citation ref-id="last-page"/>
</fo:block>
</fo:static-content>
<!-- Contenu principal -->
<fo:flow flow-name="xsl-region-body">
<!-- En-tete de facture -->
<fo:block font-size="10pt" margin-bottom="10mm">
<fo:table width="100%">
<fo:table-column column-width="50%"/>
<fo:table-column column-width="50%"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell>
<fo:block font-weight="bold">Client :</fo:block>
<fo:block><xsl:value-of select="Customer/Name"/></fo:block>
<fo:block><xsl:value-of select="Customer/Address"/></fo:block>
</fo:table-cell>
<fo:table-cell text-align="right">
<fo:block>Numero de facture : <xsl:value-of select="Header/InvoiceNumber"/></fo:block>
<fo:block>Date : <xsl:value-of select="Header/InvoiceDate"/></fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
</fo:block>
<!-- Lignes -->
<fo:block margin-top="10mm">
<fo:table width="100%" border="1pt solid black">
<fo:table-column column-width="10%"/>
<fo:table-column column-width="45%"/>
<fo:table-column column-width="15%"/>
<fo:table-column column-width="15%"/>
<fo:table-column column-width="15%"/>
<fo:table-header>
<fo:table-row background-color="#EEEEEE" font-weight="bold">
<fo:table-cell border="1pt solid black" padding="2mm">
<fo:block>Pos.</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm">
<fo:block>Description</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block>Quantite</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block>Prix unitaire</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block>Total</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-header>
<fo:table-body>
<xsl:for-each select="Items/Item">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="2mm">
<fo:block><xsl:value-of select="Position"/></fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm">
<fo:block><xsl:value-of select="Description"/></fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block><xsl:value-of select="Quantity"/></fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block><xsl:value-of select="UnitPrice"/> EUR</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block><xsl:value-of select="Total"/> EUR</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:for-each>
</fo:table-body>
</fo:table>
</fo:block>
<!-- Totaux -->
<fo:block margin-top="5mm" text-align="right">
<fo:table width="40%" margin-left="60%">
<fo:table-column column-width="60%"/>
<fo:table-column column-width="40%"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell padding="1mm"><fo:block>Sous-total :</fo:block></fo:table-cell>
<fo:table-cell padding="1mm" text-align="right">
<fo:block><xsl:value-of select="Totals/Subtotal"/> EUR</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell padding="1mm"><fo:block>TVA (19%) :</fo:block></fo:table-cell>
<fo:table-cell padding="1mm" text-align="right">
<fo:block><xsl:value-of select="Totals/Tax"/> EUR</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row font-weight="bold">
<fo:table-cell padding="1mm" border-top="2pt solid black">
<fo:block>Montant total :</fo:block>
</fo:table-cell>
<fo:table-cell padding="1mm" text-align="right" border-top="2pt solid black">
<fo:block><xsl:value-of select="Totals/GrandTotal"/> EUR</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
</fo:block>
<!-- Marquer la derniere page pour la numerotation -->
<fo:block id="last-page"/>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>

Transformer XSL-FO en PDF

" Generateur PDF avec XSL-FO
CLASS zcl_pdf_generator DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
generate_pdf
IMPORTING iv_xml TYPE xstring
iv_xsl_fo TYPE xstring
RETURNING VALUE(rv_pdf) TYPE xstring
RAISING cx_transformation_error.
METHODS:
generate_invoice_pdf
IMPORTING is_invoice TYPE zcl_invoice_xml_generator=>ty_invoice
RETURNING VALUE(rv_pdf) TYPE xstring
RAISING cx_transformation_error.
PRIVATE SECTION.
METHODS:
get_xsl_fo_template
RETURNING VALUE(rv_xsl) TYPE xstring.
ENDCLASS.
CLASS zcl_pdf_generator IMPLEMENTATION.
METHOD generate_pdf.
" Transformation XSL-FO
" Note : La generation directe de PDF a partir de XSL-FO est
" limitee dans ABAP Cloud. Pour les scenarios de production, il est
" recommande d'utiliser un service FOP externe (Apache FOP) ou SAP Forms Service.
" Alternative : Transformation vers sortie XSL-FO
DATA lv_xsl_fo_result TYPE xstring.
CALL TRANSFORMATION id
SOURCE XML iv_xml
RESULT XML lv_xsl_fo_result.
" La sortie XSL-FO doit ensuite etre envoyee a un processeur FOP
" Cela se fait generalement via un appel HTTP a un service externe
rv_pdf = call_fop_service( lv_xsl_fo_result ).
ENDMETHOD.
METHOD generate_invoice_pdf.
" Generer la facture en PDF
DATA(lo_xml_gen) = NEW zcl_invoice_xml_generator( ).
DATA(lv_xml) = lo_xml_gen->generate_xml( is_invoice ).
DATA(lv_xsl_fo) = get_xsl_fo_template( ).
rv_pdf = generate_pdf(
iv_xml = lv_xml
iv_xsl_fo = lv_xsl_fo
).
ENDMETHOD.
METHOD get_xsl_fo_template.
" Lire le template XSL-FO depuis le repository
" Dans ABAP Cloud : depuis le MIME Repository ou comme constante
DATA lv_template TYPE string.
lv_template = |<?xml version="1.0" encoding="UTF-8"?>| &&
|<xsl:stylesheet version="1.0" ...>| &&
|...| &&
|</xsl:stylesheet>|.
rv_xsl = cl_abap_conv_codepage=>create_out( )->convert( lv_template ).
ENDMETHOD.
ENDCLASS.

SAP Forms Service by Adobe

Le SAP Forms Service by Adobe est la solution Cloud-native pour les formulaires complexes. Il permet l’utilisation de templates bases sur Adobe sur SAP BTP.

Apercu du service

AspectDescription
ServiceSAP Forms Service by Adobe
ProtocoleREST API (JSON/XML)
TemplatesXDP (Adobe LiveCycle Designer)
SortiePDF, Print Stream
LicenceLicence BTP separee requise

Architecture

┌────────────────────────────────────────────────────────────────────────────┐
│ SAP Forms Service by Adobe │
│ │
│ ┌───────────────────┐ ┌───────────────────────────────────────┐ │
│ │ ABAP Cloud │ │ SAP Forms Service │ │
│ │ │ HTTP │ │ │
│ │ ┌─────────────┐ │ ─────► │ ┌─────────────────────────────────┐ │ │
│ │ │ Form Data │ │ JSON │ │ Template Storage │ │ │
│ │ │ (XML/JSON) │ │ │ │ (XDP Templates) │ │ │
│ │ └─────────────┘ │ │ └─────────────────────────────────┘ │ │
│ │ │ │ │ │ │
│ │ │ ◄───── │ ▼ │ │
│ │ ┌─────────────┐ │ PDF │ ┌─────────────────────────────────┐ │ │
│ │ │ PDF │ │ │ │ Adobe Forms Engine │ │ │
│ │ │ Output │ │ │ │ (PDF Rendering) │ │ │
│ │ └─────────────┘ │ │ └─────────────────────────────────┘ │ │
│ └───────────────────┘ └───────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘

Configuration du Communication Arrangement

  1. Communication Scenario - Creer pour SAP Forms Service
  2. Communication System - Configurer avec l’URL du BTP Forms Service
  3. Communication Arrangement - Avec les credentials OAuth 2.0

Exemple de code : Appeler le Forms Service

" Client SAP Forms Service
CLASS zcl_forms_service_client DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_form_request,
template_name TYPE string,
template_data TYPE string, " JSON ou XML
output_type TYPE string, " PDF, PCL, PS
END OF ty_form_request.
TYPES:
BEGIN OF ty_form_response,
pdf_content TYPE xstring,
page_count TYPE i,
status TYPE string,
message TYPE string,
END OF ty_form_response.
METHODS:
constructor
IMPORTING iv_comm_scenario TYPE string.
METHODS:
render_form
IMPORTING is_request TYPE ty_form_request
RETURNING VALUE(rs_response) TYPE ty_form_response
RAISING cx_http_dest_provider_error
cx_web_http_client_error.
PRIVATE SECTION.
DATA: mv_comm_scenario TYPE string.
METHODS:
get_http_client
RETURNING VALUE(ro_client) TYPE REF TO if_web_http_client
RAISING cx_http_dest_provider_error
cx_web_http_client_error.
ENDCLASS.
CLASS zcl_forms_service_client IMPLEMENTATION.
METHOD constructor.
mv_comm_scenario = iv_comm_scenario.
ENDMETHOD.
METHOD render_form.
" Creer le client HTTP
DATA(lo_client) = get_http_client( ).
TRY.
" Preparer la requete
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_header_field(
i_name = 'Content-Type"
i_value = 'application/json"
).
" Corps de la requete (JSON)
DATA(lv_request_body) = |\{| &&
|"templateSource": "storageName",| &&
|"templateName": "{ is_request-template_name }",| &&
|"outputType": "{ is_request-output_type }",| &&
|"data": { is_request-template_data }| &&
|\}|.
lo_request->set_text( lv_request_body ).
" Appeler l'API
DATA(lo_response) = lo_client->execute( if_web_http_client=>post ).
" Traiter la reponse
DATA(lv_status) = lo_response->get_status( ).
IF lv_status-code = 200.
rs_response-pdf_content = lo_response->get_binary( ).
rs_response-status = 'SUCCESS'.
ELSE.
rs_response-status = 'ERROR'.
rs_response-message = lo_response->get_text( ).
ENDIF.
CATCH cx_web_http_client_error INTO DATA(lx_http).
rs_response-status = 'ERROR'.
rs_response-message = lx_http->get_text( ).
ENDTRY.
lo_client->close( ).
ENDMETHOD.
METHOD get_http_client.
" Destination depuis le Communication Arrangement
DATA(lo_destination) = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = mv_comm_scenario
service_id = 'FORMS_SERVICE"
).
ro_client = cl_web_http_client_manager=>create_by_http_destination( lo_destination ).
ENDMETHOD.
ENDCLASS.

Preparer les donnees du template

" Constructeur de donnees de formulaire en JSON pour Forms Service
CLASS zcl_form_data_builder DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
build_invoice_data
IMPORTING is_invoice TYPE zcl_invoice_xml_generator=>ty_invoice
RETURNING VALUE(rv_json) TYPE string.
ENDCLASS.
CLASS zcl_form_data_builder IMPLEMENTATION.
METHOD build_invoice_data.
" Structure JSON pour le template Adobe Form
DATA(lo_json) = NEW /ui2/cl_json( ).
" Construire la structure de donnees
DATA: BEGIN OF ls_form_data,
header TYPE REF TO data,
customer TYPE REF TO data,
items TYPE REF TO data,
totals TYPE REF TO data,
END OF ls_form_data.
" En-tete
CREATE DATA ls_form_data-header TYPE STANDARD TABLE OF string WITH EMPTY KEY.
" ... remplir les donnees
" Serialiser en JSON
rv_json = /ui2/cl_json=>serialize(
data = ls_form_data
compress = abap_true
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
).
ENDMETHOD.
ENDCLASS.

Services PDF externes

Pour une flexibilite maximale, vous pouvez utiliser des APIs PDF externes. Les options populaires sont :

ServiceDescriptionAvantages
PDFShiftAPI HTML vers PDFSimple, flexible
DocRaptorHTML/CSS vers PDFBon support CSS
PrinceXML/HTML vers PDFExcellente typographie
AWS TextractTraitement PDFIntegration AWS

Exemple de code : Utiliser un service PDF externe

" Client de service PDF externe (ex. PDFShift)
CLASS zcl_external_pdf_service DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING iv_api_key TYPE string
iv_api_url TYPE string.
METHODS:
html_to_pdf
IMPORTING iv_html TYPE string
iv_options TYPE string OPTIONAL
RETURNING VALUE(rv_pdf) TYPE xstring
RAISING cx_web_http_client_error
zcx_pdf_generation_error.
PRIVATE SECTION.
DATA: mv_api_key TYPE string,
mv_api_url TYPE string.
ENDCLASS.
CLASS zcl_external_pdf_service IMPLEMENTATION.
METHOD constructor.
mv_api_key = iv_api_key.
mv_api_url = iv_api_url.
ENDMETHOD.
METHOD html_to_pdf.
" Destination depuis le Communication Arrangement
DATA(lo_destination) = cl_http_destination_provider=>create_by_cloud_destination(
i_name = 'Z_PDF_SERVICE"
i_authn_mode = if_a4c_cp_service=>service_specific
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination(
i_destination = lo_destination
).
TRY.
DATA(lo_request) = lo_client->get_http_request( ).
" Definir les en-tetes
lo_request->set_header_field(
i_name = 'Content-Type"
i_value = 'application/json"
).
lo_request->set_header_field(
i_name = 'Authorization"
i_value = |Basic { mv_api_key }|
).
" Corps de la requete
DATA(lv_body) = |\{| &&
|"source": "{ escape( val = iv_html format = cl_abap_format=>e_json_string ) }",| &&
|"landscape": false,| &&
|"use_print": true| &&
|\}|.
lo_request->set_text( lv_body ).
" Appeler l'API
DATA(lo_response) = lo_client->execute( if_web_http_client=>post ).
DATA(lv_status) = lo_response->get_status( ).
IF lv_status-code = 200.
rv_pdf = lo_response->get_binary( ).
ELSE.
RAISE EXCEPTION TYPE zcx_pdf_generation_error
EXPORTING
textid = zcx_pdf_generation_error=>api_error
mv_message = lo_response->get_text( ).
ENDIF.
CLEANUP.
lo_client->close( ).
ENDTRY.
ENDMETHOD.
ENDCLASS.

Template HTML pour PDF

" Generation PDF basee sur HTML
CLASS zcl_html_pdf_generator DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
generate_invoice_html
IMPORTING is_invoice TYPE zcl_invoice_xml_generator=>ty_invoice
RETURNING VALUE(rv_html) TYPE string.
ENDCLASS.
CLASS zcl_html_pdf_generator IMPLEMENTATION.
METHOD generate_invoice_html.
" Template HTML avec CSS inline pour PDF
rv_html =
|<!DOCTYPE html>| &&
|<html>| &&
|<head>| &&
|<style>| &&
|body \{ font-family: Arial, sans-serif; margin: 40px; \}| &&
|h1 \{ color: #333; \}| &&
|table \{ width: 100%; border-collapse: collapse; margin-top: 20px; \}| &&
|th, td \{ border: 1px solid #ddd; padding: 8px; text-align: left; \}| &&
|th \{ background-color: #f2f2f2; \}| &&
|.totals \{ width: 300px; float: right; margin-top: 20px; \}| &&
|.totals td:last-child \{ text-align: right; \}| &&
|.grand-total \{ font-weight: bold; border-top: 2px solid #333; \}| &&
|</style>| &&
|</head>| &&
|<body>| &&
|<h1>FACTURE</h1>| &&
|<div style="display: flex; justify-content: space-between;">| &&
|<div>| &&
|<strong>Client :</strong><br>| &&
|{ is_invoice-customer_name }<br>| &&
|{ is_invoice-customer_addr }| &&
|</div>| &&
|<div style="text-align: right;">| &&
|<strong>Numero de facture :</strong> { is_invoice-invoice_number }<br>| &&
|<strong>Date :</strong> { is_invoice-invoice_date DATE = USER }| &&
|</div>| &&
|</div>|.
" Lignes
rv_html = rv_html &&
|<table>| &&
|<tr><th>Pos.</th><th>Description</th><th>Quantite</th>| &&
|<th>Prix unitaire</th><th>Total</th></tr>|.
LOOP AT is_invoice-items INTO DATA(ls_item).
rv_html = rv_html &&
|<tr>| &&
|<td>{ ls_item-position }</td>| &&
|<td>{ ls_item-description }</td>| &&
|<td>{ ls_item-quantity }</td>| &&
|<td>{ ls_item-unit_price } EUR</td>| &&
|<td>{ ls_item-total } EUR</td>| &&
|</tr>|.
ENDLOOP.
rv_html = rv_html && |</table>|.
" Totaux
rv_html = rv_html &&
|<table class="totals">| &&
|<tr><td>Sous-total :</td><td>{ is_invoice-subtotal } EUR</td></tr>| &&
|<tr><td>TVA (19%) :</td><td>{ is_invoice-tax } EUR</td></tr>| &&
|<tr class="grand-total">| &&
|<td>Montant total :</td><td>{ is_invoice-total } EUR</td></tr>| &&
|</table>| &&
|</body></html>|.
ENDMETHOD.
ENDCLASS.

RAP Action pour la generation de PDF

La meilleure integration se fait via une RAP Action qui genere les PDF directement dans le Business Object.

Behavior Definition

managed implementation in class ZBP_I_INVOICE unique;
strict ( 2 );
define behavior for ZI_Invoice alias Invoice
persistent table zinvoice
lock master
authorization master ( instance )
{
create;
update;
delete;
// Generation PDF comme Action
action generatePdf result [1] $self;
// Static Factory Action pour le telechargement
static action downloadInvoicePdf
parameter ZA_InvoiceDownload
result [1] ZA_PdfResult;
field ( readonly ) InvoiceUUID, CreatedAt, CreatedBy;
}

Parametres et resultat de l’Action

-- Parametre pour l'Action de telechargement
@EndUserText.label: 'Parametre de telechargement de facture"
define abstract entity ZA_InvoiceDownload
{
InvoiceNumber : abap.char(10);
OutputFormat : abap.char(10); // PDF, HTML
}
-- Resultat avec PDF binaire
@EndUserText.label: 'Resultat PDF"
define abstract entity ZA_PdfResult
{
FileName : abap.char(100);
MimeType : abap.char(50);
FileContent : abap.rawstring(0); // XSTRING pour binaire
}

Implementation du Behavior

" Implementation RAP Behavior avec Action PDF
CLASS lhc_invoice DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS generatePdf FOR MODIFY
IMPORTING keys FOR ACTION Invoice~generatePdf RESULT result.
METHODS downloadInvoicePdf FOR MODIFY
IMPORTING keys FOR ACTION Invoice~downloadInvoicePdf RESULT result.
METHODS:
build_invoice_data
IMPORTING is_invoice TYPE zinvoice
RETURNING VALUE(rs_data) TYPE zcl_invoice_xml_generator=>ty_invoice.
ENDCLASS.
CLASS lhc_invoice IMPLEMENTATION.
METHOD generatePdf.
" Lire les donnees de facture
READ ENTITIES OF zi_invoice IN LOCAL MODE
ENTITY Invoice
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_invoices).
LOOP AT lt_invoices INTO DATA(ls_invoice).
" Preparer les donnees pour le PDF
DATA(ls_pdf_data) = build_invoice_data( CORRESPONDING #( ls_invoice ) ).
" Generer le PDF
TRY.
DATA(lo_pdf_gen) = NEW zcl_pdf_generator( ).
DATA(lv_pdf) = lo_pdf_gen->generate_invoice_pdf( ls_pdf_data ).
" Sauvegarder le PDF en piece jointe (optionnel)
" Ou : renvoyer dans le result
" Message de succes
APPEND VALUE #(
%tky = ls_invoice-%tky
%param = ls_invoice
) TO result.
CATCH cx_transformation_error INTO DATA(lx_error).
" Signaler l'erreur
APPEND VALUE #(
%tky = ls_invoice-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_error->get_text( )
)
) TO reported-invoice.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD downloadInvoicePdf.
" Lire le parametre
DATA(ls_param) = keys[ 1 ]-%param.
" Charger la facture
SELECT SINGLE *
FROM zinvoice
WHERE invoice_number = @ls_param-InvoiceNumber
INTO @DATA(ls_invoice).
IF sy-subrc <> 0.
APPEND VALUE #(
%tky = keys[ 1 ]-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Facture { ls_param-InvoiceNumber } non trouvee|
)
) TO reported-invoice.
RETURN.
ENDIF.
" Generer le PDF
TRY.
DATA(ls_pdf_data) = build_invoice_data( ls_invoice ).
DATA(lo_pdf_gen) = NEW zcl_pdf_generator( ).
DATA(lv_pdf) = lo_pdf_gen->generate_invoice_pdf( ls_pdf_data ).
" Renvoyer le resultat
APPEND VALUE #(
%tky = keys[ 1 ]-%tky
%param = VALUE #(
FileName = |Facture_{ ls_param-InvoiceNumber }.pdf|
MimeType = 'application/pdf"
FileContent = lv_pdf
)
) TO result.
CATCH cx_transformation_error INTO DATA(lx_error).
APPEND VALUE #(
%tky = keys[ 1 ]-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_error->get_text( )
)
) TO reported-invoice.
ENDTRY.
ENDMETHOD.
METHOD build_invoice_data.
" Preparer les donnees de facture pour le PDF
rs_data-invoice_number = is_invoice-invoice_number.
rs_data-invoice_date = is_invoice-invoice_date.
rs_data-customer_name = is_invoice-customer_name.
rs_data-customer_addr = is_invoice-customer_address.
" Charger les lignes
SELECT position, description, quantity, unit_price, total_amount
FROM zinvoice_item
WHERE invoice_uuid = @is_invoice-invoice_uuid
INTO CORRESPONDING FIELDS OF TABLE @rs_data-items.
" Calculer les totaux
LOOP AT rs_data-items INTO DATA(ls_item).
rs_data-subtotal = rs_data-subtotal + ls_item-total.
ENDLOOP.
rs_data-tax = rs_data-subtotal * '0.19'.
rs_data-total = rs_data-subtotal + rs_data-tax.
ENDMETHOD.
ENDCLASS.

Gestion des erreurs

Exception personnalisee pour la generation PDF

" Exception de generation PDF
CLASS zcx_pdf_generation_error DEFINITION
PUBLIC
INHERITING FROM cx_static_check
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_t100_message.
CONSTANTS:
BEGIN OF template_not_found,
msgid TYPE symsgid VALUE 'ZPDF',
msgno TYPE symsgno VALUE '001',
attr1 TYPE scx_attrname VALUE 'MV_TEMPLATE',
attr2 TYPE scx_attrname VALUE '',
attr3 TYPE scx_attrname VALUE '',
attr4 TYPE scx_attrname VALUE '',
END OF template_not_found,
BEGIN OF api_error,
msgid TYPE symsgid VALUE 'ZPDF',
msgno TYPE symsgno VALUE '002',
attr1 TYPE scx_attrname VALUE 'MV_MESSAGE',
attr2 TYPE scx_attrname VALUE '',
attr3 TYPE scx_attrname VALUE '',
attr4 TYPE scx_attrname VALUE '',
END OF api_error,
BEGIN OF transformation_error,
msgid TYPE symsgid VALUE 'ZPDF',
msgno TYPE symsgno VALUE '003',
attr1 TYPE scx_attrname VALUE 'MV_MESSAGE',
attr2 TYPE scx_attrname VALUE '',
attr3 TYPE scx_attrname VALUE '',
attr4 TYPE scx_attrname VALUE '',
END OF transformation_error.
DATA: mv_template TYPE string READ-ONLY,
mv_message TYPE string READ-ONLY.
METHODS:
constructor
IMPORTING
textid LIKE if_t100_message=>t100key OPTIONAL
previous LIKE previous OPTIONAL
mv_template TYPE string OPTIONAL
mv_message TYPE string OPTIONAL.
ENDCLASS.
CLASS zcx_pdf_generation_error IMPLEMENTATION.
METHOD constructor.
super->constructor( previous = previous ).
me->mv_template = mv_template.
me->mv_message = mv_message.
CLEAR me->textid.
IF textid IS INITIAL.
if_t100_message~t100key = if_t100_message=>default_textid.
ELSE.
if_t100_message~t100key = textid.
ENDIF.
ENDMETHOD.
ENDCLASS.

Generation PDF robuste

" Service PDF avec logique de retry
CLASS zcl_robust_pdf_service DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
generate_pdf_with_retry
IMPORTING is_data TYPE any
iv_template TYPE string
iv_max_retries TYPE i DEFAULT 3
RETURNING VALUE(rv_pdf) TYPE xstring
RAISING zcx_pdf_generation_error.
PRIVATE SECTION.
CONSTANTS:
gc_retry_delay_ms TYPE i VALUE 1000.
ENDCLASS.
CLASS zcl_robust_pdf_service IMPLEMENTATION.
METHOD generate_pdf_with_retry.
DATA: lv_attempts TYPE i VALUE 0,
lx_last_error TYPE REF TO cx_root.
WHILE lv_attempts < iv_max_retries.
lv_attempts = lv_attempts + 1.
TRY.
" Generer le PDF
DATA(lo_service) = NEW zcl_forms_service_client( 'Z_FORMS_COMM' ).
DATA(ls_response) = lo_service->render_form(
is_request = VALUE #(
template_name = iv_template
template_data = /ui2/cl_json=>serialize( data = is_data )
output_type = 'PDF"
)
).
IF ls_response-status = 'SUCCESS'.
rv_pdf = ls_response-pdf_content.
RETURN. " Succes
ELSE.
" Erreur de service
lx_last_error = NEW zcx_pdf_generation_error(
textid = zcx_pdf_generation_error=>api_error
mv_message = ls_response-message
).
ENDIF.
CATCH cx_http_dest_provider_error
cx_web_http_client_error INTO DATA(lx_http).
" Erreur reseau - Retry possible
lx_last_error = lx_http.
ENDTRY.
" Attendre avant le retry
IF lv_attempts < iv_max_retries.
DATA(lv_wait) = gc_retry_delay_ms * lv_attempts.
" Dans ABAP Cloud : utiliser cl_abap_timers
ENDIF.
ENDWHILE.
" Toutes les tentatives ont echoue
RAISE EXCEPTION TYPE zcx_pdf_generation_error
EXPORTING
textid = zcx_pdf_generation_error=>api_error
previous = lx_last_error
mv_message = |Generation PDF echouee apres { iv_max_retries } tentatives|.
ENDMETHOD.
ENDCLASS.

Alternatives et solutions de contournement

Option 1 : Export HTML au lieu de PDF

Pour des documents simples, un export HTML peut suffire :

" HTML comme alternative au PDF
CLASS zcl_html_document_generator DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
generate_html_document
IMPORTING is_data TYPE any
RETURNING VALUE(rv_html) TYPE string.
METHODS:
get_as_data_uri
IMPORTING iv_html TYPE string
RETURNING VALUE(rv_data_uri) TYPE string.
ENDCLASS.
CLASS zcl_html_document_generator IMPLEMENTATION.
METHOD generate_html_document.
" Document HTML complet avec CSS optimise pour l'impression
rv_html =
|<!DOCTYPE html>| &&
|<html>| &&
|<head>| &&
|<style>| &&
|@media print \{| &&
| body \{ margin: 0; padding: 20mm; \}| &&
| .no-print \{ display: none; \}| &&
| @page \{ size: A4; margin: 20mm; \}| &&
|\}| &&
|</style>| &&
|</head>| &&
|<body>| &&
|<button class="no-print" onclick="window.print()">Imprimer</button>| &&
|<!-- Contenu du document -->| &&
|</body>| &&
|</html>|.
ENDMETHOD.
METHOD get_as_data_uri.
" HTML comme Data URI pour telechargement direct
DATA(lv_base64) = cl_http_utility=>encode_base64(
cl_abap_conv_codepage=>create_out( )->convert( iv_html )
).
rv_data_uri = |data:text/html;base64,{ lv_base64 }|.
ENDMETHOD.
ENDCLASS.

Option 2 : Export Excel comme alternative

" Export Excel pour les donnees tabulaires
CLASS zcl_excel_exporter DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
export_to_excel
IMPORTING it_data TYPE ANY TABLE
RETURNING VALUE(rv_excel) TYPE xstring.
ENDCLASS.

Option 3 : Generation PDF asynchrone

Pour les gros documents ou la generation en masse :

" Generation PDF asynchrone avec bgPF
CLASS zcl_async_pdf_generator DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
schedule_pdf_generation
IMPORTING iv_invoice_id TYPE zinvoice_id
RETURNING VALUE(rv_job_id) TYPE string.
METHODS:
check_pdf_status
IMPORTING iv_job_id TYPE string
EXPORTING ev_status TYPE string
ev_pdf TYPE xstring.
ENDCLASS.

Bonnes pratiques

Recommandations pour la generation PDF

AspectRecommandation
Documents simplesHTML vers PDF avec service externe
Formulaires complexesSAP Forms Service by Adobe
Volumes elevesGeneration asynchrone avec bgPF
Scenarios hors ligneExport HTML avec CSS Print
ConformiteImplementer la journalisation des acces

Conseils de performance

  1. Mise en cache : Mettre en cache les templates et contenus statiques
  2. Traitement par lots : Generer plusieurs PDF en une seule requete
  3. Compression : Activer la compression PDF
  4. Asynchrone : Generer les gros PDF en arriere-plan

Sujets connexes

Resume

La generation de PDF dans ABAP Cloud necessite de nouvelles approches :

  1. XSL-FO pour la transformation basee sur XML (avec processeur FOP externe)
  2. SAP Forms Service by Adobe pour les formulaires complexes avec templates Adobe
  3. APIs PDF externes pour une flexibilite maximale (PDFShift, DocRaptor)
  4. RAP Actions integrent la generation PDF de maniere transparente dans les Business Objects
  5. Gestion des erreurs avec logique de retry pour une production robuste

Le choix de la bonne option depend de vos exigences : complexite des documents, templates existants, volume et budget.