Como criar formulários customizados no S/4HANA Cloud Public Edition (Adobe Forms + RAP)
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_SERVICESeCL_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 comoxstring).
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_SERVICESeCL_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:
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.
Data Provider — uma CDS especial, marcada com
#OUTPUT_FORM_DATA_PROVIDERe exposta por uma service definition. É dela que o Form Object gera o esquema XSD que você usa pra desenhar o layout.Form Object + XDP — o objeto no ADT que referencia o Data Provider e guarda o layout desenhado no LiveCycle Designer.
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.
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 campoFormURLcom 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.
Comentários 0
Ainda sem comentários
Seja o primeiro a comentar este artigo.
Entre na conversa
Faça login para deixar seu comentário neste artigo.