Upload de arquivos no RAP: como construir um app de upload (CSV/XML) do zero
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.rawstringpara os bytes, maismimetypeefile_name.CDS:
@Semantics.mimeType: truena interface e@Semantics.largeObject{ ... }na consumption — é isso que cria o controle de upload.Behavior:
with draft, campoAttachmentmandatory,determinationpara nome/status e umaactionde processamento.Handler: ler o
Attachment(xstring), converter para string comcl_abap_conv_codepagee 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.
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
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.