Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
manuelr
Advisor
Advisor
In this blog I will show how to load master data and transactional data into a SAP Analytics Cloud Planning model from a copy paste from an excel table.

In certain business scenarios it is advised not to give business users access to the data management features of the planning modes, due to confidential data and also because it is risky to create data loading jobs using flat files / excel files, a wrong click might delete too much.

There are already other blogs explaining how to deal with this requirement, in this blog I will present a different approach and explain the differences between the approaches.

Existing Solutions


 

Upload Data From Excel file using custom widget

This solution allows the direct import of an excel file from an Analytic Application using a custom widget, in terms of UX is amazing,however, it could be complex to implement as it requires a dedicated web-server to store the custom widget JS file and it requires some of JavaScript coding.

Flat File Upload using dataset

This approach is quite simple to implement, however, it requires the users to access a separate screen when importing the data (dataset import screen), also, it poses some challenges in terms of confidentiality, because all users who access the dataset will be able to see that data.

 

New Approach: Copy paste data from Excel Table / Flat File into Analytic Application


 

Step 1: User select the and copies the required data to be uploaded.


 



Step 2: User pastes the copied data into a text input field in the analytic application and clicks on a button to import/process data.




Step 3: Data is processed, If master data does not exist then it will be created, if it exists it will be updated. Transactional data will be added also.




Video of functionality




 

Used code / Script


The script converts the excel table which was copied in a tab delimited like string into an planning model members array, that can be used to call the createmembers & updatemembers functions for master data management and setUserInput function for transactional data.
//Input text field that receives the copy paste from excel
var text = InputField_1.getValue();

//Creation of an array type planning model members of dimension Project, this array will be used in the create and update of master data functions
var result = RB_FACTORY.getMembers("Project", {
limit: 1
});
console.log(result);

//Creation of an array type planning model members of dimension Project,containing all projects to be used in the create master data functions, to validate if the member already exsists
var existing_projects = RB_FACTORY.getMembers("Project", {
limit: 5000
});

//Creation of array to be used in the set user input function
var values = ArrayUtils.create(Type.string);

//Creation of one string variable per column to be used in the excel file.

var ID = "";
var DESCRIPTION = "";
var INC_TO_BOTTOMUP = "";
var INPLAN_OOPLAN = "";
var INCL_TP = "";
var BRAND = "";
var BRAND_SEGMENT = "";
var COST_BUCKET = "";
var START_DATE = "";
var END_DATE = "";
var STATUS = "";
var VALUE = "";
var DATE = "";

// Split by tab, the text received in the input text field
var file = text.split("\t");


console.log(file);

//Initialization of var count and row to be used in the processing of the file array
var count = 0;
var row = 0;


for (var i = 0; i < file.length; i++) {


if (count < 14) {
switch (count) {
//Map the position of the columns in the array to the respective variables.
case 0:
ID = file[i].split(" ")[1];
if (ID === undefined) {
ID = file[i];
} else if (ID !== undefined) {
ID = file[i].split(" ")[1];
}

case 1:
DESCRIPTION = file[i];
case 2:
INC_TO_BOTTOMUP = file[i];
case 3:
INPLAN_OOPLAN = file[i];
case 4:
INCL_TP = file[i];
case 5:
BRAND = file[i];
case 6:
BRAND_SEGMENT = file[i];
case 7:
COST_BUCKET = file[i];
case 8:
START_DATE = file[i];
case 9:
END_DATE = file[i];
case 10:
STATUS = file[i];
case 12:
DATE = file[i].split(" ")[0];
}


}
count = count + 1;
if (count > 13) {
// Push dimension and property values into the result planning model members type array (to be used in the create and update members functions)
result.push({
id: ID,
description: DESCRIPTION,
hierarchies: {
H1: {
parentId: "ALL_PROJECT"
}
},
properties: {
INC_TO_BOTTOMUP: INC_TO_BOTTOMUP,
INPLAN_OOPLAN: INPLAN_OOPLAN,
INCL_TP: INCL_TP,
BRAND: BRAND,
BRAND_SEGMENT: BRAND_SEGMENT,
COST_BUCKET: COST_BUCKET,
START_DATE: START_DATE,
END_DATE,
STATUS: STATUS
}
});

count = 0;
row = row + 1;
//Map the value to a variable and create an array of values to be used in the ser user input function for the transactional data
VALUE = file[i + 1].split(" ")[0];
values[row] = VALUE;


//Validate if dimemsion member already exists if yes, then update the member with the data from the file.
//Any additional business logic can be included here to validate the master data.
if (existing_projects.indexOf(result[row])) {
RB_FACTORY.updateMembers("Project", result[row]);
Application.showMessage(ApplicationMessageType.Success, "Updated Project:" + result[row].id);

var quarter = "[Date].[YQM].[Date.CALQUARTER].[" + DATE.substr(0, 4) + DATE.substr(DATE.length - 1, 1) + "]";
console.log("VALUE IS" + values[row]);

}
//If the dimemsion member does not exsis create the new member with all the attributes in the file.
//Any additional business logic can be included here to validate the master data.
else {
RB_FACTORY.createMembers("Project", result[row]);
Table_Projects.getDataSource().refreshData();



Application.showMessage(ApplicationMessageType.Success, "Created Project:" + result[row].id);

}
//Create transactional data in the model in case amount/value exists, if not only master data will be created

if(values[row] ){


Table_Projects.getDataSource().refreshData();

var input = Table_Projects.getPlanning().setUserInput({
"Version": "public." + "FC1",
"@MeasureDimension": "Productivity",
"Date": quarter,
"Project": "[Project].[H1].&[" + ID + "]"
}, values[row]);

Table_Projects.getPlanning().submitData();

console.log (input);
}
}

}


 

Conclusion


This approach shows a different way to upload external master data and transactional data into SAP Analytics Cloud Planning models, the logic / script can be adjusted to accommodate more complex data validations / business rules.

Looking forward to read your comments / feedbacks!

 
18 Comments
SimonLH
Explorer
This is a very useful addition to the user experience. Thank you
arpitshah1
Explorer
0 Kudos
This is very useful. Can you tell me how many line items we can upload at a time ?
manuelr
Advisor
Advisor
0 Kudos
I don't know the maximum limit, but I tested with 10k rows and it worked. Please take into consideration that if you load big amounts of data it might take a while to run.
andreia_palma
Discoverer
0 Kudos
This is an excellent insight, very helpful. Thank you for sharing your experience.
0 Kudos
Good information Manuel! Is there also an option to not create master data but rather gives some feedback to the user if it is not existing or wrong IDs etc?
JefB
Active Contributor
0 Kudos
Not necessarily nr. of items, but you will quickly hit limits if you add more dimensions to the table. This is because the used setUserInput() API can only work on displayed cells (in unbooked mode). For example: 6 dimensions in a sparse data set of just 10 rows will cause generation of 1M cells, which is a showstopper for performance.

What is good in above use case is that a lot of attributes were used in the Project dimension, avoiding the use of additional dimensions in the planning model.
manuelr
Advisor
Advisor
0 Kudos
Yes, after copying the table into the text input field and "digest" the string into an array you can enrich it with other validations depending on the business requirement.
manuelr
Advisor
Advisor
0 Kudos
In a situation with multiple dimensions you could potentially replace the setUseinput () with a triggering of a data action to create transactional data, with that approach you would not be required to have unbooked data.
JefB
Active Contributor
0 Kudos
Hi Manuel, I really don't see how that can work. Please elaborate. 🙂
manuelr
Advisor
Advisor
0 Kudos

Instead of using the setUserinput() API to create transactional data, you can use the data action.execute() and pass the same parameters to write transactional data.


You would need to create a simple data action to support this process.

https://blogs.sap.com/2021/03/29/the-data-action-api-for-analytics-designer-beautiful-game-changer/
JefB
Active Contributor
If you believe that works, I want to see that in your next blog!
former_member40251
Participant
0 Kudos
Great post!

Thanks for sharing! Very useful functionality. Importing Excel data to models wasn't friendly at all. I will test it.

 

kind regards

Mayumi
Hi Manuel,

 

Great Solution.

Just like @sangamesh.reshmi even I am getting error for Transaction data but able to update Master data. I am trying to replicate your solution but the date format for us is in "YYYYMM". Please find the code below.
//Input text field that receives the copy paste from excel
var text = InputField_1.getValue();

//Creation of an array type planning model members of dimension Project, this array will be used in the create and update of master data functions
var result = Model_Planning.getMembers("DIMENSION", {
limit: 1
});
console.log(result);

//Creation of an array type planning model members of dimension Project,containing all projects to be used in the create master data functions, to validate if the member already exsists
var existing_projects = Model_Planning.getMembers("DIMENSION", {
limit: 5000
});

//Creation of array to be used in the set user input function
var values = ArrayUtils.create(Type.string);

//Creation of one string variable per column to be used in the excel file.

var ID = "";
var DESCRIPTION = "";
var ENTITY = "";
var ENTITY_DESC = "";
var SAP_ENTITY = "";
var SAP_ENTITY_DESC = "";
var VALUE = "";
var DATE = "";

// Split by tab, the text received in the input text field
var file = text.split("\t");


console.log(file);

//Initialization of var count and row to be used in the processing of the file array
var count = 0;
var row = 0;


for (var i = 0; i < file.length; i++) {


if (count < 9) {
switch (count) {
//Map the position of the columns in the array to the respective variables.
case 0:
ID = file[i].split(" ")[1];
if (ID === undefined) {
ID = file[i];
} else if (ID !== undefined) {
ID = file[i].split(" ")[1];
}

case 1:
DESCRIPTION = file[i];
case 2:
ENTITY = file[i];
case 3:
ENTITY_DESC = file[i];
case 4:
SAP_ENTITY = file[i];
case 5:
SAP_ENTITY_DESC = file[i];
case 6:
DATE = file[i].split(" ")[0];
}


}
count = count + 1;
if (count > 😎 {
// Push dimension and property values into the result planning model members type array (to be used in the create and update members functions)
result.push({
id: ID,
description: DESCRIPTION,
hierarchies: {
ALL_LOCATION: {
parentId: "ALL LOCATION"
}
},
properties: {
ENTITY: ENTITY,
ENTITY_DESC: ENTITY_DESC,
SAP_ENTITY: SAP_ENTITY,
SAP_ENTITY_DESC: SAP_ENTITY_DESC,

}
});

count = 0;
row = row + 1;
//Map the value to a variable and create an array of values to be used in the ser user input function for the transactional data
VALUE = file[i + 1].split(" ")[0];
values[row] = VALUE;


//Validate if dimemsion member already exists if yes, then update the member with the data from the file.
//Any additional business logic can be included here to validate the master data.
if (existing_projects.indexOf(result[row])) {
Model_Planning.updateMembers("DIMENSION", result[row]);
Application.showMessage(ApplicationMessageType.Success, "Updated Location:" + result[row].id);
var quarter = "[Date].[YM].[Date.CALMONTH].[" + DATE.substr(0, 6) + "]";
console.log("VALUE IS" + values[row]);
}
//If the dimemsion member does not exsis create the new member with all the attributes in the file.
//Any additional business logic can be included here to validate the master data.
else {
Model_Planning.createMembers("DIMENSION", result[row]);
Table_Projects.getDataSource().refreshData();
Application.showMessage(ApplicationMessageType.Success, "Created Location:" + result[row].id);

}
//Create transactional data in the model in case amount/value exists, if not only master data will be created

if(values[row] ){
Table_Projects.getDataSource().refreshData();

var input = Table_Projects.getPlanning().setUserInput({
"Version": "public." + "Actual",
"@MeasureDimension": "AMOUNT_GC",
"Date": quarter,
"DIMENSION": "[DIMENSION].[ALL_LOCATION].&[" + ID + "]"
}, values[row]);

Table_Projects.getPlanning().submitData();

console.log (input);
}

}

}

 

Data to be uploaded


 

Error



 

Thanks in Advance

Paramesh
paul_mckinsey
Discoverer
0 Kudos
Hi,

I was struggling really hard with this feature.

The code above is good as a starting point but there were several places where I had to adjust or amend it.

I've finally got the "Copy & Paste" feature for some simplified models BUT..

There was NO WAY to set properly the setUserInput command for the selection of standard dimension Date, because of its own standard hierarchies and properties.

Even THERE IS NO SAP Documentation (explanatory enough) around this, especially when the dimension has hierarchies.

For sure if you do not get the proper setUserInput when coding, you will get the red message saying the proper conditions were not met when running application

I would really appreciate SAP could release the proper information around this basic configuration: setting setUserInput for standard dimension Date.

Regards,
paul_mckinsey
Discoverer
0 Kudos
Hi,

I final tip...

I really hope I am wrong but I think the trick of this this code resides on the Dimensions included into the SAC table (DataSource) MUST be configured with the "Unbooked Data" option, checked on.

So, in case your table has many Dimensions with a lot of members each, and this representing a huge number of unbooked combination records (Number of Dimensions x Number of Members), the code will not work because SAC would not support that number of records.

More specifically, the own API Reference Guide, when referring to setUserInput, it mentions:

"setUserInput(selectedDataSelectionvaluestring)boolean
Sets a value to data cells. The data cells are specified by the selection. The selection references a visible data cell in the rows or columns of the table"
. (Clear enough)

It is really a pity this still has these restrictions and the expectation could not be covered. We will have to see if Q2-2023 SAC release will include a working feature for Copy & Paste from Excel as scheduled or it would be postponed.

Best regards,
former_member9278
Discoverer
0 Kudos

Great blog manuelr An excellent starting point for this alternative method!

I agree with the above comments on having unbooked data and "visible" intersections for this to work.  Some use cases involve multiple dimensions, resulting in a very big table when unbooked.  The other concern is submitting 1 record at a time.

Tips below for testing:

Tip #1 - Check the Granularity of the Model - is it Yearly, Quarterly, or Monthly

Tip #2 - Check the syntax of the ID's used in the dimension members AND hierarchy

Tip #3 - Check what the Date dimension hierarchy is set in the application table - is it YQM or YM.

Tip #4 - Check application ID's are in line with your logic - Table_1 or Table_Project

Tip #5 - Dimensions can be flat (except for Date) or show leaves.  The intersections have to be visible.  This will not work if the hierarchy is collapsed.

Tip #6 - Use consol.log() on every variable for a better understanding of the logic.  You can use the below code to identify variables more easily with Chrome Debugger (F12) using different "BREAKS".

console.log("BREAK1");
console.log(period);
console.log("BREAK2");

 

Below is a condensed version for transactional data upload only.  Master data maintenance can be added from the original code.  holger.laemmel2

var text = InputField_1.getValue();
var values = ArrayUtils.create(Type.string);
var file = text.split("\t");
var count = 0;
var row = 0;
var ID = "";
var VALUE = "";
var DATE = "";
var EMPLOYEE = "";
for (var i = 0; i < file.length; i++) {
if (count < 5) {
switch (count) {
case 0:
ID = file[i].split(" ")[1];
if (ID === undefined) {
ID = file[i];
} else if (ID !== undefined) {
ID = file[i].split(" ")[1];
}
case 2:
DATE = file[i].split(" ")[0];
case 3:
EMPLOYEE = file[i].split(" ")[0];
}
}
count = count + 1;
if (count > 4) {
count = 0;
row = row + 1;
VALUE = file[i + 1].split(" ")[0];
values[row] = VALUE;
//var quarter = "[Date].[YQM].&[" + DATE.substr(0, 4) + DATE.substr(DATE.length - 1, 1) + "]"; **Original
var period = "[Date].[YQM].&[" + DATE + "]";
if(values[row] ){
Table_Projects.getDataSource().refreshData();
var input = Table_Projects.getPlanning().setUserInput({
"Version": "public." + "FC1",
"@MeasureDimension": "Productivity",
"Date": period,
"Project": "[Project].[H1].&[" + ID + "]",
"Employee":"[Employee].[H1].&[" + EMPLOYEE + "]"
}, values[row]);
Table_Projects.getPlanning().submitData();
console.log (input);
}
}
}

Here is my dataset with YYYYMM.  paramesh9192

 

012345
IDVERSIONDATEEMPLOYEEMEASUREVALUE
PJT_1FC1202301AProductivity50000
PJT_2FC1202301BProductivity70000

Happy Learning!

 

Questions I have for anyone to answer.

  • Can we submit all entries at once instead of 1 record at a time? Please provide the code if you are able to solve it.
  • What is the pretty printer hotkey combination? 🙂
priyanka_Y
Participant
0 Kudos
Hi Manuel,

 

Great Blog,

By using the above code It is allowing to submit one record at a time, Is there any alternate solution for that.

 

Regards,

Priyanka

 
priyanka_Y
Participant
0 Kudos
Hi Manuel,

Great Blog.

Is there any alternative to submit multiple records at a time, As of now It is allowing us to submit only one record.

Regards,

Priyanka