/* ************************************************************************************ * Copyright (C) 2012-2020 Openbravo S.L.U. * Licensed under the Openbravo Commercial License version 1.0 * You may obtain a copy of the License at http://www.openbravo.com/legal/obcl.html * or in the legal folder of this module distribution. ************************************************************************************ */ /*global OB, _, enyo, Backbone, ServiceWorkerUtil, CryptoJS, Promise */ OB.Model = window.OB.Model || {}; OB.MobileApp = OB.MobileApp || {}; OB.Data = OB.Data || {}; // used for test purposes to keep track that sync dialog was shown OB.Data.showSynchronizedDialogChangeTime = 0; OB.MobileApp.windowRegistry = new (Backbone.Model.extend({ registeredWindows: [], registerWindow: function(window) { if ( OB && OB.MobileApp && OB.MobileApp.model && OB.UTIL.isNullOrUndefined(OB.MobileApp.model.get('defaultWindow')) && window.defaultWindow ) { OB.MobileApp.model.set('defaultWindow', window.route); } else if (window.defaultWindow) { if (OB && OB.error) { OB.warn('Default window ignored. ' + window.windowClass); } else { OB.warn('Default window ignored. ' + window.windowClass); } } this.registeredWindows.push(window); } }))(); OB.MobileApp.actionsRegistry = new OB.Actions.Registry(); OB.MobileApp.statesRegistry = new OB.State.Registry(); OB.Model.Collection = Backbone.Collection.extend({ constructor: function(data) { this.ds = data.ds; Backbone.Collection.prototype.constructor.call(this); }, inithandler: function(init) { if (init) { init.call(this); } }, exec: function(filter) { var me = this; if (this.ds) { this.ds.exec(filter, function(data, info) { var i; me.reset(); me.trigger('info', info); if (data.exception) { OB.UTIL.showError(data.exception.message); } else { for (i in data) { if (data.hasOwnProperty(i)) { me.add(data[i]); } } } }); } } }); // Terminal model. OB.Model.Terminal = Backbone.Model.extend({ defaults: { terminal: null, context: null, permissions: null, businesspartner: null, location: null, pricelist: null, pricelistversion: null, currency: null, connectedToERP: null, terminalLogContextPopUp: [], loginUtilsUrl: '../../org.openbravo.mobile.core.loginutils', loginUtilsParams: {}, supportsOffline: false, loginHandlerUrl: '../../org.openbravo.mobile.core/LoginHandler', applicationFormatUrl: '../../org.openbravo.mobile.core/OBCLKER_Kernel/Application', logConfiguration: { deviceIdentifier: '', logPropertiesExtension: [] }, windows: null, appName: 'OBMOBC', useBarcode: false, useEmbededBarcode: true, profileOptions: { showOrganization: true, showWarehouse: true, defaultProperties: { role: null, organization: null, warehouse: null, languagage: null } }, localDB: { size: OB.UTIL.VersionManagement.current.mobileCore.WebSQLDatabase.size, name: OB.UTIL.VersionManagement.current.mobileCore.WebSQLDatabase.name, displayName: OB.UTIL.VersionManagement.current.mobileCore.WebSQLDatabase.displayName }, shouldExecuteBenchmark: false, propertiesLoaders: [], dataSyncModels: [] }, initialize: function() { var me = this; // expose terminal globally window.OB.MobileApp = OB.MobileApp || {}; window.OB.MobileApp.model = this; OB.UTIL.HookManager.executeHooks('OBMOBC_InitActions', {}, function(args) { if (args && args.cancelOperation && args.cancelOperation === true) { return; } else { me.initActions(function() {}); } }); // attach objects to model this.router = new OB.Model.RouterHelper(this); this.windowRegistry = OB.MobileApp.windowRegistry; OB.UTIL.VersionManagement.deprecated( 27349, function() { this.hookManager = OB.UTIL.HookManager; }, function(deprecationMessage) { this.hookManager = { registerHook: function(qualifier, func) { OB.warn(deprecationMessage + ". Hook: '" + qualifier + "'"); OB.UTIL.HookManager.registerHook(qualifier, func); }, executeHooks: function(qualifier, args, callback) { OB.warn(deprecationMessage + ". Hook: '" + qualifier + "'"); OB.UTIL.HookManager.executeHooks(qualifier, args, callback); }, callbackExecutor: function(args, callback) { OB.warn(deprecationMessage); OB.UTIL.HookManager.callbackExecutor(args, callback); } }; }, this ); // DEVELOPER: activate this to see all the events that are being fired by the backbone components of the terminal // this.on('all', function(eventName, object, third) { // if (eventName.indexOf('change') >= 0) { // return; // } // var enyoName = object ? object.name : ''; // OB.info('event fired: ' + eventName + ', ' + enyoName + ', ' + third); // }); // model events this.on('window:ready', this.renderContainerWindow); // When the active window is loaded, the window is rendered with the renderContainerWindow function this.on('terminalInfoLoaded', function() { OB.debug('next process: processPropertyLoaders'); OB.UTIL.localStorage.setItem( 'cacheAvailableForUser:' + OB.MobileApp.model.get('orgUserId'), true ); this.processPropertyLoaders(); }); this.on( 'propertiesLoadersReady', function() { OB.debug( 'next process: loadRegisteredWindows, renderMain, postLoginActions' ); OB.UTIL.completeLoadingStep(); me.saveBrowserTabId(); // Now we can start synchronizing pending messages OB.App.SynchronizationBuffer.goOnline('Backend'); var callbackAlreadyExecuted = false; this.loadRegisteredWindows(function() { if (!callbackAlreadyExecuted) { callbackAlreadyExecuted = true; me.renderMain(); me.postLoginActions(); } }); }, this ); // when the page has finished loading the components, load the root webpage window.addEventListener( 'load', function(e) { OB.MobileApp.model.initializeGlobalState(); // verifying that all dataSyncModels have modelFunc defined _.each( OB.MobileApp.model.get('dataSyncModels'), function(syncModel) { if (syncModel && OB.UTIL.isNullOrUndefined(syncModel.modelFunc)) { if ( syncModel.model && syncModel.model.prototype && syncModel.model.prototype.modelName ) { syncModel.modelFunc = 'OB.Model.' + syncModel.model.prototype.modelName; } } }, this ); // explicitly setting pushState to false tries to ensure... // that future default modes will not activate the pushState... // breaking the POS navigation // setTimeout to move the execution until all 'load' listeners have finished setTimeout(function() { Backbone.history.start({ root: '', pushState: false }); }, 0); }, false ); }, initializeGlobalState: function() { const persistence = new OB.App.Class.StatePersistence(); window.OB.App.State = new OB.App.Class.State(persistence); window.OB.App.PersistenceChangeListenerManager = new OB.App.Class.PersistenceChangeListenerManager( persistence ); OB.App.State.TerminalLog.setContext({ context: 'refresh(F5)' }); }, /** * Method to be called when there is an error while loading the application * @param {String} caller a string that identifies the process that failed */ loadingErrorsActions: function(caller) { // arguments check OB.UTIL.Debug.execute(function() { if (!caller) { throw 'missing parameter: caller'; } }); // remember that the loading has failed. used by the LogClient OB.MobileApp.model.loadingErrorsActions.executed = true; var link = 'NA'; try { link = OB.UTIL.getStackLink(3); } catch (ex) { OB.error('executeSqlErrorHandler.getStackLink: ' + ex); } OB.error( "loadingErrorsActions: '" + caller + "' failed while loading the application. line: " + link ); }, cleanTerminalData: function() { _.each( this.get('propertiesLoaders'), function(curPropertiesToLoadProcess) { _.each( curPropertiesToLoadProcess.properties, function(curProperty) { this.set(curProperty, null); }, this ); }, this ); }, returnToOnline: function() { //DEVELOPERS: overwrite this function to perform actions when it happens OB.info('The system has come back to online'); }, //DEVELOPER: This function allow us to execute some code to add properties to to terminal model. Each position of the array // must identify the properties which are set and needs to define a loadFunction which will be executed to get the data. // when the data is ready terminalModel.propertiesReady(properties) should be executed. addPropertiesLoader: function(obj) { this.get('propertiesLoaders').push(obj); this.syncAllPropertiesLoaded = _.after( this.get('propertiesLoaders').length, this.allPropertiesLoaded ); }, handlePropertiesLoader: function( properties, source, params, successCallback ) { function handleError() { var msg = OB.I18N.getLabel('OBMOBC_TerminalPropertiesErrorBody', [ properties.join(', ') ]) + OB.I18N.getLabel('OBMOBC_LoadingErrorBody'), isErrorPopupShown = OB.MobileApp.view.$.confirmationContainer.getCurrentPopup() && OB.MobileApp.view.$.confirmationContainer .getCurrentPopup() .getShowing(); if ( OB.MobileApp.model.get('isLoggingIn') === true && !isErrorPopupShown ) { OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_TerminalPropertiesErrorHeader'), msg, [ { label: OB.I18N.getLabel('OBMOBC_Reload'), action: function() { window.location.reload(); } } ], { onShowFunction: function(popup) { OB.UTIL.localStorage.removeItem( 'cacheAvailableForUser:' + OB.MobileApp.model.get('orgUserId') ); popup.$.headerCloseButton.hide(); OB.MobileApp.view.$.containerWindow.destroyComponents(); }, autoDismiss: false } ); } } new OB.DS.Request(source).exec( params, function(data) { if (data.exception) { handleError(); } else { if (typeof successCallback === 'function') { successCallback(data); } } }, function(data) { handleError(); } ); }, propertiesReady: function(properties) { OB.info("[sdrefresh] '" + properties + "' property loaded"); this.syncAllPropertiesLoaded(); }, processPropertyLoaders: function() { OB.debug('next process: allPropertiesLoaded'); var termInfo, msg, key, keysToSkip = ['dataSyncModels', 'connectedToERP']; if (this.get('loggedOffline')) { //Load from termInfo if (this.usermodel.get('terminalinfo')) { termInfo = JSON.parse(this.usermodel.get('terminalinfo')); for (key in termInfo) { if (termInfo.hasOwnProperty(key) && keysToSkip.indexOf(key) === -1) { //If it is not in the keys to skip this.set(key, termInfo[key]); } } //overwrite loaded values which was stored into DB when system was online this.set('loggedOffline', true); this.set('connectedToERP', false); //Funtions are not recoverd when obj is loaded from DB. Recover Them this.set('logConfiguration', { deviceIdentifier: OB.UTIL.localStorage.getItem('terminalAuthentication') === 'Y' ? OB.UTIL.localStorage.getItem('terminalName') : OB.UTIL.getParameterByName('terminal'), logPropertiesExtension: [ function() { return { isOnline: OB.MobileApp.model.get('connectedToERP') }; } ] }); _.each(this.get('dataSyncModels'), function(item) { item.model = eval(item.modelFunc); }); //Overwrite offline values this.set('connectedToERP', false); this.set('loggedOffline', true); this.allPropertiesLoaded(); } else { // not enough data in cache OB.MobileApp.model.loadingErrorsActions( 'processPropertyLoaders: not enough data in cache' ); msg = OB.I18N.getLabel('OBMOBC_NotEnoughDataInCache') + OB.I18N.getLabel('OBMOBC_LoadingErrorBody'); OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_TerminalPropertiesErrorHeader'), msg, [ { label: OB.I18N.getLabel('OBMOBC_Reload'), action: function() { window.location.reload(); } } ], { onShowFunction: function(popup) { OB.UTIL.localStorage.removeItem( 'cacheAvailableForUser:' + OB.MobileApp.model.get('orgUserId') ); popup.$.headerCloseButton.hide(); }, autoDismiss: false } ); } return; } this.set('loggedOffline', false); //Loading the properties of the array OB.info( '[terminal] Starting to load properties based on properties loaders, count: ' + this.get('propertiesLoaders').length ); OB.UTIL.completeLoadingStep(); OB.UTIL.showLoadingMessage(OB.I18N.getLabel('OBMOBC_LoadingTerminalProps')); if ( this.get('propertiesLoaders') && this.get('propertiesLoaders').length > 0 ) { _.each( this.get('propertiesLoaders'), function(curProperty) { //each loadFunction will call to propertiesReady function. This function will trigger //allPropertiesLoaded when all of the loadFunctions are done. curProperty.loadFunction(this); }, this ); } else { this.allPropertiesLoaded(); } }, //DEVELOPER: this function will be automatically called when all the properties defined in //this.get('propertiesLoaders') are loaded. To indicate that a property is loaded //me.propertiesReady(properties) should be executed by loadFunction of each property allPropertiesLoaded: function() { var me = this; OB.debug('next process: propertiesLoadersReady'); if (!OB.MobileApp.model.get('datasourceLoadFailed')) { OB.UTIL.HookManager.executeHooks( 'OBMOBC_TerminalLoaded', { terminalInfo: me }, function(args) { OB.info('[terminal] Properties have been successfully loaded'); // activate this line to see all the attributes (all the key data from the server) // OB.debug('[terminal] Properties have been successfully loaded', this.attributes); if (!me.get('loggedOffline')) { //In online mode, we save the terminal information in the local db const terminalInfo = JSON.stringify(me); me.usermodel.set('terminalinfo', terminalInfo); OB.App.OfflineSession.updateTerminalInfo( me.usermodel.get('id'), terminalInfo ).then( () => {}, error => OB.error('allPropertiesLoaded', error, arguments) ); } //renderMain me.trigger('propertiesLoadersReady'); } ); } }, /** * This function calls to route function of backbone. * Replace parameter by default is true (if not passed), it means that the current history point will be replaced with the new one without create a new point. * If you want to create a new point in the history then pReplace should be passed as false * http://backbonejs.org/#Router-navigate */ navigate: function(route, pReplace, parameters) { if (route === Backbone.history.getFragment()) { OB.info( 'Navigating to same window: ' + route + '. Navigation cancelled.' ); return true; } if (pReplace !== false) { pReplace = true; } OB.debug("Navigating to '" + route + "' Replace: " + pReplace); if (route) { OB.App.State.TerminalLog.setContext({ context: route }); } OB.MobileApp.view.keyReceivers.splice( 0, OB.MobileApp.view.keyReceivers.length ); OB.MobileApp.view.currentWindowParameters = parameters; this.router.navigate(route, { trigger: true, replace: pReplace }); }, /** * Loads registered windows and calls renderMain method implemented by each app */ renderTerminalMain: function() { OB.info('next process: databaseInitialization'); OB.UTIL.Debug.execute(function() { if (!this.renderMain) { throw 'There is no renderMain method in Terminal Model'; } }, this); OB.UTIL.localStorage.setItem('POSSessionActive', true); this.databaseInitialization(); }, checkAllPermissions: function(permissions) { // Verify all permissions var i; for (i in permissions) { if (permissions.hasOwnProperty(i)) { if (this.get('permissions')) { if (permissions[i] !== this.get('permissions')[i]) { return true; } } } } return false; }, /** * Database initialization */ databaseInitialization: function() { OB.info('next process: loadTerminalInfo'); if (window.openDatabase) { this.initLocalDB(); } else { this.loadTerminalInfo(); } }, /** * Loads all needed stuff for the terminal such as permissions, Application... */ loadTerminalInfo: function() { OB.info('next process: terminalInfoLoaded'); OB.UTIL.completeLoadingStep(); OB.UTIL.showLoadingMessage(OB.I18N.getLabel('OBMOBC_LoadingTerminalInfo')); var me = this; if (OB.MobileApp.model.get('loggedOffline')) { OB.Format = JSON.parse(OB.UTIL.localStorage.getItem('AppFormat')); OB.I18N.labels = JSON.parse( OB.UTIL.localStorage.getItem( 'I18NLabels_' + OB.UTIL.localStorage.getItem( 'languageForUser_' + me.usermodel.get('id') ) ) ); } this.setUserModelOnline(function() { me.trigger('terminalInfoLoaded'); }); }, isSafeToResetDatabase: function(callbackIsSafe, callbackIsNotSafe) { callbackIsSafe(); }, databaseCannotBeResetAction: function() { //Will never happen in standard mobile core. Should be overwritten in applications if isSafeToResetDatabase is implemented }, openLocalDB: async function() { if (OB.Data.localDB) { OB.Data.localDB.transaction( function() {}, function() { OB.Dal.openWebSQL(); }, null ); } else { OB.Dal.openWebSQL(); } await OB.App.MasterdataController.openDatabase(); await OB.App.MessageModelController.openDatabase(); await OB.App.OfflineSessionController.openDatabase(); }, initLocalDB: function() { function dropTable(modelObj) { return new Promise(function(resolve, reject) { function dropTableFunction(modelObj) { OB.Dal.dropTable( modelObj, function() { var modelName = modelObj.prototype.modelName; var modelChecksum = OB.Data.getModelStructureChecksum(modelObj); //If this table belongs to a masterdata model, the masterdata needs to be fully loaded if ( OB.UTIL.localStorage.getItem('lastUpdatedTimestamp' + modelName) ) { OB.UTIL.localStorage.removeItem( 'lastUpdatedTimestamp' + modelName ); OB.UTIL.localStorage.removeItem('POSLastTotalRefresh'); OB.UTIL.localStorage.removeItem('POSLastIncRefresh'); } OB.Dal.initCache( modelObj, [], function() { //Set checksum after the model has been created to prevent inconsistent states (e.g. checksum set but missing model) OB.debug( 'Set model ' + modelName + ' checksum to ' + modelChecksum ); OB.UTIL.localStorage.setItem( 'structureChecksum-' + modelName, modelChecksum ); resolve(); }, function() { OB.error("Couldn't initialize table for model: " + modelName); resolve(); } ); }, function() { OB.error(OB.UTIL.argumentsToStringifyed('dropTable', arguments)); reject(); } ); } var syncModel = OB.MobileApp.model.getSyncModel(modelObj); if (syncModel) { OB.MobileApp.model.syncModelHasData( syncModel, function() { //Sync model has data. We will try to synchronize, and if we fail then the databaseCannotBeReset action will be executed OB.MobileApp.model.syncAllModels(function() { dropTableFunction(modelObj); }, OB.MobileApp.model.databaseCannotBeResetAction); }, function() { dropTableFunction(modelObj); } ); } else { dropTableFunction(modelObj); } }); } function relatedTablesToRebuild() { //CashUp tables related var cashupRelatedTablesToDrop = [ OB.Model.CashUp, OB.Model.PaymentMethodCashUp, OB.Model.TaxCashUp ], tablesRelatedToDrop = [], removeRelated; _.each(OB.MobileApp.model.get('modelsRebuilded'), function(model) { removeRelated = _.find(cashupRelatedTablesToDrop, function( modelRelated ) { return model === modelRelated; }); if (!OB.UTIL.isNullOrUndefined(removeRelated)) { _.each(cashupRelatedTablesToDrop, function(modelRelated) { if ( OB.MobileApp.model .get('modelsRebuilded') .indexOf(modelRelated) === -1 ) { tablesRelatedToDrop.push(modelRelated); } }); } }); return tablesRelatedToDrop; } function checkChangedInModels() { var tablesToDrop = [], relatedTablesToDrop = []; _.each(OB.Model, function(model) { if ( !model.prototype || !model.prototype.modelName || model.prototype.legacyModel ) { return; } var modelName = model.prototype.modelName; var modelChecksum = OB.Data.getModelStructureChecksum(model); if (modelChecksum === '') { return; } if ( OB.UTIL.localStorage.getItem('structureChecksum-' + modelName) !== modelChecksum ) { if (!OB.UTIL.localStorage.getItem('structureChecksum-' + modelName)) { OB.debug( "Model '" + modelName + "' should not exists because there isn't a paired entry in the localStorage. Performning preemptive table dropping..." ); } else { OB.info("Model '" + modelName + "' changed. Rebuilding..."); } tablesToDrop.push(dropTable(model)); OB.MobileApp.model.get('modelsRebuilded').push(model); } }); OB.info('Checking database models...'); if (tablesToDrop.length === 0) { OB.info('No models changed.'); OB.MobileApp.model.loadTerminalInfo(); return; } if (OB.MobileApp.model.get('modelsRebuilded').length > 0) { relatedTablesToDrop = relatedTablesToRebuild(); _.each(relatedTablesToDrop, function(relatedModel) { tablesToDrop.push(dropTable(relatedModel)); OB.MobileApp.model.get('modelsRebuilded').push(relatedModel); }); } Promise.all(tablesToDrop).then( function() { OB.MobileApp.model.loadTerminalInfo(); }, function() { OB.MobileApp.model.loadingErrorsActions('initLocalDB.dropTalbles'); } ); } // check if terminal id has changed if ( OB.UTIL.localStorage.getItem('loggedTerminalName') && OB.UTIL.localStorage.getItem('loggedTerminalName') !== OB.MobileApp.model.get('terminalName') ) { OB.warn( OB.UTIL.argumentsToStringifyed( 'terminal changed (' + OB.UTIL.localStorage.getItem('loggedTerminalName') + ' -> ' + OB.MobileApp.model.get('terminalName') + ')' ) ); OB.info( 'Terminal has been changed. Resetting database and local storage information.' ); OB.UTIL.localStorage.clear(function() { OB.UTIL.localStorage.setItem( 'terminalName', OB.MobileApp.model.get('terminalName') ); OB.MobileApp.model.logout(); return; }); } else { OB.MobileApp.model.set('modelsRebuilded', []); checkChangedInModels(); } }, /** * initActions actions is executed before initializeCommonComponents. * * Override this in case you app needs to do something special * Do not forget to execute callback when overriding the method */ initActions: function(callback) { var params = {}; var cacheSessionId = null; if ( OB.UTIL.localStorage.getItem('cacheSessionId') && OB.UTIL.localStorage.getItem('cacheSessionId').length === 32 ) { cacheSessionId = OB.UTIL.localStorage.getItem('cacheSessionId'); } params.cacheSessionId = cacheSessionId; params.command = 'initActions'; new OB.OBPOSLogin.UI.LoginRequest({ url: this.get('loginUtilsUrl') ? this.get('loginUtilsUrl') : '../../org.openbravo.mobile.core.loginutils', data: params }) .response(this, function(inSender, inResponse) { if ( !( OB.UTIL.localStorage.getItem('cacheSessionId') && OB.UTIL.localStorage.getItem('cacheSessionId').length === 32 ) ) { OB.info( 'cacheSessionId is not defined and we will set the id generated in the backend: ' + inResponse.cacheSessionId ); OB.UTIL.localStorage.setItem( 'cacheSessionId', inResponse.cacheSessionId ); OB.UTIL.localStorage.setItem( 'LastCacheGeneration', new Date().getTime() ); } // Save available servers and services and initialize Request Router layer and Proccess Controller _.each(_.keys(inResponse.properties), function(key) { if (inResponse.properties[key]) { OB.UTIL.localStorage.setItem( key, typeof inResponse.properties[key] === 'string' ? inResponse.properties[key] : JSON.stringify(inResponse.properties[key]) ); } }); OB.RR.RequestRouter.initialize(); OB.UTIL.ProcessController.initialize(); if (callback) { callback(); } }) .error(function() { if (callback) { callback(); } }) .go(params); }, /** * PreLoadContext actions is executed before loading the context. * * Override this in case you app needs to do something special * Do not forget to execute callback when overriding the method */ preLoadContext: function(callback) { callback(); }, /** * Returns the corresponding sync model for a data model. Returns null if it's not a sync model */ getSyncModel: function(dataModel) { var theSyncModel = null; _.each(this.get('dataSyncModels'), function(syncModel) { if (syncModel.model === dataModel) { theSyncModel = syncModel; } }); return theSyncModel; }, /** * Returns the corresponding sync model for a data model. Returns null if it's not a sync model */ getSyncModelByModelName: function(modelName) { //iterate models var modelFound = _.find(this.get('dataSyncModels'), function(curModel) { return curModel.name === modelName ? curModel : null; }); if (modelFound) { return modelFound; } return null; }, /** * Checks if the corresponding synchronization model has data pending to be synchronized. * - If data is still present, hasDataCallback will be executed, and the data will be sent * as a parameter in the callback * - If there is no data, doesntHaveDataCallback will be executed */ syncModelHasData: function( modelObj, hasDataCallback, doesntHaveDataCallback ) { var criteria = modelObj.changesPendingCriteria ? modelObj.changesPendingCriteria : modelObj.getCriteria ? modelObj.getCriteria() : modelObj.criteria ? modelObj.criteria : null; var modelObject = modelObj; if (modelObj.model) { modelObject = modelObj.model; } if (criteria) { criteria._limit = -1; } OB.Dal.find( modelObject, criteria, function(dataToSync) { if (dataToSync.length === 0) { if (doesntHaveDataCallback) { doesntHaveDataCallback(modelObject); } } else { if (hasDataCallback) { hasDataCallback(modelObject, dataToSync); } } }, function() { if (doesntHaveDataCallback) { doesntHaveDataCallback(modelObject); } }, { doNotShowErrors: true } ); }, /** * @return {Array} The list of the Sync Models (models which data is sent to the server) with a flag indicating if they have data pending to be sent to the server * * Howto use it: * * OB.MobileApp.model.getSyncModelsWithHasData().then(function(syncModelsWithHasData) { * OB.info(syncModelsWithHasData); * }) * * Please note that this function is experimental and that can be changed or removed at any time without notice */ getSyncModelsWithHasData: function() { return new Promise(function(resolve, reject) { var syncModelsWithHasData = []; // list of models that requires synchronization and their data status var promisesOfModelsToVerify = []; _.each(OB.Model, function(model) { if (!model.prototype || !model.prototype.modelName) { return; } var modelName = model.prototype.modelName; var modelChecksum = OB.Data.getModelStructureChecksum(model); if (modelChecksum === '') { return; } //TerminalLog synchronization shouldn't be mandatory for tests if (modelName === 'TerminalLog') { return; } var promiseOfModelToVerify = new Promise(function(resolve, reject) { var syncModel = OB.MobileApp.model.getSyncModel(model); if (syncModel) { // add the Sync Model to the array with a hasData flag var callbackHasData = function() { syncModelsWithHasData.push({ model: model, hasData: true }); resolve(); }; var callbackNoData = function() { syncModelsWithHasData.push({ model: model, hasData: false }); resolve(); }; OB.MobileApp.model.syncModelHasData( syncModel, callbackHasData, callbackNoData ); } else { resolve(); } }); promisesOfModelsToVerify.push(promiseOfModelToVerify); }); Promise.all(promisesOfModelsToVerify).then( function() { resolve(syncModelsWithHasData); }, function() { OB.error('Could not gather information about the models'); reject(); } ); }); }, /** * Is used by the synchronized mode logic to reset tables to their previous state * before making the synchronized call to the server. So if the user then * refreshes the browser during the call at least the tables are in their previous * versions. This is mainly relevant for cashup information which gets updated * along the way and needs to be reversed if needed. * * If the synchronized call fails then the old cashup information still applies. * * The flow is as follows: * # application code calls saveBackupBeforeChange function, it stores all the relevant tables * for that action * # application then changes what it wants * # then calls runsync process * # runsyncprocess will just before calling the server backup the new version of the * tables (saveBackupBeforeSyncRestorePreChangeBackup function) and restore the * before change version * # when the server encounters an error or something else then nothing is changed as * all the data is already in the previous version * # when the server returns correctly the backup tables are restored to their state just * before the send to the server * */ _synchronizedCheckpointModels: [], addSyncCheckpointModel: function(model) { this._synchronizedCheckpointModels.push(model); }, _prechangeCheckpoint: null, _presyncCheckpoint: null, synchProcessingTransaction: null, setSynchronizedCheckpoint: function(callback) { var me = OB.MobileApp.model; if (!OB.MobileApp.model.hasPermission('OBMOBC_SynchronizedMode', true)) { return; } // already show the dialog now if (!me.showSynchronizedDialog) { me.showSynchronizingDialog(); } OB.Dal.createDataDump(me._synchronizedCheckpointModels, function( checkPoint ) { me._prechangeCheckpoint = checkPoint; if (callback) { callback(); } }); }, synchronizingDialogConditions: new Backbone.Collection(), showSynchronizingDialog: function(condition) { var me = OB.MobileApp.model; if (condition) { this.synchronizingDialogConditions.push(condition); } if (!OB.MobileApp.model.get('avoidSyncronizeModePopup')) { me.showSynchronizedDialog = OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_ProcessingTransactionTitle'), OB.I18N.getLabel('OBMOBC_ProcessingTransaction'), [], { hideCloseButton: true, autoDismiss: false, closeOnEscKey: false } ); } OB.Data.showSynchronizedDialogChangeTime = new Date().getTime(); OB.UTIL.preventRefreshByUser(); }, hideSynchronizingDialog: function(condition) { var me = OB.MobileApp.model; if (condition) { this.synchronizingDialogConditions.pop(condition); } if ( me.showSynchronizedDialog && this.synchronizingDialogConditions.length === 0 ) { me.showSynchronizedDialog.hide(); me.showSynchronizedDialog = null; OB.UTIL.resetPreventRefreshByUser(); } }, createPresyncCheckpoint: function(callback) { var me = OB.MobileApp.model; OB.Dal.createDataDump(me._synchronizedCheckpointModels, function( checkPoint ) { me._presyncCheckpoint = checkPoint; if (callback) { callback(); } }); }, moveToPrechangeCheckpoint: function(callback) { var me = OB.MobileApp.model, dataDump = me._prechangeCheckpoint; if (!dataDump) { if (callback) { callback(); } return; } // be on the save side, set it to null if it fails we won't get it // back anyway me._prechangeCheckpoint = null; OB.Dal.restoreDataDump(dataDump, function() { me._prechangeCheckpoint = null; if (callback) { callback(); } }); }, resetCheckpointData: function() { var me = OB.MobileApp.model; me._presyncCheckpoint = null; me._prechangeCheckpoint = null; }, moveToPresyncCheckpoint: function(callback) { var me = OB.MobileApp.model; // reset this one also me._prechangeCheckpoint = null; if (!me._presyncCheckpoint) { if (callback) { callback(); } return; } OB.Dal.restoreDataDump(me._presyncCheckpoint, function() { me._presyncCheckpoint = null; if (callback) { callback(); } }); }, preSyncPromises: [], doPreSynchronizedCallActions: function(callback, syncData) { var me = OB.MobileApp.model; OB.UTIL.localStorage.setItem('synchronizedMessageId', syncData.messageId); new Promise(function(resolve, reject) { me.createPresyncCheckpoint(function() { resolve(); }); }) .then(function() { return new Promise(function(resolve, reject) { me.moveToPrechangeCheckpoint(function() { resolve(); }); }); }) .then(function() { return Promise.all(me.preSyncPromises); }) .then( function() { me.preSyncPromises = []; if (callback) { callback(); } }, function(reason) { OB.error('Error while processing promise ' + reason); } ); }, doPostSynchronizedCallActions: function(callback, syncData) { if ( OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('synchronizedMessageId') ) || OB.UTIL.localStorage.getItem('synchronizedMessageId') === syncData.messageId ) { OB.UTIL.localStorage.removeItem('synchronizedMessageId'); } else { OB.error( "Message id of the last request doesn't much with the current messageId" ); } if (callback) { callback(); } }, /** * Sets the OBMOBC_SynchronizedMode permission and returns the previous value. * Can be used to temporarily disable synchronized mode. */ setSynchronizedPreference: function(newValue) { var currentValue = OB.MobileApp.model.hasPermission( 'OBMOBC_SynchronizedMode', true ); OB.MobileApp.model.get('permissions').OBMOBC_SynchronizedMode = newValue; return currentValue; }, /** * Should not be called directly. syncAllModels should be used instead * * Recursively syncs models. The recursion will end if: * - all models have successfully synchronized * or if * - an exception is raised * */ syncModelQueue: null, syncModel: function() { var me = this; // stop the recursion when the queue of syncModels is empty // the rest of the actions are in the removeSyncedElemsCallback if (me.syncModelQueue.length === 0) { //There aren't anymore elements to synchronize if (OB.MobileApp.model.hasPermission('OBMOBC_SynchronizedMode', true)) { // clean up in any case after success or error // note: moveToPresyncCheckpoint must be called before the reset me.syncModelSuccessCallbacks.unshift(function() { me.resetCheckpointData(); }); me.syncModelErrorCallbacks.push(function() { me.resetCheckpointData(); }); // nothing to do go away if (me.synchronizedData.length === 0) { me.syncModelExecuteSuccessCallbacks(); return; } // already show the dialog now if ( !me.showSynchronizedDialog && !OB.MobileApp.view.$.containerLoading.showing ) { me.showSynchronizingDialog(); } //now send all the collected json as one request var syncProc = new OB.DS.Process( 'org.openbravo.mobile.core.servercontroller.SynchronizedServerProcessCaller' ); var syncData = { messageId: OB.UTIL.get_UUID(), _source: 'WEBPOS', _executeInOneServer: true, _tryCentralFromStore: true, posTerminal: OB.MobileApp.model.get('terminal') ? OB.MobileApp.model.get('terminal').id : null, data: me.synchronizedData, extraParams: { isSynchronizeModeTransaction: true } }; // clear some memory me.synchronizedData = []; // and execute the complete request, first do the presync actions me.doPreSynchronizedCallActions(function() { syncProc.exec( syncData, function() { var args = arguments; me.doPostSynchronizedCallActions(function() { var params; if (args.length > 0) { params = args[0]; if (args[0].result && args[0].result.length > 0) { params = args[0].result[0]; } } me.syncModelExecuteSuccessCallbacks(params); }, syncData); }, function() { var args = arguments; me.syncModelExecuteErrorCallbacks(args); }, null, me.synchronizedTimeOut ); }, syncData); } else { me.syncModelExecuteSuccessCallbacks(); } return; } var modelObj = me.syncModelQueue.shift(); var model = modelObj.model; var criteria; if (modelObj.getCriteria) { criteria = modelObj.getCriteria(); } else if (modelObj.criteria) { criteria = modelObj.criteria; } else { criteria = { hasBeenProcessed: 'Y' }; } criteria._limit = -1; OB.Dal.find( model, criteria, function(dataToSync) { me.skipSyncModel = dataToSync.length === 0; if (modelObj.preSendModel) { // preSendModel is like a hook which can be used by custom code // to influence the logic, for example setting me.skipSyncModel modelObj.preSendModel(me, dataToSync); } if (me.skipSyncModel) { me.skipSyncModel = false; me.syncModel(); return; } var className = modelObj.className; if (model === OB.Model.Order && OB.UTIL.processOrderClass) { className = OB.UTIL.processOrderClass; } var mdl, modelIndex = 0, modelNotFullySynced = false, dataCorruption = false, newDataToSync = new Backbone.Collection(); while (modelIndex < dataToSync.length) { mdl = dataToSync.at(modelIndex); if ( !_.isUndefined(mdl.get('json')) && (_.isEmpty(mdl.get('json')) || _.isNull(mdl.get('json'))) ) { OB.error( '[syncModel] Wrong model to Synchronize in backend: ' + mdl.modelName + ' because json column is undefined.' ); dataCorruption = true; } else { newDataToSync.add(dataToSync.at(modelIndex)); } modelIndex++; // partition the data in chunks if (modelIndex >= 100 && modelIndex < dataToSync.length) { modelNotFullySynced = true; // add the model again to the beginning of the queue me.syncModelQueue.unshift(modelObj); break; } } if (dataCorruption) { var criteria = { whereClause: ' WHERE json is null OR json= "" ' }; OB.Dal.removeAll(model, criteria); } var dataToSend = []; newDataToSync.each(function(record) { if (record.get('json')) { dataToSend.push(JSON.parse(record.get('json'))); } else if (!_.isUndefined(record.get('objToSend'))) { if (!_.isNull(record.get('objToSend'))) { dataToSend.push(JSON.parse(record.get('objToSend'))); } } else { dataToSend.push(record); } }); var timeout = modelObj.timeout || 20000; var timePerRecord = modelObj.timePerRecord || 1000; var data = { messageId: OB.UTIL.get_UUID(), modelName: modelObj.name, data: dataToSend, isSyncModel: true }; // add an additional pre-sync action, remove any non-persistent data // before really going to the server if (OB.MobileApp.model.hasPermission('OBMOBC_SynchronizedMode', true)) { newDataToSync.each(function(record) { me.preSyncPromises.push( new Promise(function(resolve, reject) { if (!modelObj.isPersistent) { OB.Dal.remove( record, function() { resolve(); }, function(tx, err) { OB.UTIL.showError(err); reject(); } ); } else { resolve(); } }) ); }); } var procErrorCallBack = function() { // proc.exec mcallbackerror OB.warn( "Error while synchronizing model '" + OB.Dal.getTableName(model) + "'" ); me.syncModelExecuteErrorCallbacks(); }; var procSuccessCallBack = function( data, message, lastUpdated, endRow, tx, requestCallback ) { // error if (data && data.exception) { OB.warn( "The model '" + OB.Dal.getTableName(model) + "'' has not been synchronized with the server" ); if ( data.exception.invalidPermission && !me.get('displayedInvalidPermission') ) { // invalid permission message only will be displayed once time me.set('displayedInvalidPermission', true); OB.UTIL.showConfirmation.display( 'Info', OB.I18N.getLabel('OBMOBC_NoPermissionToSyncModel', [ OB.Dal.getTableName(model), OB.Dal.getTableName(model) ]), [ { label: OB.I18N.getLabel('OBMOBC_LblOk'), isConfirmButton: true, action: function() {} } ] ); } me.syncModelExecuteErrorCallbacks(); return; } // success. Elements can be now deleted from the database var removeSyncedElemsCallback = function(tx) { OB.info("Purging the '" + OB.Dal.getTableName(model) + "' table"); var promises = []; if (modelObj.successSendModel) { promises.push( new Promise(function(resolve, reject) { modelObj.successSendModel(); resolve(); }) ); } newDataToSync.each(function(record) { promises.push( new Promise(function(resolve, reject) { if (modelObj.isPersistent) { // Persistent model. Do not delete, just mark it as processed. OB.Dal.updateRecordColumn( record, 'isbeingprocessed', 'Y', function() { if (requestCallback) { requestCallback(); } resolve(); }, function(tx, err) { reject(); }, tx ); } else { // no persistent model (Default). OB.Dal.removeInTransaction( tx, record, function() { if (requestCallback) { requestCallback(); } resolve(); }, function(tx, err) { OB.UTIL.showError(err); reject(); } ); } }) ); }); Promise.all(promises).then( function() { // if the model has been partitioned if (modelNotFullySynced) { OB.info( newDataToSync.length + " records of the table '" + OB.Dal.getTableName(model) + "' have been successfully synchronized with the server. " + (dataToSync.length - newDataToSync.length) + ' records remaining to be synchronized.' ); me.syncModel(); return; } OB.info( "The table '" + OB.Dal.getTableName(model) + "' has been fully synchronized with the server" ); }, function(err) { OB.error( "Could not purge the '" + OB.Dal.getTableName(model) + "' table. Error message: " + err ); } ); // not synchronized mode do the next in the success callback // with synchronized mode the recall syncmodel is below in the code if ( !OB.MobileApp.model.hasPermission('OBMOBC_SynchronizedMode', true) ) { me.syncModel(); } }; if (modelObj.removeSyncedElemsCallback) { modelObj.removeSyncedElemsCallback( newDataToSync, tx, requestCallback ); } else { removeSyncedElemsCallback(tx); } if (modelObj.postProcessingFunction) { modelObj.postProcessingFunction( newDataToSync, removeSyncedElemsCallback ); } }; if (OB.MobileApp.model.hasPermission('OBMOBC_SynchronizedMode', true)) { me.collectSyncData( className, data, timeout + timePerRecord * newDataToSync.length ); me.syncModelSuccessCallbacks.push(procSuccessCallBack); me.syncModel(); } else { var proc = new OB.DS.Process(className); //if the model of the message requires polling, add it to the list var tmpMsg = OB.PollingUtils.createMessage(data, proc.source); data.messageId = tmpMsg.id; if (OB.PollingUtils.getPollingUrl(tmpMsg)) { OB.Polling.PollingRequestHandler.planedToSendPollingRequest.add( new OB.Model.PollingRequest({ message: tmpMsg }) ); } proc.exec( data, procSuccessCallBack, procErrorCallBack, null, timeout + timePerRecord * newDataToSync.length ); } }, function() { // there is no model in the local database. this is ok. move to the next model me.syncModel(); }, { // do not show an error when the table is not present in the local database because it could happen when the cache is still clean doNotShowErrors: true } ); }, syncModelSuccessCallbacks: [], syncModelErrorCallbacks: [], syncModelExecuteSuccessCallbacks: function(result) { result = result || {}; OB.UTIL.Debug.execute(function() { if ( OB.MobileApp.model.syncModelQueue && OB.MobileApp.model.syncModelQueue.length > 0 ) { throw "The 'syncModelQueue' should be empty at this point"; } }); // exception occurred but was returned in the json as a valid response if (result && result.exception) { OB.MobileApp.model.syncModelExecuteErrorCallbacks(arguments); return; } if (OB.MobileApp.model.showSynchronizedDialog) { OB.MobileApp.model.hideSynchronizingDialog(); } OB.MobileApp.model.syncModelQueue = []; OB.MobileApp.model.syncModelErrorCallbacks = []; function execCallbacks() { new Promise(function(resolve, reject) { // is only doing anything in case of synchronized mode OB.MobileApp.model.moveToPresyncCheckpoint(function() { resolve(); }); }).then(function() { OB.UTIL.executeCallbackQueue( OB.MobileApp.model.syncModelSuccessCallbacks, result ); }); } //Get messages from local database only when polling models exists if (OB.Polling.PollingRequestHandler.getPollingSyncModels().length > 0) { //set in result messages pending to synchronize OB.RR.ServTypeTransaction.getMessages( function(messages) { var promises = []; _.each(messages.models, function(msg) { //if the model of the message requires polling, add it to the list if (OB.PollingUtils.getPollingUrl(msg)) { promises.push( new Promise(function(resolve, reject) { OB.Polling.PollingRequestHandler.planedToSendPollingRequest.add( new OB.Model.PollingRequest({ message: msg }) ); resolve(); }) ); } }); Promise.all(promises).then(function() { result.messages = OB.Polling.PollingRequestHandler.planedToSendPollingRequest; execCallbacks(); }); }, function() { execCallbacks(); } ); } else { execCallbacks(); } }, syncModelExecuteErrorCallbacks: function() { OB.MobileApp.model.syncModelQueue = []; OB.MobileApp.model.syncModelSuccessCallbacks = []; if (OB.MobileApp.model.hasPermission('OBMOBC_SynchronizedMode', true)) { var msg = arguments[0][0] && arguments[0][0].exception ? arguments[0][0].exception.message : 'No Details'; if (OB.MobileApp.model.showSynchronizedDialog) { OB.MobileApp.model.hideSynchronizingDialog(); } OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_TransactionFailedTitle'), OB.I18N.getLabel('OBMOBC_TransactionFailed', [msg]), [ { isConfirmButton: true, label: OB.I18N.getLabel('OBMOBC_LblOk'), action: function() { if (OB.MobileApp.view.currentWindow !== 'retail.pointofsale') { OB.POS.navigate('retail.pointofsale'); return true; } } } ] ); } OB.UTIL.executeCallbackQueue(OB.MobileApp.model.syncModelErrorCallbacks); }, // collect the data to sync collectSyncData: function(source, params, timeout) { var data = OB.DS.Process.prepareData(params); data._serviceName = source; this.synchronizedTimeOut = this.synchronizedTimeOut + timeout; this.synchronizedData.push(data); }, /** * Synchronizes all dataSyncModels (models which data is kept localy until flushed to the server) * * If this method is called while a previous data synchronization is in place: * - the data synchronization of the model being synchronized at the time this method is called, will be finished * - the remaining models will not be synchronized * - the synchronization will start over with the first model * - all the callbacks (success or error) are gathered in a queue, all of them will be executed when the synchronization ends */ syncAllModels: function(successCallback, errorCallback) { this.syncAllModelsImpl(successCallback, errorCallback); }, syncAllModelsImpl: function(successCallback, errorCallback) { var me = this; // if the user is not authenticated, the server will reject all the connections if (!OB.MobileApp.model.isUserAuthenticated()) { OB.UTIL.Debug.execute(function() { OB.warn( "'OB.MobileApp.model.syncAllModels' cannot be executed because it requires an autheticated user" ); }); if (errorCallback) { errorCallback(); } return; } // if there is no model to be synchronized if (!me.get('dataSyncModels') || me.get('dataSyncModels').length === 0) { if (successCallback) { successCallback(); } return; } // flow note: this line must be located before the queues are reset. Check models queue and callbacks queues, the process finishes when the callback are called var isCurrentlySynchronizing = (me.syncModelQueue && me.syncModelQueue.length > 0) || (me.syncModelSuccessCallbacks && me.syncModelSuccessCallbacks.length > 0) || (me.syncModelErrorCallbacks && me.syncModelErrorCallbacks.length > 0); // start the synchronization if it was not already in progress if (!isCurrentlySynchronizing) { me.syncModelSuccessCallbacks = []; me.syncModelErrorCallbacks = []; // remember the callbacks in the callbacks queue me.syncModelSuccessCallbacks.push(successCallback); me.syncModelErrorCallbacks.push(errorCallback); // all models must restart the synchronization when syncAllModels is called. this is attained reseting the queue me.syncModelQueue = []; // add all the models to the queue var i; for (i = 0; i < this.get('dataSyncModels').length; i++) { var modelObj = this.get('dataSyncModels')[i]; me.syncModelQueue.push(modelObj); } // reset some members used in synchronized comms me.synchronizedData = []; me.synchronizedTimeOut = 0; me.syncModel( this.syncModelExecuteSuccessCallbacks, this.syncModelExecuteErrorCallbacks ); } }, /** * Returns true if a user is authenticated */ isUserAuthenticated: function() { return !!OB.MobileApp.model.get('context'); }, /** * If any module needs to perform operations with properties coming from the server * this function must be overwritten */ isUserCacheAvailable: function() { return false; }, /** * Invoked from initComponents in terminal view */ initializeCommonComponents: function() { this.openLocalDB().finally(() => { this.initializeCommonComponentsImp(); }); }, initializeCommonComponentsImp: function() { OB.info( "next process: navigate 'login' or renderTerminalMain or loginUsingCache" ); var me = this, params = {}; OB.UTIL.localStorage.setItem('LOGINTIMER', new Date().getTime()); params = this.get('loginUtilsParams') || {}; params.command = 'preRenderActions'; params.appModuleId = this.get('appModuleId'); params.supportsExternalBusinessPartnerIntegration = this.get( 'supportsExternalBusinessPartnerIntegration' ); // initialize labels and other common stuff var rr, ajaxRequest = new enyo.Ajax({ url: this.get('loginUtilsUrl'), cacheBust: false, timeout: 5000, // don't do retries as this gives side effects as re-showing login page maxNumOfRequestRetries: 0, method: 'GET', handleAs: 'json', contentType: 'application/json;charset=utf-8', data: params, success: function(inSender, inResponse) { OB.info('loginUtilsUrl success'); if (inResponse.csrfToken) { OB.MobileApp.model.set('csrfToken', inResponse.csrfToken); } if (inResponse.externalBpIntegration) { OB.MobileApp.model.set( 'externalBpIntegration', inResponse.externalBpIntegration ); } if ( _.isEmpty(inResponse.labels) || _.isNull(inResponse.labels) || _.isUndefined(inResponse.labels) ) { OB.I18N.labels = JSON.parse( OB.UTIL.localStorage.getItem('I18NLabels') ); } else { OB.appCaption = inResponse.appCaption; OB.UTIL.localStorage.setItem( 'POSlanguageId', inResponse.labels.languageId ); OB.UTIL.localStorage.setItem( 'POSlanguageSK', inResponse.labels.languageSk ); OB.I18N.labels = inResponse.labels; if (inResponse.activeSession) { // The labels will only be saved in the localStorage if they come from an active session // We do this to prevent the labels loaded in the login page from being stored here and overwritting // the labels in the correct language (see issue 23613) OB.UTIL.localStorage.setItem( 'languageForUser_' + OB.MobileApp.model.get('orgUserId'), inResponse.labels.languageId ); OB.UTIL.localStorage.setItem( 'I18NLabels', JSON.stringify(OB.I18N.labels) ); OB.UTIL.localStorage.setItem( 'I18NLabels_' + inResponse.labels.languageId, JSON.stringify(OB.I18N.labels) ); } } if ( _.isEmpty(inResponse.lists) || _.isNull(inResponse.lists) || _.isUndefined(inResponse.lists) ) { OB.I18N.lists = JSON.parse( OB.UTIL.localStorage.getItem('I18NLists') ); } else { OB.I18N.lists = inResponse.lists; if (inResponse.activeSession) { // The reference list will only be saved in the localStorage if they come from an active session // We do this to prevent the reference list loaded in the login page from being stored here and overwritting // the reference list in the correct language OB.UTIL.localStorage.setItem( 'I18NLists', JSON.stringify(OB.I18N.lists) ); OB.UTIL.localStorage.setItem( 'I18NLists_' + inResponse.lists.languageId, JSON.stringify(OB.I18N.lists) ); } } if ( _.isEmpty(inResponse.dateFormats) || _.isNull(inResponse.dateFormats) || _.isUndefined(inResponse.dateFormats) ) { OB.Format = JSON.parse(OB.UTIL.localStorage.getItem('AppFormat')); } else { OB.Format = inResponse.dateFormats; } var permissions = {}, i, separator; if (inResponse.permissions) { for (i = 0; i < inResponse.permissions.length; i++) { permissions[inResponse.permissions[i].key] = inResponse.permissions[i].value; // Add the permission value. separator = inResponse.permissions[i].key.indexOf('_'); if (separator >= 0) { permissions[ inResponse.permissions[i].key.substring(separator + 1) ] = inResponse.permissions[i].value; // if key has a DB prefix, add also the permission value without this prefix } } } else { permissions = null; } me.set('permissions', permissions); if ('invalidRequestOrigin' === inResponse.exception) { OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_InvalidOriginTitle'), OB.I18N.getLabel('OBMOBC_InvalidOriginMsg') ); return; } me.trigger('initializedCommonComponents'); me.preLoadContext(function() { if (!inResponse.contextInfo) { //Not authenticated, need to navigate to login page OB.MobileApp.model.navigate('login'); return; } OB.MobileApp.model.triggerOnLine(); OB.info('Set isLoggingIn true'); OB.MobileApp.model.set('isLoggingIn', true); OB.MobileApp.model.set('orgUserId', inResponse.contextInfo.user.id); me.set('context', inResponse.contextInfo); // fires the 'change:context' event window.OB.Format = inResponse.format; window.OB.UTIL.localStorage.setItem( 'AppFormat', JSON.stringify(OB.Format) ); OB.UTIL.startLoadingSteps(); me.renderTerminalMain(); return; }); }, fail: function(inSender, inResponse) { OB.info('loginUtilsUrl fail'); if (this.alreadyProcessed) { return; } this.alreadyProcessed = true; // we are likely offline. Attempt to navigate to the login page OB.I18N.labels = JSON.parse( OB.UTIL.localStorage.getItem('I18NLabels') ); OB.Format = JSON.parse(OB.UTIL.localStorage.getItem('AppFormat')); me.trigger('initializedCommonComponents'); if (!OB.I18N.labels || !OB.Format) { // These 2 objects are needed for the application to show i18n messages and date and number formats OB.UTIL.showLoading(false); var errorMsg = 'The server is not available and the cache has not enough data. Connect to the server and try again.'; OB.error('initializeCommonComponents', errorMsg); OB.UTIL.showConfirmation.display( 'Error', errorMsg, [ { label: 'Retry', isConfirmButton: true, action: function() { // Retry. If the server is not available, the message will open again me.initializeCommonComponents(); } } ], { onHideFunction: function() { // Retry. If the server is not available, the message will open again me.initializeCommonComponents(); } } ); return; } me.attemptToLoginOffline(); } }); rr = new OB.RR.Request({ ajaxRequest: ajaxRequest }); OB.info('loginUtilsUrl request'); rr.exec(ajaxRequest.url); }, renderLogin: function() { if (!OB.MobileApp.view) { // the terminal view has yet to be created OB.UTIL.Debug.execute(function() { throw 'OB.MobileApp.view must be defined'; }); OB.MobileApp.model.navigate(''); return; } if (!OB.MobileApp.view.$.containerWindow) { // this can happen if the webpage is reloaded // OB.UTIL.Debug.execute(function () { // OB.error("OB.MobileApp.view.$.containerWindow must be defined"); // }); OB.MobileApp.model.navigate(''); return; } this.initApplicationTasks(function() { OB.MobileApp.view.$.containerWindow.destroyComponents(); OB.MobileApp.view.$.containerWindow .createComponent({ kind: OB.OBPOSLogin.UI.Login, classes: 'obUiTerminal-containerWindow-obObposLoginUiLogin' }) .render(); OB.UTIL.showLoading(false); }); }, initApplicationTasks: async function(callback) { const me = this; // ensure exists message table, to check if there are messages pending to sync, // thats needs to be synchonized before update the sources const stateMessages = OB.App.State.getState().Messages; const syncBufferMessages = await OB.App.SynchronizationBuffer.getPendingMessages(); const hasNotSyncedMessages = stateMessages.length > 0 || syncBufferMessages.length > 0; if (hasNotSyncedMessages) { // do not update sources me.checkBenchMark(callback); OB.warn( 'On preparing login page, but cannot check for new sources becasue there are messages to be sync. Needed to be logged in to sync them' ); } else { // update sources this.loadApplicationSources(callback); } }, checkBenchMark: function(finalCallback) { const browserInfo = OB.UTIL.getSupportedBrowserInfo(); OB.MobileApp.model.set('ApplicationSourcesLoaded', true); if (browserInfo.isSupported) { if ( !OB.UTIL.Debug.getDebugCauses() || (!OB.UTIL.Debug.getDebugCauses().isInDevelopment && !OB.UTIL.Debug.getDebugCauses().isTestEnvironment) ) { if ( OB.MobileApp.model.get('shouldExecuteBenchmark') && OB.UTIL.localStorage.getItem('doNotExecuteBenchmark') === null && OB.UTIL.localStorage.getItem('executePerformanceTest') !== 'N' ) { this.dialog = OB.MobileApp.view.$.confirmationContainer.createComponent( { kind: 'OB.UI.ModalBenchmark', name: 'modalBenchmark', classes: 'obUiTerminal-confirmationContainer-modalBenchmark', context: this } ); OB.UTIL.showLoading(false); this.dialog.show({ callback: finalCallback }); return; } } } finalCallback(); }, loadApplicationSources: function(callback) { var finalCallback, permissionSW; const me = this; finalCallback = function() { if (callback instanceof Function) { callback(); } }; if (typeof ServiceWorkerUtil !== 'undefined') { permissionSW = ServiceWorkerUtil.useServiceWorkers; } else { permissionSW = false; } if (permissionSW === true) { OB.UTIL.fetchApplicationSources( function() { me.checkBenchMark(finalCallback); }, function() { OB.error('Error in fetching the application sources.'); OB.MobileApp.model.set('loadManifeststatus', { type: 'error', reason: 'fail' }); finalCallback(); } ); } else { //If preference is not set, we will use AppCache me.checkBenchMark(finalCallback); } }, loadModels: function(windows, incremental, parentCallback, background) { var i, j, windowName, windowClass, windowDatasources, datasources = [], w, c, path; const initLogParamsForBackgroundProcesses = function() { OB.UTIL.localStorage.setItem('totalRecordsBackgroundRequest', 0); OB.UTIL.localStorage.setItem('totalTimeBackgroundRequest', 0); OB.UTIL.localStorage.setItem('totalRecordsBackgroundSave', 0); OB.UTIL.localStorage.setItem('totalTimeBackgroundSave', 0); }; const showLogsForBackgroundProcesses = function() { let processDetails = incremental ? 'inc' : 'full'; let totalRecordsBackgroundRequest = OB.UTIL.localStorage.getItem( 'totalRecordsBackgroundRequest' ); let totalTimeBackgroundRequest = OB.UTIL.localStorage.getItem( 'totalTimeBackgroundRequest' ); let totalRecordsBackgroundSave = OB.UTIL.localStorage.getItem( 'totalRecordsBackgroundSave' ); let totalTimeBackgroundSave = OB.UTIL.localStorage.getItem( 'totalTimeBackgroundSave' ); if (Number(totalTimeBackgroundRequest) > 0) { OB.info( '[background-request-' + processDetails + '] Loaded ' + totalRecordsBackgroundRequest + ' records in the process. Finished after ' + totalTimeBackgroundRequest + ' miliseconds.' ); } if (Number(totalTimeBackgroundSave) > 0) { OB.info( '[background-save-' + processDetails + '] Saved ' + totalRecordsBackgroundSave + ' records in the process. Finished after ' + totalTimeBackgroundSave + ' miliseconds.' ); } }; const validateIncrementalRefreshModelsLoaded = function(isValidCallback) { const failedModels = [ ...OB.App.MasterdataController.getFailedMasterdataModels(), ...(OB.Dal.Datasources._failedModels || []) ]; if (failedModels.length === 0) { // All models have finished the incremental load process. // We can safely set the POSLastIncRefresh timestamp now. OB.UTIL.localStorage.setItem('POSLastIncRefresh', new Date().getTime()); } isValidCallback(); }; const validateModelsLoaded = function(isValidCallback) { const failedModels = [ ...OB.App.MasterdataController.getFailedMasterdataModels(), ...(OB.Dal.Datasources._failedModels || []) ]; if (failedModels.length > 0) { // Remove all timestamp to force a full refresh OB.UTIL.localStorage.removeItem('POSLastTotalRefresh'); OB.UTIL.localStorage.removeItem('POSLastIncRefresh'); _.forEach(failedModels, function(model) { OB.UTIL.localStorage.removeItem('lastUpdatedTimestamp' + model); }); OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_MasterDataErrorHeader'), OB.I18N.getLabel('OBMOBC_MasterDataErrorBody', [ failedModels.join(', ') ]) + OB.I18N.getLabel('OBMOBC_LoadingErrorBody'), [ { label: OB.I18N.getLabel('OBMOBC_Reload'), action: function() { window.location.reload(); } } ], { onShowFunction: function(popup) { popup.$.headerCloseButton.hide(); }, autoDismiss: false } ); } else { // At this point we can ensure that all models have loaded correctly. // If we are performing a full refresh of the data, now we can set the POSLastTotalRefresh timestamp. OB.UTIL.localStorage.setItem( 'POSLastTotalRefresh', new Date().getTime() ); //Data might be old, but it should be enough to work isValidCallback(); } }; const callback = function() { showLogsForBackgroundProcesses(); OB.info( '[sdrefresh] Finished loading models ' + (incremental ? 'incrementally' : 'full') + '.' + (background ? ' (' + background + ' )' : '') ); if (OB.MobileApp.model.get('loggedOffline')) { parentCallback(); } else { const statusCallback = function statusFunction(model, offset, limit) { if (offset === 0) { OB.UTIL.completeLoadingStep(); OB.UTIL.showLoadingMessage( OB.I18N.getLabel('OBMOBC_LoadingMessageModel', [model]) ); } else { OB.UTIL.showLoadingMessage( OB.I18N.getLabel('OBMOBC_LoadingMessageModelPage', [ model, offset / limit + 1 ]) ); } }; if (incremental) { OB.App.MasterdataController.incrementalMasterdataRefresh( statusCallback ).then(function() { OB.UTIL.showLoadingMessage( OB.I18N.getLabel('OBMOBC_ForcingRefreshOfLocalDatabase') ); validateIncrementalRefreshModelsLoaded(parentCallback); }); } else { OB.App.MasterdataController.fullMasterdataRefresh( statusCallback ).then(function() { OB.UTIL.showLoadingMessage( OB.I18N.getLabel('OBMOBC_ForcingRefreshOfLocalDatabase') ); validateModelsLoaded(parentCallback); }); } } }; if (OB.MobileApp.model.get('loggedOffline')) { if (callback instanceof Function) { callback(); } return; } if (OB.UTIL.isNullOrUndefined(windows) || windows.length === 0) { windows = []; _.each(this.windowRegistry.registeredWindows, function(windowp) { windows.push(windowp); }); } OB.info( '[sdrefresh] Load models ' + (incremental ? 'incrementally' : 'full') + '.' + (background ? ' (' + background + ' )' : '') ); initLogParamsForBackgroundProcesses(); for (i = 0; i < windows.length; i++) { windowClass = windows[i].windowClass; windowName = windows[i].route; if (OB && OB.DATA && OB.DATA[windowName]) { // old way of defining datasources... windowDatasources = OB.DATA[windowName]; } else if ( windowClass.prototype && windowClass.prototype.windowmodel && windowClass.prototype.windowmodel.prototype && windowClass.prototype.windowmodel.prototype.models ) { windowDatasources = windowClass.prototype.windowmodel.prototype.models; } else if (typeof windowClass === 'string') { w = window; // global window path = windowClass.split('.'); for (c = 0; c < path.length; c++) { w = w[path[c]]; } if ( w.prototype && w.prototype.windowmodel && w.prototype.windowmodel.prototype && w.prototype.windowmodel.prototype.models ) { windowDatasources = w.prototype.windowmodel.prototype.models; } } for (j = 0; j < windowDatasources.length; j++) { if (datasources.indexOf(windowDatasources[j]) === -1) { datasources.push(windowDatasources[j]); } } } _.extend(datasources, Backbone.Events); OB.Dal.Datasources = datasources; OB.debug( '[sdrefresh] window: ' + windowName + (background ? '. (' + background + ' )' : '') ); OB.Dal.loadModels( false, datasources, null, incremental, callback, background ); this.postLoadModels(); }, postLoadModels: function() {}, cleanWindows: function() { this.windows = new (Backbone.Collection.extend({ comparator: function(window) { // sorts by menu position, 0 if not defined var position = window.get('menuPosition'); return position ? position : 0; } }))(); }, registerWindow: function(window) { this.windowRegistry.registerWindow(window); }, /** * Iterates over all registered windows loading their models */ loadRegisteredWindows: function(callback) { var me = this; var func_handleRouting = function(windowName, params) { //TODO default route var defaultRoute = OB.MobileApp.model.get('defaultWindow'); //Find dest window var destinationWindow = _.find( OB.MobileApp.windowRegistry.registeredWindows, function(win) { return win.route === windowName; } ); //Check if destination window is valid. If not navigate to main window if (OB.UTIL.isNullOrUndefined(destinationWindow)) { OB.MobileApp.model.navigate(defaultRoute); } //If destination window has permission, check if user is allowed to navigate //if not, navigate to main window if (destinationWindow.permission) { if (!OB.MobileApp.model.hasPermission(destinationWindow.permission)) { OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_NotAllowed'), OB.I18N.getLabel('OBMOBC_AccessNotAllowed', [ destinationWindow.permission ]), null, { onHideFunction: function() { OB.MobileApp.model.navigate(defaultRoute); } } ); return; } } //Execute hook -> OBMOBC_PreWindowNavigate OB.UTIL.HookManager.executeHooks( 'OBMOBC_PreWindowNavigate', { window: destinationWindow, route: destinationWindow.route, params: params }, function(args) { if (args && args.cancellation && args.cancellation === true) { if (args.forceLogout) { OB.MobileApp.model.logout(); } else { OB.MobileApp.model.navigate(defaultRoute); } return; } if (args.window && args.window.navigateTo) { args.window.navigateTo( args, function() { me.renderGenericWindow(args.route, params); }, function() { OB.MobileApp.model.navigate(defaultRoute); } ); } else if (args.route) { me.renderGenericWindow(args.route, params); } } ); }; OB.debug('next process: renderMain'); this.cleanWindows(); OB.MobileApp.model.set('modelsToLoad', []); if ( OB.MobileApp.model.get('permissions') && (OB.MobileApp.model.hasPermission( 'OBMOBC_EnableTerminalLogUserActions', true ) || OB.MobileApp.model.hasPermission( 'OBMOBC_EnableTerminalLogProcess', true )) ) { OB.UTIL.activateTerminalLogSync(); } else { OB.info( 'Terminal log is disabled because both preferences (OBMOBC_EnableTerminalLogUserActions and OBMOBC_EnableTerminalLogProcess) are set to false.' ); } var countOfLoadedWindows = 0; var registeredWindows = this.windowRegistry.registeredWindows; _.each( registeredWindows, function(windowp) { var windowName = windowp.route; this.windows.add(windowp); this.router.route(windowName, windowName, function() { func_handleRouting(windowName); }); this.router.route(windowName + '/:params', windowName, function( params ) { func_handleRouting(windowName, params); }); countOfLoadedWindows += 1; }, this ); // checks if the windows loaded are more than the windows registered, which never should happen var countOfRegisteredWindows = this.windowRegistry.registeredWindows.length; OB.assert( countOfRegisteredWindows === countOfLoadedWindows, 'DEVELOPER: There are ' + countOfRegisteredWindows + ' registered windows but ' + countOfLoadedWindows + ' are being loaded' ); var terminalType = OB.MobileApp.model.get('terminal') && OB.MobileApp.model.get('terminal').terminalType; var minTotalRefresh, minIncRefresh, lastTotalRefresh, lastIncRefresh, now, intervalTotal, intervalInc; if (terminalType) { minTotalRefresh = OB.MobileApp.model.get('terminal').terminalType .minutestorefreshdatatotal * 60 * 1000; minIncRefresh = OB.MobileApp.model.get('terminal').terminalType .minutestorefreshdatainc; lastTotalRefresh = OB.UTIL.localStorage.getItem('POSLastTotalRefresh'); lastIncRefresh = OB.UTIL.localStorage.getItem('POSLastIncRefresh'); // lastTotalRefresh should be used to set lastIncRefresh when it is null or minor. if (lastIncRefresh === null) { lastIncRefresh = lastTotalRefresh; } else { if (lastTotalRefresh > lastIncRefresh) { lastIncRefresh = lastTotalRefresh; } } minIncRefresh = (minIncRefresh > 99999 ? 99999 : minIncRefresh) * 60 * 1000; } if ( (!minTotalRefresh && !minIncRefresh) || (!lastTotalRefresh && !lastIncRefresh) ) { // If no configuration of the masterdata loading has been done, // or an initial load has not been done, then always do // a total refresh during the login OB.UTIL.showLoadingTitleMessage( true, 'OBMOBC_LblLoadingFull', 'OBMOBC_LblLoadingFullMsg' ); this.loadModels(registeredWindows, false, callback); } else { now = new Date().getTime(); intervalTotal = lastTotalRefresh ? now - lastTotalRefresh - minTotalRefresh : 0; intervalInc = lastIncRefresh ? now - lastIncRefresh - minIncRefresh : 0; if (intervalTotal > 0) { this.set('FullRefreshWasDone', true); //It's time to do a full refresh this.loadModels(registeredWindows, false, callback); } else { if ( OB.MobileApp.model.hasPermission( 'OBMOBC_NotAutoLoadIncrementalAtLogin', true ) && OB.UTIL.isNullOrUndefined(this.get('forceLoadModelsOnLogin')) ) { if ( intervalInc >= 0 || OB.UTIL.localStorage.getItem('POSForceIncrementalRefresh') ) { if (OB.UTIL.localStorage.getItem('POSForceIncrementalRefresh')) { OB.UTIL.localStorage.removeItem('POSForceIncrementalRefresh'); } this.loadModels(registeredWindows, true, callback); } else { if (callback instanceof Function) { // Call the callback directly in case incremental refresh is not required. callback(); } } } else { if (OB.UTIL.isNullOrUndefined(lastTotalRefresh)) { this.set('FullRefreshWasDone', true); //It's time to do a full refresh this.loadModels(registeredWindows, false, callback); } else { this.loadModels(registeredWindows, true, callback); } } } if (this.get('FullRefreshWasDone')) { OB.UTIL.showLoadingTitleMessage( true, 'OBMOBC_LblLoadingFull', 'OBMOBC_LblLoadingFullMsg' ); } else { OB.UTIL.showLoadingTitleMessage(true, 'OBMOBC_LblLoadingInc'); } } }, showFrozenPopUp: function() { OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_BrowserTabHeader', [ OB.MobileApp.model.get('appName') ]), OB.I18N.getLabel('OBMOBC_BrowserTabBody', [ OB.MobileApp.model.get('appName') ]), [], { autoDismiss: false, hideCloseButton: true, closeOnEscKey: false } ); }, saveBrowserTabId: function() { var me = this; var browserTabId = OB.UTIL.get_UUID(); if (OB.MobileApp.model.get('terminal')) { OB.MobileApp.model.get('terminal').browserTabId = browserTabId; } else { OB.MobileApp.model.browserTabId = browserTabId; } OB.UTIL.localStorage.setItem('browserTabId', browserTabId); // When focus the browser tab window.onfocus = function() { var actualBrowserTabId = OB.MobileApp.model.get('terminal') ? OB.MobileApp.model.get('terminal').browserTabId : OB.MobileApp.model.browserTabId; if (OB.UTIL.localStorage.getItem('browserTabId') !== actualBrowserTabId) { OB.MobileApp.view.applicationLocked = true; me.showFrozenPopUp(); } }; }, isWindowOnline: function(route) { var i, windows; windows = this.windows.toArray(); for (i = 0; i < windows.length; i++) { if (windows[i].get('route') === route) { return windows[i].get('online'); } } return false; }, renderGenericWindow: function(windowName, params) { if (!OB.MobileApp.view.$.containerWindow) { OB.UTIL.Debug.execute(function() { OB.error('OB.MobileApp.view.$.containerWindow must be defined'); }); OB.MobileApp.model.loadingErrorsActions('renderGenericWindow I'); return; } OB.MobileApp.view.currentWindow = 'unknown'; OB.MobileApp.model.set('currentWindow', 'unknown'); OB.MobileApp.view.currentWindowState = 'unknown'; OB.MobileApp.model.set('currentWindowState', 'unknown'); OB.UTIL.showLoading(true); var windowObject = this.windows.where({ route: windowName })[0]; var windowClass = windowObject.get('windowClass'); OB.MobileApp.view.$.containerWindow.destroyComponents(); OB.MobileApp.view.$.containerWindow.createComponent({ kind: windowClass, windowName: windowName, classes: 'obUiTerminal-containerWindow-windowClass-generic', params: params }); //remove possible external classes _.each(this.windows.models, function(win) { if (win && win.get('containerCssClass')) { if (OB.MobileApp.view.hasClass(win.get('containerCssClass'))) { OB.MobileApp.view.removeClass(win.get('containerCssClass')); } } }); //Add original css-class if (!OB.MobileApp.view.hasClass('pos-container')) { OB.MobileApp.view.addClass('pos-container'); } //If window is overwritting container, remove original and put new one if (windowObject && windowObject.get('containerCssClass')) { if (windowObject.get('overwriteOriginalContainerCssClass')) { OB.MobileApp.view.removeClass('pos-container'); } OB.MobileApp.view.addClass(windowObject.get('containerCssClass')); } OB.MobileApp.view.currentWindow = windowName; OB.MobileApp.model.set('currentWindow', windowName); }, renderContainerWindow: function() { OB.MobileApp.view.$.containerWindow.render(); OB.UTIL.showLoading(false); OB.MobileApp.view.$.containerWindow.resized(); OB.UTIL.HookManager.executeHooks( 'OBPOS_AfterRenderContainerWindow', { context: OB.MobileApp.view.$.containerWindow, window: OB.MobileApp.view.currentWindow, parameters: OB.MobileApp.view.currentWindowParameters }, function() {} ); }, updateSession: async function(user, callback) { // argument check OB.UTIL.Debug.execute(function() { if (!callback) { throw 'This method requires a callback'; } }); try { const session = await OB.App.OfflineSession.activateSession( user.get('id'), OB.MobileApp.model.get('terminalName') ); OB.MobileApp.model.set('session', session.id); callback(); } catch (e) { OB.error( 'updateSession: failed to save the existing session', e, arguments ); } }, /** * This method is invoked when closing session, it is in charge * of dealing with all stuff needed to close session. After it is * done it MUST do triggerLogout */ postCloseSession: function(session) { OB.MobileApp.model.triggerLogout(); }, closeSession: async function() { const sessionId = OB.MobileApp.model.get('session'); if (!this.get('supportsOffline') || !sessionId) { OB.MobileApp.model.triggerLogout(); return; } try { await OB.App.OfflineSession.closeSession(sessionId); } catch (e) { OB.error('closeSession', e); } OB.MobileApp.model.triggerLogout(); }, /** * @deprecated Use PasswordHash.generateHash instead */ generate_sha1: function(theString) { return CryptoJS.enc.Hex.stringify(CryptoJS.SHA1(theString)); }, attemptToLoginOffline: async function() { if ( !OB.MobileApp.model.get('isLoggingIn') && OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('POSSessionActive') ) ) { OB.MobileApp.model.navigate('login'); return; } // If the user tries to login offline and exceeded the maximum of minutes offline, don't allow login if ( !OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('maxTimeInOffline') ) ) { if ( !OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('lastSuccessfulOnlineRequest') ) && new Date().getTime() - OB.UTIL.localStorage.getItem('lastSuccessfulOnlineRequest') >= OB.UTIL.localStorage.getItem('maxTimeInOffline') * 60 * 1000 ) { OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_MaximumOfflineTimeExceeded_Title'), OB.I18N.getLabel('OBMOBC_MaximumOfflineTimeExceeded_Message'), [ { isConfirmButton: true, label: OB.I18N.getLabel('OBMOBC_LblOk'), action: function() { window.location.reload(); } } ], { onHideFunction: function() { window.location.reload(); } } ); return; } } if (!this.user) { // Reloading page // If the user tries to login offline and exceeded the maximum of minutes offline, don't allow login if ( !OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('offlineSessionTimeExpiration') ) && !OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('lastUserIdLogin') ) ) { if ( !OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('lastSuccessfulOnlineRequest') ) && new Date().getTime() - OB.UTIL.localStorage.getItem('lastSuccessfulOnlineRequest') < OB.UTIL.localStorage.getItem('offlineSessionTimeExpiration') * 60 * 1000 ) { OB.MobileApp.model.set('windowRegistered', undefined); OB.MobileApp.model.set('loggedOffline', true); try { const session = await OB.App.OfflineSession.getActiveSessionForUser( OB.UTIL.localStorage.getItem('lastUserIdLogin') ); if (!session) { OB.warn( 'No active session for user with id ' + OB.UTIL.localStorage.getItem('lastUserIdLogin') + '. Navigating to login page' ); OB.MobileApp.model.navigate('login'); return; } const user = await OB.App.OfflineSession.withId(session.userId); if (user) { this.usermodel = new OB.Model.User(user); this.set('orgUserId', user.id); this.renderTerminalMain(); return; } else { OB.UTIL.showConfirmation.display( '', OB.I18N.getLabel('OBMOBC_OfflinePasswordNotCorrect'), null, { onHideFunction: () => OB.MobileApp.model.navigate('login') } ); return; } } catch (e) { OB.error('Error getting active sessions', e); } } } OB.MobileApp.model.navigate('login'); return; } OB.MobileApp.model.set('windowRegistered', undefined); OB.MobileApp.model.set('loggedOffline', true); OB.MobileApp.model.set('datasourceLoadFailed', false); const user = await OB.App.OfflineSession.login(this.user, this.password); if (user) { // TODO: replace backbone model with new app state this.usermodel = new OB.Model.User(user); this.set('orgUserId', user.id); this.updateSession(this.usermodel, () => { OB.UTIL.localStorage.setItem('lastUserIdLogin', user.id); OB.UTIL.localStorage.setItem('lastUserNameLogin', user.name); OB.UTIL.localStorage.setItem('lastLogInDate', new Date().getTime()); this.renderTerminalMain(); }); } else { OB.UTIL.showConfirmation.display( '', OB.I18N.getLabel('OBMOBC_OfflinePasswordNotCorrect'), null, { onHideFunction: function() { window.location.reload(); } } ); } }, postLoginActions: function() { OB.debug('next process: none'); }, /** * Prelogin actions is executed before login, it should take care of * removing all session values specific by application. * * Override this in case you app needs to do something special */ preLoginActions: function() { OB.debug('next process: none. set your application start point here'); }, login: function(user, password, mode, command) { OB.info('isLoggingIn ' + this.get('isLoggingIn')); if (this.get('isLoggingIn') === true) { OB.UTIL.Debug.execute(function() { throw 'There is already a logging process in progress'; }); return; } this.set('isLoggingIn', true); var params; OB.info('Login loading'); OB.UTIL.showLoading(true); var me = this; me.user = user; me.password = password; // invoking app specific actions this.preLoginActions(); params = JSON.parse(JSON.stringify(this.get('loginUtilsParams'))) || {}; params.user = user; params.password = password; params.Command = command ? command : 'DEFAULT'; if (params.Command === 'FORCE_RESET_PASSWORD') { params.resetPassword = true; } params.IsAjaxCall = 1; params.appName = this.get('appName'); var rr, ajaxRequest = new enyo.Ajax({ url: this.get('loginHandlerUrl'), cacheBust: false, method: 'POST', timeout: 5000, // don't do retries as this gives side effects as re-showing login page maxNumOfRequestRetries: 0, contentType: 'application/x-www-form-urlencoded; charset=UTF-8', data: params, success: function(inSender, inResponse) { OB.info('loginHandlerUrl success'); if (this.alreadyProcessed) { OB.info('loginHandlerUrl alreadyProcessed'); return; } this.alreadyProcessed = true; if (inResponse && inResponse.sourceVersion) { OB.UTIL.sourceVersionFromResponse = inResponse.sourceVersion; OB.UTIL.checkSourceVersion(inResponse.sourceVersion, true, false); } if (OB.MobileApp.model.get('timeoutWhileLogin')) { OB.info('loginHandlerUrl timeoutWhileLogin'); OB.MobileApp.model.set('timeoutWhileLogin', false); return; } if (inResponse && inResponse.showMessage) { if (inResponse.command === 'FORCE_NAMED_USER') { OB.UTIL.showConfirmation.display( inResponse.messageTitle, inResponse.messageText, [ { label: OB.I18N.getLabel('OBMOBC_LblOk'), action: function() { OB.MobileApp.model.set('isLoggingIn', false); OB.MobileApp.model.login( user, password, mode, 'FORCE_NAMED_USER' ); return; } }, { label: OB.I18N.getLabel('OBMOBC_LblCancel'), action: function() { OB.MobileApp.model.set('isLoggingIn', false); return; } } ], { autoDismiss: false } ); return; } else if (inResponse.resetPassword) { OB.UTIL.showLoading(false); this.dialog = OB.MobileApp.view.$.confirmationContainer.createComponent( { kind: 'OB.UI.ExpirationPassword', classes: 'obUiTerminal-confirmationContainer-obUiExpirationPassword', context: this } ); this.dialog.show(); if (inResponse.attemptedChange) { this.dialog.$.body.$.error.setContent(inResponse.messageText); } else { this.dialog.$.body.$.newheader.setContent( inResponse.messageTitle ); } return; } else { me.triggerLoginFail(401, mode, inResponse); return; } } if ( OB.MobileApp.model.get('terminalName') && !OB.UTIL.localStorage.getItem('loggedTerminalName') ) { OB.UTIL.localStorage.setItem( 'loggedTerminalName', OB.MobileApp.model.get('terminalName') ); } if ( (!OB.UTIL.isNullOrUndefined( OB.MobileApp.model.get('terminalName') ) || !OB.UTIL.isNullOrUndefined( OB.UTIL.localStorage.getItem('loggedTerminalName') )) && OB.MobileApp.model.get('terminalName') !== OB.UTIL.localStorage.getItem('loggedTerminalName') ) { OB.info( 'Terminal has been changed. Resetting database and local storage information.' ); OB.UTIL.localStorage.clear(function() { OB.MobileApp.model.logout(); return; }); } else { OB.MobileApp.model.set('orgUserId', inResponse.userId); OB.UTIL.localStorage.setItem('lastUserIdLogin', inResponse.userId); OB.UTIL.localStorage.setItem('lastUserNameLogin', me.user); OB.UTIL.localStorage.setItem('lastLogInDate', new Date().getTime()); me.set('loggedOffline', false); var setUserModelOnlineCallback = function() { me.initializeCommonComponents(); }; if (me.get('supportsOffline')) { me.setUserModelOnline(setUserModelOnlineCallback); } else { setUserModelOnlineCallback(); } } }, fail: function(inSender, inResponse) { OB.info('loginHandlerUrl fail'); var lastTotalRefresh, lastIncRefresh; if (this.alreadyProcessed) { return; } this.alreadyProcessed = true; lastTotalRefresh = OB.UTIL.localStorage.getItem( 'POSLastTotalRefresh' ); lastIncRefresh = OB.UTIL.localStorage.getItem('POSLastIncRefresh'); if (!lastTotalRefresh && !lastIncRefresh) { //There hasn't been a complete login yet, so we can't allow to login offline. var msg = OB.I18N.getLabel('OBMOBC_OfflineModeNoLogin') + OB.I18N.getLabel('OBMOBC_LoadingErrorBody'); OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_Error'), msg, [ { label: OB.I18N.getLabel('OBMOBC_Reload'), action: function() { window.location.reload(); } } ], { onShowFunction: function(popup) { OB.UTIL.localStorage.removeItem( 'cacheAvailableForUser:' + OB.MobileApp.model.get('orgUserId') ); popup.$.headerCloseButton.hide(); }, autoDismiss: false } ); } else { me.attemptToLoginOffline(); } } }); rr = new OB.RR.Request({ ajaxRequest: ajaxRequest }); OB.info('loginHandlerUrl request'); rr.exec(this.get('loginHandlerUrl')); }, setUserModelOnline: function(callback) { OB.debug('next process:', callback); // argument checks OB.UTIL.Debug.execute(function() { if (!callback) { throw 'This method should not be executed asynchronously. Please provide a callback'; } }); const user = { id: OB.MobileApp.model.get('orgUserId'), name: this.user, formatInfo: JSON.stringify(OB.Format) }; OB.App.OfflineSession.upsertUser(user, this.password).then( dbUser => { // TODO: replace backbone model with new app state const backboneUser = new OB.Model.User(dbUser); this.usermodel = backboneUser; this.updateSession(backboneUser, callback); }, error => { OB.error('setUserModelOnline', error, arguments); } ); }, /** * Set of app specific actions executed before loging out. * * Override this method if your app needs to do something special */ preLogoutActions: function(callback) { callback(); }, logout: function() { var me = this; function callback() { OB.UTIL.showLoggingOut(true); OB.UTIL.localStorage.removeItem('authenticationClient'); OB.UTIL.localStorage.removeItem('authenticationToken'); var rr, ajaxRequest = new enyo.Ajax({ url: '../../org.openbravo.mobile.core.logout', cacheBust: false, method: 'GET', handleAs: 'json', timeout: 20000, contentType: 'application/json;charset=utf-8', success: function(inSender, inResponse) { me.closeSession(); }, fail: function(inSender, inResponse) { me.closeSession(); } }); rr = new OB.RR.Request({ ajaxRequest: ajaxRequest }); rr.exec(ajaxRequest.url); } this.preLogoutActions(function() { me.flushMessagesWhenSynchReady(0, me, callback); }); }, flushMessagesWhenSynchReady: function( iteration, context, callback, iterationLimit = 10 ) { const doTheQuestion = () => { //Ask the user to continue OB.UTIL.showConfirmation.display( OB.I18N.getLabel('OBMOBC_PendingMessages'), OB.I18N.getLabel('OBMOBC_PendingMessagesBody'), [ { label: OB.I18N.getLabel('OBMOBC_LogoutAndDoNotSync'), // label: 'Logout and keep pending messages', action: () => { callback(); } }, { label: OB.I18N.getLabel('OBMOBC_TryAgain'), // label: 'Continue trying to syncrhonize', isConfirmButton: true, action: () => { context.flushMessagesWhenSynchReady( iteration, context, callback, iterationLimit + 10 ); } } ], { autoDismiss: false, closeOnEscKey: false } ); }; OB.Dal.find( OB.Model.CashUp, { isprocessed: 'Y', _orderByClause: 'creationDate desc' }, function(cashUpProcessed) { // Check if the cashup is moved to the State // Check if runSyncProccess has been finished // Check if Messages are moved from the State to IndexedDB OB.UTIL.showLoading(true); if ( cashUpProcessed.length === 0 && !OB.MobileApp.model.pendingSyncProcess && OB.App.State.getState().Messages.length === 0 ) { OB.App.SynchronizationBuffer.internalFlush().then(async () => { const stateMessages = OB.App.State.getState().Messages; const syncBufferMessages = await OB.App.SynchronizationBuffer.getPendingMessages(); const hasPendingtSyncedMessages = stateMessages.length > 0 || syncBufferMessages.length > 0; if (!hasPendingtSyncedMessages) { callback(); } else { iteration++; if (iteration < iterationLimit) { setTimeout(() => { context.flushMessagesWhenSynchReady( iteration, context, callback, iterationLimit ); }, 1000); } else { doTheQuestion(); } } }); } else { OB.info( '[CashupAndLogout] Cannot do logout after cashup, iteration: ' + iteration + '- Cashup in WebSQL: ' + cashUpProcessed.length + ', runSyncprocess is running: ' + OB.MobileApp.model.pendingSyncProcess + ', Messages in the State: ' + OB.App.State.getState().Messages.length ); iteration++; if (iteration < iterationLimit) { setTimeout(() => { context.flushMessagesWhenSynchReady( iteration, context, callback, iterationLimit ); }, 1000); } else { doTheQuestion(); } } } ); }, lock: function() { var me = this; if (OB && OB.POS && OB.POS.hwserver !== undefined) { OB.POS.hwserver.print( new OB.DS.HWResource(OB.OBPOSPointOfSale.Print.GoodByeTemplate), {} ); } function callback() { OB.UTIL.localStorage.removeItem('authenticationClient'); OB.UTIL.localStorage.removeItem('authenticationToken'); var rr, ajaxRequest = new enyo.Ajax({ url: '../../org.openbravo.mobile.core.logout', cacheBust: false, method: 'GET', handleAs: 'json', timeout: 20000, contentType: 'application/json;charset=utf-8', success: function(inSender, inResponse) { me.triggerLogout(); }, fail: function(inSender, inResponse) { me.triggerLogout(); } }); rr = new OB.RR.Request({ ajaxRequest: ajaxRequest }); rr.exec(ajaxRequest.url); } me.flushMessagesWhenSynchReady(0, me, callback); }, keepWorking: function() { this.set('keepWorking', true); }, isKeepWorking: function() { return this.get('keepWorking'); }, triggerLogout: function() { // deactivate the loginfail trigger this.off('loginfail'); // trigger for custom applications this.trigger('logout'); OB.UTIL.localStorage.removeItem('POSSessionActive'); OB.UTIL.showLoading(true); const timeToWaitBeforReload = OB.MobileApp.model.hasPermission('OBMOBC_WaitUntilReload', true) || 3000; setTimeout(() => { window.location.reload(); }, timeToWaitBeforReload); }, triggerLoginSuccess: function() { this.trigger('loginsuccess'); }, triggerOnLine: function() { OB.info('Triggered online - isLoggingIn ' + OB.MobileApp.model.isLoggingIn); if (!OB.MobileApp.model.isLoggingIn) { OB.info('connectedToERP'); this.set('connectedToERP', true); } }, triggerOffLine: function() { if (!OB.MobileApp.model.isLoggingIn) { this.set('connectedToERP', false); OB.App.SynchronizationBuffer.goOffline('Backend'); } }, triggerLoginFail: function(e, mode, data) { OB.UTIL.showLoading(false); if (mode === 'userImgPress') { this.trigger('loginUserImgPressfail', e); } else { this.trigger('loginfail', e, data); } this.set('isLoggingIn', false); }, hasPermission: function(p, checkForAutomaticRoles, smartDbPrefix) { var permission; var dbPrefix; var separator = '_'; var resolvedPreference; permission = p.approval ? p.approval : p; //This code will get the preference based on the app if (smartDbPrefix === true) { dbPrefix = OB.MobileApp.model.get('appModulePrefix'); if (dbPrefix) { resolvedPreference = dbPrefix + separator + permission; if ( this.get('permissions') && this.get('permissions')[resolvedPreference] !== null && this.get('permissions')[resolvedPreference] !== undefined ) { permission = resolvedPreference; } else { resolvedPreference = dbPrefix + permission; if ( this.get('permissions') && this.get('permissions')[resolvedPreference] !== null && this.get('permissions')[resolvedPreference] !== undefined ) { permission = resolvedPreference; } else { return false; } } } else { return false; } } return ( (!checkForAutomaticRoles && !(this.get('context') && this.get('context').role.manual)) || (this.get('permissions') && this.get('permissions')[permission]) ); }, supportLogClient: function() { var supported = true; if ( (OB.MobileApp && OB.MobileApp.model && OB.MobileApp.model.get('supportsOffline')) === false ) { supported = false; } return supported; } });