6. apríla 2022

Ako obísť obmedzenia formulárov SAP FSM pomocou Google Forms

Inteligentné formuláre SAP FSM stále nie sú také chytré, ako by sme si priali. V prípadoch, keď je ponuka možností odpovedí závislá od hodnoty predchádzajúceho poľa formulára, má nástroj SAP FSM limitácie. Našťastie sa dajú použiť jednoduché a bezplatné Google forms.

FSM síce ponúka možnosť vytvárať Smartformuláre priamo prostredníctvom natívneho editora, avšak tento spôsob má jednu veľkú limitáciu a tou je absencia dynamického generovania týchto protokolov na základe vyplnených údajov. (Mimo skrytia a zobrazenia niektorých sekcií, pričom dokáže reagovať iba na dáta obsiahnuté priamo v Smartformulári, nie napr. na Materiál vytvorený na Aktivite).

V prípade jedného z našich zákazníkov z oblasti telekomunikácií, sme sa tak počas tohto projektu rozhliadli, po iných flexibilnejších možnostiach generovania týchto protokolov. Ako najideálnejšie riešenie sme zvolili integráciu na Google Forms, ktoré spracúva a dynamicky dopĺňa potrebné sekcie, určuje dizajn daného protokolu a vo výsledku generuje jeho PDF verziu spolu s jej odoslaním na zákazníka.

Pre odoslanie potrebných informácií do tohto nástroja sme preto vytvorili v SAP FSM biznis pravidlo, zhromažďujúce všetky potrebné informácie zabezpečujúce tiež aj ich príslušný formát očakávaný nástrojom Google Forms.

Definícia biznis pravidla

Najdôležitejším nastavením v definícii tohto pravidla, je v tomto prípade výber možnosti použitia plnej podpory Javascriptových výrazov. Hneď z dvoch dôvodov:

  1. Výber možnosti bez podpory týchto výrazov je zastaralý (deprecated).
  2. V ďalších sekciách tohto pravidla budeme potrebovať aktívne využívať Javascriptové funkcie, pre správne naformátovanie dát odosielaných do Google Forms.
overcoming-fsm-form-limitations-with-google-forms-1

Trigger (Spúšťač)

overcoming-fsm-form-limitations-with-google-forms-2

Je možné podľa potrieb použiť akýkoľvek. V našom prípade sme z viacerých dôvodov, či už procesných, tak optimalizácie výkonu, zvolili nasledovný:

Variables (premenné)

overcoming-fsm-form-limitations-with-google-forms-3

V tomto blogu sa nebudeme venovať všetkým, ale sústredíme sa iba tie, ktoré zabezpečujú odoslanie podpisov technika a zákazníka.
 

Definície a komentár jednotlivých premenných

Názov premennej: elements

Typ premennej: Array

Definícia CoreSQL:

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

Komentár:

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

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:

Názov premennej (Variable Name): signatureCustomer

Typ premennej (Variable Type): Object

Typ objektu (Object Type): Attachment

Definícia CoreSQL (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}

)

Poznámka: V prípade, že nám stačí iba ID objektu Attachment pre dotiahnutie binárnych dát podpisov a nepotrebujeme posielať ďalej, žiadnu inú informáciu nachádzajúcu sa v zázname objektu Attachment, môžeme tieto dve premenné vynechať.

Pre načítanie dát jednotlivých podpisov v pravidle používame dve akcie typu FSM Webhook volajúce /data API systému FSM:

  1. Akcia doťahujúca podpis zákazníka do premennej „signatureCustomerContent“Action that retrieves the customer’s signature into the „signatureCustomerContent“ variable. overcoming-fsm-form-limitations-with-google-forms-4
 
  1. Akcia doťahujúca podpis technika do premennej „signatureTechnicianContent“, vyzerá tak isto, ako signatureCustomerContent, líši sa iba v použitom ID pre načítanie podpisu.

 

Komentár:

V tejto akcii využívame premennú ${signatureCustomer.id} – ID záznamu podpisu v tabuľke Attachment (Prílohy), pre vytiahnutie podpisu zákazníka z aktuálne spracovávanej aktivity.

Okrem tejto premennej potom pre ľahšiu prenositeľnosť pravidla medzi systémami využívame systémové premenné – ${account.name} a ${company.name} + naše vlastné ${clientID} a ${clientSecret} o ktorých sa môžete viac dočítať tu

Vrátené hodnoty „signatureCustomerContent“ a „signatureTechnicianContent“ obsahujú binárne dáta podpisov v znakovej sade Latin1, ktoré následnou akciou odošleme do Google Forms. V danej akcii dáta ešte upravujeme do formátu podľa požiadaviek Google Forms. 

Akcia pre odoslanie dát do Google Forms

Action: Webhook

Execution Count: 1

Method: Post

URL: ${google-forms-api-url}

Headers: žiadne

Content-Type: application/json

Body:

${

// https://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).

Táto akcia môže na prvý pohľad vyzerať hrozivo, avšak po jej vysvetlení v nasledujúcich riadkoch na ňu podľa nás rýchlo zmeníte názor.

Ako prvé do očí hneď udrie blok kódu v úvode tela tejto akcie:

https://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},{}]},{},[])(„/“)});

 

Táto časť definuje javascriptovú funkciu, pre enkódovanie binárnych dát obrázkov podpisov do base64 formátu, v ktorom posielame podpisy do Google Forms. Túto konverziu vykonávame, aby počas odosielania týchto dát bola zachovaná ich integrita. Keďže systém SAP FSM, v rámci pravidiel nepodporuje import, či inštaláciu javascriptových modulov, tak ako sme na to zvyknutí pri štandardnom vývoji aplikácii, bolo v našom prípade potrebné zadefinovať relevantné funkcie, takto na začiatku body akcie. Pre zachytenie zdroja tejto definície obsahuje naše body v zakomentovanej sekcii odkaz na GitHub repository z ktorej pochádza – https://github.com/beatgammit/base64-js.

Funkcia latin1ToUint8Array potom zabezpečuje správnu znakovú sadu pre použitie base64 enkódovania cez funkciu fromByteArray:


function latin1ToUint8Array(str) {

return Array.prototype.map.call(str, function (c) { return c.charCodeAt(0) });
}

Pre úspešné zbehnutie metódy JSON.stringify() je potrebné zadefinovať záložné hodnoty, pre prípad, že by boli použité premenné prázdne, inak by pri tejto metóde mohlo dôjsť k chybe – ‚cannot read property … of undefined‘, následkom ktorej by bolo odoslanie dát do Google Forms neúspešné. Záložné hodnoty zabezpečujú nasledovné odstavce:

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 || [];

V závere body našej akcie potom už iba ostáva namapovať jednotlivé premenné na atribúty API vytvorenej na strane Google Forms, spolu s konverziou Javascriptových objektov na JSON string pomocou metódy JSON.stringify():

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,
})

(Skrátený príklad s vynechaním mapovania zvyšných premenných)

Ako môžete vidieť, tak práve v tomto poslednom kroku využívame vyššie zadefinované funkcie „fromByteArray“ a „latin1ToUint8Array“, pre správne enkódovanie dát podpisov, tak aby mohli byť z týchto informácie na druhej strane opäť zrekonštruované do ich grafickej podoby, ktorú je možné použiť vo finálnom vygenerovanom .pdf protokole.


Tomáš Potzy
, CX Consultant