Blog SAPienteSAPiente
FormsRAPABAP Cloud

Como criar formulários customizados no S/4HANA Cloud Public Edition (Adobe Forms + RAP)

SAPiente5 de jun. de 2026· 8 min read

Para criar formulários customizados no S/4HANA Cloud Public Edition você tem dois caminhos: adaptar templates existentes via Key User (in-app, sem código) ou construirdo zero via Developer Extensibility, combinando Adobe Forms + RAP e usando o Adobe Document Services (ADS) embarcado — sem precisar de BTP externo. Neste guia, mostro o passo a passo de cada caminho e quando usar cada um.

Se você desenvolve ABAP no S/4HANA Cloud Public Edition, já sabe que muita coisa que era trivial no on-premise virou um quebra-cabeça. Quer imprimir um relatório customizado em PDF? Esquece o SMARTFORMS, esquece a API clássica de Adobe Forms (FP_JOB_OPEN e companhia), esquece o SE80. Tudo bloqueado no ABAP Cloud.

Recentemente precisei criar um "Romaneio de Entrada", aquela listagem de itens de uma remessa de entrada que o pessoal do armazém imprime pra conferir o recebimento físico. O sistema antigo cuspia isso numa matricial. A missão: recriar no S/4HANA Cloud Public.

Este artigo conta como montei a solução de ponta a ponta, que é justamente o que nenhum tutorial menciona. A arquitetura geral se inspira no material da comunidade SAP sobre Custom Adobe Forms com Developer Extensibility, mas aqui trago um caminho diferente no frontend (Fiori Elements List Report) e, principalmente, o relato honesto dos erros.

Por que não dá pra fazer "do jeito antigo"

No Public Cloud você não tem acesso ao sistema de arquivos, não pode chamar a API de função clássica do Adobe, não tem SE80 nem transações de design de formulário. O que você tem é:

  • Form Objects criados direto no ADT/Eclipse, que guardam o template XDP.

  • Adobe Document Services (ADS) embarcado no próprio tenant (a partir de uma nota SAP específica), acessível por classes liberadas: CL_FP_FORM_READER, CL_FP_FDP_SERVICES e CL_FP_ADS_UTIL. Não precisa de BTP, nem do Forms Service externo, nem de Communication Arrangement — o ADS roda dentro do tenant.

  • RAP para expor os dados e o PDF como serviço OData.

  • Adobe LiveCycle Designer (instalado localmente) para desenhar o layout.

Dois caminhos para customizar forms no Cloud (e por que escolhi um)

Antes de mergulhar, vale entender que existem duas rotas no Public Cloud, e escolher errado custa caro:

Rota 1 — Extensibilidade in-app (Key User). Você usa o app Maintain Form Templates, copia um template padrão de um documento (ex.: Purchase Order), edita o layout no Adobe LiveCycle, e — se faltar campo — cria Custom Fields (prefixo yy1) e Custom Logic (BAdI), transportando depois via Software Collections. É o caminho mais leve, sem ADT, ideal quando você quer adaptar uma saída que já existe.

Rota 2 — Developer Extensibility (Tier 2). Você desce pro ADT/Eclipse e constrói com RAP: Form Object, Data Provider, custom entity, classe de renderização. Mais trabalho, mas necessário quando você precisa de algo que não existe como saída padrão.

Por que fui de Developer Extensibility? Duas razões mataram a Rota 1 pro meu caso: (1) o romaneio precisa do saldo em estoque de cada item, que não está em nenhum data source de output padrão de remessa; e (2) não existe um "romaneio de entrada" standard pra copiar — é um relatório novo, com composição própria (cabeçalho + itens + estoque) e uma tela própria pra disparar a impressão. Quando o dado ou o form não existem no padrão, a Rota 1 não alcança — é RAP ou nada.

Se o seu caso for adaptar um form que já existe (mudar layout, somar um campo yy1), vá de Rota 1 é bem mais simples. O resto deste artigo é sobre a Rota 2.

Anatomia de um Form Object

No on-premise, um formulário era uma transação a um clique. No Public Cloud, sem as transações clássicas, tudo gira em torno do Form Object — e vale conhecer as peças antes de codar:

  • Form Object — o objeto criado no Form Editor do ADT. Gera formulários de impressão (e interativos) via ADS. É o "container" do layout.

  • XDP — o arquivo de layout, em formato Adobe XFA. É o desenho do formulário (cabeçalho, tabela, campos), feito no Adobe LiveCycle Designer. O ADT só consome o XDP; quem desenha é o LiveCycle.

  • ADS (Adobe Document Services) — o motor que renderiza o PDF, embarcado no tenant. Acionado pela classe liberada CL_FP_ADS_UTIL.

  • Data Provider — uma Service Definition que fornece os dados em XML pro formulário (os "RAP Data Services for Print Forms"). É dele que sai o XSD usado pra desenhar o layout. A API que lê esses dados é a CL_FP_FDP_SERVICES.

  • CL_FP_FORM_READER — em runtime, lê o layout XDP carregado (get_layout( ) devolve o XML como xstring).

Resumindo o pipeline: dados (XML) + layout (XDP) → ADS → PDF.

Pré-requisitos e ferramentas

  • S/4HANA Cloud Public Edition com perfil de Developer Extensibility (Tier 2).

  • ABAP Development Tools (ADT/Eclipse) — onde você cria CDS, classes, Form Object e service bindings.

  • Adobe LiveCycle Designer instalado localmente — edita o layout .xdp.

  • Adobe Document Services (ADS) embarcado no tenant (habilitado por nota SAP), acessível pelas classes liberadas CL_FP_FORM_READER, CL_FP_FDP_SERVICES e CL_FP_ADS_UTIL. Não precisa de BTP nem Forms Service externo.

  • Fiori tools (gerador de apps) — pro List Report do frontend.

A arquitetura, em quatro camadas

Antes do código, o desenho mental:

  1. CDS de dados — views normais que buscam os itens da remessa, o saldo de estoque agregado, e servem de matchcode (value help) pro número da remessa.

  2. Data Provider — uma CDS especial, marcada com #OUTPUT_FORM_DATA_PROVIDER e exposta por uma service definition. É dela que o Form Object gera o esquema XSD que você usa pra desenhar o layout.

  3. Form Object + XDP — o objeto no ADT que referencia o Data Provider e guarda o layout desenhado no LiveCycle Designer.

  4. RAP + Fiori — uma custom entity que expõe o PDF como "media stream" (anexo), uma classe que renderiza o PDF na hora da leitura, e um Fiori Elements List Report que o usuário enxerga.

Fluxo em runtime: o usuário filtra pelo número da remessa → o Fiori chama a custom entity → a classe de renderização busca o XML dos dados via Data Provider, injeta data/hora de impressão, lê o layout XDP e manda tudo pro ADS → o ADS devolve o PDF → o Fiori entrega como download.

diagrama-arquitetura-romaneio.svg

Mãos à obra

As CDS de dados

View de itens, juntando item da remessa com descrição do produto e saldo:

@AccessControl.authorizationCheck: #CHECK
define view entity ZI_RomaneioInb_Item
  as select from I_DeliveryDocumentItem as Item
  association [0..1] to ZI_RomaneioInb_Stock as _Stock
    on _Stock.Material = Item.Material and _Stock.Plant = Item.Plant
  association [0..1] to I_ProductDescription as _ProdText
    on _ProdText.Product = Item.Material and _ProdText.Language = $session.system_language
{
  key Item.DeliveryDocument         as DeliveryDocument,
  key Item.DeliveryDocumentItem     as DeliveryDocumentItem,
      Item.Material                 as Codigo,
      _ProdText.ProductDescription  as DescProduto,
      Item.DeliveryQuantityUnit     as Un,
      Item.ActualDeliveryQuantity   as Qtde,
      Item.WarehouseStorageBin      as Endereco,
      _Stock.Saldo                  as Saldo,
      Item.CreationDate             as DtRef,
      Item.Plant                    as Plant
}

O Data Provider (a peça que confunde)

O Form Object precisa de um Data Provider pra gerar o XSD. É uma CDS root view entity com a capability de provedor de formulário:

"Annottation obrigatório para a CDS para que ela atue como um provedor de dados para o objeto FORM.
@ObjectModel.supportedCapabilities: [ #OUTPUT_FORM_DATA_PROVIDER ]
define root view entity ZI_RomaneioInb_FormHdr
  as select from I_DeliveryDocument as Delivery
  association [0..*] to ZI_RomaneioInb_FormItem as _Item
    on $projection.DeliveryDocument = _Item.DeliveryDocument
{
  key Delivery.DeliveryDocument       as DeliveryDocument,
      cast( '' as abap.char( 10 ) )   as Filial,    // shape-only
      cast( '' as abap.char( 10 ) )   as DtRef,     // (valores reais
      cast( '' as abap.char( 10 ) )   as Emissao,   //  injetados em runtime)
      cast( '' as abap.char( 8 ) )    as Hora,
      cast( 0  as abap.dec( 15, 3 ) ) as Total,
      _Item
}
where Delivery.SDDocumentCategory = '7'

A hierarquia cabeçalho → itens é uma associação exposta (_Item), não composition. A service definition do Data Provider não precisa de service binding:

@ObjectModel.leadingEntity.name: 'ZI_RomaneioInb_FormHdr'
define service ZSR_RomaneioInb_Form {
  expose ZI_RomaneioInb_FormHdr;
}

O Form Object e o layout

No ADT, crie um Form Object pelo creation wizard, atribua uma transport request, aponte o "Data Provider" pra service definition e use Download Schema pra gerar o XSD. Importe esse XSD no LiveCycle como "New Data Connection → XML Schema".

Após inserir o Serviço faça o Download do Schema para criar o Layout.

Após abrir o XDP no ALC, você terá na aba de visualização de dados, todos os dados que você estruturou na CDS.

Após finalizar o desenho do layout, salve e faça Upload Layout no Form Object e ative.

O PDF como anexo + o frontend sem código

Em vez de devolver base64 e decodificar com JavaScript, exponha o PDF como large object numa custom entity:

@ObjectModel.query.implementedBy: 'ABAP:ZCL_ROMANEIO_INB_QUERY'
@UI.headerInfo: { typeName: 'Romaneio', typeNamePlural: 'Romaneios' }
define custom entity ZCE_RomaneioInb_App
{
  @UI.selectionField: [{ position: 10 }]
  @Consumption.filter: { selectionType: #SINGLE, mandatory: true }
  @Consumption.valueHelpDefinition: [{
    entity: { name: 'ZI_RomaneioInb_DeliveryVH', element: 'DeliveryDocument' } }]
  key DeliveryDocument : abap.char( 10 );

  @UI.lineItem: [{ position: 20, label: 'Arquivo' }]
  Filename : abap.char( 80 );
  Mimetype : abap.char( 50 );

  @UI.lineItem: [{ position: 30, label: 'Romaneio' }]
  @Semantics.largeObject: { mimeType: 'Mimetype', fileName: 'Filename',
                            contentDispositionPreference: #ATTACHMENT }
  Form     : abap.rawstring;
}

A @Semantics.largeObject com #ATTACHMENT faz o Form virar um link de download direto. O @Consumption.valueHelpDefinition dá o matchcode F4 automático. E o frontend é um Fiori Elements List Report gerado pelo Fiori tools — sem uma linha de JavaScript: filtro, matchcode, validação, tabela e download vêm das annotations. A única config importante no manifest.json é "initialLoad": "Disabled".

A classe de renderização busca o XML via FDP, injeta data/hora e chama o ADS:

DATA(lo_fdp) = cl_fp_fdp_services=>get_instance(
                 iv_service_definition = c_service_def
                 iv_root_node          = 'ZI_RomaneioInb_FormHdr' ).
DATA(lt_keys) = lo_fdp->get_keys( ).
lt_keys[ name = 'DELIVERYDOCUMENT' ]-value = |{ iv_delivery ALPHA = IN }|.
DATA(lv_xml) = lo_fdp->read_to_xml_v2( it_select = lt_keys iv_language = sy-langu ).
" ... injeta Emissao/Hora/DtRef no XML ...
DATA(lv_layout) = cl_fp_form_reader=>create_form_reader( c_form_name )->get_layout( ).
cl_fp_ads_util=>render_pdf( EXPORTING iv_xml_data   = lv_xml
                                      iv_xdp_layout = lv_layout
                                      iv_locale     = 'pt_BR'
                            IMPORTING ev_pdf        = DATA(lv_pdf) ... ).

A query provider lê a chave do filtro, chama esse render e devolve a linha com o xstring do PDF no campo Form.

Caminho alternativo de download (#WITH_URL). Se o link do largeObject não renderizar como você quer, dá pra expor um campo FormURL com a URL do serviço montada (.../RomaneioInb('<numero>')/Form) e marcá-lo com @UI.lineItem: [{ type: #WITH_URL, url: 'FormURL' }]. Vira um hyperlink de texto explícito na linha — um segundo caminho ao lado do stream.

As classes ABAP

São três: a exceção, a classe de renderização (o coração da solução) e a query

provider que liga o Fiori à renderização.

A exceção (ZCX_ROMANEIO_INB) — uma exceção checada simples, para empacotar

qualquer falha do ADS/FDP com mensagem amigável:

CLASS zcx_romaneio_inb DEFINITION
  PUBLIC
  INHERITING FROM cx_static_check
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_t100_message.
    METHODS constructor
      IMPORTING textid   LIKE if_t100_message=>t100key OPTIONAL
                previous LIKE previous                 OPTIONAL.
ENDCLASS.

CLASS zcx_romaneio_inb IMPLEMENTATION.
  METHOD constructor.
    super->constructor( previous = previous ).
    me->if_t100_message~t100key = COND #(
      WHEN textid IS INITIAL THEN if_t100_message=>default_textid
      ELSE textid ).
  ENDMETHOD.
ENDCLASS.

A classe de renderização (ZCL_ROMANEIO_INB_FORM) — busca o XML via FDP

injeta Emissão/Hora/DtRef no XML, lê o layout XDP e chama o ADS.

CLASS zcl_romaneio_inb_form DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_result,
             filename TYPE c LENGTH 80,
             mimetype TYPE c LENGTH 50,
             form     TYPE xstring,
           END OF ty_result.

    METHODS render
      IMPORTING iv_delivery      TYPE c
      RETURNING VALUE(rs_result) TYPE ty_result
      RAISING   zcx_romaneio_inb.

  PRIVATE SECTION.
    CONSTANTS c_form_name   TYPE c LENGTH 30 VALUE 'ZROMANEIO_INBOUND'.
    CONSTANTS c_service_def TYPE c LENGTH 40 VALUE 'ZSR_ROMANEIOINB_FORM'.
    CONSTANTS c_locale      TYPE string      VALUE 'pt_BR'.
ENDCLASS.

CLASS zcl_romaneio_inb_form IMPLEMENTATION.
  METHOD render.
    TRY.
        " 1) XML via FDP (note o iv_root_node explicito e o ALPHA na chave)
        DATA(lo_fdp) = cl_fp_fdp_services=>get_instance(
                         iv_service_definition = c_service_def
                         iv_root_node          = 'ZI_RomaneioInb_FormHdr' ).

        DATA(lt_keys) = lo_fdp->get_keys( ).
        DATA lv_delivery_alpha TYPE c LENGTH 10.
        lv_delivery_alpha = |{ iv_delivery ALPHA = IN }|.
        lt_keys[ name = 'DELIVERYDOCUMENT' ]-value = lv_delivery_alpha.

        DATA(lv_data_xml) = lo_fdp->read_to_xml_v2(
                              it_select   = lt_keys
                              iv_language = sy-langu ).

        " 2) Injeta Emissao/Hora/DtRef (CDS nao tem hora de sessao nem format de data)
        DATA(lv_date) = cl_abap_context_info=>get_system_date( ).
        DATA(lv_time) = cl_abap_context_info=>get_system_time( ).

        DATA lv_creation TYPE d.
        SELECT SINGLE creationdate FROM i_deliverydocument
          WHERE deliverydocument = @lv_delivery_alpha
          INTO @lv_creation.

        DATA(lv_emissao) = |{ lv_date DATE = USER }|.
        DATA(lv_hora)    = |{ lv_time(2) }:{ lv_time+2(2) }:{ lv_time+4(2) }|.
        DATA(lv_dtref)   = |{ lv_creation DATE = USER }|.

        DATA lv_str TYPE string.
        lv_str = cl_abap_conv_codepage=>create_in( codepage = `UTF-8`
                                                )->convert( lv_data_xml ).

        lv_str = replace( val = lv_str sub = `<Emissao></Emissao>` with = |<Emissao>{ lv_emissao }</Emissao>| ).
        lv_str = replace( val = lv_str sub = `<Emissao/>`          with = |<Emissao>{ lv_emissao }</Emissao>| ).
        lv_str = replace( val = lv_str sub = `<Hora></Hora>`       with = |<Hora>{ lv_hora }</Hora>| ).
        lv_str = replace( val = lv_str sub = `<Hora/>`             with = |<Hora>{ lv_hora }</Hora>| ).
        lv_str = replace( val = lv_str sub = `<DtRef></DtRef>`     with = |<DtRef>{ lv_dtref }</DtRef>| ).
        lv_str = replace( val = lv_str sub = `<DtRef/>`            with = |<DtRef>{ lv_dtref }</DtRef>| ).

        lv_data_xml = cl_abap_conv_codepage=>create_out( codepage = `UTF-8`
                                                )->convert( lv_str ).

        " 3) Layout XDP
        DATA(lo_reader) = cl_fp_form_reader=>create_form_reader( c_form_name ).
        DATA(lv_layout) = lo_reader->get_layout( ).

        " 4) Render via ADS
        DATA lv_pdf   TYPE xstring.
        DATA lv_pages TYPE i.
        DATA lv_trace TYPE string.

        cl_fp_ads_util=>render_pdf(
          EXPORTING iv_xml_data     = lv_data_xml
                    iv_xdp_layout   = lv_layout
                    iv_locale       = c_locale
          IMPORTING ev_pdf          = lv_pdf
                    ev_pages        = lv_pages
                    ev_trace_string = lv_trace ).

        rs_result-form     = lv_pdf.
        rs_result-mimetype = 'application/pdf'.
        rs_result-filename = |Romaneio_{ iv_delivery }.pdf|.

      CATCH cx_root INTO DATA(lx_err).
        RAISE EXCEPTION TYPE zcx_romaneio_inb EXPORTING previous = lx_err.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

A query provider (ZCL_ROMANEIO_INB_QUERY) — implementa

IF_RAP_QUERY_PROVIDER, lê o número da remessa do filtro, chama o render e devolve

a linha.

CLASS zcl_romaneio_inb_query DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_rap_query_provider.
ENDCLASS.

CLASS zcl_romaneio_inb_query IMPLEMENTATION.
  METHOD if_rap_query_provider~select.

    " Consumir paging e sort, mesmo sem usar
    DATA(lo_paging) = io_request->get_paging( ).
    DATA(lv_top)    = lo_paging->get_page_size( ).
    DATA(lv_skip)   = lo_paging->get_offset( ).
    DATA(lt_sort)   = io_request->get_sort_elements( ).

    " Le o numero da remessa do filtro
    DATA lv_delivery TYPE c LENGTH 10.
    TRY.
        DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
      CATCH cx_rap_query_filter_no_range.
        io_response->set_total_number_of_records( 0 ).
        RETURN.
    ENDTRY.

    LOOP AT lt_filter INTO DATA(ls_filter).
      IF ls_filter-name = 'DELIVERYDOCUMENT'.
        READ TABLE ls_filter-range INTO DATA(ls_range) INDEX 1.
        IF sy-subrc = 0.
          lv_delivery = ls_range-low.
        ENDIF.
      ENDIF.
    ENDLOOP.

    DATA lt_result TYPE STANDARD TABLE OF zce_romaneioinb_app.

    IF lv_delivery IS NOT INITIAL.
      TRY.
          DATA(ls_pdf) = NEW zcl_romaneio_inb_form( )->render( lv_delivery ).
          APPEND VALUE #( deliverydocument = lv_delivery
                          filename         = ls_pdf-filename
                          mimetype         = ls_pdf-mimetype
                          form             = ls_pdf-form ) TO lt_result.
        CATCH zcx_romaneio_inb INTO DATA(lx_err).
          " loga em SLG1 se quiser; retorna vazio para nao quebrar a UI
      ENDTRY.
    ENDIF.

    IF io_request->is_total_numb_of_rec_requested( ).
      io_response->set_total_number_of_records( lines( lt_result ) ).
    ENDIF.

    IF io_request->is_data_requested( ).
      io_response->set_data( lt_result ).
    ENDIF.

  ENDMETHOD.
ENDCLASS.

O fluxo fecha aqui: o List Report chama a custom entity → a query provider lê a

chave → a classe de renderização monta o PDF → o xstring volta no campo Form →

o navegador baixa.

Testando o APP

Como a CDS custom Entity nesse caso precisa de um input de valor para realizar as buscas, primeiro precisa informar o número da Remessa para depois dar o search

E ao dar o search ele trás a linha com o pdf pronto para download.

Alguns Pontos de Atenção

Primeiro é que aqui não entrei nos detalhes de construção do app/ui, como metadata extension, service binding para focar apenas no funcionamento da geração do form.

Oustros pontos que ocorreram, foi que cheguei a ver a tela retornar "Nenhum resultado encontrado" e quase achei que a abordagem estava errada. Não estava — eram pegadinhas, cada uma com sintoma enganoso.

1. O query provider precisa consumir o paging. O Fiori sempre manda paginação. Se o IF_RAP_QUERY_PROVIDER~select não chamar io_request->get_paging( ), o runtime acusa Query not fully covered: get_paging missing. Não precisa usar — só tocar:

DATA(lo_paging) = io_request->get_paging( ).
DATA(lv_top)    = lo_paging->get_page_size( ).
DATA(lv_skip)   = lo_paging->get_offset( ).

2. O FDP precisa do root node explícito. Sem o iv_root_node, o read_to_xml_v2 estoura com CX_SADL_GW_V4_NOT_FOUND — Resource not found for entity. Parece problema de nome de objeto, mas é a detecção automática do nó raiz que falha. Passe o nome da CDS de cabeçalho explicitamente.

Conclusão e próximos passos

Gerar PDF customizado no Public Cloud é viável, mas é um quebra-cabeça de peças que vivem em documentações separadas. A escolha entre Key User e Developer Extensibility define metade do trabalho: se o dado ou o form não existem no padrão, é RAP. Usar List Report em vez de service binding Web API trouxe matchcode, validação e download de graça, sem JavaScript.

Mas o que mais economiza tempo de quem vier depois são as armadilhas: consumir o paging, passar o root node, conformar o ALPHA, e desconfiar de campos de cabeçalho vazios.

Onde ir além: o mesmo Form Object pode gerar interactive forms (PDFs editáveis), e o serviço OData do PDF pode ser consumido por outros apps (não só o List Report) — qualquer app que monte a URL .../RomaneioInb('<num>')/Form baixa o documento. Dá também pra evoluir o layout com logos, código de barras e agrupamentos por centro.


Conteúdo baseado em uma implementação real. A rota de Developer Extensibility se apoia no guia "Custom Adobe Forms in SAP S/4 HANA Public Cloud using Developer Extensibility" (YogiPavan, SAP Community); o contraste com a rota in-app (Key User) toma como referência "Building Custom Form in SAP Cloud" (SAP Community). O caminho de frontend em List Report e todo o relato de troubleshooting são da minha própria experiência. Use prints do seu próprio tenant.

TagsAdobe FormsClean CoreDesenvolvedorSAP Pubic Cloud
Avalie este conteúdo
para avaliar
SAPiente
@sapiente

Blog sobre SAP, ABAP, BTP, Fiori e tudo que envolve o ecossistema SAP.

Ver perfil →
FacebookInstagramYouTubeTwitter

Comentários 0

Entre na conversa

Faça login para deixar seu comentário neste artigo.

Ainda sem comentários

Seja o primeiro a comentar este artigo.