PDF-Generierung in ABAP Cloud

kategorie
Integration
Veröffentlicht
autor
Johannes

PDF-Generierung in ABAP Cloud unterscheidet sich grundlegend von On-Premise. Klassische Adobe Forms (SFP) und SmartForms stehen auf SAP BTP nicht zur Verfuegung. Stattdessen nutzt du Cloud-native Alternativen wie XSL-FO, den SAP Forms Service by Adobe oder externe PDF-Services.

Verfuegbare Optionen im Ueberblick

In ABAP Cloud gibt es mehrere Wege, PDFs zu generieren:

OptionBeschreibungAnwendungsfall
XSL-FOXML-basierte Transformation zu PDFEinfache bis mittlere Dokumente
SAP Forms Service by AdobeCloud-basierter Adobe Forms DienstKomplexe Formulare mit Adobe-Templates
Externe PDF-ServicesHTTP-basierte PDF-APIsFlexibilitaet, spezielle Anforderungen
SAP Document ManagementIntegration mit DMSDokumenten-Workflows

Architektur-Uebersicht

┌─────────────────────────────────────────────────────────────────────────────┐
│ PDF-Generierung in ABAP Cloud │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ RAP Business Object / Service │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ RAP Action │ │ │
│ │ │ "Generate PDF" │ │ │
│ │ └──────────┬──────────┘ │ │
│ └─────────────┼────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ PDF-Generierungs-Optionen │ │
│ │ │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │
│ │ │ XSL-FO │ │ SAP Forms │ │ Externe APIs │ │ │
│ │ │ │ │ Service │ │ (PDF-Services) │ │ │
│ │ │ - XML + XSLT │ │ - Adobe Templates│ │ - REST APIs │ │ │
│ │ │ - cl_xslt_proc │ │ - BTP Service │ │ - HTTP Client │ │ │
│ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ PDF Output │ │
│ │ - Download via RAP Action Result │ │
│ │ - Attachment an Business Object │ │
│ │ - Email-Versand │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

XSL-FO: XML-basierte PDF-Generierung

XSL-FO (Extensible Stylesheet Language Formatting Objects) ist eine W3C-Standardtechnik fuer die Transformation von XML zu paginierten Dokumenten wie PDF.

Grundprinzip

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ XML-Daten │ + │ XSL-FO │ = │ PDF │
│ │ │ Stylesheet │ │ │
└──────────────┘ └──────────────┘ └──────────────┘

XML-Datenstruktur

Zunaechst definierst du eine XML-Struktur fuer deine Dokumentdaten:

" XML-Daten fuer Rechnung generieren
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.
" XML-Dokument aufbauen
DATA(lo_doc) = cl_ixml=>create( )->create_document( ).
" Root-Element
DATA(lo_root) = lo_doc->create_element( name = 'Invoice' ).
lo_doc->append_child( lo_root ).
" Header-Elemente
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 ).
" Kundendaten
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 ).
" Positionen
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.
" Summen
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 als String
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.

XSL-FO Stylesheet

Das XSL-FO Stylesheet definiert das Layout deines PDFs:

<?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>
<!-- Seitenlayout definieren -->
<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">
<!-- Kopfzeile -->
<fo:static-content flow-name="xsl-region-before">
<fo:block font-size="18pt" font-weight="bold" text-align="center">
RECHNUNG
</fo:block>
</fo:static-content>
<!-- Fusszeile -->
<fo:static-content flow-name="xsl-region-after">
<fo:block font-size="8pt" text-align="center">
Seite <fo:page-number/> von <fo:page-number-citation ref-id="last-page"/>
</fo:block>
</fo:static-content>
<!-- Hauptinhalt -->
<fo:flow flow-name="xsl-region-body">
<!-- Rechnungskopf -->
<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">Kunde:</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>Rechnungsnummer: <xsl:value-of select="Header/InvoiceNumber"/></fo:block>
<fo:block>Datum: <xsl:value-of select="Header/InvoiceDate"/></fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
</fo:block>
<!-- Positionen -->
<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>Beschreibung</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block>Menge</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block>Einzelpreis</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="2mm" text-align="right">
<fo:block>Gesamt</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>
<!-- Summen -->
<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>Zwischensumme:</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>MwSt (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>Gesamtbetrag:</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>
<!-- Letzte Seite markieren fuer Seitenzahlen -->
<fo:block id="last-page"/>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>

XSL-FO zu PDF transformieren

" PDF-Generator mit 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.
" XSL-FO Transformation
" Hinweis: Die direkte PDF-Generierung aus XSL-FO ist in ABAP Cloud
" eingeschraenkt. Fuer produktive Szenarien wird empfohlen,
" einen externen FOP-Service (Apache FOP) oder SAP Forms Service zu nutzen.
" Alternative: Transformation zu XSL-FO Output
DATA lv_xsl_fo_result TYPE xstring.
CALL TRANSFORMATION id
SOURCE XML iv_xml
RESULT XML lv_xsl_fo_result.
" Der XSL-FO Output muss dann an einen FOP-Prozessor gesendet werden
" Dies erfolgt typischerweise ueber einen HTTP-Aufruf an einen externen Service
rv_pdf = call_fop_service( lv_xsl_fo_result ).
ENDMETHOD.
METHOD generate_invoice_pdf.
" Rechnung als PDF generieren
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.
" XSL-FO Template aus Repository lesen
" In ABAP Cloud: Aus MIME Repository oder als Konstante
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

Der SAP Forms Service by Adobe ist die Cloud-native Loesung fuer komplexe Formulare. Er ermoeglicht die Nutzung von Adobe-basierten Templates auf SAP BTP.

Service-Ueberblick

AspektBeschreibung
ServiceSAP Forms Service by Adobe
ProtokollREST API (JSON/XML)
TemplatesXDP (Adobe LiveCycle Designer)
OutputPDF, Print Stream
LizenzSeparate BTP-Lizenz erforderlich

Architektur

┌────────────────────────────────────────────────────────────────────────────┐
│ 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) │ │ │
│ │ └─────────────┘ │ │ └─────────────────────────────────┘ │ │
│ └───────────────────┘ └───────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘

Communication Arrangement einrichten

  1. Communication Scenario erstellen fuer SAP Forms Service
  2. Communication System mit BTP Forms Service URL konfigurieren
  3. Communication Arrangement mit OAuth 2.0 Credentials

Code-Beispiel: Forms Service aufrufen

" SAP Forms Service Client
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 oder 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.
" HTTP Client erstellen
DATA(lo_client) = get_http_client( ).
TRY.
" Request vorbereiten
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_header_field(
i_name = 'Content-Type'
i_value = 'application/json'
).
" Request Body (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 ).
" API aufrufen
DATA(lo_response) = lo_client->execute( if_web_http_client=>post ).
" Response verarbeiten
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 aus 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.

Template-Daten vorbereiten

" Form-Daten als JSON fuer 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.
" JSON-Struktur fuer Adobe Form Template
DATA(lo_json) = NEW /ui2/cl_json( ).
" Datenstruktur aufbauen
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.
" Header
CREATE DATA ls_form_data-header TYPE STANDARD TABLE OF string WITH EMPTY KEY.
" ... Daten fuellen
" JSON serialisieren
rv_json = /ui2/cl_json=>serialize(
data = ls_form_data
compress = abap_true
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
).
ENDMETHOD.
ENDCLASS.

Externe PDF-Services

Fuer maximale Flexibilitaet kannst du externe PDF-APIs nutzen. Populaere Optionen sind:

ServiceBeschreibungVorteile
PDFShiftHTML zu PDF APIEinfach, flexibel
DocRaptorHTML/CSS zu PDFGute CSS-Unterstuetzung
PrinceXML/HTML zu PDFHervorragende Typografie
AWS TextractPDF-VerarbeitungAWS-Integration

Code-Beispiel: Externen PDF-Service nutzen

" Externer PDF-Service Client (z.B. 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 aus 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( ).
" Headers setzen
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 }|
).
" Request Body
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 ).
" API aufrufen
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.

HTML-Template fuer PDF

" HTML-basierte PDF-Generierung
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.
" HTML-Template mit Inline-CSS fuer 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>RECHNUNG</h1>| &&
|<div style="display: flex; justify-content: space-between;">| &&
|<div>| &&
|<strong>Kunde:</strong><br>| &&
|{ is_invoice-customer_name }<br>| &&
|{ is_invoice-customer_addr }| &&
|</div>| &&
|<div style="text-align: right;">| &&
|<strong>Rechnungsnummer:</strong> { is_invoice-invoice_number }<br>| &&
|<strong>Datum:</strong> { is_invoice-invoice_date DATE = USER }| &&
|</div>| &&
|</div>|.
" Positionen
rv_html = rv_html &&
|<table>| &&
|<tr><th>Pos.</th><th>Beschreibung</th><th>Menge</th>| &&
|<th>Einzelpreis</th><th>Gesamt</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>|.
" Summen
rv_html = rv_html &&
|<table class="totals">| &&
|<tr><td>Zwischensumme:</td><td>{ is_invoice-subtotal } EUR</td></tr>| &&
|<tr><td>MwSt (19%):</td><td>{ is_invoice-tax } EUR</td></tr>| &&
|<tr class="grand-total">| &&
|<td>Gesamtbetrag:</td><td>{ is_invoice-total } EUR</td></tr>| &&
|</table>| &&
|</body></html>|.
ENDMETHOD.
ENDCLASS.

RAP Action fuer PDF-Generierung

Die beste Integration erfolgt ueber eine RAP Action, die PDFs direkt im Business Object generiert.

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;
// PDF-Generierung als Action
action generatePdf result [1] $self;
// Static Factory Action fuer Download
static action downloadInvoicePdf
parameter ZA_InvoiceDownload
result [1] ZA_PdfResult;
field ( readonly ) InvoiceUUID, CreatedAt, CreatedBy;
}

Action Parameter und Result

-- Parameter fuer Download Action
@EndUserText.label: 'Invoice Download Parameter'
define abstract entity ZA_InvoiceDownload
{
InvoiceNumber : abap.char(10);
OutputFormat : abap.char(10); // PDF, HTML
}
-- Result mit PDF Binary
@EndUserText.label: 'PDF Result'
define abstract entity ZA_PdfResult
{
FileName : abap.char(100);
MimeType : abap.char(50);
FileContent : abap.rawstring(0); // XSTRING fuer Binary
}

Behavior Implementation

" RAP Behavior Implementation mit PDF-Action
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.
" Rechnungsdaten lesen
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).
" Daten fuer PDF aufbereiten
DATA(ls_pdf_data) = build_invoice_data( CORRESPONDING #( ls_invoice ) ).
" PDF generieren
TRY.
DATA(lo_pdf_gen) = NEW zcl_pdf_generator( ).
DATA(lv_pdf) = lo_pdf_gen->generate_invoice_pdf( ls_pdf_data ).
" PDF als Attachment speichern (optional)
" Oder: In result zurueckgeben
" Erfolgsmeldung
APPEND VALUE #(
%tky = ls_invoice-%tky
%param = ls_invoice
) TO result.
CATCH cx_transformation_error INTO DATA(lx_error).
" Fehler melden
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.
" Parameter auslesen
DATA(ls_param) = keys[ 1 ]-%param.
" Rechnung laden
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 = |Rechnung { ls_param-InvoiceNumber } nicht gefunden|
)
) TO reported-invoice.
RETURN.
ENDIF.
" PDF generieren
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 ).
" Result zurueckgeben
APPEND VALUE #(
%tky = keys[ 1 ]-%tky
%param = VALUE #(
FileName = |Rechnung_{ 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.
" Rechnungsdaten fuer PDF aufbereiten
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.
" Positionen laden
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.
" Summen berechnen
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.

Fehlerbehandlung

Custom Exception fuer PDF-Generierung

" PDF-Generierungs-Exception
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.

Robuste PDF-Generierung

" PDF-Service mit Retry-Logik
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.
" PDF generieren
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. " Erfolg
ELSE.
" Service-Fehler
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).
" Netzwerk-Fehler - Retry moeglich
lx_last_error = lx_http.
ENDTRY.
" Warten vor Retry
IF lv_attempts < iv_max_retries.
DATA(lv_wait) = gc_retry_delay_ms * lv_attempts.
" In ABAP Cloud: cl_abap_timers verwenden
ENDIF.
ENDWHILE.
" Alle Versuche fehlgeschlagen
RAISE EXCEPTION TYPE zcx_pdf_generation_error
EXPORTING
textid = zcx_pdf_generation_error=>api_error
previous = lx_last_error
mv_message = |PDF-Generierung nach { iv_max_retries } Versuchen fehlgeschlagen|.
ENDMETHOD.
ENDCLASS.

Alternativen und Workarounds

Option 1: HTML-Export statt PDF

Fuer einfache Dokumente kann ein HTML-Export ausreichen:

" HTML als Alternative zu 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.
" Vollstaendiges HTML-Dokument mit Print-optimiertem CSS
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()">Drucken</button>| &&
|<!-- Dokumentinhalt -->| &&
|</body>| &&
|</html>|.
ENDMETHOD.
METHOD get_as_data_uri.
" HTML als Data URI fuer direkten Download
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: Excel-Export als Alternative

" Excel-Export fuer tabellarische Daten
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: Asynchrone PDF-Generierung

Fuer grosse Dokumente oder Massengenerierung:

" Asynchrone PDF-Generierung mit 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.

Best Practices

Empfehlungen fuer PDF-Generierung

AspektEmpfehlung
Einfache DokumenteHTML-zu-PDF mit externem Service
Komplexe FormulareSAP Forms Service by Adobe
Hohe VolumenAsynchrone Generierung mit bgPF
Offline-SzenarienHTML-Export mit Print-CSS
ComplianceZugriffsprotokollierung implementieren

Performance-Tipps

  1. Caching: Templates und statische Inhalte cachen
  2. Batch-Verarbeitung: Mehrere PDFs in einem Request generieren
  3. Komprimierung: PDF-Komprimierung aktivieren
  4. Asynchron: Grosse PDFs im Hintergrund generieren

Weiterfuehrende Themen

Zusammenfassung

PDF-Generierung in ABAP Cloud erfordert neue Ansaetze:

  1. XSL-FO fuer XML-basierte Transformation (mit externem FOP-Prozessor)
  2. SAP Forms Service by Adobe fuer komplexe Formulare mit Adobe-Templates
  3. Externe PDF-APIs fuer maximale Flexibilitaet (PDFShift, DocRaptor)
  4. RAP Actions integrieren PDF-Generierung nahtlos in Business Objects
  5. Fehlerbehandlung mit Retry-Logik fuer robuste Produktion

Die Wahl der richtigen Option haengt von deinen Anforderungen ab: Komplexitaet der Dokumente, vorhandene Templates, Volumen und Budget.