Overcoming FSM form Limitations with Google Forms
FSM smart forms are still not as smart as many of us would like. For cases where the pulldown menu is dependent on the value of a previous form field, this tool is simply not enough smart. Fortunately, simple & free google forms can be used.
Although FSM offers the ability to create Smartforms directly through the native editor, this method has one major limitation and that is the absence of dynamic generation of these protocols, based on the data entered. (Except for hiding and showing some sections. It is only able to respond to data contained directly in the Smartform, not for example to the Material created on the Activity)
In the case of one of our customers in the field of telecommunications, we have looked at other more flexible options for generating these protocols during this project. As the most ideal solution, we chose integration on Google Forms, which processes and dynamically completes the necessary sections, determines the design of the protocol, and, as a result, generates a PDF version of it together with its sending to the customer.
For this integration, we used the business rules functionality on the SAP FSM side to create one in order to send and format the information required to generate the final protocol in Google forms, sent to the customer.
Definition of business rules
The most important setting in the definition of this rule is the choice of using full support for Javascript expressions. Namely for these two reasons:
- The choice of option without the support of these expressions is deprecated.
- In the next sections of this rule, we will need to actively use Javascript functions, for proper formatting of data sent to Google Forms.
Trigger
Depending on your specific requirements, you can pick any. In our case, we have chosen the following one for several reasons (like performance optimization and data availability):
Variables
In this blog, we will not address each of them, but we will focus only on those that are working with the signatures of technicians and customers.
Definitions and comments of individual variables
Variable name: elements
Variable type: Array
CoreSQL definition:
SELECT cie.value, cie.elementId, cie.description FROM ChecklistInstance ci
JOIN ChecklistInstanceElement cie ON ci.id = cie.checklistInstance
WHERE cie.elementId IN (‘z_f_sf_podpisucas’, ‘z_f_sf_podpistech’, ‘z_f_sf_typvyjazdu’, ‘z_f_sf_techpoznamka’) AND ci.object.objectId = ${activity.id}
DTO: ChecklistInstance.18;ChecklistInstanceElement.17
Commentary: V rámci zadania od nášho zákazníka bolo nevyhnutné, aby výsledný PDF protokol obsahoval podpisy oboch strán (technika a zákazníka). Pre ich odoslanie do Google Forms, sme tak potrebovali z databázy systému FSM načítať ich dáta v binárnej podobe enkódované, ako base64 reťazec. Pre tento účel najprv vyťahujeme zo smartformuláru (checklistu) ID záznamov týchto obrázkov uložených v objekte Attachment, na základe názvov týchto elementov (‘z_f_sf_podpistech’ a ‘z_f_sf_podpisucas’), ktoré sme zadefinovali v rámci Smartformuláru vypĺňaného technikom pri Servise. Pomocou týchto ID následne dotiahneme ich záznamy z objektu Attachment prostredníctom premenných „signatureCustomer“ a „signatureTechnician“:
As part of the requirement from our customer, it was necessary that the resulting PDF protocol contained the signatures of both parties (technician and customer). To send them to Google Forms, we needed to retrieve their data from the FSM database in binary form-encoded as a base64 string. For this purpose, we first extract from the smart form (checklist) the record IDs of these images stored in the Attachment object, based on the names of these elements (‘z_f_sf_podpistech’ and ‘z_f_sf_podpisucas’), which we defined within the Smartform filled out by the technician executing the Service call. Using these IDs, we then get their records from the Attachment object using the “signatureCustomer” and “signatureTechnician” variables:
Variable Name: signatureCustomer
Variable Type: Object
Object Type: Attachment
CoreSQL WHERE Clause:
signatureCustomer.id = ${elements.filter(function (e) { return e.elementId === ‘z_f_sf_podpisucas’ })[0].value}
– (The signatureTechnician variable differs only in CoreSQl WHERE Clause:
signatureTechnician.id = ${elements.filter(function (e) { return e.elementId === ‘z_f_sf_podpistech’ })[0].value}
)
Note: If we only need the ID of the Attachment object to retrieve the binary signature data and we do not need to forward any other information found in the Attachment object record, we can omit these two variables.
We use two FSM Webhook actions calling the Data API endpoints to retrieve the signature data from the database in our rule:
- Action that retrieves the customer’s signature into the “signatureCustomerContent” variable.
- The action that pulls the technician’s signature into the “signatureTechnicianContent” variable looks the same for the signatureCustomerContent, differing only in the ID used to retrieve the signature.
Commentary: In this action, we use the variable $ {signatureCustomer.id} – ID of the signature record in the Attachment table, to extract the customer’s signature from the currently processed activity.
In addition to this variable, we use the system variables – $ {account.name} and $ {company.name} + our own ${clientID} and ${clientSecret}, which you can read more about here for easier portability of the rule.
The “signatureCustomerContent” and “signatureTechnicianContent” return values contain binary signature data in the Latin1 character set, which we then after some additional processing send to Google Forms
The action sending data to Google Forms
Action: Webhook
Execution Count: 1
Method: Post
URL: ${google-forms-api-url}
Headers: none
Content-Type: application/json
Body:
${
//github.com/beatgammit/base64-js
(function(a){if(“object”==typeof exports&&”undefined”!=typeof module)module.exports=a();else if(“function”==typeof define&&define.amd)define([],a);else{var b;b=”undefined”==typeof window?”undefined”==typeof global?”undefined”==typeof self?this:self:global:window,b.base64js=a()}})(function(){return function(){function b(d,e,g){function a(j,i){if(!e[j]){if(!d[j]){var f=”function”==typeof require&&require;if(!i&&f)return f(j,!0);if(h)return h(j,!0);var c=new Error(“Cannot find module ‘”+j+”‘”);throw c.code=”MODULE_NOT_FOUND”,c}var k=e[j]={exports:{}};d[j][0].call(k.exports,function(b){var c=d[j][1][b];return a(c||b)},k,k.exports,b,d,e,g)}return e[j].exports}for(var h=”function”==typeof require&&require,c=0;c<g.length;c++)a(g[c]);return a}return b}()({“/”:[function(a,b,c){‘use strict’;function d(a){var b=a.length;if(0<b%4)throw new Error(“Invalid string. Length must be a multiple of 4”);var c=a.indexOf(“=”);-1===c&&(c=b);var d=c===b?0:4-c%4;return[c,d]}function e(a,b,c){return 3*(b+c)/4-c}function f(a){var b,c,f=d(a),g=f[0],h=f[1],j=new m(e(a,g,h)),k=0,n=0<h?g-4:g;for(c=0;c<n;c+=4)b=l[a.charCodeAt(c)]<<18|l[a.charCodeAt(c+1)]<<12|l[a.charCodeAt(c+2)]<<6|l[a.charCodeAt(c+3)],j[k++]=255&b>>16,j[k++]=255&b>>8,j[k++]=255&b;return 2===h&&(b=l[a.charCodeAt(c)]<<2|l[a.charCodeAt(c+1)]>>4,j[k++]=255&b),1===h&&(b=l[a.charCodeAt(c)]<<10|l[a.charCodeAt(c+1)]<<4|l[a.charCodeAt(c+2)]>>2,j[k++]=255&b>>8,j[k++]=255&b),j}function g(a){return k[63&a>>18]+k[63&a>>12]+k[63&a>>6]+k[63&a]}function h(a,b,c){for(var d,e=[],f=b;f<c;f+=3)d=(16711680&a[f]<<16)+(65280&a[f+1]<<8)+(255&a[f+2]),e.push(g(d));return e.join(“”)}function j(a){for(var b,c=a.length,d=c%3,e=[],f=16383,g=0,j=c-d;g<j;g+=f)e.push(h(a,g,g+f>j?j:g+f));return 1===d?(b=a[c-1],e.push(k[b>>2]+k[63&b<<4]+”==”)):2===d&&(b=(a[c-2]<<8)+a[c-1],e.push(k[b>>10]+k[63&b>>4]+k[63&b<<2]+”=”)),e.join(“”)}c.byteLength=function(a){var b=d(a),c=b[0],e=b[1];return 3*(c+e)/4-e},c.toByteArray=f,c.fromByteArray=j;for(var k=[],l=[],m=”undefined”==typeof Uint8Array?Array:Uint8Array,n=”ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”,o=0,p=n.length;o<p;++o)k[o]=n[o],l[n.charCodeAt(o)]=o;l[45]=62,l[95]=63},{}]},{},[])(“/”)});
function latin1ToUint8Array(str) {
return Array.prototype.map.call(str, function (c) { return c.charCodeAt(0) });
}
function findWithFallback(arr, fn) {
var item = arr.filter(fn);
return item.length ? item[0] : {};
}
function MissingValue() { return this.constructor === MissingValue ? this : new MissingValue() }
MissingValue.ensure = function(value) { return value || MissingValue() }
MissingValue.prototype.toString = function() { return ‘[object MissingValue]’ }
MissingValue.prototype.toJSON = function() { return null }
// fallbacks – we want to avoid ‘cannot read property … of undefined’ error
serviceCall = MissingValue.ensure(serviceCall);
contract = MissingValue.ensure(contract);
businessPartner = MissingValue.ensure(businessPartner);
address = MissingValue.ensure(address);
technician = MissingValue.ensure(technician);
signatureTechnician = MissingValue.ensure(signatureTechnician);
signatureCustomer = MissingValue.ensure(signatureCustomer);
elements = elements || [];
creds = creds || [];
devices = devices || [];
JSON.stringify({
“event”: “pdf”,
“podpis_obrazok_1”: signatureTechnician instanceof MissingValue
? null
: base64js.fromByteArray(latin1ToUint8Array(signatureTechnicianContent)),
“podpis_typ_1”: signatureTechnician.type,
“podpis_obrazok_2”: signatureCustomer instanceof MissingValue
? null
: base64js.fromByteArray(latin1ToUint8Array(signatureCustomerContent)),
“podpis_typ_2”: signatureCustomer.type,
})
}
(Abbreviated example omitting the mapping of the remaining variables).
This event may seem complicated at first glance, but we believe, that you will change your opinion of it, once we go through it in the following lines.
The most intimidating looking block of code is located right at the beginning of the action:
//github.com/beatgammit/base64-js
(function(a){if(“object”==typeof exports&&”undefined”!=typeof module)module.exports=a();else if(“function”==typeof define&&define.amd)define([],a);else{var b;b=”undefined”==typeof window?”undefined”==typeof global?”undefined”==typeof self?this:self:global:window,b.base64js=a()}})(function(){return function(){function b(d,e,g){function a(j,i){if(!e[j]){if(!d[j]){var f=”function”==typeof require&&require;if(!i&&f)return f(j,!0);if(h)return h(j,!0);var c=new Error(“Cannot find module ‘”+j+”‘”);throw c.code=”MODULE_NOT_FOUND”,c}var k=e[j]={exports:{}};d[j][0].call(k.exports,function(b){var c=d[j][1][b];return a(c||b)},k,k.exports,b,d,e,g)}return e[j].exports}for(var h=”function”==typeof require&&require,c=0;c<g.length;c++)a(g[c]);return a}return b}()({“/”:[function(a,b,c){‘use strict’;function d(a){var b=a.length;if(0<b%4)throw new Error(“Invalid string. Length must be a multiple of 4”);var c=a.indexOf(“=”);-1===c&&(c=b);var d=c===b?0:4-c%4;return[c,d]}function e(a,b,c){return 3*(b+c)/4-c}function f(a){var b,c,f=d(a),g=f[0],h=f[1],j=new m(e(a,g,h)),k=0,n=0<h?g-4:g;for(c=0;c<n;c+=4)b=l[a.charCodeAt(c)]<<18|l[a.charCodeAt(c+1)]<<12|l[a.charCodeAt(c+2)]<<6|l[a.charCodeAt(c+3)],j[k++]=255&b>>16,j[k++]=255&b>>8,j[k++]=255&b;return 2===h&&(b=l[a.charCodeAt(c)]<<2|l[a.charCodeAt(c+1)]>>4,j[k++]=255&b),1===h&&(b=l[a.charCodeAt(c)]<<10|l[a.charCodeAt(c+1)]<<4|l[a.charCodeAt(c+2)]>>2,j[k++]=255&b>>8,j[k++]=255&b),j}function g(a){return k[63&a>>18]+k[63&a>>12]+k[63&a>>6]+k[63&a]}function h(a,b,c){for(var d,e=[],f=b;f<c;f+=3)d=(16711680&a[f]<<16)+(65280&a[f+1]<<8)+(255&a[f+2]),e.push(g(d));return e.join(“”)}function j(a){for(var b,c=a.length,d=c%3,e=[],f=16383,g=0,j=c-d;g<j;g+=f)e.push(h(a,g,g+f>j?j:g+f));return 1===d?(b=a[c-1],e.push(k[b>>2]+k[63&b<<4]+”==”)):2===d&&(b=(a[c-2]<<8)+a[c-1],e.push(k[b>>10]+k[63&b>>4]+k[63&b<<2]+”=”)),e.join(“”)}c.byteLength=function(a){var b=d(a),c=b[0],e=b[1];return 3*(c+e)/4-e},c.toByteArray=f,c.fromByteArray=j;for(var k=[],l=[],m=”undefined”==typeof Uint8Array?Array:Uint8Array,n=”ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”,o=0,p=n.length;o<p;++o)k[o]=n[o],l[n.charCodeAt(o)]=o;l[45]=62,l[95]=63},{}]},{},[])(“/”)});
This section defines a javascript function for encoding the binary data of signature images into the base64 format in which we send signatures to Google Forms. We are performing this conversion to maintain the integrity of this data as it is being sent. Since the SAP FSM system does not support the import or installation of javascript modules within the rules, as we are used to in standard application development, in our case it was necessary to define the relevant functions in the body of the action. To capture the source of this definition, our body in the commented section contains a link to the GitHub repository from which it comes – https://github.com/beatgammit/base64-js.
The latin1ToUint8Array function then provides the correct character set to use base64 encoding through the fromByteArray function
function latin1ToUint8Array(str) {
return Array.prototype.map.call(str, function (c) { return c.charCodeAt(0) });
}
For the JSON.stringify() method to run successfully, it is necessary to define backup values, in case the used variables are empty, otherwise, this method could cause an error – ‘cannot read property … of undefined’, which would result in failure during sending data to Google Forms. The availability of backup values is ensured by the following code blocks:
function findWithFallback(arr, fn) {
var item = arr.filter(fn);
return item.length ? item[0] : {};
}
function MissingValue() { return this.constructor === MissingValue ? this : new MissingValue() }
MissingValue.ensure = function(value) { return value || MissingValue() }
MissingValue.prototype.toString = function() { return ‘[object MissingValue]’ }
MissingValue.prototype.toJSON = function() { return null }
// fallbacks – we want to avoid the ‘cannot read property … of undefined’ error
serviceCall = MissingValue.ensure(serviceCall);
contract = MissingValue.ensure(contract);
businessPartner = MissingValue.ensure(businessPartner);
address = MissingValue.ensure(address);
technician = MissingValue.ensure(technician);
signatureTechnician = MissingValue.ensure(signatureTechnician);
signatureCustomer = MissingValue.ensure(signatureCustomer);
elements = elements || [];
creds = creds || [];
devices = devices || [];
At the end of our action body, all that remains is to map the individual variables to the attributes of the API created on the Google Forms side, together with the conversion of Javascript objects to JSON strings using the JSON.stringify () method:
JSON.stringify({
“event”: “pdf”,
“podpis_obrazok_1”: signatureTechnician instanceof MissingValue
? null
: base64js.fromByteArray(latin1ToUint8Array(signatureTechnicianContent)),
“podpis_typ_1”: signatureTechnician.type,
“podpis_obrazok_2”: signatureCustomer instanceof MissingValue
? null
: base64js.fromByteArray(latin1ToUint8Array(signatureCustomerContent)),
“podpis_typ_2”: signatureCustomer.type,
})
(Abbreviated example omitting the mapping of the remaining variables)
As you can see, in this last step we use the above-defined functions “fromByteArray” and “latin1ToUint8Array“, for the correct encoding of signature data, so that they can be reconstructed from this information into their graphical form in Google Forms, for their use in the final generated .pdf protocol.
Tomáš Potzy, CX Consultant