Introduction
My name is Julian Kuipers and I am a Full Stack SAP Developer working in The Netherlands. Currently I am working at INNOV8iON a consultancy club focussed on SAP-innovation and development.
During a recent project for a client, a technical solution was needed to allow business users to create requests for new Business Objects. To address this requirement, I developed an SAPUI5 application, enabling business users to initiate such requests. These requests then trigger a workflow in the backend, based on a custom SAP Business Object named "Request."
One essential feature of this application is the ability for users to upload attachments to our SAP S/4 HANA system before the actual Business Object is instantiated. Once the object is created, the attachments are associated with that specific Business Object.
Overview
While this implementation involves a custom Business Object, the same approach should be applicable to standard SAP Business Objects such as Purchase Requisitions or Sales Orders.
While researching this topic, I discovered an informative blog post:
Attachment Service to Your Rescue in S4HANA Fiori Elements Using Reuse Components (GOS & DMS). This article introduced me to the SAP Attachment Service, for which further details can be found in the official
SAP Documentation.
Despite my research, I was unable to locate a comprehensive guide on implementing this feature in an SAPUI5 project. This prompted me to write this blog post, detailing the implementation of the SAP Attachment Service within an SAPUI5 project backed by SAP S/4 HANA.
Please note: the code shared in this blog post is a simplified version of what I implemented in my application.
Solution
The implementation of the SAP Attachment Service encompasses several distinct steps, which I will detail in the following sections of this blog post. Here are the essential phases that I will guide you through:
- Short Recap of the Architecture of the SAP Attachment Service
- Activating the SAP Attachment Service
- Implementing the SAP Attachment Service in my OData Service
- Implementing the SAP Attachment Service in my SAPUI5 Project
- Implementing Authorization on Uploading Attachments
Recap of the necessary knowledge
Before delving into the implementation details, I recommend reading the
blog and resources by Mahesh Palavalli on the SAP Attachment Service in an SAP Fiori Elements app. This material provides a basic understanding of how this service operates.
Here is a quick recap:
- Posting the Attachment with a Temporary Key: The recommended approach involves uploading the attachment directly to the backend (with a temporary key) as soon as the user initiates the upload. Once the user has completed creating the actual Business Object, the attachment is permanently saved and linked to the newly created object via my OData service.
- Overview of the Solution: A summary of the solution can be obtained from the SAP Attachment Service documentation.
For a more in-depth understanding and additional insights, please refer to the official
SAP Attachment Service documentation.
Activating the SAP Attachment Service in your SAP S/4 HANA system
First and foremost, I need to ensure that the Attachment Service is active, as without it, I can't use the service. Here are the steps I take to activate it in my SAP S/4 HANA system:
- Activate the OData-service CV_ATTACHMENT_SRV: I simply navigate to the transaction /n/IWFND/MAINT_SERVICE, where I activate the required OData service.
- Activate the ICF Node to Consume the Library: Next, I need to make sure my application can access the library where the Attachment Service front-end component resides. To do this, I use the transaction SICF and activate the node /sap/bc/ui5_ui5/sap/plm_ath_cres1.
These two actions form the initial setup for using the SAP Attachment Service within my project, laying the groundwork for its subsequent integration into my SAPUI5 application.
Implementing the SAP Attachment Service in my OData-service
Now I need to implement the back-end logic in my OData-service. This assures that when the user creates the objects, I can utilize the provided temporary key (used to temporarily save the attachments) to permanently save the attachments to my Business Object.
To accomplish this, I must implement specific code in the create_entity method within my DPC_EXT class of the OData-service:
* PUT YOUR CODE WHICH CREATES THE ACTUAL BUSINESS OBJECT HERE AND USE THE OBJECT KEY (LIKE A GUID) BELOW!
"Save attachments
DATA: lt_ret type bapiret2_t,
lt_ret_fail TYPE IF_ODATA_CV_ATTACHMENT_API=>TY_T_ATTACH,
lv_obj_key TYPE objky,
lv_incoming_draft_key type objky,
lv_objtype TYPE dokob,
lv_objkey TYPE objky.
"Map OBJ type to short variant
lv_objtype := ‘<MY_BUSINESS_OBJECT>’. "Like ‘ZMY_OBJ’
"Map OBJ key to right type
lv_objkey := ‘<MY_OBJECT_KET>’. "I used a generated GUID
"Get request headers
DATA(lt_headers) = io_tech_request_context->get_request_headers( ).
lv_incoming_draft_key := VALUE #( lt_headers[ name = 'temp-obj-key' ]-value OPTIONAL ).
"Create instance of attachment service
DATA(lo_atta_srv) = cl_odata_cv_attachment_api=>get_instance( ).
ASSERT lo_atta_srv IS BOUND.
"Save attachments
lo_atta_srv->if_odata_cv_attachment_api~save(
EXPORTING
iv_objecttype = lv_objtype " Linked SAP Object
iv_objecttype_long = iv_obj_type " Type of Object
iv_objectkey = lv_objkey " Document management object key
iv_temp_objectkey = lv_incoming_draft_key " Temporary Key - to be replaced
iv_no_commit = abap_false
IMPORTING
ev_success = rv_succes
et_messages = lt_ret " Return parameter table
et_failed_keys = lt_ret_fail
).
The code segment mentioned earlier utilizes the cl_odata_cv_attachment_api-> if_odata_cv_attachment_api~save method to take the temporarily saved attachment and permanently save it, connecting it to the Business Object. Here's how the process works:
- We assume that we receive the temporary key used for the attachments from the SAPUI5 application in the header, identified by the key 'temp-obj-key.'
- At this stage in the process, my Business Object has already been created.
- I use the key of that object (e.g., GUID) and the Business Object Type (e.g., ZMY_OBJ) to connect the attachments to that particular instance of the object.
This method ensures that the attachments are seamlessly integrated with the associated Business Object, providing a coherent and efficient solution for managing related files.
Implementing the SAP Attachment Service in my SAPUI5-project
To integrate the Attachment Service into my SAPUI5 application, I must first include it as a dependency within my project. This is a straightforward step achieved by adding the following code snippet to my manifest.json file under the sap.ui5/dependencies section:
"components": {
"sap.se.mi.plm.lib.attachmentservice.attachment": {
"lazy": true
}
}
By including this code, the Attachment Service becomes part of my project's dependencies, making it available for utilization within the SAPUI5 application.
After declaring the dependencies, the next step is to specify the usage of the component within the manifest.json file, under the sap.ui5 section. This should be done right after the dependencies declaration:
"componentUsages": {
"attachmentReuseComponent": {
"name": "sap.se.mi.plm.lib.attachmentservice.attachment",
"settings": {}
}
}
By making this declaration, I am ensuring that the SAPUI5 application recognizes and appropriately uses the Attachment Service component. It establishes a proper connection, enabling the SAPUI5 application to leverage the Attachment Service as intended.
That concludes the necessary modifications to the manifest.json file. With that setup, I'm ready to create the Attachment Service component within my application. This essential component is responsible for handling both the user interface (UI) for uploading attachments and the uploading process itself.
The UI of the Attachment Service component looks like this:
UI
To achieve this functionality, I must create a component container where the instantiated Attachment Service component will reside. Utilizing the
sap.ui.core.ComponentContainer class, I designate an appropriate ID for the container. This ID allows me to retrieve the container later and attach the specific component.
Here's the code snippet illustrating how I create the component container:
<!-- Attachments -->
<core:ComponentContainer id="attachmentComponentContainer" propagateModel="true" />
This part of the implementation ensures that the Attachment Service component has a designated place within the application, and that it can be readily accessed and manipulated as needed.
After defining the component container, the next step in my process is to implement the code within my controller to instantiate the Attachment Service component. Once instantiated, I place it into the previously created component container.
Here's the code that carries out these actions:
/**
* Set up the file uploader component for the specified object.
*
* @function
* @private
* @param {string} sObject - The main object identifier.
*/
_setFileUploader: function (sObject) { // like ‘ZMY_OBJ’
const that = this;
const sId = "attachmentComponentContainer";
// Check if FileUploader does not exist yet
const oCurrentFileUploader = this.byId(sId).getComponent();
if (oCurrentFileUploader === null) {
// Create Object Key
const sObjKey = this._generateUniqueObjectKey();
this._storeTempKeyInModel(sObjKey);
// Create Upload Component
const oPromise = this.getOwnerComponent().createComponent({
usage: "attachmentReuseComponent",
settings: {
mode: "I",
objectKey: sObjKey,
objectType: sObject,
onupload: [that.handleUpload, that],
ondelete: [that.handleDelete, that]
}
});
// Set upload component
oPromise.then(function (attachmentComponent) {
const oAttrList = attachmentComponent.getAttributes();
const oUpdatedAttrList = this._prepareAttrList(oAttrList);
attachmentComponent.setAttributes(oUpdatedAttrList);
that.byId(sId).setComponent(attachmentComponent);
// Update counter for uploading files
attachmentComponent
.page
.getController()
.getUploadCollectionControl()
.attachBeforeUploadStarts(
function (oEvent) {
that.fileUploadBusyCounter++;
}
);
}.bind(this));
}
}
By following these steps, the Attachment Service component is not only created but also properly placed within the application structure. This ensures that it can effectively manage user uploads and other related tasks within the SAPUI5 application.
Within the implementation, I carefully manage a sequence of tasks to ensure proper attachment handling:
- Check for Existing Component Container: I start by checking if the component container does not already exist.
- Create a Unique Temporary Key: If the container doesn't exist, I generate a unique temporary key. This key's uniqueness ensures that the uploaded attachment can be specifically identified by the user, Business Object, and session. I also save this key in my model to retrieve it later during the CREATE call of my Business Object.
- Create the Component: I then create the component, which is a promise, using the following settings:
- Mode ‘I’ for uploading new attachments;
- Object key as the unique key I generated;
- Functions for the ‘onUpload’ and ‘onDelete’ events and a reference to the current ‘this‘ scope.
- Promise Resolution and Component Modification: Once the promise has resolved, I modify the component's attributes to determine UI visibility and allowed actions.
- Set Component in Container: I retrieve the component container and use the setComponent function to set the component.
- Manage Busy Indicator: This part is optional but useful. I add a global variable called ‘fileUploadBusyCounter’ to manage the busy indication and control the ‘next’ button's enablement.
- File Upload Management: I use the ‘attachBeforeUploadStarts’ function of the File Uploader to update the counter each time an attachment gets uploaded.
In the process of creating the Business Object in the backend, a crucial step is to ensure that the right attachments are retrieved and properly connected to the actual Business Object. This connection relies on a temporary key that is generated earlier in the process.
/**
* Makes a backend call to a given path to perform an action.
*
* @function
* @private
* @param {string} sPath - Path for the OData service.
* @param {Object} oContext - Context to be used for the action.
* @param {string} sTempKeyPath - Path of temporary key for the object.
* @param {string} sCurrentFileUploader - Current file uploader context.
*
*/
_makeBackendCall: function (sPath, oContext) {
const oViewModel = this.getView().getModel();
// Set the payload
const oData = {
d: oContext.getObject()
};
// Clean up the oData from __metadata attribute
delete oData.d.__metadata;
// Add headers
const sTempObjKey = this._getTempKeyFromModel();
oViewModel.setHeaders({
'temp-obj-key': sTempObjKey
})
// Call backend for simulation
oViewModel.create(sPath, oData, {
// Callback functions for success and error
});
}
In the implementation of the logic for attaching files to the Business Object from a SAPUI5 project, the temporary key plays a pivotal role. This unique identifier, generated earlier in the process, becomes instrumental in linking the right attachments to the respective Business Object.
Here's a brief recap of the steps:
- Retrieve the Temporary Key: I get the temporary key from my model, where it was stored after generation.
- Set the Key in the Header: I incorporate this key into the header of my request, which will be used in the CREATE call to the backend.
- Make the CREATE Call: With the header properly configured, I make the CREATE call to the backend, initiating the process to attach the files and create the Business Object.
- Finalize: The backend processes the temporary key to retrieve and link the attachments, finalizing the creation of the Business Object with the connected files.
This implementation ensures a seamless integration of file attachments within your Business Objects, leveraging the powerful features of SAPUI5 and the SAP Attachment Service. By following this approach, you can provide your users with a flexible and efficient way to manage attachments within your SAPUI5-project.
And with that, the frontend part of uploading attachments to your Business Object from a SAPUI5 project is complete!
I will explain the other functions used in this process, starting with the handleUpload function. This function is triggered when an attachment is being uploaded from the component.
/**
* Handles the upload event and processes the uploaded attachment.
* Depending on the source container, it saves the attachment to the respective data model path.
*
* @function
* @public
* @param {sap.ui.base.Event} oEvent - The event triggered by the attachment upload.
* @returns {void}
*/
handleUpload: function (oEvent) {
const { status, fileName } = oEvent.getParameters();
const sDataPath = '/attachmentString';
if (status === 'UPLOADSUCCESS') {
this._addAttachmentToString(fileName, sDataPath);
// Update counter for uploading files
this.fileUploadBusyCounter--;
// Add more logic if needed
}
}
The handleUpload function checks the status of the upload process. If the status equals 'UPLOADSUCCESS', the function performs the following actions:
- Adding the Uploaded Filename: It adds the uploaded attachment's filename to a string containing all uploaded attachments. This string can be displayed to the user to provide information about the files that have been successfully uploaded.
- Updating the Counter: The function decrements the fileUploadBusyCounter, indicating that this particular attachment has finished uploading.
You can further extend this function to incorporate additional logic as needed for your application. Also, be sure to consult the official documentation or debug the code to understand other statuses that may need handling.
Next, I will explain the handleDelete function, which is triggered when an attachment is deleted from the component.
/**
* Handles the delete event and removes the deleted attachment.
* Depending on the source container, it removes the attachment from the respective data model path.
*
* @function
* @public
* @param {sap.ui.base.Event} oEvent - The event triggered by the attachment deletion.
* @returns {void}
*/
handleDelete: function (oEvent) {
const { status, fileName } = oEvent.getParameters();
const sDataPath = '/attachmentString';
if (status === 'DELETED') {
this._removeAttachmentFromString(fileName, sDataPath);
// Add more logic if needed
}
}
The handleDelete function evaluates the status of the deletion process. If the status equals 'DELETED', the function performs the following tasks:
Removing the Deleted Filename: It removes the deleted attachment's filename from a string containing all uploaded attachments. This updated string can then be displayed to the user to reflect the current state of uploaded files.
You may extend this function by incorporating additional logic specific to your application's needs. To understand other statuses that may require handling, you can debug the code or refer to the official documentation.
Now I will delve into the _prepareAttrList function. This particular function is responsible for modifying the attributes of the attachments component within the SAPUI5 application.
/**
* Prepares the attributes list by setting visibility attributes and actions.
*
* @function
* @private
* @param {object} oList - The list of attributes.
* @returns {object} The updated list of attributes.
*/
_prepareAttrList: function (oList) {
// Determine visible attributes
const aVisibleAttributes = [
'UPLOADEDBY',
'UPLOADEDON',
'FILESIZE',
'ENABLELINK',
'ATTACHMENTSTATUS',
'ATTACHMENTTITLE',
'DIRDETAILS',
'SOURCE'
];
// Determine visible actions
const aVisibleActions = ['DELETE', 'ADD'];
// Set visible attributes
Object.keys(oList._VisibleAttributes).forEach((sAttribute) => {
oList._VisibleAttributes[sAttribute] = aVisibleAttributes.includes(sAttribute);
});
// Set visible actions
Object.keys(oList._VisibleActions).forEach((sAction) => {
oList._VisibleActions[sAction] = aVisibleActions.includes(sAction);
});
return oList;
}
The function’s main role is to define which attributes and actions should be visible or enabled within the interface. It performs this by iterating over the attributes and actions in the original list and altering their values as needed.
It's essential to refer to the documentation or utilize debugging to explore all available options. This will allow you to fully understand and manipulate the component’s attributes as per your application's requirements.
Lastly, I want to discuss a piece of code that addresses a scenario where a user uploads attachments but doesn't complete the creation of the Business Object. In such cases, it's essential to maintain a clean environment in the table for temporary attachments (ODATA_CV_ATTACH). Hence, manual cancellation of uploads becomes necessary.
Below, you'll find a function that deletes the uploaded attachments by invoking the cancel function of the Attachment Service Component within an SAPUI5 project. This ensures that any unnecessary or orphaned attachments are removed, keeping the system clean and efficient.
/**
* Deletes uploaded attachments.
*
* @function
* @private
*/
_deleteUploadedAttachments: function () {
const oUploadComponent = this
.byId('attachmentComponentContainer')
.getComponentInstance();
if (oUploadComponent) {
oUploadComponent.cancel(false);
}
}
Refer to the SAP documentation to get more details on how this cancellation process functions within the context of your project.
Furthermore, to automate this cleanup process, you can use the report
DELETE_DRAFT_ATTACHMENTS to scan the ODATA_CV_ATTACH table, identifying specified Business Objects older than a designated age, and then purge them accordingly.
By incorporating these mechanisms into your project, you ensure a more streamlined and efficient management of attachments, especially in scenarios where the creation process of Business Objects is not completed.
Implementing authorisation on uploading attachments
To enable the Attachment Service, it's necessary to implement the BAdi BADI_CV_ODATA_ATTACHMENTS_AUTH. Within this BAdi, the system checks whether the user who intends to upload the attachments to the actual Business Object is authorized.
Here's the code used in the method IF_EX_CV_ODATA_ATTACHMENT_AUTH~CHECK_AUTHORIZATION:
METHOD if_ex_cv_odata_attachment_auth~check_authorization.
AUTHORITY-CHECK OBJECT '<AUTHORIZATION_OBJECT>' FOR USER iv_user
ID '<AUTH_FIELD>' FIELD iv_objecttype_long
ID 'ACTVT' FIELD iv_activity.
IF sy-subrc = 0.
cv_no_authorization = abap_false.
ENDIF.
ENDMETHOD.
This code validates whether the user, specified by iv_user, is authorized for the activity referenced by iv_activity on the Business Object defined by iv_objecttype_long. In this instance, I employed a custom authorization object.
Should this BAdi not be implemented, the authorization check will fail by default, and the user will be unable to upload any attachments. This step ensures the integrity and security of the data.
Conclusion
Figuring all of this out proved to be a complex task, especially as the documentation available was not entirely complete. However, through a blend of creativity and strategic debugging, I managed to accomplish the desired outcome.
With just a few code snippets in your SAPUI5 application (specifically within the manifest.json, view, and controller), it's possible to leverage the Attachment Service Component to facilitate user attachment uploading. I've also integrated optional logic to control the application's flow during the upload process and utilized a temporary key in the headers of the CREATE call.
In addition, some ABAP code has been added in the create_entity method of our OData DPC_EXT class. This helps save the temporary attachments and connect them to the actual Business Object. Lastly, don't overlook the implementation of the BADI_CV_ODATA_ATTACHMENTS_AUTH BAdi, ensuring that the user is authorized to upload attachments to the specific Business Object. Forgetting this part will render the entire process ineffective!
I wish you the best of luck with your implementation and I am looking forward to your reactions!
Kind regards,
Julian Kuipers
References: