Blog SAPienteSAPiente
RAP

Upload de arquivos no RAP: como construir um app de upload (CSV/XML) do zero

SAPiente11 de jun. de 2026· 10 min read

Como o Fiori sabe mostrar um botão de upload?

Você tem uma app RAP com Fiori Elements e precisa que o usuário suba um arquivo — um CSV, um XML, uma planilha. A boa notícia: o RAP já tem tudo para isso. Você não escreve nenhuma linha de JavaScript. O controle de upload aparece sozinho na tela quando você marca o campo certo com a annotation certa.

O segredo está em três peças que conversam entre si: um campo RawString (o xstring que guarda os bytes do arquivo), a annotation @Semantics.largeObject na CDS de consumo, e uma action no behavior que lê esse xstring e faz algo útil com ele. Este guia constrói o app inteiro a partir de um caso real: o Upload_CTE_RAP, que recebe um CSV de CTe (Conhecimento de Transporte Eletrônico), valida e grava os itens. Todo o código abaixo é verbatim do projeto.

O essencial do upload no RAP

  • Tabela: um campo attachment : abap.rawstring para os bytes, mais mimetype e file_name.

  • CDS: @Semantics.mimeType: true na interface e @Semantics.largeObject{ ... } na consumption — é isso que cria o controle de upload.

  • Behavior: with draft, campo Attachment mandatory, determination para nome/status e uma action de processamento.

  • Handler: ler o Attachment (xstring), converter para string com cl_abap_conv_codepage e processar.

Quais são as peças do app de upload?

Antes do código, o mapa. Um app de upload no RAP é a stack RAP normal — tabela, CDS, behavior, service — com dois detalhes específicos: o campo binário na tabela e a annotation largeObject na CDS de consumo. O resto é RAP managed com draft, como qualquer app transacional.

upload-de-arquivos-no-rap-arquitetura.svg

A stack de upload no RAP — o controle no Fiori nasce da annotation @Semantics.largeObject.

No exemplo do CTe há ainda uma entidade filha (os itens lidos do CSV) ligada por composition. O arquivo é gravado no header; a action lê o arquivo, faz o parse e cria os itens. Esse é o padrão que se repete em quase todo upload: cabeçalho guarda o arquivo, filhos guardam o conteúdo estruturado.

Passo 1: As tabelas (header e itens)

Tudo começa na tabela. O campo que guarda o arquivo é um abap.rawstring — um xstring, ou seja, uma sequência de bytes de tamanho variável. Ao lado dele, dois campos de apoio: o mimetype (tipo do arquivo) e o file_name (nome original). Sem esses três, não há upload.

A tabela de cabeçalho, ztcte_up_file, guarda o arquivo e os dados de carga:

@EndUserText.label : 'Tabela de Arquivos para Upload de CTe'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table ztcte_up_file {

  key client            : abap.clnt not null;
  key upload_id         : sysuuid_x16 not null;
  file_name             : abap.char(255);
  file_status           : abap.char(50);
  empresa               : bukrs;
  centro                : werks_d;
  material              : matnr;
  fornecedor            : lifnr;
  cte_complementar      : abap_boolean;
  attachment            : abap.rawstring(0);
  mimetype              : abap.char(128);
  messagem_log          : abap.string(0);

  // Campos de controle managed
  created_at            : abp_creation_tstmpl;
  created_by            : abp_creation_user;
  last_changed_by       : abp_lastchange_user;
  last_changed_at       : abp_lastchange_tstmpl;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;

}

Por que rawstring e não string? Um arquivo é binário. Mesmo um CSV "de texto" chega como bytes, e a codificação (UTF-8, Windows-1252, codepage 1160…) só é resolvida na hora de ler. Guardar como xstring preserva o conteúdo exato; a conversão para texto fica por conta do handler, no momento certo. Detalhe de portabilidade: aqui o tipo embutido abap.rawstring(0) é usado direto no DDL (desenvolvimento on-stack/ADT); no ABAP Cloud (BTP) é comum criar um domínio próprio com tipo RAWSTRING e comprimento 0.

A tabela de itens, ztcte_up_items, recebe o conteúdo já estruturado — uma linha por registro lido do arquivo, com os tipos corretos (datas como dats, valores como dec(16,2)):

@EndUserText.label : 'Tabela de Itens de CTE'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table ztcte_up_items {

  key client            : abap.clnt not null;
  key upload_id         : sysuuid_x16 not null;
  key item_id           : abap.numc(10) not null;
  chave_cte             : abap.char(44);
  data_emissao_cte      : abap.dats;
  valor_faturar_total   : abap.dec(16,2);
  protocolo_autorizacao : abap.char(15);
  uf_destino            : abap.char(2);
  cep                   : abap.char(8);
  icms                  : abap.dec(16,2);
  processing_status     : abap.char(25);
  nf_number             : j_1bnfnumb;
  nf_year               : gjahr;

  // Campos de controle managed
  created_at            : abp_creation_tstmpl;
  created_by            : abp_creation_user;
  last_changed_by       : abp_lastchange_user;
  last_changed_at       : abp_lastchange_tstmpl;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
}

Passo 2: As CDS e a annotation que cria o upload

Aqui mora o detalhe que faz o Fiori desenhar o controle de upload. São duas camadas: a composite/interface, que apenas expõe os campos e marca o mimetype; e a consumption, que aplica a @Semantics.largeObject. Sem essa annotation, o campo binário seria invisível na UI.

A CDS composite do header (interface)

A view de interface expõe o attachment como um campo normal, marca o mimetype com @Semantics.mimeType: true e calcula um CriticalityStatus a partir do texto do status (é o número que vira cor na UI). Note também a composition com a entidade-filha e os campos de auditoria com suas @Semantics.

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Composite - Arquivo CTe Upload'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
    serviceQuality: #X,
    sizeCategory: #S,
    dataClass: #MIXED
}
define root view entity ZI_CTE_UPLOAD_FILE
  as select from ztcte_up_file
  composition [0..*] of ZI_CTE_UPLOAD_ITEMS    as _CteItems
{
  key upload_id             as UploadId,
      file_name             as FileName,
      empresa               as Empresa,
      centro                as Centro,
      material              as Material,
      fornecedor            as Fornecedor,
      cte_complementar      as CteComplementar,
      attachment            as Attachment,
      messagem_log          as MessagemLog,
      file_status           as Filestatus,

      case file_status
      when 'Arquivo com erro'           then 1
      when 'Nenhum arquivo selecionado' then 2
      when 'CSV Carregado'              then 3
      when 'Pronto para upload'         then 5
      else 0
      end                   as CriticalityStatus,

      // annotation necessária
      @Semantics.mimeType: true
      mimetype              as Mimetype,

      @Semantics.user.createdBy: true
      created_by            as CreatedBy,

      @Semantics.systemDateTime.createdAt: true
      created_at            as CreatedAt,

      @Semantics.user.lastChangedBy: true
      last_changed_by       as LastChangedBy,

      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at as LocalLastChangedAt,

      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at       as LastChangedAt,

      _CteItems
}

A CDS de consumo do header (largeObject)

Esta é a peça-chave. A @Semantics.largeObject diz ao Fiori Elements: "este campo é um arquivo; mostre um controle de upload/download". Os atributos definem o comportamento: mimeType e fileName apontam para os campos correspondentes, acceptableMimeTypes restringe os formatos aceitos, e contentDispositionPreference: #INLINE faz o arquivo abrir no navegador em vez de forçar download. Os @Consumption.valueHelpDefinition ligam os campos de negócio aos seus value helps.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Consumption - Arquivo CSV para upload de CTe'
@Metadata.allowExtensions: true
@Metadata.ignorePropagatedAnnotations: false
@Search.searchable: true

define root view entity ZC_CTE_UPLOAD_FILE
  provider contract transactional_query
  as projection on ZI_CTE_UPLOAD_FILE

{
  key UploadId,

      FileName,
      Filestatus,
      CriticalityStatus,

      @Semantics.largeObject: { mimeType: 'Mimetype',
                                fileName: 'FileName',
                                acceptableMimeTypes: [ 'text/csv',
                                                       'application/vnd.ms-excel',
                                                       'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ],
                                contentDispositionPreference: #INLINE }
      Attachment,

      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      @Consumption.valueHelpDefinition: [
        { entity: { name: 'I_CompanyCode', element: 'CompanyCode' } }
      ]
      Empresa,

      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      @Consumption.valueHelpDefinition: [
        { entity: { name: 'I_Plant', element: 'Plant' } }
      ]
      Centro,

      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      @Consumption.valueHelpDefinition: [
        { entity: { name: 'I_Material', element: 'Material' } }
      ]
      Material,

      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      @Consumption.valueHelpDefinition: [
        { entity: { name: 'ZI_TRANS_CTE', element: 'ContaFornecedor' } }
      ]
      Fornecedor,
      CteComplementar,
      MessagemLog,
      Mimetype,
      CreatedBy,
      CreatedAt,
      LastChangedBy,
      LocalLastChangedAt,
      LastChangedAt,
      /* Associations */
      _CteItems : redirected to composition child ZC_CTE_UPLOAD_ITEMS

}

O mimeType e o fileName apontam para CAMPOS, não para valores. Eles recebem o nome do elemento da própria view ('Mimetype', 'FileName') — é de lá que o Fiori lê o tipo e o nome ao baixar o arquivo. Trocar por uma string literal qualquer quebra o download. E o acceptableMimeTypes é o que limita o seletor de arquivos do usuário aos formatos certos.

As CDS da entidade-filha (itens)

A entidade-filha é uma CDS comum, ligada ao header por uma association to parent. A interface:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Basic - Itens CSV para upload de CTe'
@Metadata.ignorePropagatedAnnotations: false
@ObjectModel.usageType: {
    serviceQuality: #X,
    sizeCategory: #S,
    dataClass: #MIXED
  }

define view entity ZI_CTE_UPLOAD_ITEMS
  as select from ztcte_up_items

  association to parent ZI_CTE_UPLOAD_FILE as _CteFile on $projection.UploadId = _CteFile.UploadId

{
  key upload_id             as UploadId,
  key item_id               as ItemId,

      chave_cte             as ChaveCte,
      data_emissao_cte      as DataEmissaoCte,
      valor_faturar_total   as ValorFaturarTotal,
      protocolo_autorizacao as ProtocoloAutorizacao,
      uf_destino            as UfDestino,
      cep                   as Cep,
      icms                  as Icms,
      processing_status     as ProcessingStatus,
      nf_number             as NfNumber,
      nf_year               as NfYear,

      @Semantics.user.createdBy: true
      created_by            as CreatedBy,

      @Semantics.systemDateTime.createdAt: true
      created_at            as CreatedAt,

      @Semantics.user.lastChangedBy: true
      last_changed_by       as LastChangedBy,

      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at as LocalLastChangedAt,

      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at       as LastChangedAt,

      _CteFile
}

E a projeção de consumo dos itens, com o redirected to parent:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Consumption - Itens CSV para upload de CTe'
@Metadata.ignorePropagatedAnnotations: false
@Metadata.allowExtensions: true
define view entity ZC_CTE_UPLOAD_ITEMS as projection on ZI_CTE_UPLOAD_ITEMS
{
  key UploadId,
  key ItemId,
      ChaveCte,
      DataEmissaoCte,
      ValorFaturarTotal,
      ProtocoloAutorizacao,
      UfDestino,
      Cep,
      Icms,
      ProcessingStatus,
      NfNumber,
      NfYear,
      CreatedBy,
      CreatedAt,
      LastChangedBy,
      LocalLastChangedAt,
      LastChangedAt,
      /* Associations */
      _CteFile: redirected to parent ZC_CTE_UPLOAD_FILE
}

Passo 3: O behavior — draft, campo obrigatório e a action

O behavior amarra as regras. Ele é managed, com draft (o usuário monta o registro, anexa o arquivo e só depois ativa). Três pontos importam para o upload: o Attachment e o Mimetype são mandatory; há determinations que reagem ao anexo; e existe uma action que processa o arquivo. Abaixo, a definição completa — header e itens.

managed implementation in class zbp_i_cte_upload_file unique;
strict ( 2 );
with draft;

define behavior for ZI_CTE_UPLOAD_FILE alias FileCTe
persistent table ztcte_up_file
lock master
total etag LastChangedAt
draft table zcte_up_file_d
authorization master ( instance )
etag master LocalLastChangedAt
{
  create;
  update;
  delete;

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

  field ( numbering : managed, readonly ) UploadId;
  field ( mandatory ) Attachment, Mimetype, Empresa, Fornecedor, Material, Centro;
  field ( readonly ) CreatedBy, CreatedAt, LocalLastChangedAt, FileStatus, MessagemLog;

  association _CteItems { create; with draft; }

  action ( features : instance ) uploadCsv result [1] $self;

  determination SetInitialFileStatus on modify { create; field Attachment; }
  determination SetFileName on modify { create; field Attachment; }

  validation ValidateMandatoryFields on save { create; field Empresa, Centro, Material, Fornecedor; }

  side effects
  {

    field Attachment affects field FileStatus, entity _CteItems;
    field Fornecedor affects messages;
    action uploadCsv affects $self, entity _CteItems, messages;

  }

  mapping for ztcte_up_file
    {
      UploadID           = upload_id;
      FileName           = file_name;
      FileStatus         = file_status;
      Attachment         = attachment;
      Empresa            = empresa;
      Centro             = centro;
      Material           = material;
      Fornecedor         = fornecedor;
      CteComplementar    = cte_complementar;
      MessagemLog        = messagem_log;
      MimeType           = mimetype;
      CreatedBy          = created_by;
      CreatedAt          = created_at;
      LastChangedBy      = last_changed_by;
      LocalLastChangedAt = local_last_changed_at;
      LastChangedAt      = last_changed_at;
    }

}

define behavior for ZI_CTE_UPLOAD_ITEMS alias CTeItem
implementation in class zbp_i_cte_upload_items unique
persistent table ztcte_up_items
draft table zcte_up_items_d
lock dependent by _CteFile
authorization dependent by _CteFile
{
  update;
  delete;

  field ( readonly ) UploadId, ItemID, ChaveCte, DataEmissaoCte,
                     ValorFaturarTotal, ProtocoloAutorizacao,
                     Icms, NfNumber, NfYear, Cep, UfDestino, ProcessingStatus,
                     CreatedAt, CreatedBy;

  association _CteFile { with draft; }

 // Chama BAPI
  action processData result [1] $self;

  side effects
  {
    action processData affects $self, entity _CteFile, messages;
  }
  mapping for ztcte_up_items
    {
      UploadID             = upload_id;
      ItemID               = item_id;
      ChaveCTe             = chave_cte;
      DataEmissaoCTe       = data_emissao_cte;
      ValorFaturarTotal    = valor_faturar_total;
      ProtocoloAutorizacao = protocolo_autorizacao;
      Icms                 = icms;
      ProcessingStatus     = processing_status;
      NFNumber             = nf_number;
      NFYear               = nf_year;
      Cep                  = cep;
      UfDestino            = uf_destino;
      CreatedBy            = created_by;
      CreatedAt            = created_at;
      LastChangedBy        = last_changed_by;
      LocalLastChangedAt   = local_last_changed_at;
      LastChangedAt        = last_changed_at;
    }
}

Repare nos side effects: quando o campo Attachment muda, o framework sabe que precisa recalcular o FileStatus e a entidade filha _CteItems. É isso que atualiza a tela na hora certa, sem refresh manual. E a action uploadCsv declara que afeta $self, os itens e as mensagens — o Fiori então recarrega tudo isso após a execução.

Por que duas determinations no mesmo gatilho? Ambas disparam em create + field Attachment. SetFileName grava o nome do arquivo anexado; SetInitialFileStatus define o status inicial ("Pronto para upload" assim que há anexo, "Nenhum arquivo selecionado" quando vazio). Separar responsabilidades deixa cada determination simples e testável.

Passo 4: A metadata extension que desenha a tela

As annotations de UI ficam numa metadata extension, separadas da CDS de consumo (note o @Metadata.allowExtensions: true lá no Passo 2). Para o upload, dois trechos importam: a @UI.identification no campo Attachment, que renderiza o controle de seleção de arquivo, e a @UI.identification do tipo #FOR_ACTION, que cria o botão que chama a action. Abaixo, a metadata extension completa do header.

@Metadata.layer: #CORE

@UI.headerInfo: { typeName: 'Nome do Arquivo',
                  typeNamePlural: 'Nomes dos Arquivos',
                  typeImageUrl: 'sap-icon://attachment',
                  title: { type: #STANDARD, value: 'FileName' } }

@UI.lineItem: [ { criticality: 'CriticalityStatus' } ] // Destaca a cor do crit. no inicio da linha

annotate entity ZC_CTE_UPLOAD_FILE with

{
  @UI.facet: [
  {
    id: 'idEndUser',
    purpose: #HEADER,
    position: 10,
    type: #DATAPOINT_REFERENCE, // Adiciona campo no Header
    targetQualifier: 'tqEndUser'
  },
  {
    id: 'idStatus',
    purpose: #HEADER,
    position: 20,
    type: #DATAPOINT_REFERENCE,
    targetQualifier: 'tqStatus'
  },
  {
    id: 'idChgAt',
    purpose: #HEADER,
    position: 30,
    type: #DATAPOINT_REFERENCE,
    targetQualifier: 'tqChgAt'
  },
  {
    id: 'idGeneralInfo',
    purpose: #STANDARD,
    label: 'Informações Gerais de Carga',
    type: #COLLECTION,
    position: 10
  },
  {
     id: 'idData', 
     purpose: #STANDARD,
     position: 20,
     label: 'Dados do Arquivo',
     type: #LINEITEM_REFERENCE, // Adiciona a tabela filha no Obj. Page
     targetElement: '_CteItems'
  },
  {
     id: 'idDemandDetails',
     parentId: 'idGeneralInfo',
     type: #FIELDGROUP_REFERENCE, // Cria uma área separada agrupando campos
     label: 'Detalhes da Carga',
     position: 10,
     targetQualifier: 'idDemandDetailsGroup'
  },
  {
     id: 'idAdminData',
     parentId: 'idGeneralInfo',
     type: #FIELDGROUP_REFERENCE,
     label: 'Dados Administrativos',
     position: 20,
     targetQualifier: 'idAdminDataGroup'
     }
  ]

  //──────────────────────────────────────────────────────────
  // Id
  //──────────────────────────────────────────────────────────
  @UI.identification: [ { type: #FOR_ACTION, label: 'Carregar Dados', dataAction: 'uploadCsv' } ]
  UploadId;

  //──────────────────────────────────────────────────────────
  // Empresa
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Empresa'
  @UI.selectionField: [ { position: 10 } ]  // Posição Filtros de Pesquisa
  @UI.lineItem:       [ { position: 10, label: 'Empresa' } ]  // Posição Grid Principal
  @UI.identification: [ { position: 10, label: 'Empresa' } ]  // Posição Object Page
  @UI.fieldGroup:     [ { qualifier: 'idDemandDetailsGroup', position: 10 } ] // Posição Posição Agrupamentos do Obj.Page
  Empresa;

  //──────────────────────────────────────────────────────────
  // Centro
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Centro'
  @UI.selectionField: [ { position: 20 } ]
  @UI.lineItem:       [ { position: 20, label: 'Centro' } ]
  @UI.identification: [ { position: 20, label: 'Centro' } ]
  @UI.fieldGroup:     [ { qualifier: 'idDemandDetailsGroup', position: 20 } ]
  Centro;

  //──────────────────────────────────────────────────────────
  // Material
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Material'
  @UI.selectionField: [ { position: 30 } ]
  @UI.lineItem:       [ { position: 30, label: 'Material' } ]
  @UI.identification: [ { position: 30, label: 'Material' } ]
  @UI.fieldGroup:     [ { qualifier: 'idDemandDetailsGroup', position: 30 } ]
  Material;

  //──────────────────────────────────────────────────────────
  // Transportadora
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Transportadora'
  @UI.selectionField: [ { position: 40 } ]
  @UI.lineItem:       [ { position: 40, label: 'Transportadora' } ]
  @UI.identification: [ { position: 40, label: 'Transportadora' } ]
  @UI.fieldGroup:     [ { qualifier: 'idDemandDetailsGroup', position: 40 } ]
  Fornecedor;

  //──────────────────────────────────────────────────────────
  // CTe Complementar
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'CTe Complementar'
  @UI.lineItem:       [ { position: 50, label: 'CTe Complementar' } ]
  @UI.identification: [ { position: 50, label: 'CTe Complementar' } ]
  @UI.fieldGroup:     [ { qualifier: 'idDemandDetailsGroup', position: 50 } ]
  CteComplementar;

  //──────────────────────────────────────────────────────────
  // Nome do Arquivo
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Nome do Arquivo'
  @UI.selectionField: [ { position: 50 } ]
  @UI.lineItem:       [ { position: 60, label: 'Nome do Arquivo' } ]
  @UI.identification: [ { position: 60, label: 'Nome do Arquivo' } ]
  FileName;

  //──────────────────────────────────────────────────────────
  // Status do Arquivo
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Status Arquivo'
  @UI.lineItem:       [ { position: 70, label: 'Status do Arquivo', criticality: 'CriticalityStatus' } ]
  @UI.dataPoint:      { qualifier: 'tqStatus', title: 'Status Arquivo', criticality: 'CriticalityStatus' } // Posição Header Object Page
  Filestatus;

  //──────────────────────────────────────────────────────────
  // Criado Por
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Criado Por'
  @UI.selectionField: [ { position: 60 } ]
  @UI.lineItem:       [ { position: 80, label: 'Responsável' } ]
  @UI.dataPoint:      { qualifier: 'tqEndUser', title: 'Responsável' }
  @UI.fieldGroup:     [ { qualifier: 'idAdminDataGroup', position: 10 } ]
  CreatedBy;

  //──────────────────────────────────────────────────────────
  // Criado Em
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Criado Em'
  @UI.selectionField: [ { position: 70 } ]
  @UI.lineItem:       [ { position: 90, label: 'Criado Em' } ]
  @UI.dataPoint:      { qualifier: 'tqChgAt', title: 'Criado Em' }
  @UI.fieldGroup:     [ { qualifier: 'idAdminDataGroup', position: 20 } ]
  CreatedAt;

  //──────────────────────────────────────────────────────────
  // Ultima Alteração
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Ultima Alteração'
  @UI.fieldGroup:     [ { qualifier: 'idAdminDataGroup', position: 30 } ]
  LocalLastChangedAt;

  //──────────────────────────────────────────────────────────
  // Arquivo
  //──────────────────────────────────────────────────────────
  @EndUserText.label: 'Attachment'
  @UI.identification: [ { position: 70, label: 'Selecione o Arquivo para Upload' } ]
  @UI.fieldGroup:   [ { position: 70, qualifier: 'idDemandDetailsGroup' } ]
  Attachment;

  @EndUserText.label: 'Log Upload'
  @UI.fieldGroup:     [ { qualifier: 'idAdminDataGroup', position: 40 } ]
  MessagemLog;

  //──────────────────────────────────────────────────────────
  // Ultima Alteração
  //──────────────────────────────────────────────────────────
  @UI.hidden: true
  LastChangedBy;

  //──────────────────────────────────────────────────────────
  // Tipo do Arquivo
  //──────────────────────────────────────────────────────────
  @UI.hidden: true
  Mimetype;

  //──────────────────────────────────────────────────────────
  // Associação
  //──────────────────────────────────────────────────────────
  @UI.hidden: true
  _CteItems;

}

O typeImageUrl: 'sap-icon://attachment' coloca o ícone de clipe no header. E o criticality — apontando para o campo CriticalityStatus calculado na CDS (1 = erro/vermelho, 3 = carregado/verde, 5 = pronto/azul) — pinta o status com cores, dando feedback visual imediato do que aconteceu com o arquivo. A linha de itens aparece pela facet idData, do tipo #LINEITEM_REFERENCE com targetElement: '_CteItems' (as colunas dos itens ficam numa metadata extension própria da entidade-filha).

Passo 5: O handler que lê e converte o arquivo

Chegamos ao coração da lógica. O arquivo está na tabela como xstring; o handler precisa lê-lo, converter para texto e processar. Antes do handler, a classe global de behavior — abstrata e final — declara os tipos auxiliares usados na leitura do CSV:

CLASS zbp_i_cte_upload_file DEFINITION PUBLIC ABSTRACT FINAL FOR BEHAVIOR OF zi_cte_upload_file.
  TYPES: BEGIN OF ty_csv_row,
           chave_cte             TYPE string,
           data_emissao          TYPE string,
           valor_total           TYPE string,
           protocolo_autorizacao TYPE string,
           uf_destino            TYPE string,
           cep                   TYPE string,
           icms                  TYPE string,
         END OF ty_csv_row.

  TYPES: BEGIN OF ty_cte_data,
           line_no               TYPE i,
           item_id               TYPE ztcte_up_items-item_id,
           chave_cte             TYPE ztcte_up_items-chave_cte,
           data_emissao_cte      TYPE ztcte_up_items-data_emissao_cte,
           valor_faturar_total   TYPE ztcte_up_items-valor_faturar_total,
           protocolo_autorizacao TYPE ztcte_up_items-protocolo_autorizacao,
           uf_destino            TYPE ztcte_up_items-uf_destino,
           cep                   TYPE ztcte_up_items-cep,
           icms                  TYPE ztcte_up_items-icms,
         END OF ty_cte_data.

ENDCLASS.

CLASS zbp_i_cte_upload_file IMPLEMENTATION.
ENDCLASS.

Agora a classe local de behavior, lhc_FileCTe, na íntegra. A conversão do xstring é o ponto que mais gera bug: bytes não são texto até você dizer qual a codificação. O método uploadCsv tenta a conversão padrão (UTF-8) e, se falhar, cai para a codepage 1160. A definição da classe:

CLASS lhc_FileCTe DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    CONSTANTS: BEGIN OF lc_csv_header,
                 chave_cte             TYPE string VALUE 'CHAVE CTE/NFS',
                 data_emissao          TYPE string VALUE 'DATA EMISSÃO CTE/NFS',
                 valor_total           TYPE string VALUE 'VALOR FATURAR TOTAL',
                 protocolo_autorizacao TYPE string VALUE 'PROTOCOLO AUTORIZAÇÃO SEFAZ',
                 uf_destino            TYPE string VALUE 'UF DESTINO',
                 cep                   TYPE string VALUE 'CEP',
                 icms                  TYPE string VALUE 'ICMS',
               END OF lc_csv_header.

    CONSTANTS c_msg_none   TYPE string VALUE `Nenhum arquivo selecionado`.
    CONSTANTS c_msg_ready  TYPE string VALUE `Pronto para upload`.
    CONSTANTS c_msg_upload TYPE string VALUE `CSV Carregado`.
    CONSTANTS c_delimiter  TYPE string VALUE ';'.


    METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
      IMPORTING keys REQUEST requested_authorizations FOR FileCTe RESULT result.

    METHODS setinitialfilestatus FOR DETERMINE ON MODIFY
      IMPORTING keys FOR filecte~setinitialfilestatus.

    METHODS setfilename FOR DETERMINE ON MODIFY
      IMPORTING keys FOR filecte~setfilename.

    METHODS uploadCsv FOR MODIFY
      IMPORTING keys FOR ACTION filecte~uploadCsv RESULT result.

    METHODS ValidateMandatoryFields FOR VALIDATE ON SAVE
      IMPORTING keys FOR FileCTe~ValidateMandatoryFields.

    METHODS get_instance_features FOR INSTANCE FEATURES
      IMPORTING keys REQUEST requested_features FOR FileCTe RESULT result.

    METHODS delete_existing_items
      IMPORTING it_keys TYPE STANDARD TABLE.

    METHODS validate_csv_header
      IMPORTING is_header          TYPE zbp_i_cte_upload_file=>ty_csv_row
      RETURNING VALUE(rv_is_valid) TYPE abap_bool.

    METHODS convert_date
      IMPORTING iv_datestr     TYPE string
      RETURNING VALUE(rv_date) TYPE ztcte_up_items-data_emissao_cte.

    METHODS convert_decimal
      IMPORTING iv_value          TYPE string
      RETURNING VALUE(rv_decimal) TYPE ztcte_up_items-valor_faturar_total.

    METHODS validate_chave_cte
      IMPORTING iv_chave           TYPE string
      RETURNING VALUE(rv_is_valid) TYPE abap_bool.

    METHODS clean_string
      IMPORTING iv_input         TYPE string
      RETURNING VALUE(rv_output) TYPE string.

ENDCLASS.

A implementação. Comece pelas autorizações de instância, pela determination que grava o nome do arquivo e pela que limpa itens antigos:

CLASS lhc_FileCTe IMPLEMENTATION.
  METHOD get_instance_authorizations.
    result = VALUE #( FOR key IN keys
                      ( %tky              = key-%tky
                        %update           = if_abap_behv=>auth-allowed
                        %delete           = if_abap_behv=>auth-allowed
                        %action-uploadCsv = if_abap_behv=>auth-allowed ) ).
  ENDMETHOD.

  METHOD get_instance_features.
  ENDMETHOD.

  METHOD SetFileName.
    READ ENTITIES OF zi_cte_upload_file IN LOCAL MODE
         ENTITY FileCTe
         FIELDS ( Attachment FileName )
         WITH CORRESPONDING #( keys )
         RESULT DATA(lt_files).

    LOOP AT lt_files INTO DATA(ls_file).
      IF ls_file-Attachment IS NOT INITIAL.
        DATA(lv_filename) = ls_file-FileName.

        MODIFY ENTITIES OF zi_cte_upload_file IN LOCAL MODE
               ENTITY FileCTe
               UPDATE FIELDS ( FileName )
               WITH VALUE #( ( %tky     = ls_file-%tky
                               FileName = lv_filename ) ).
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD delete_existing_items.
    READ ENTITIES OF zi_cte_upload_file IN LOCAL MODE
         ENTITY FileCTe BY \_cteitems
         ALL FIELDS WITH CORRESPONDING #( it_keys )
         RESULT DATA(lt_existing_items).

    IF lt_existing_items IS NOT INITIAL.
      MODIFY ENTITIES OF zi_cte_upload_file IN LOCAL MODE
             ENTITY cteitem DELETE FROM VALUE #( FOR ls_item IN lt_existing_items
                                                 ( %key      = ls_item-%key
                                                   %is_draft = ls_item-%is_draft ) ).
    ENDIF.
  ENDMETHOD.

  METHOD SetInitialFileStatus.
    READ ENTITIES OF zi_cte_upload_file IN LOCAL MODE
         ENTITY FileCTe
         FIELDS ( Filestatus Attachment )
         WITH CORRESPONDING #( keys )
         RESULT DATA(lt_file).

    DATA lt_status_file TYPE TABLE FOR UPDATE zi_cte_upload_file.

    lt_status_file = VALUE #( FOR ls_file IN lt_file
                              LET lv_status = COND string(
                              WHEN ls_file-Attachment IS INITIAL
                              THEN c_msg_none
                              WHEN ls_file-Attachment IS NOT INITIAL AND ls_file-FileStatus <> c_msg_upload
                              THEN c_msg_ready
                              ELSE ls_file-FileStatus )
                              IN  ( %tky                = ls_file-%tky
                                    %data-FileStatus    = lv_status
                                    %control-FileStatus = if_abap_behv=>mk-on ) ).

    IF lt_status_file IS NOT INITIAL.
      MODIFY ENTITIES OF zi_cte_upload_file IN LOCAL MODE
             ENTITY FileCTe
             UPDATE FIELDS ( FileStatus )
             WITH lt_status_file.
    ENDIF.
  ENDMETHOD.

O método central, uploadcsv, do começo ao fim — leitura do anexo, conversão, parse linha a linha com as validações, e criação dos itens filhos:

  METHOD uploadcsv.
    DATA lt_csv_lines  TYPE STANDARD TABLE OF string.
    DATA lv_csv_string TYPE string.
    DATA lt_csv_fields TYPE STANDARD TABLE OF string.
    DATA lt_cte_data   TYPE STANDARD TABLE OF zbp_i_cte_upload_file=>ty_cte_data.
    DATA ls_cte_data   TYPE zbp_i_cte_upload_file=>ty_cte_data.
    DATA lt_items      TYPE TABLE FOR CREATE zi_cte_upload_file\_cteitems.
    DATA lv_has_error  TYPE abap_bool.
    DATA lv_line_index TYPE i.

    READ ENTITIES OF zi_cte_upload_file IN LOCAL MODE
         ENTITY FileCte
         FIELDS ( Attachment FileName Empresa Fornecedor Material Centro UploadId ) WITH CORRESPONDING #( keys )
         RESULT DATA(lt_head).

    IF lt_head IS NOT INITIAL.
      DATA(ls_head) = lt_head[ 1 ].
    ELSE.
      RETURN.
    ENDIF.

    " Validar se arquivo foi selecionado
    IF ls_head-Attachment IS INITIAL.
      APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
      APPEND VALUE #( %tky = ls_head-%tky
                      %msg = new_message( id       = 'ZCL_MM_CTE'
                                          number   = '001'
                                          severity = if_abap_behv_message=>severity-error ) ) TO reported-filecte.
      RETURN.
    ENDIF.

    " Limpar itens existentes
    delete_existing_items( keys ).

    DATA(lv_attachment) = ls_head-Attachment.

    TRY.
        lv_csv_string = cl_abap_conv_codepage=>create_in( )->convert( lv_attachment ).
      CATCH cx_root INTO DATA(lx_err_conv).
        " Se falhar, tenta a codificação original (1160)
        TRY.
            DATA(lo_conv) = cl_abap_conv_in_ce=>create( input    = lv_attachment
                                                        encoding = '1160' ).
            lo_conv->read( IMPORTING data = lv_csv_string ).
          CATCH cx_root INTO lx_err_conv.
            " Em caso de erro final, registra a mensagem em 'reported'
            APPEND VALUE #( %tky = ls_head-%tky
                            %msg = new_message_with_text(
                                       severity = if_abap_behv_message=>severity-error
                                       text     = |Erro ao converter arquivo: { lx_err_conv->get_text( ) }| ) )
            TO reported-filecte.
            RETURN.
        ENDTRY.
    ENDTRY.

    SPLIT lv_csv_string AT cl_abap_char_utilities=>cr_lf INTO TABLE lt_csv_lines.

    DELETE lt_csv_lines WHERE table_line IS INITIAL.

    IF lt_csv_lines IS INITIAL.
      RETURN.
    ENDIF.

    " Processar cabeçalho
    DATA(lv_header_line) = lt_csv_lines[ 1 ].
    SPLIT lv_header_line AT c_delimiter INTO TABLE DATA(lt_header_fields).

    " Determinar se a coluna ICMS está presente (7 campos) ou ausente (6 campos - CT-e complementar)
    DATA(lv_icms_column_present) = xsdbool( lines( lt_header_fields ) <> 6 ).
    DATA(lv_expected_fields)     = COND i( WHEN lines( lt_header_fields ) = 6 THEN 6 ELSE 7 ).

    DATA(ls_header_check) = VALUE zbp_i_cte_upload_file=>ty_csv_row(
                                      chave_cte             = lt_header_fields[ 1 ]
                                      data_emissao          = lt_header_fields[ 2 ]
                                      valor_total           = lt_header_fields[ 3 ]
                                      protocolo_autorizacao = lt_header_fields[ 4 ]
                                      uf_destino            = lt_header_fields[ 5 ]
                                      cep                   = lt_header_fields[ 6 ]
                                      icms                  = COND string( WHEN lv_icms_column_present = abap_true
                                                                           THEN lt_header_fields[ 7 ]
                                                                           ELSE lc_csv_header-icms ) ).

    DATA(lv_header_valid) = validate_csv_header( ls_header_check ).

    IF lv_header_valid = abap_false.

      APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
      APPEND VALUE #( %tky = ls_head-%tky
                      %msg = new_message( id       = 'ZCL_MM_CTE'
                                          number   = '003'
                                          severity = if_abap_behv_message=>severity-error ) ) TO reported-filecte.


      APPEND VALUE #( %tky = ls_head-%tky
                      %param = VALUE #( %tky        = ls_head-%tky
                                        FileStatus  = 'Arquivo com erro'
                                        MessagemLog = 'Formato de arquivo incorreto! Verifique o layout' )
                    ) TO result.


      RETURN.
    ENDIF.

    " Remover linha de cabeçalho
    DELETE lt_csv_lines INDEX 1.

    " Processar cada linha de dados
    LOOP AT lt_csv_lines INTO DATA(lv_data_line).
      CLEAR: lt_csv_fields,
             ls_cte_data.
      lv_line_index = sy-tabix.
      ls_cte_data-line_no = lv_line_index.

      " Dividir linha em campos
      SPLIT lv_data_line AT c_delimiter INTO TABLE lt_csv_fields.

      " Extrair campos
      DATA(lv_chave)     = clean_string( lt_csv_fields[ 1 ] ).
      DATA(lv_data)      = clean_string( lt_csv_fields[ 2 ] ).
      DATA(lv_valor)     = clean_string( lt_csv_fields[ 3 ] ).
      DATA(lv_protocolo) = clean_string( lt_csv_fields[ 4 ] ).
      DATA(lv_uf)        = clean_string( lt_csv_fields[ 5 ] ).
      DATA(lv_cep)       = clean_string( lt_csv_fields[ 6 ] ).
      DATA(lv_icms)      = COND string( WHEN lv_icms_column_present = abap_true
                                        THEN clean_string( lt_csv_fields[ 7 ] )
                                        ELSE '' ).

      " Pular linhas vazias
      IF lv_chave IS INITIAL.
        CONTINUE.
      ENDIF.

      ls_cte_data-chave_cte        = lv_chave.

      " Converter Data de Emissão
      ls_cte_data-data_emissao_cte = convert_date( lv_data ).
      IF ls_cte_data-data_emissao_cte IS INITIAL OR ls_cte_data-data_emissao_cte = '00000000'.
        lv_has_error = abap_true.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky = ls_head-%tky
                        %msg = new_message( id       = 'ZCL_MM_CTE'
                                            number   = '004'
                                            severity = if_abap_behv_message=>severity-error
                                            v1       = lv_line_index ) ) TO reported-filecte.
        CONTINUE.
      ENDIF.

      " Validar e converter Chave CTe
      IF validate_chave_cte( lv_chave ) = abap_false.
        lv_has_error = abap_true.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky = ls_head-%tky
                        %msg = new_message( id       = 'ZCL_MM_CTE'
                                            number   = '005'
                                            severity = if_abap_behv_message=>severity-error
                                            v1       = lv_line_index ) ) TO reported-filecte.
        CONTINUE.
      ENDIF.

      " Converter Valor Total
      ls_cte_data-valor_faturar_total = convert_decimal( lv_valor ).
      IF ls_cte_data-valor_faturar_total <= 0.
        lv_has_error = abap_true.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky = ls_head-%tky
                        %msg = new_message( id       = 'ZCL_MM_CTE'
                                            number   = '006'
                                            severity = if_abap_behv_message=>severity-error
                                            v1       = lv_line_index ) ) TO reported-filecte.
        CONTINUE.
      ENDIF.

      " Remover possível ".0" do protocolo (vem do CSV como número)
      IF lv_protocolo CS '.'.
        " TODO: variable is assigned but never used (ABAP cleaner)
        SPLIT lv_protocolo AT '.' INTO lv_protocolo DATA(lv_dummy).
      ENDIF.

      " Remover espaços e zeros à esquerda
      lv_protocolo = condense( lv_protocolo ).
      lv_protocolo = |{ lv_protocolo ALPHA = OUT }|.

      " Validar Protocolo de Autorização (15 dígitos)
      IF strlen( lv_protocolo ) <> 15 OR lv_protocolo CN '0123456789'.
        lv_has_error = abap_true.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky = ls_head-%tky
                        %msg = new_message( id       = 'ZCL_MM_CTE'
                                            number   = '007'
                                            severity = if_abap_behv_message=>severity-error
                                            v1       = lv_line_index ) ) TO reported-filecte.
        CONTINUE.
      ENDIF.
      ls_cte_data-protocolo_autorizacao = lv_protocolo.

      " Validar UF Destino
      DATA(lv_uf_upper) = to_upper( condense( lv_uf ) ).
      IF strlen( lv_uf_upper ) <> 2.
        lv_has_error = abap_true.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky = ls_head-%tky
                        %msg = new_message( id       = 'ZCL_MM_CTE'
                                            number   = '008'
                                            severity = if_abap_behv_message=>severity-error
                                            v1       = lv_line_index ) ) TO reported-filecte.
        CONTINUE.
      ENDIF.

      ls_cte_data-uf_destino = CONV #( lv_uf_upper ).

      DATA(lv_cep_clean) = condense( lv_cep ).
      IF lv_cep_clean CS '.'.
        " TODO: variable is assigned but never used (ABAP cleaner)
        SPLIT lv_cep_clean AT '.' INTO lv_cep_clean DATA(lv_dummy_cep).
      ENDIF.

      " Remover hífens e espaços
      REPLACE ALL OCCURRENCES OF '-' IN lv_cep_clean WITH ''.
      lv_cep_clean = condense( lv_cep_clean ).

      " Garantir 8 dígitos com zeros à esquerda se necessário
      IF strlen( lv_cep_clean ) > 0 AND strlen( lv_cep_clean ) < 8.
        lv_cep_clean = |{ lv_cep_clean WIDTH = 8 ALIGN = RIGHT PAD = '0' }|.
      ENDIF.

      ls_cte_data-cep = CONV #( lv_cep_clean ).

      IF lv_icms IS INITIAL.
        ls_cte_data-icms = 0.
      ELSE.
        ls_cte_data-icms = convert_decimal( lv_icms ).
      ENDIF.

      APPEND ls_cte_data TO lt_cte_data.

    ENDLOOP.

    " Se houver erros, não criar itens
    IF lv_has_error = abap_true.
      MODIFY ENTITIES OF zi_cte_upload_file IN LOCAL MODE
             ENTITY FileCTe
             UPDATE FIELDS ( FileStatus MessagemLog )
             WITH VALUE #( ( %tky        = ls_head-%tky
                             FileStatus  = 'Erro na Validação'
                             MessagemLog = 'Verifique os erros nas mensagens acima' ) ).
      RETURN.
    ENDIF.

    " Preparar e Criar itens filhos
    DATA(lv_item_counter) = 0.

    " Prepare Data for Child Entity (CTeItems)
    lt_items = VALUE #(
        ( %cid_ref  = keys[ 1 ]-%cid_ref
          %is_draft = keys[ 1 ]-%is_draft
          UploadId  = ls_head-UploadId
          %target   = VALUE #( FOR ls_data IN lt_cte_data INDEX INTO lv_index
                               ( %cid      = keys[ 1 ]-%cid_ref
                                 %is_draft = keys[ 1 ]-%is_draft
                                 %data     = VALUE #(
                                     ItemId               = |{ lv_index WIDTH = 10 ALIGN = RIGHT PAD = '0' }|
                                     UploadId             = ls_head-UploadId
                                     ChaveCte             = ls_data-chave_cte
                                     DataEmissaoCte       = ls_data-data_emissao_cte
                                     ValorFaturarTotal    = ls_data-valor_faturar_total
                                     ProtocoloAutorizacao = ls_data-protocolo_autorizacao
                                     UfDestino            = ls_data-uf_destino
                                     Cep                  = ls_data-cep
                                     Icms                 = ls_data-icms
                                     ProcessingStatus     = 'Pronto para Processar' )
                                 %control  = VALUE #( ItemId               = if_abap_behv=>mk-on
                                                      UploadId             = if_abap_behv=>mk-on
                                                      ChaveCte             = if_abap_behv=>mk-on
                                                      DataEmissaoCte       = if_abap_behv=>mk-on
                                                      ValorFaturarTotal    = if_abap_behv=>mk-on
                                                      ProtocoloAutorizacao = if_abap_behv=>mk-on
                                                      UfDestino            = if_abap_behv=>mk-on
                                                      Cep                  = if_abap_behv=>mk-on
                                                      Icms                 = if_abap_behv=>mk-on
                                                      ProcessingStatus     = if_abap_behv=>mk-on ) ) ) ) ).

    " Criar itens
    MODIFY ENTITIES OF zi_cte_upload_file IN LOCAL MODE
           ENTITY FileCTe CREATE BY \_CteItems
           AUTO FILL CID
           WITH lt_items
           MAPPED DATA(ls_mapped)
           FAILED DATA(ls_failed)
           REPORTED DATA(ls_reported).

    " Atualizar status
    MODIFY ENTITIES OF zi_cte_upload_file IN LOCAL MODE
           ENTITY FileCTe
           UPDATE FIELDS ( FileStatus MessagemLog )
           WITH VALUE #( ( %tky        = ls_head-%tky
                           FileStatus  = c_msg_upload
                           MessagemLog = |{ lines( lt_cte_data ) } CTe(s) carregado(s) com sucesso| ) ).

    " Ler resultado atualizado
    READ ENTITIES OF zi_cte_upload_file IN LOCAL MODE
         ENTITY FileCTe
         ALL FIELDS WITH CORRESPONDING #( keys )
         RESULT DATA(lt_result).

    result = VALUE #( FOR ls_res IN lt_result
                      ( %tky = ls_res-%tky %param = ls_res ) ).
  ENDMETHOD.

A conversão dupla não é firula. cl_abap_conv_codepage=>create_in( ) assume UTF-8 por padrão. Arquivos exportados de Excel/Windows frequentemente vêm em Windows-1252 (codepage SAP 1160) e estouram com acentos. O TRY/CATCH em cascata salva o upload nesses casos — uma das pegadinhas mais comuns de upload em ambiente brasileiro.

Nota para ABAP Cloud: o fallback usa cl_abap_conv_in_ce, a classe clássica de conversão (ela aceita o número de codepage SAP, ex.: 1160). Essa classe está marcada como obsoleta e não é liberada no ABAP Cloud. O equivalente moderno é informar o codepage direto no cl_abap_conv_codepage=>create_in( codepage = ... ), que usa o nome externo do codepage ('UTF-8', 'WINDOWS-1252') em vez do número. Em projetos novos, prefira essa via.

Por fim, os métodos auxiliares: a validação do cabeçalho, as conversões de data e decimal, a validação da chave de 44 dígitos, a validação dos campos obrigatórios (com checagem do fornecedor na tabela ztrans_cte) e a limpeza de strings:

  METHOD validate_csv_header.
    rv_is_valid = abap_true.

    " Converter cabeçalhos para maiúsculas e remover espaços
    DATA(lv_col1) = to_upper( condense( is_header-chave_cte ) ).
    DATA(lv_col2) = to_upper( condense( is_header-data_emissao ) ).
    DATA(lv_col3) = to_upper( condense( is_header-valor_total ) ).
    DATA(lv_col4) = to_upper( condense( is_header-protocolo_autorizacao ) ).
    DATA(lv_col5) = to_upper( condense( is_header-uf_destino ) ).
    DATA(lv_col6) = to_upper( condense( is_header-cep ) ).
    DATA(lv_col7) = to_upper( condense( is_header-icms ) ).

    IF    lv_col1 <> lc_csv_header-chave_cte
       OR lv_col2 <> lc_csv_header-data_emissao
       OR lv_col3 <> lc_csv_header-valor_total
       OR lv_col4 <> lc_csv_header-protocolo_autorizacao
       OR lv_col5 <> lc_csv_header-uf_destino
       OR lv_col6 <> lc_csv_header-cep
       OR lv_col7 <> lc_csv_header-icms.
      rv_is_valid = abap_false.
    ENDIF.
  ENDMETHOD.

  METHOD convert_date.
    CHECK iv_datestr IS NOT INITIAL.
    DATA(lv_date_clean) = condense( iv_datestr ).

    " Remover hora
    SPLIT lv_date_clean AT space INTO lv_date_clean DATA(lv_time_dummy).

    TRY.
        " Tentar formato ISO (YYYY-MM-DD ou YYYY/MM/DD)
        IF lv_date_clean CA '-'.
          rv_date = |{ lv_date_clean+0(4) }{ lv_date_clean+5(2) }{ lv_date_clean+8(2) }|.
        ELSEIF lv_date_clean CA '/'.
          " Verificar se é DD/MM/YYYY ou YYYY/MM/DD
          IF lv_date_clean+0(2) CO '0123456789' AND lv_date_clean+0(2) <= '31'.
            " Formato DD/MM/YYYY
            rv_date = |{ lv_date_clean+6(4) }{ lv_date_clean+3(2) }{ lv_date_clean+0(2) }|.
          ELSE.
            " Formato YYYY/MM/DD
            rv_date = |{ lv_date_clean+0(4) }{ lv_date_clean+5(2) }{ lv_date_clean+8(2) }|.
          ENDIF.
        ELSEIF strlen( lv_date_clean ) = 8.
          " Formato YYYYMMDD
          rv_date = lv_date_clean.
        ENDIF.
      CATCH cx_root.
        rv_date = '00000000'.
    ENDTRY.
  ENDMETHOD.

  METHOD convert_decimal.
    CHECK iv_value IS NOT INITIAL.
    DATA(lv_value_clean) = condense( iv_value ).

    TRY.
        " Substituir vírgula por ponto
        REPLACE ALL OCCURRENCES OF ',' IN lv_value_clean WITH '.'.
        rv_decimal = lv_value_clean.
      CATCH cx_root.
        rv_decimal = 0.
    ENDTRY.
  ENDMETHOD.

  METHOD validate_chave_cte.
    rv_is_valid = abap_false.
    IF iv_chave IS INITIAL.
      RETURN.
    ENDIF.

    DATA(lv_chave_clean) = condense( iv_chave ).

    " Validar se tem 44 dígitos
    IF strlen( lv_chave_clean ) = 44 AND lv_chave_clean CO '0123456789'.
      rv_is_valid = abap_true.
    ENDIF.
  ENDMETHOD.

  METHOD ValidateMandatoryFields.
    READ ENTITIES OF zi_cte_upload_file IN LOCAL MODE
    ENTITY FileCTe
    FIELDS ( Empresa Centro Material Fornecedor ) WITH CORRESPONDING #( keys )
    RESULT DATA(lt_head).

    LOOP AT lt_head INTO DATA(ls_head).
      APPEND VALUE #( %tky        = ls_head-%tky
                      %state_area = 'VALIDATE_MANDATORY' ) TO reported-filecte.

      IF ls_head-Empresa IS INITIAL.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky             = ls_head-%tky
                        %state_area      = 'VALIDATE_MANDATORY'
                        %msg             = new_message( id       = 'ZCL_MM_CTE'
                                                        number   = '010'
                                                        severity = if_abap_behv_message=>severity-error )
                        %element-empresa = if_abap_behv=>mk-on ) TO reported-filecte.
      ENDIF.

      IF ls_head-Centro IS INITIAL.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky            = ls_head-%tky
                        %state_area     = 'VALIDATE_MANDATORY'
                        %msg            = new_message( id       = 'ZCL_MM_CTE'
                                                       number   = '011'
                                                       severity = if_abap_behv_message=>severity-error )
                        %element-centro = if_abap_behv=>mk-on ) TO reported-filecte.
      ENDIF.

      IF ls_head-Material IS INITIAL.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky              = ls_head-%tky
                        %state_area       = 'VALIDATE_MANDATORY'
                        %msg              = new_message( id       = 'ZCL_MM_CTE'
                                                         number   = '012'
                                                         severity = if_abap_behv_message=>severity-error )
                        %element-material = if_abap_behv=>mk-on ) TO reported-filecte.
      ENDIF.

      IF ls_head-Fornecedor IS INITIAL.
        APPEND VALUE #( %tky = ls_head-%tky ) TO failed-filecte.
        APPEND VALUE #( %tky                = ls_head-%tky
                        %state_area         = 'VALIDATE_MANDATORY'
                        %msg                = new_message( id       = 'ZCL_MM_CTE'
                                                           number   = '013'
                                                           severity = if_abap_behv_message=>severity-error )
                        %element-fornecedor = if_abap_behv=>mk-on ) TO reported-filecte.
      ELSE.

        DATA(lv_lifnr) = ls_head-Fornecedor.

        SELECT FROM ztrans_cte
          FIELDS ( lifnr )
          WHERE lifnr = @lv_lifnr
          INTO TABLE @DATA(lt_check_sup).

        IF NOT line_exists( lt_check_sup[ lifnr = ls_head-Fornecedor ] ).
          APPEND VALUE #( %tky = ls_head-%tky ) TO failed-Filecte.
          APPEND VALUE #( %tky                = ls_head-%tky
                          %state_area         = 'VALIDATE_FORNECEDOR'
                          %msg                = new_message( id       = 'ZCL_MM_CTE'
                                                             number   = '017'
                                                             severity = if_abap_behv_message=>severity-error
                                                             v1       = |{ ls_head-Fornecedor ALPHA = OUT }| )
                          %element-fornecedor = if_abap_behv=>mk-on ) TO reported-Filecte.
        ENDIF.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD clean_string.
    rv_output = iv_input.
    " Remove o apóstrofo inicial se existir
    IF rv_output CS '''' AND rv_output+0(1) = ''''.
      rv_output = rv_output+1.
    ENDIF.
    " Remove espaços em branco
*    rv_output = condense( rv_output ).
  ENDMETHOD.
ENDCLASS.

O parsing em si (split por ;, validação de cabeçalho, conversão de datas em vários formatos, normalização de decimais com vírgula, validação da chave de 44 dígitos) é específico do CTe. O importante para o seu caso é o esqueleto: ler o xstring, converter, transformar em registros e gravar via CREATE BY \_associação. Troque o miolo pelo parser do seu formato — XML, JSON, planilha — e a estrutura se mantém.

Passo 6: Service definition e binding

Por fim, expor como serviço. A service definition publica as duas entidades (header e itens); o service binding do tipo OData V4 / UI gera o serviço que o Fiori Elements consome. É no binding que você publica e, depois, gera o app (ou usa o preview).

@EndUserText.label: 'Service Def. - Upload de Arquivos CTE'
define service ZDEF_UPLOAD_CTE {
  expose ZC_CTE_UPLOAD_FILE;
  expose ZC_CTE_UPLOAD_ITEMS;
}

Criado o service binding (OData V4 UI) e publicado, o Fiori Elements monta a tela sozinho: a lista, o object page, o controle de upload no campo Attachment e o botão "Carregar Dados" ligado à action. Nenhuma linha de front-end.

O ganho do RAP aqui: de uma annotation (@Semantics.largeObject) e uma action, você ganha upload, download, validação de MIME type e feedback de status — tudo gerado. O trabalho real fica onde deve: na regra de negócio dentro do handler, não na plumbing da UI.

O que acontece depois do upload?

No projeto original, o upload é só o começo. Depois de carregar os itens, uma segunda entidade (ZI_TRANS_CTE) com sua própria action processa cada item — chamando um function module que executa a BAPI de criação do documento. É um padrão comum: a app de upload é a porta de entrada; o processamento de verdade (lançamento, integração, BAPI) vem numa etapa seguinte, já com os dados validados e estruturados.

Separar as duas fases — carregar/validar e processar — é uma decisão de design que vale a pena copiar. O usuário sobe o arquivo, vê os erros de formato na hora, corrige e só então dispara o processamento. Menos retrabalho, e um log claro em cada etapa.

Perguntas frequentes

Qual annotation faz aparecer o botão de upload no Fiori?

A @Semantics.largeObject na CDS de consumo, aplicada ao campo binário (rawstring). Ela informa ao Fiori Elements que o campo é um arquivo e renderiza o controle de upload/download. Os atributos mimeType e fileName apontam para os campos que guardam o tipo e o nome; acceptableMimeTypes restringe os formatos aceitos.

Por que o campo do arquivo é rawstring e não string?

Porque arquivo é binário. abap.rawstring mapeia para um xstring, que preserva os bytes exatos sem assumir codificação. A conversão para texto acontece no handler, no momento da leitura, com cl_abap_conv_codepage. Guardar como string arriscaria corromper acentos e caracteres especiais já na gravação.

Como leio o conteúdo do xstring dentro da action?

Lendo o campo Attachment com READ ENTITIES e convertendo com cl_abap_conv_codepage=>create_in( )->convert( ) (UTF-8 por padrão). Para arquivos Windows-1252 (codepage SAP 1160), o projeto usa cl_abap_conv_in_ce=>create( encoding = '1160' ) como fallback. Um TRY/CATCH em cascata cobre os dois casos e evita o erro de acentuação mais comum.

Preciso de draft para fazer upload no RAP?

Não é obrigatório, mas ajuda muito. Com with draft, o usuário cria o registro, anexa o arquivo e ajusta os campos antes de salvar — e a action de upload roda sobre o draft. Sem draft, você teria que persistir o registro antes de anexar, o que piora a experiência num fluxo de "montar e enviar".

Dá para subir XML ou planilha em vez de CSV?

Sim. O mecanismo é idêntico — muda só o parser dentro da action. Para XML, troque o split por uma transformação (CALL TRANSFORMATION ou iXML); para planilha, use uma classe de leitura de XLSX. Inclua o MIME type correto em acceptableMimeTypes e o controle de upload aceitará o formato.

Conclusão

Upload de arquivos no RAP se resume a quatro decisões: um campo rawstring na tabela, a annotation @Semantics.largeObject na consumption, uma action com draft no behavior e um handler que converte o xstring com a codepage certa. O Fiori Elements faz o resto — controle de upload, download, validação de MIME e status colorido, tudo sem front-end.

Referências oficiais

TagsEMLDesenvolvedorBO InterfaceClean Core
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.