// // Use this in Background Script // Don't make a Fix Script as there are limitations // At least Fix Scripts don't record changes in update sets // // If you come from version 7, you need first to delete manually the 4 scripts include copies of: // TimeCardPortalService // TimeCardDailies // TimeSheet // TimeSheetPolicy // If you come from version 8.0 or 8.1, you need to save what you did in Widget Instance "Time Card Portal - Task Selector", and delete this widget instance // From version 8.2, these options are managed in OOTB widget instance "Time Card Portal Container", where you can paste what you saved, see sample code below // For same reason, the time_sheet_policy field u_selector_tables must be deleted, and the ACL time_sheet_policy.u_selector_tables must be deleted => NO KEEP THE FIELD & ACL, STILL USED IN v8.2 BY POLICY // Version 8.2.1 fixes a minor bug when filtering an editable reference field in the Time Sheet Portal // Do care to widget instance "Time Card Portal Container" in the Additional options, JSON format, as a new option came with a Quebec patch which will break the Time Sheet Portal when it is missing. // It doesn't break the customization but the OOTB portal, the new option must exists as: // "pageSize": 20, // "restrictedStatesForDuplicateMerge" : ['approved', 'processed', 'recalled'], // "header_fields": [{ // // This script must run on an OOTB time sheet portal configuration // The time sheet portal is quite complex and upgrades, and even patches, can break the person-days feature // If something goes wrong after an upgrade or a patch, the script must be run again to revert the objects to OOTB and to apply again the person-days changes // // // Features: // > manage person-days with new time sheet policy option "Manage person-days" // > save person-days in the table "Time card dailies" // > manage time sheet portal editable fields // > manage time sheet portal fields to display, before (left) or after (right) the week day columns // > show colored time off columns with new time sheet policy check box "Show time off days" // > select the list of other categories to display with the new time sheet policy option "Allow categories" // > add secondary fields to task selector cards // > manage effort numbers with enforced increment // The features are set independently, and they are set per time sheet policy // But the feature which saves the person-days in the time card dailies table always applies and can't be disabled // But the feature which sets editable fields is set in the widget instance "Time Card Portal Container" applies to all time sheet policies // But the feature to add secondary fields is set in the widget instance of the "Time Card Portal - Task Selector" and applies to all time sheet policies // > set a filter when adding unassigned tasks in the time sheet portal, an admin can create a system property with the filter and set it in the time sheet policies // > set allowed tables in the task selector tab, an admin can create a system property to list available tables and set it in time sheet policies // // // Feature notes: // > Person-days calculations are based on the user average daily FTE // Person-days are calculated using sys_user.average_daily_fte value, or default to system property com.snc.resource_management.average_daily_fte when user value is missing // This is done calling OOTB script RMUtil.averageDailyFteForUser(sys_user.sys_id) // > The table time_card_daily has been updated to record person-days in the new field "Person-days worked", this field is always calculated whatever is the time sheet policy option // > Added editable fields are read from the Widget Instance "Time Card Portal Container", only custom time_card fields are editable // The fields must have the property editable: true, to be editable // The property type: 'effort' is set to manage in hours or person-days depending of the time sheet policy option, only decimal fields can be set wih type 'effort' // Decimal, Currency, String, True/False, Date, Date/Time, Reference fields can be edited, the values are all text entries -but not for reference- entered with the keyboard and there is no picklist // > The time sheet policy field "Show time off days" sets to show time off columns with a specific background color // The default colors are LightCyan (for the time off column) and PaleTurquoise (for the top of the time off column) // These default colors can be overriden in the system propperties 'time.sheet.portal.time.off.color' and 'time.sheet.portal.time.off.color.top' // The colors can be named colors or Hexa colors (as #E0FFFF and #AFEEEE). The colors must be light colors as the text is black // // Source oode note, the variable name decimalSeperator is used instead of decimalSeparator, due to OOTB definition "decimalSeperator" for instance in server script of widget Time Sheet treemap [ts-treemap] // Sample code for Widget Instance "Time Card Portal Container" // The "tables" property allows to filter the tables to show in the task selector panel, this exists OOTB and you can remove tables you don't want to show in the time sheet portal // The "addedSecondaryFields" is new and has been added with the customization, to add fields in the cards. In the sample below, fields have been able added for table pm_project_task // The other options are OOTB in Quebec version /* { "tm_grid_options": { "displayValue": "Time card grid options", "value": { "pageSize": 20, "header_fields": [{ "name": "task.short_description", "width_in_percent": 16 }, { "name": "project_time_category", "width_in_percent": 20 }, { "name": "resource_plan", "width_in_percent": 16 }, {"name":"task.parent.short_description", "width_in_percent": 6}, {"name":"u_remaining_hours", "editable": true, "type": "effort", "width_in_percent": 5, "alignment":"left"} ] } }, "task_selector_options": { "displayValue": "Time Sheet Portal Task Selector Options, set maxVisibleTask, maxVisibleGroupTasks to positive integer to limit task selector cards.", "value": { "tables": "incident,problem,change_request,rm_staory,rm_scrum_task,pm_project,pm_project_task,tm_test_plan,rm_defect,rm_enhancement,dmn_demand_task,sn_customerservice_case,wm_order,wm_task,sn_audit_task,sn_audit_engagement,sn_audit_advanced_milestone,sn_audit_advanced_engagement_project,sn_grc_issue", "maxVisibleTasks": -1, "maxVisibleGroupTasks": -1, "disableTaskSelectorSync": false, "addedSecondaryFields": { "pm_project_task" : [ { "index": 0, "field":"ref_pm_project_task.sub_tree_root.short_description" }, { "index": 1, "field":"parent.short_description" } ] } } } } */ // // Set translation strings here // var messages = []; // Person-days var translatePersonDays = []; translatePersonDays.push( { language: 'fr', message: 'Jours-hommes' } ); messages.push( { key: 'Person-days', translations: translatePersonDays } ); // {0} dys var translateDays = []; translateDays.push( { language: 'fr', message: '{0} jh' } ); messages.push( { key: '{0} dys', translations: translateDays } ); // Maximum days per day cannot exceed {0} days. The following days have exceeded maximum days - {1} var translatePerDayError = []; translatePerDayError.push( { language: 'fr', message: 'Le nombre de jours-homme par jour ne peut pas dépasser {0} jours. Les jours suivants ont dépassés ce maximum: {1}' } ); messages.push( { key: 'Maximum days per day cannot exceed {0} days. The following days have exceeded maximum days - {1}', translations: translatePerDayError } ); // Maximum days per week cannot exceed {0} days var translatePerWeekError = []; translatePerWeekError.push( { language: 'fr', message: 'Le nombre de jours par semaine ne peut pas dépasser {0} jours' } ); messages.push( { key: 'Maximum days per week cannot exceed {0} days', translations: translatePerWeekError } ); // 'Total' which has been set to 'Total Hrs', so translate 'Total Hrs' to keep default OOTB label var translateTotalHrs = []; translateTotalHrs.push( { language: 'en', message: 'Total' } ); translateTotalHrs.push( { language: 'fr', message: 'Total' } ); messages.push( { key: 'Total Hrs', translations: translateTotalHrs } ); // // // Please dont touch further code // // // Write translations for (var t1 = 0; t1 < messages.length; t1++) { for (var t2 = 0; t2 < messages[t1].translations.length; t2++) { // Look for existing translation var grMessage = new GlideRecord('sys_ui_message'); grMessage.addEncodedQuery('key=' + messages[t1].key + '^language=' + messages[t1].translations[t2].language); grMessage.query(); if (grMessage.next()) { // Update message grMessage.message = messages[t1].translations[t2].message; grMessage.update(); } else { // Create new message grMessage.initialize(); grMessage.key = messages[t1].key; grMessage.language = messages[t1].translations[t2].language; grMessage.message = messages[t1].translations[t2].message; grMessage.insert(); } } } // Constants to tag the updated objects var customFeatureFlag = '@TIMESHEET IN MAN-DAYS V9.0.0@'; var customComment = customFeatureFlag + " Installed by script. Update time sheet policies to enable and disable this feature."; var scriptCommentStart = '// start TIMESHEET IN MAN-DAYS V9.0.0'; var scriptCommentEnd = '// end TIMESHEET IN MAN-DAYS V9.0.0'; revertToOutOfTheBox = function (tableName, sysId) { // Revert the object to OOTB var reverted = false; var noHistory = true; // revert to sys_update_version.name having the most recent reference source in table sys_upgrade_history var grVersion = new GlideRecord('sys_update_version'); grVersion.addEncodedQuery('name=' + tableName + '_' + sysId); grVersion.orderByDesc('sys_created_on'); grVersion.query(); var grHistory = new GlideRecord('sys_upgrade_history'); while (grVersion.next()) { noHistory = false; gs.info( 'revertToOutOfTheBox ' + tableName + ' ' + sysId + ' ' + grVersion.name + ' ' + grVersion.sys_created_on+ ' ' + grVersion.source + ' ' + grVersion.getDisplayValue('source') ); if (grHistory.get(grVersion.source)) { gs.info('revertToOutOfTheBox ' + tableName + ' ' + sysId + ' from ' + grHistory.from_version + ' ; to ' + grHistory.to_version ); // Code from UI Action "Revert to this version" var grCollision = new GlideRecord("sys_sync_history_version"); grCollision.addQuery("version", grVersion.getValue('sys_id')); grCollision.addQuery("state", "collision"); grCollision.query(); if (grCollision.next()) { gs.info(getMessage("revertToOutOfTheBox " + tableName + " " + sysId + " Your update version entry cannot be reverted because it is in the Collision state. Resolve the collision first.")); return ''; } // Code from Script Include "RevertUpdateVersionAjax.revert" var isReverted = (new GlideappUpdateVersion()).revert(grVersion.getValue('sys_id')); reverted = true; break; } } if (reverted || noHistory) { // Get the reverted object var grRevertedObject = new GlideRecord(tableName); if (grRevertedObject.get(sysId)) { return grRevertedObject; } else return ''; } else return ''; } // Get Portal Widget, revert it back to OOTB if updated, and returns it resetAndGetObject = function(tableName, fieldId, objectId, tagField) { var grObject = new GlideRecord(tableName); grObject.addQuery(fieldId, objectId); grObject.query(); if (grObject.next()) { var grRevertedObject = revertToOutOfTheBox(tableName, grObject.getValue('sys_id')); if (JSUtil.notNil(grRevertedObject) && JSUtil.notNil(tagField)) grRevertedObject.setValue(tagField, customComment); return grRevertedObject; } else return ''; }; // // Pieces of code to insert to script includes and widgets // getRMCapacityAPICode = function() { return "\ " + scriptCommentStart + "\ // Manage grouped resource capacity\ var isEmployeePluginActive = GlidePluginManager.isActive('sn_employee');\ if(isEmployeePluginActive) {\ var grScript = new GlideRecord('sys_script_include');\ if (grScript.get('api_name','sn_employee.SN_SPM_RSC_UTILS')) {\ var headCount = new sn_employee.SN_SPM_RSC_UTILS().getHeadcount(userId);\ if (headCount > 0.0) {\ for (var i = 0; i < scheduledHours.length; i++) {\ scheduledHours[i] = scheduledHours[i] * headCount;\ }\ }\ }\ }\ // End manage grouped resource capacity\ " + scriptCommentEnd; }; getTimeSheetPolicyCode = function() { return "\ " + scriptCommentStart + "\ // Manage grouped resource capacity\ enableHeadcount: function(userId) {\ this.headCount = 0.0;\ if (this.gr.getValue('u_max_employment_headcount') == '1') {\ var isEmployeePluginActive = GlidePluginManager.isActive('sn_employee');\ if(isEmployeePluginActive) {\ var grScript = new GlideRecord('sys_script_include');\ if (grScript.get('api_name','sn_employee.SN_SPM_RSC_UTILS')) {\ this.headCount = new sn_employee.SN_SPM_RSC_UTILS().getHeadcount(userId);\ }\ }\ }\ this.dailyUserFTE = parseFloat(RMUtil.averageDailyFteForUser(userId));\ },\ \ managePersonDays: function() {\ return this.gr.getValue('u_manage_person_days') == '1';\ },\ \ addedColumns: function() {\ var columns = [];\ var fieldAdded = {};\ var returnColumns = {};\ var grField = new GlideRecord('sys_dictionary');\ if (JSUtil.notNil(this.gr.u_columns))\ columns = this.gr.u_columns.split(',');\ for (var i = 0; i < columns.length; i++) {\ if (grField.get(columns[i])) {\ var colName = grField.getValue('element');\ fieldAdded[colName] = true;\ returnColumns[colName] = {};\ returnColumns[colName].name = colName;\ returnColumns[colName].label = grField.getValue('column_label');\ returnColumns[colName].type = grField.getValue('internal_type');\ returnColumns[colName].maxLengh = grField.getValue('max_length');\ returnColumns[colName].active = (grField.getValue('active') == '1');\ returnColumns[colName].readOnly = (grField.getValue('read_only') == '1');\ returnColumns[colName].mandatory = (grField.getValue('mandatory') == '1');\ returnColumns[colName].reference = grField.getValue('reference');\ returnColumns[colName].choice = grField.getValue('choice');\ var refQualType = grField.getValue('use_reference_qualifier');\ if (refQualType == 'simple')\ returnColumns[colName].qualifier = grField.getValue('reference_qual_condition');\ else if (refQualType == 'dynamic')\ returnColumns[colName].qualifier = grField.getValue('dynamic_ref_qual');\ else if (refQualType == 'advanced')\ returnColumns[colName].qualifier = grField.getValue('reference_qual');\ }\ }\ returnColumns._fieldAdded = fieldAdded;\ return returnColumns;\ },\ \ showTimeOffDays: function() {\ return this.gr.getValue('u_show_time_off') == '1';\ },\ \ categories: function() {\ var categories = {};\ var grCategoryChoices = new GlideRecord('sys_choice');\ grCategoryChoices.addEncodedQuery('sys_idIN' + this.gr.getValue('u_categories'));\ grCategoryChoices.query();\ while (grCategoryChoices.next())\ categories[grCategoryChoices.getValue('value')] = true;\ return categories;\ },\ \ effortStep: function() {\ return this.gr.getValue('u_effort_step');\ },\ \ unassignedTaskFilter: function() {\ var taskFilter = this.gr.getValue('u_unassigned_task_filter');\ return gs.getProperty(taskFilter);\ },\ \ selectorTables: function() {\ var selectorTables = this.gr.getValue('u_selector_tables');\ return gs.getProperty(selectorTables);\ },\ \ resourceScheduleID: function() {\ return this.gr.getValue('u_resource_schedule');\ },\ " + scriptCommentEnd + "\ "; }; getTimeSheetPolicyCode2 = function() { return "\ " + scriptCommentStart + "\ if (this.headCount == 0.0) {\ " + scriptCommentEnd; }; getTimeSheetPolicyCode3 = function() { return "\ " + scriptCommentStart + "\ } else {\ return this.headCount * parseFloat(this.gr.getValue('max_hours_per_week'));\ }\ " + scriptCommentEnd; }; getTimeSheetPolicyCode4 = function() { return "\ " + scriptCommentStart + "\ } else if (maxHours < 0) {\ return 999999;\ } else {\ return this.headCount * maxHours; // WAS return this.headCount * this.dailyUserFTE;\ }\ " + scriptCommentEnd; }; getTimeSheetCode = function() { return "\ " + scriptCommentStart + "\ var decimalSeperator = GlideLocale.get().getdecimalSeparator();\ if (decimalSeperator == undefined) decimalSeperator = '.';\ var effortRatio = 1;\ if (policy.managePersonDays()) {\ effortRatio = parseFloat(RMUtil.averageDailyFteForUser(this.gr.user));\ if (effortRatio != 0)\ effortRatio = 1 / effortRatio;\ }\ var messageParamMaxPerDay = (Math.round( maxHoursPerDay * effortRatio * 100 ) / 100).toString().replace('.', decimalSeperator);\ var messageParamMaxPerWeek = (Math.round( maxHoursPerWeek * effortRatio * 100 ) / 100).toString().replace('.', decimalSeperator);\ if (exceedingMaxHours) {\ if (policy.managePersonDays())\ gs.addErrorMessage(gs.getMessage('Maximum days per day cannot exceed {0} days. The following days have exceeded maximum days - {1}', [messageParamMaxPerDay, exceedingDays.join(',')]));\ else\ gs.addErrorMessage(gs.getMessage('Maximum hours per day cannot exceed {0} hours. The following days have exceeded maximum hours - {1}', [messageParamMaxPerDay, exceedingDays.join(',')]));\ }\ if (maxHoursPerWeek > 0 && parseFloat(this.gr.getValue('total_hours')) > maxHoursPerWeek) {\ if (policy.managePersonDays())\ gs.addErrorMessage(gs.getMessage('Maximum days per week cannot exceed {0} days', messageParamMaxPerWeek));\ else\ gs.addErrorMessage(gs.getMessage('Maximum hours per week cannot exceed {0} hours', messageParamMaxPerWeek));\ exceedingMaxHours = true;\ }\ " + scriptCommentEnd + "\ "; }; getTimeSheetCode2 = function() { return "\ " + scriptCommentStart + "\ policy.enableHeadcount(this.gr.user);\ " + scriptCommentEnd; }; getTimePortalCode1 = function() { return "\ \ " + scriptCommentStart + "\ var effortRatio = 1;\ var policy = TimeSheetPolicy.getFromUserId(timecard.user);\ if (policy.managePersonDays()) {\ effortRatio = parseFloat(RMUtil.averageDailyFteForUser(timecard.user));\ if (effortRatio != 0)\ effortRatio = 1 / effortRatio;\ }\ var calcFields = {monday:true, tuesday:true, wednesday:true, thursday:true, friday:true, saturday:true, sunday:true, total:true};\ var customEffortFields = (this.gRequest.getParameter('sysparm_custom_effort_fields') || '').split(',');\ for (var c = 0; c < customEffortFields.length; c++)\ calcFields[customEffortFields[c]] = true;\ var decimalSeperator = GlideLocale.get().getdecimalSeparator();\ if (decimalSeperator == undefined) decimalSeperator = '.';\ if (policy.managePersonDays()) {\ for(var j in fields){\ var field = fields[j];\ if (calcFields[field]) {\ // Manage only 8 fields, from Sunday to Saturday, plus Total\ // Multiply entered numbers by person day ratio\ timecard.setValue(field, timecard.getValue(field)/effortRatio);\ }\ }\ }\ " + scriptCommentEnd + "\ "; }; getTimePortalCode2 = function() { return "\ \ " + scriptCommentStart + "\ // Manage only 8 fields, from Sunday to Saturday, plus Total\ if (policy.managePersonDays() && calcFields[field]) {\ // Manage only 8 fields, from Sunday to Saturday, plus Total\ // Divide time_card numbers by person day ratio\ fieldData.value = (Math.round( fieldData.value * effortRatio * 100 ) / 100).toString().replace('.', decimalSeperator);\ fieldData.display_value = fieldData.value;\ }\ " + scriptCommentEnd + "\ "; }; getTimeDailies1 = function() { return "\ " + scriptCommentStart + "\ this.effortRatio = parseFloat(RMUtil.averageDailyFteForUser(timeCard.getValue('user')));\ if (this.effortRatio != 0)\ this.effortRatio = 1 / this.effortRatio;\ " + scriptCommentEnd + "\ "; }; getTimeDailies2 = function() { return "\ " + scriptCommentStart + "\ tcd.u_day_worked = tcd.time_worked * this.effortRatio;\ " + scriptCommentEnd + "\ "; }; getTimeDailies3 = function() { return "\ " + scriptCommentStart + "\ timeCardDaily.u_day_worked = timeCardDaily.time_worked * this.effortRatio;\ " + scriptCommentEnd + "\ "; }; getTimeDailies4 = function() { return "\ " + scriptCommentStart + "\ getTCPersonDayDaily : function (currentDay) { //monday, tuesday, wednesday, thursday, friday,saturday, sunday\ return currentDay * this.effortRatio;\ },\ " + scriptCommentEnd + "\ "; }; getTimeBreakdownCode = function() { return "\ " + scriptCommentStart + "\ var decimalSeperator = GlideLocale.get().getdecimalSeparator();\ if (decimalSeperator == undefined) decimalSeperator = '.';\ var groupingSeparator = GlideLocale.get().getGroupingSeparator();\ var timesheetPolicy = TimeSheetPolicy.getFromUserId(userId);\ data.effortRatio = 1;\ if (timesheetPolicy.managePersonDays()) {\ data.effortRatio = parseFloat(RMUtil.averageDailyFteForUser(userId));\ if (data.effortRatio != 0)\ data.effortRatio = 1 / data.effortRatio;\ }\ data.HrsLabel = gs.getMessage('Hrs');\ if (timesheetPolicy.managePersonDays())\ data.HrsLabel = gs.getMessage('Person-days');\ for (var v = 0; v < data.breakdowns.length; v++) {\ var dataHours = data.breakdowns[v].hours.toString().replace(decimalSeperator,'.').replace(groupingSeparator,'');\ data.breakdowns[v].effortCalc = ( Math.round(dataHours*data.effortRatio*100)/100 ).toString().replace('.', decimalSeperator);\ }\ " + scriptCommentEnd + "\ "; }; getTimeTreemapCode1 = function() { return "\ \ " + scriptCommentStart + "\ // Read Value instead DisplayValue to get a number and not a string having decimal seprator to manage\ data.total_hours = timesheet.getValue('total_hours');\ var timesheetPolicy = TimeSheetPolicy.getFromUserId(userId);\ data.effortRatio = 1;\ if (timesheetPolicy.managePersonDays()){\ data.effortRatio = parseFloat(RMUtil.averageDailyFteForUser(userId));\ if (data.effortRatio != 0)\ data.effortRatio = 1 / data.effortRatio;\ }\ data.effortLabel = 'hrs';\ data.HrsLabel = gs.getMessage('Hrs');\ if (timesheetPolicy.managePersonDays()) {\ data.effortLabel = 'dys';\ data.HrsLabel = gs.getMessage('Person-days');\ }\ data.effortCalc = ( Math.round(data.total_hours*data.effortRatio*100)/100 ).toString().replace('.', decimalSeperator);\ " + scriptCommentEnd + "\ "; }; getTimeTreemapCode2 = function() { return "\ " + scriptCommentStart + "\ series.total = gs.getMessage('{0} ' + data.effortLabel, ( Math.round(total*data.effortRatio*100)/100 ).toString().replace('.', decimalSeperator));\ " + scriptCommentEnd + "\ "; }; getTimeTreemapCode3 = function() { return "\ " + scriptCommentStart + "\ seriesObj.total = gs.getMessage('{0} ' + data.effortLabel, ( Math.round(sum*data.effortRatio*100)/100 ).toString().replace('.', decimalSeperator));\ " + scriptCommentEnd + "\ "; }; getTimeGridCode1_old = function() { return "\ \ " + scriptCommentStart + "\ /* Add custom editable fields */\ data.custom_editable_fields = [];\ if(customFields) {\ customFields.forEach(function(fieldConf) {\ if (JSUtil.notNil(fieldConf.editable) && fieldConf.editable) {\ data.editable_fields.push(fieldConf.name);\ data.custom_editable_fields.push(fieldConf.name);\ }\ });\ }\ " + scriptCommentEnd + "\ "; }; getTimeGridCode1 = function() { return "\ \ " + scriptCommentStart + "\ /* Add custom editable fields */\ data.custom_editable_fields = [];\ if(customFields) {\ customFields.forEach(function(fieldConf) {\ if (JSUtil.notNil(fieldConf.editable) && fieldConf.editable) {\ data.editable_fields.push(fieldConf.name);\ var customField = { name: fieldConf.name, type: fieldConf.type };\ if (customField.type != 'string' && customField.type != 'decimal')\ customField.type = 'effort';\ data.custom_editable_fields.push(customField);\ }\ });\ }\ " + scriptCommentEnd + "\ "; }; getTimeGridCode2 = function() { return "\ " + scriptCommentStart + "\ if(data.column_meta[data.editable_fields[i]]) {\ data.column_meta[data.editable_fields[i]]['editable'] = true;\ data.column_meta[data.editable_fields[i]]['effort'] = true;\ }\ " + scriptCommentEnd + "\ "; }; getTimeGridCode3 = function() { return "\ " + scriptCommentStart + "\ for (var i = 0; i < data.custom_editable_fields.length; i++){\ if (data.custom_editable_fields[i].type != 'effort')\ data.column_meta[data.custom_editable_fields[i].name]['effort'] = false;\ }\ " + scriptCommentEnd + "\ "; }; getTimeGridCode4_old = function() { return "\ \ " + scriptCommentStart + "\ var decimalSeperator = GlideLocale.get().getdecimalSeparator();\ if (decimalSeperator == undefined) decimalSeperator = '.';\ data.effortRatio = 1;\ if (timesheetPolicy.managePersonDays())\ data.effortRatio = RMUtil.averageDailyFteForUser(data.userId);\ setListValues = function(record) {\ record.display_value = ( Math.round( record.value / data.effortRatio * 100 ) / 100 ).toString().replace('.', decimalSeperator);\ record.value = Math.round( record.value / data.effortRatio * 100 ) / 100;\ };\ for (var j = 0; j 0 && choiceValues.length == choiceLabels.length) { var grSysChoice = new GlideRecord('sys_choice'); for (var c = 0; c < choiceValues.length; c++) { grSysChoice.initialize(); grSysChoice.name = tableName; grSysChoice.element = fieldName; grSysChoice.language = 'en'; grSysChoice.sequence = c+1; grSysChoice.value = choiceValues[c]; grSysChoice.label = choiceLabels[c]; grSysChoice.insert(); } } } }; aclCreateManaged = function(name, operation, roles, description) { var aclSysId = ''; // Check if it exists var grAcl = new GlideRecord('sys_security_acl'); grAcl.addQuery('name', name); grAcl.addQuery('operation', operation); grAcl.query(); if (grAcl.next()) { aclSysId = grAcl.getValue('sys_id'); } else { // Create if missing var grAclNew = new GlideRecord('sys_security_acl'); grAclNew.initialize(); grAclNew.setDisplayValue('type', 'record'); grAclNew.setValue('operation', operation); grAclNew.active = true; grAclNew.name = name; grAclNew.description = description; aclSysId = grAclNew.insert(); } // Build look up roles var lookupRoles = {}; for (var i = 0; i < roles.length; i++) { lookupRoles[roles[i]] = { action: 'add' }; } // Check roles, if no roles this means nobody can do it and so add admin role if (roles.length == 0) lookupRoles['admin'] = { action: 'add' }; // List existing roles var deleteRoles = []; var grAclRole = new GlideRecord('sys_security_acl_role'); grAclRole.addQuery('sys_security_acl', aclSysId); grAclRole.query(); while (grAclRole.next()) { if (JSUtil.nil(lookupRoles[grAclRole.getDisplayValue('sys_user_role')])) { // Flag to delete role this role we don't know lookupRoles[grAclRole.getDisplayValue('sys_user_role')] = { action: 'delete' }; deleteRoles.push(grAclRole.getValue('sys_user_role')); } else { // Flag role ok lookupRoles[grAclRole.getDisplayValue('sys_user_role')].action = 'ok'; } } // Add new roles for (var eachrole in lookupRoles) { if (lookupRoles[eachrole].action == 'add') { var grNewRole = new GlideRecord('sys_security_acl_role'); grNewRole.initialize(); grNewRole.sys_security_acl = aclSysId; grNewRole.setDisplayValue('sys_user_role', eachrole); grNewRole.insert(); } } // Delete old roles var grDeleteRole = new GlideRecord('sys_security_acl_role'); grDeleteRole.addQuery('sys_security_acl', aclSysId); grDeleteRole.addEncodedQuery('sys_user_roleIN' + deleteRoles.join(',')); grDeleteRole.query(); while (grDeleteRole.next()) { grDeleteRole.deleteRecord(); } }; // Main function to update time sheet portal objects (script includes and portal widgets) enablePersonDays = function() { // Add time sheet policy field "Maximum employee headcount" check box createNewField( 'time_sheet_policy', 'u_max_employment_headcount', 'Maximum employee headcount', 'boolean'); // Add time sheet policy field "Manage person-days" check box createNewField( 'time_sheet_policy', 'u_manage_person_days', 'Manage person-days', 'boolean'); // Add time card daily field "Person-days worked" decimal createNewField( 'time_card_daily', 'u_day_worked', 'Person-days worked', 'decimal'); // Add time sheet policy field "Allow columns" //createNewField( 'time_sheet_policy', 'u_columns', 'Allow columns', 'glide_list', 'sys_dictionary', 'name=time_card^internal_typeINreference,boolean,currency,decimal,glide_date,glide_date_time,string^elementSTARTSWITHu_^ORelement=project_time_category^EQ'); createNewField( 'time_sheet_policy', 'u_columns', 'Allow columns', 'glide_list', 'sys_dictionary', 'name=time_card^elementSTARTSWITHu_^ORelement=project_time_category^EQ'); // Add time sheet policy field "Show time off days" check box createNewField( 'time_sheet_policy', 'u_show_time_off', 'Show time off days', 'boolean'); // Add time sheet policy field "Allow categories" createNewField( 'time_sheet_policy', 'u_categories', 'Allow categories', 'glide_list', 'sys_choice', 'name=time_card^element=category^inactive=false^valueNOT INtask_work,project_work^language=javascript:gs.getUser().getLanguage()^EQ'); // Add time sheet policy field "Effort increment" // When using person-days, it must be coherent with hours having 2 decimals as hours are stored // Values in the list, with "." to be able to parse strings as Floats createNewField( 'time_sheet_policy', 'u_effort_step', 'Effort increment', 'string', '', '', ['0.05', '0.10', '0.20', '0.25', '0.50', '1.00', '2.00', '4.00'], ['0.05', '0.10', '0.20', '0.25', '0.50', '1.00', '2.00', '4.00']); // Add time sheet policy field "Unassigned tasks filter" createNewField( 'time_sheet_policy', 'u_unassigned_task_filter', 'Unassigned tasks filter', 'string'); // Add field ACL to allow only admin to write aclCreateManaged('time_sheet_policy.u_unassigned_task_filter', 'write', [ 'admin' ], customFeatureFlag); // Add time sheet policy field "Allow selector tables" createNewField( 'time_sheet_policy', 'u_selector_tables', 'Allow selector tables', 'string'); // Add field ACL to allow only admin to write aclCreateManaged('time_sheet_policy.u_selector_tables', 'write', [ 'admin' ], customFeatureFlag); // Add time sheet policy field "Resource Schedule Sys ID" createNewField( 'time_sheet_policy', 'u_resource_schedule', 'Resource Schedule Sys ID', 'string'); // Add field ACL to allow only admin to write aclCreateManaged('time_sheet_policy.u_resource_schedule', 'write', [ 'admin' ], customFeatureFlag); // NO, FOR QUEBEC VERSION NO CUSTOMISATION, SECONDARY FIELDS ARE SET IN THE "Time Card Portal Container" WIDGET INSTANCE var SnowVersion = gs.getProperty("com.glide.embedded_help.version").substr(0,1); if (SnowVersion < "Q") { // Create new widget instance for "Time Card Portal - Task Selector" widget var grWidgetInstance = new GlideRecord('sp_instance'); grWidgetInstance.addQuery('sp_widget.id','timecard-task-selector'); grWidgetInstance.query(); if (!grWidgetInstance.next()) { grWidgetInstance.initialize(); grWidgetInstance.title = 'Time Card Portal - Task Selector'; grWidgetInstance.setDisplayValue('sp_widget', 'Time Card Portal - Task Selector'); grWidgetInstance.widget_parameters = "{\ \"taskSelectorOptions\": {\ \"cardFields\": {\ \"addedSecondaryFields\": {\ \"pm_project_task\" : [\ { \"index\": 0, \"field\":\"ref_pm_project_task.sub_tree_root.short_description\" },\ { \"index\": 1, \"field\":\"parent.short_description\" }\ ]\ }\ }\ }\ }"; grWidgetInstance.insert(); } } // // REVERT ALL TO OOTB // // Scrips Includes resetAndGetObject('sys_script_include', 'api_name', 'global.TimeSheetPolicy', 'description'); resetAndGetObject('sys_script_include', 'api_name', 'global.TimeSheet', 'description'); resetAndGetObject('sys_script_include', 'api_name', 'global.TimeCardPortalService', 'description'); resetAndGetObject('sys_script_include', 'api_name', 'global.TimeCardDailies', 'description'); resetAndGetObject('sys_script_include', 'api_name', 'global.RMCapacityAPI', 'description'); // Widgets resetAndGetObject('sp_widget', 'id', 'timesheet-week-breakdown', 'description'); resetAndGetObject('sp_widget', 'id', 'timecard-portal-container', 'description'); resetAndGetObject('sp_widget', 'id', 'ts-treemap', 'description'); resetAndGetObject('sp_widget', 'id', 'tc-grid', 'description'); resetAndGetObject('sp_widget', 'id', 'timecard-task-selector', 'description'); // Angular Provider resetAndGetObject('sp_angular_provider', 'name', 'TimeCardPortalService'); // Update the "RMCapacityAPI" script include var grRMCapacityAPI = resetAndGetObject('sys_script_include', 'api_name', 'global.RMCapacityAPI', 'description'); if (JSUtil.notNil(grRMCapacityAPI)) { // // SCRIPT // var updatedScript = grRMCapacityAPI.script; // Get insert position var pos1 = updatedScript.indexOf('fetchHoursToCalculateCapacity:'); var pos2 = updatedScript.indexOf("hoursObj['scheduledHours']", pos1); var posStart = updatedScript.lastIndexOf('\n', pos2); if (pos1 > 0 && pos2 > 0 && posStart > 0) { updatedScript = updatedScript.substring(0,posStart) + getRMCapacityAPICode() + updatedScript.substring(posStart); grRMCapacityAPI.script = updatedScript; } // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grRMCapacityAPI.update(); } // Update the "TimeSheetPolicy" script include var grTimePolicy = resetAndGetObject('sys_script_include', 'api_name', 'global.TimeSheetPolicy', 'description'); if (JSUtil.notNil(grTimePolicy)) { // // SCRIPT // var updatedScript = grTimePolicy.script; // Get insert position var pos1 = updatedScript.lastIndexOf('TimeSheetPolicy'); var posStart = updatedScript.lastIndexOf('\n', pos1); if (pos1 > 0 && posStart > 0 && posStart < pos1) { updatedScript = updatedScript.substring(0,posStart) + getTimeSheetPolicyCode() + updatedScript.substring(posStart); grTimePolicy.script = updatedScript; } // var pos3 = updatedScript.indexOf('maxHoursPerWeek'); pos3 = updatedScript.indexOf('\n', pos3); var pos4 = updatedScript.indexOf('},', pos3); pos4 = updatedScript.lastIndexOf('\n', pos4); var keepCode = updatedScript.substring(pos3,pos4); if (pos3 > 0 && pos4 > pos3) { updatedScript = updatedScript.substring(0,pos3) + getTimeSheetPolicyCode2() + keepCode + getTimeSheetPolicyCode3() + updatedScript.substring(pos4); grTimePolicy.script = updatedScript; } // var pos5 = updatedScript.indexOf('maxHoursPerDay'); pos5 = updatedScript.indexOf('var maxHours = parseFloat', pos5); pos5 = updatedScript.indexOf('\n', pos5); var pos6 = updatedScript.indexOf('},', pos5); pos6 = updatedScript.lastIndexOf('\n', pos6); pos6 = updatedScript.lastIndexOf('\n', pos6-1); keepCode = updatedScript.substring(pos5,pos6); if (pos5 > 0 && pos6 > pos5) { updatedScript = updatedScript.substring(0,pos5) + getTimeSheetPolicyCode2() + keepCode + getTimeSheetPolicyCode4() + updatedScript.substring(pos6); grTimePolicy.script = updatedScript; } // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimePolicy.update(); } // Update the "TimeSheet" script include var grTimeSheet = resetAndGetObject('sys_script_include', 'api_name', 'global.TimeSheet', 'description'); if (JSUtil.notNil(grTimeSheet)) { // // SCRIPT // var updatedScript = grTimeSheet.script; // Get start position var pos1 = updatedScript.indexOf('validateMaxHours'); var pos2 = updatedScript.indexOf('gs.addErrorMessage', pos1); var pos3 = updatedScript.lastIndexOf('exceedingMaxHours', pos2); var posStart = updatedScript.lastIndexOf('\n', pos3); // Get end position var pos4 = updatedScript.lastIndexOf('exceedingMaxHours'); var posEnd = updatedScript.lastIndexOf('\n', pos4); if (posStart > 0 && posEnd > 0 && posStart < posEnd) { updatedScript = updatedScript.substring(0,posStart) + getTimeSheetCode() + updatedScript.substring(posEnd); grTimeSheet.script = updatedScript; } var pos5 = updatedScript.indexOf('validateMaxHours'); var pos6 = updatedScript.indexOf('exceedingMaxHours', pos5); pos6 = updatedScript.lastIndexOf('\n', pos6); if (pos5 > 0 && pos6 > pos5) { updatedScript = updatedScript.substring(0,pos6) + getTimeSheetCode2() + updatedScript.substring(pos6); grTimeSheet.script = updatedScript; } // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimeSheet.update(); } // Update the "TimeCardPortalService" script include var grTimePortal = resetAndGetObject('sys_script_include', 'api_name', 'global.TimeCardPortalService', 'description'); if (JSUtil.notNil(grTimePortal)) { // // SCRIPT // var updatedScript = grTimePortal.script; // Get insert position var pos1 = updatedScript.indexOf('updateTimecard'); var pos2 = updatedScript.indexOf('!timecard.update()', pos1); var posStart = updatedScript.lastIndexOf('\n', pos2); // Get second insert position var pos3 = updatedScript.indexOf('for', pos2); var pos4 = updatedScript.indexOf('responseData', pos3); var posEnd = updatedScript.lastIndexOf('\n', pos4); if (posStart > 0 && posEnd > 0 && posStart < posEnd) { updatedScript = updatedScript.substring(0,posStart) + getTimePortalCode1() + updatedScript.substring(posStart, posEnd) + getTimePortalCode2() + updatedScript.substring(posEnd); grTimePortal.script = updatedScript; } // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimePortal.update(); } // Update the "TimeCardDailies" script include var grTimeDailies = resetAndGetObject('sys_script_include', 'api_name', 'global.TimeCardDailies', 'description'); if (JSUtil.notNil(grTimeDailies)) { // // SCRIPT // var updatedScript = grTimeDailies.script; // Get insert position var pos1 = updatedScript.indexOf('this.timeCard'); var posStart1 = updatedScript.indexOf('\n', pos1); if (pos1 > 0 && posStart1 > 0 && pos1 < posStart1) { updatedScript = updatedScript.substring(0,posStart1) + getTimeDailies1() + updatedScript.substring(posStart1); } var pos2 = updatedScript.indexOf('tcd.time_worked'); var posStart2 = updatedScript.indexOf('\n', pos2); if (pos1 > 0 && posStart1 > 0 && pos1 < posStart1) { updatedScript = updatedScript.substring(0,posStart2) + getTimeDailies2() + updatedScript.substring(posStart2); } var pos3 = updatedScript.indexOf('timeCardDaily.time_worked'); var posStart3 = updatedScript.indexOf('\n', pos3); if (pos1 > 0 && posStart1 > 0 && pos1 < posStart1) { updatedScript = updatedScript.substring(0,posStart3) + getTimeDailies3() + updatedScript.substring(posStart3); } var pos4 = updatedScript.indexOf("type: 'TimeCardDailies'"); var posStart = updatedScript.lastIndexOf('\n', pos4); if (posStart > 0 && posStart < pos4) { updatedScript = updatedScript.substring(0,posStart) + getTimeDailies4() + updatedScript.substring(posStart); } grTimeDailies.script = updatedScript; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimeDailies.update(); } // Update the "Time Sheet Week Breakdown" widget var grTimeBreakdown = resetAndGetObject('sp_widget', 'id', 'timesheet-week-breakdown', 'description'); if (JSUtil.notNil(grTimeBreakdown)) { // // SCRIPT // var updatedScript = grTimeBreakdown.script; // Get insert position //var pos1 = updatedScript.toString().length; //var pos2 = updatedScript.lastIndexOf('}', pos1); //var posStart = updatedScript.lastIndexOf('\n', pos2); var pos1 = updatedScript.lastIndexOf('}'); var posStart = updatedScript.lastIndexOf('\n', pos1); if (posStart > 0 && posStart < pos1) { updatedScript = updatedScript.substring(0,posStart) + getTimeBreakdownCode() + updatedScript.substring(posStart); grTimeBreakdown.script = updatedScript; } // // TEMPLATE // var updatedTemplate = grTimeBreakdown.template; updatedTemplate = updatedTemplate.replace(/{{breakdown.hours}}/g, '{{breakdown.effortCalc}}'); updatedTemplate = updatedTemplate.replace(/{Hrs}/g, '{{{data.HrsLabel}}}'); grTimeBreakdown.template = updatedTemplate; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimeBreakdown.update(); } // Update the "Time Card Portal Main Container" widget var grTimePortalContainer = resetAndGetObject('sp_widget', 'id', 'timecard-portal-container', 'description'); if (JSUtil.notNil(grTimePortalContainer)) { // // SCRIPT // var updatedScript = grTimePortalContainer.script; // Get insert position // Add variable to store the allowed tables, set it to default tables if empty\ var pos1 = updatedScript.indexOf('taskSelector'); var pos2 = updatedScript.lastIndexOf('input', pos1); var pos3 = updatedScript.indexOf('\n', pos2); var code30 = "\ " + scriptCommentStart + "\ // Add variable to store the allowed tables, set it to default tables if empty\ var timesheetPolicy = TimeSheetPolicy.getFromUserId(data.userId);\ var selectorTables = timesheetPolicy.selectorTables();\ if (JSUtil.notNil(selectorTables)) {\ if (JSUtil.nil(options.task_selector_options))\ options.task_selector_options = {};\ options.task_selector_options.tables = selectorTables;\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,pos3) + code30 + updatedScript.substring(pos3); grTimePortalContainer.script = updatedScript; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimePortalContainer.update(); } // Update the "Time Sheet treemap" widget var grTimeTreemap = resetAndGetObject('sp_widget', 'id', 'ts-treemap', 'description'); if (JSUtil.notNil(grTimeTreemap)) { // // SCRIPT // var updatedScript = grTimeTreemap.script; // Get insert position var pos1 = updatedScript.indexOf('agg.next()'); var posStart = updatedScript.lastIndexOf('\n', pos1); if (posStart > 0 && posStart < pos1) { updatedScript = updatedScript.substring(0,posStart) + getTimeTreemapCode1() + updatedScript.substring(posStart); } var pos11 = updatedScript.indexOf('series.total'); var posStart2 = updatedScript.lastIndexOf('\n', pos11); var posEnd2 = updatedScript.indexOf('\n', pos11); if (posStart2 > 0 && posEnd2 > 0 && posStart2 < posEnd2) { updatedScript = updatedScript.substring(0,posStart2) + getTimeTreemapCode2() + updatedScript.substring(posEnd2); } var pos21 = updatedScript.indexOf('seriesObj.total'); var posStart3 = updatedScript.lastIndexOf('\n', pos21); var posEnd3 = updatedScript.indexOf('\n', pos21); if (posStart3 > 0 && posEnd3 > 0 && posStart3 < posEnd3) { updatedScript = updatedScript.substring(0,posStart3) + getTimeTreemapCode3() + updatedScript.substring(posEnd3); } grTimeTreemap.script = updatedScript; // // TEMPLATE // var updatedTemplate = grTimeTreemap.template; updatedTemplate = updatedTemplate.replace(/{{data.total_hours}} hrs/g, '{{data.effortCalc}} {{data.HrsLabel}}'); grTimeTreemap.template = updatedTemplate; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimeTreemap.update(); } // Update the "Time Card Grid" widget var grTimeGrid = resetAndGetObject('sp_widget', 'id', 'tc-grid', 'description'); if (JSUtil.notNil(grTimeGrid)) { // // BODY HTML TEMPLATE // var updatedTemplate = grTimeGrid.template; // Manage tme off column colors updatedTemplate = updatedTemplate.replace('class="flex-item {{field}}" ng-class="{\'selected\':data.highlightedField == field}"', 'class="flex-item {{field}}" style="{{data.timeOffTop[field]}}" ng-class="{\'selected\':data.highlightedField == field}"'); updatedTemplate = updatedTemplate.replace('role="cell"', 'role="cell" style="{{data.timeOff[field]}}"'); // Manage custom reference fields with rate_type and resource_plan // Place change in the missle to not loop forever // updatedTemplate = updatedTemplate.replace(/field === 'rate_type' || field === 'resource_plan'/g, "field==='rate_type' || data.customReferenceColumns.fields['field'] || field==='resource_plan'"); updatedTemplate = updatedTemplate.replace("field === 'rate_type' || field === 'resource_plan'", "field === 'rate_type' || data.customReferenceColumns.fields['field'] || field === 'resource_plan'"); updatedTemplate = updatedTemplate.replace("field === 'rate_type' || field === 'resource_plan'", "field === 'rate_type' || data.customReferenceColumns.fields['field'] || field === 'resource_plan'"); updatedTemplate = updatedTemplate.replace("field === 'rate_type' || field === 'resource_plan'", "field === 'rate_type' || data.customReferenceColumns.fields['field'] || field === 'resource_plan'"); updatedTemplate = updatedTemplate.replace("field !== 'rate_type' && field !== 'project_time_category' && field !== 'resource_plan'", "field !== 'rate_type' && field !== 'project_time_category' && field !== 'resource_plan' && !data.customReferenceColumns.fields['field']"); // Manage the edit mode only for effort fields updatedTemplate = updatedTemplate.replace('class="edit_mode"', 'ng-class="{\'edit_mode\': data.column_meta[field].effort}"'); // Record the HTML Body updates grTimeGrid.template = updatedTemplate; // // SCRIPT // var updatedScript = grTimeGrid.script; // Add the code to get the timesheetPolicy.addedColumns(); var pos10 = updatedScript.indexOf('data.defaultRateType'); var posStart10 = updatedScript.indexOf('\n', pos10); var code10 = "\ " + scriptCommentStart + "\ // Add the code to get the timesheetPolicy.addedColumns();\ data.addedColumns = timesheetPolicy.addedColumns();\ data.effortStep = timesheetPolicy.effortStep();\ data.unassignedTaskFilter = timesheetPolicy.unassignedTaskFilter();\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart10) + code10 + updatedScript.substring(posStart10); // Add the code to manage the project_time_category in the defaultHeaderFields only if the field is present in the addedColumns var pos20 = updatedScript.indexOf('defaultHeaderFields', posStart10); var posStart20 = updatedScript.lastIndexOf('\n', pos20); var posEnd20 = updatedScript.indexOf('\n', pos20); var code20 = "\ " + scriptCommentStart + "\ // Add the code to manage the project_time_category in the defaultHeaderFields only if the field is present in the addedColumns\ var defaultHeaderFields = ['task.short_description'];\ if (data.addedColumns._fieldAdded['project_time_category'])\ defaultHeaderFields.push('project_time_category');\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart20) + code20 + updatedScript.substring(posEnd20); // Add the code to remove not allowed fields from the customFields var pos30 = updatedScript.indexOf('customFields', posEnd20); var posStart30 = updatedScript.indexOf('\n', pos30); var code30 = "\ " + scriptCommentStart + "\ // Add the code to remove not allowed fields from the customFields\ /* Cleanup custom fields and only keep those allowed in time sheet policy */\ if(customFields) {\ for (var s = customFields.length-1; s >= 0; s--) {\ if (customFields[s].name == 'project_time_category' || customFields[s].name.indexOf('u_') == 0) {\ if (!data.addedColumns._fieldAdded[customFields[s].name])\ customFields.splice(s,1);\ }\ }\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart30) + code30 + updatedScript.substring(posStart30); // Add the code to manage field alignment, before the time card days var pos33 = updatedScript.indexOf('data.fields.indexOf(fieldConf.name)', posStart30); var posEnd33 = updatedScript.indexOf('\n', pos33); var code33 = "\ " + scriptCommentStart + "\ // Add the code to manage field alignment, before the time card days\ if (JSUtil.nil(fieldConf.alignment) || fieldConf.alignment == 'left')\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posEnd33) + code33 + updatedScript.substring(posEnd33); // Add the code to manage field alignment, after the time card days var pos36 = updatedScript.indexOf('field_labels', posEnd33); var posEnd36 = updatedScript.lastIndexOf('\n', pos36); var code36 = "\ " + scriptCommentStart + "\ // Add the code to manage field alignment, after the time card days\ /* Inlcude additional configured fields to the right */\ if(customFields) {\ customFields.forEach(function(fieldConf) {\ if(data.fields.indexOf(fieldConf.name)==-1 && JSUtil.notNil(fieldConf.alignment) || fieldConf.alignment == 'right') {\ data.fields.push(fieldConf.name);\ }\ });\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posEnd36) + code36 + updatedScript.substring(posEnd36); // Add the field label Total to make it translatable (to fix ITBM bug) // Add 'Total Hrs' to not conflict with the original 'Total' meaning already used elsewhere var pos370 = updatedScript.indexOf("'sunday': gs.getMessage('Sun')", posEnd33); var pos371 = updatedScript.indexOf('}', pos370); var pos372 = updatedScript.indexOf('\n', pos371); var posEnd372 = updatedScript.indexOf('\n', pos372+1); var code370 = "\ " + scriptCommentStart + "\ /* Fix ITBM bug and add Total field to be able to translate it */\ field_labels.total = gs.getMessage('Total Hrs');\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posEnd372) + code370 + updatedScript.substring(posEnd372); // Add the code to edit the project_time_category in the defaultHeaderFields only if the field is present in the addedColumns, and build the data.customReferenceColumns used in HTML+Client+Link var pos40 = updatedScript.indexOf('data.editable_fields', posEnd372); var posStart40 = updatedScript.lastIndexOf('\n', pos40); var posEnd40 = updatedScript.indexOf('\n', pos40); var code40 = "\ " + scriptCommentStart + "\ // Add the code to edit the project_time_category in the defaultHeaderFields only if the field is present in the addedColumns, and build the data.customReferenceColumns used in HTML+Client+Link\ data.editable_fields = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];\ if (data.addedColumns._fieldAdded['project_time_category'])\ data.editable_fields.push('project_time_category');\ \ /* Add custom editable fields */\ data.customReferenceColumns = {};\ data.customReferenceColumns.fields = {};\ // Don't need to add them as these fields are kept in clear in the Body HTML section\ //data.customReferenceColumns.fields['rate_type'] = true;\ //data.customReferenceColumns.fields['resource_plan'] = true;\ if(customFields) {\ customFields.forEach(function(fieldConf) {\ if (JSUtil.notNil(fieldConf.editable) && fieldConf.editable && fieldConf.name.indexOf('u_') == 0 && !data.addedColumns[fieldConf.name].readOnly && JSUtil.nil(data.addedColumns[fieldConf.name].choice)) {\ // Add edtiable field\ var fType = data.addedColumns[fieldConf.name].type;\ if (fType == 'reference' || fType == 'boolean' || fType == 'currency' || fType == 'decimal' || fType == 'glide_date' || fType == 'glide_date_time' || fType == 'string')\ data.editable_fields.push(fieldConf.name);\ // Record the custom field type\ if (data.addedColumns[fieldConf.name].type == 'reference') {\ // The reference is added to data.customReferenceColumns which is used in the Body HTML + Client script + Link sections\ data.customReferenceColumns.fields[fieldConf.name] = true;\ data.customReferenceColumns[fieldConf.name] = data.addedColumns[fieldConf.name];\ data.customReferenceColumns[fieldConf.name].display_value = fieldConf.display_value;\ fieldConf.type = data.addedColumns[fieldConf.name].type;\ } else if (fieldConf.type == 'effort' && data.addedColumns[fieldConf.name].type == 'decimal') {\ // Manage effort type only for decimal fields\ data.addedColumns[fieldConf.name].type = fieldConf.type;\ } else {\ // Other types are copied as is\ fieldConf.type = data.addedColumns[fieldConf.name].type;\ }\ }\ });\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart40) + code40 + updatedScript.substring(posEnd40); // Add the code to declare all editable fieds as effort fields var pos50 = updatedScript.indexOf('grForLabels', posEnd40); var pos51 = updatedScript.indexOf('data.editable_fields', pos50); var pos52 = updatedScript.indexOf('\n', pos51); var posStart50 = updatedScript.indexOf('\n', pos52); var pos53 = updatedScript.indexOf('}', posStart50); var posEnd50 = updatedScript.lastIndexOf('\n', pos53); var code50 = "\ " + scriptCommentStart + "\ // Add the code to declare all editable fieds as effort fields\ if(data.column_meta[data.editable_fields[i]]) {\ data.column_meta[data.editable_fields[i]]['editable'] = true;\ data.column_meta[data.editable_fields[i]]['effort'] = true;\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart50) + code50 + updatedScript.substring(posEnd50); // Add the code to set effort fields only for custom fields of type effort var pos60 = updatedScript.indexOf('data.isAllowMultipleRateTypes', posStart50); var posStart60 = updatedScript.lastIndexOf('\n', pos60); var code60 = "\ " + scriptCommentStart + "\ // Add the code to set effort fields only for custom fields of type effort\ for (var i = 0; i < customFields.length; i++){\ if (customFields[i].type != 'effort' && data.column_meta[customFields[i].name])\ data.column_meta[customFields[i].name]['effort'] = false;\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart60) + code60 + updatedScript.substring(posStart60); // Add calculation do divide the effort by person-day ratio var pos70 = updatedScript.indexOf('data.defaultField', posStart60); var posStart70 = updatedScript.lastIndexOf('\n', pos70); var code70 = "\ " + scriptCommentStart + "\ // Add calculation do divide the effort by person-day ratio\ data.decimalSeperator = GlideLocale.get().getdecimalSeparator();\ if (data.decimalSeperator == undefined) data.decimalSeperator = '.';\ data.effortRatio = 1;\ if (timesheetPolicy.managePersonDays()) {\ data.effortRatio = RMUtil.averageDailyFteForUser(data.userId);\ if (data.effortRatio != 0)\ data.effortRatio = 1 / data.effortRatio;\ }\ setListValues = function(record) {\ record.display_value = ( Math.round( record.value * data.effortRatio * 100 ) / 100 ).toString().replace('.', data.decimalSeperator);\ record.value = Math.round( record.value * data.effortRatio * 100 ) / 100;\ };\ for (var j = 0; j 0 && effortFields[field] && isValidNumber(valRound)) {\ valRound = parseFloat(valRound.replace(g_user_decimal_separator, '.'));\ valRound = Math.round( valRound / effortStep ) * effortStep;\ record[field].display_value = valRound.toString().replace('.', g_user_decimal_separator);\ }\ " + scriptCommentEnd + "\ "; clientScript = clientScript.substring(0,pocStart30) + codec30 + clientScript.substring(pocStart30); // Add code to call the script TimeCardPortalService.updateTimeCard var poc40 = clientScript.indexOf('TimeCardPortalService.updateTimeCard', pocStart30); var pocStart40 = clientScript.lastIndexOf('\n', poc40); var pocEnd40 = clientScript.indexOf('\n', poc40); var codec40 = "\ " + scriptCommentStart + "\ // Add code to call the script TimeCardPortalService.updateTimeCard\ var customEfforts = [];\ for (var key in addedColumns._fieldAdded)\ if (addedColumns[key].type == 'effort')\ customEfforts.push(addedColumns[key].name);\ TimeCardPortalService.updateTimeCard(record.sys_id, data, editabelFields.join(',')+',total', customEfforts.join('')).then(function(response){\ " + scriptCommentEnd + "\ "; clientScript = clientScript.substring(0,pocStart40) + codec40 + clientScript.substring(pocEnd40); // Record the client updates grTimeGrid.client_script = clientScript; // LINK var linkScript = grTimeGrid.link; // Add editable field labels var pol10 = linkScript.indexOf('editFieldlabels'); var pol11 = linkScript.indexOf('}', pol10); var polStart10 = linkScript.indexOf('\n', pol11); var codel10 = "\ \ " + scriptCommentStart + "\ // Add editable field labels\ var customReferenceDetails = scope.data.customReferenceColumns;\ for (var key in customReferenceDetails.fields) {\ editFieldlabels[key] = scope.data.column_meta[key]['label'];\ }\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart10) + codel10 + linkScript.substring(polStart10); // Add custom reference field and query filter before to read from server var pol40 = linkScript.indexOf('getDataHandler', polStart10); var polStart40 = linkScript.indexOf('\n', pol40); var codel40 = "\ " + scriptCommentStart + "\ // Add custom reference field and query filter before to read from server\ if (customReferenceDetails.fields[field]) {\ return function(term, page) {\ var containsTermQuery = '^' + customReferenceDetails[field].display_value + 'LIKE' + term;\ return {\ sysparm_query : customReferenceDetails[field].qualifier + containsTermQuery,\ sysparm_fields: 'sys_id,' + customReferenceDetails[field].display_value,\ sysparm_display_value: 'all',\ sysparm_limit: scope.data.pageSize,\ sysparm_offset: (page-1)*scope.data.pageSize,\ sysparm_exclude_reference_link: true\ };\ }\ }\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart40) + codel40 + linkScript.substring(polStart40); // Add custom reference display value when reading the Ajax results from the server var pol50 = linkScript.indexOf('getResultHandler', polStart40); var polStart50 = linkScript.indexOf('\n', pol50); var codel50 = '\ ' + scriptCommentStart + '\ // Add custom reference display value when reading the Ajax results from the server\ if (customReferenceDetails.fields[field]) {\ return function(data, page) {\ var more = (page * 20) < data.total;\ var responseData = [];\ if(data) {\ var items = data.items;\ responseData.push({\ "id" : "",\ "text" : scope.data.messages.none,\ "field": field,\ "sys_id": record.sys_id\ });\ for (var i = 0; i < items.length; i++) {\ responseData.push({\ "id": items[i].sys_id.display_value,\ "text": items[i][customReferenceDetails[field].display_value].display_value,\ "field": field,\ "sys_id": record.sys_id\ });\ }\ }\ return { results: responseData, more: more };\ }\ }\ ' + scriptCommentEnd + '\ '; linkScript = linkScript.substring(0,polStart50) + codel50 + linkScript.substring(polStart50); // Add ajax call to fill reference lists var pol20 = linkScript.indexOf('_initializeFieldSelect2', polStart10); var pol21 = linkScript.indexOf('resource_plan', pol20); var pol22 = linkScript.indexOf('time_card', pol21); var polStart20 = linkScript.indexOf('\n', pol22); var codel20 = "\ " + scriptCommentStart + "\ // Add ajax call to fill reference lists\ else if (customReferenceDetails.fields[field]) {\ url += \"table/\" + customReferenceDetails[field].reference;\ //if (customReferenceDetails[field].qualifier != '')\ // url += \"?sysparm_query=\" + customReferenceDetails[field].qualifier;\ }\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart20) + codel20 + linkScript.substring(polStart20); // Add call calling the function which fills the reference lists var pol30 = linkScript.indexOf('scope.enableEditMode', polStart20); var pol31 = linkScript.indexOf('$timeout', pol30); var pol32 = linkScript.indexOf('isProjectWork', pol31); var polStart30 = linkScript.lastIndexOf('\n', pol32); var codel30 = "\ " + scriptCommentStart + "\ // Add call calling the function which fills the reference lists\ for (var key in customReferenceDetails.fields)\ _initializeFieldSelect2(key, closestElem, evt, record, scope.data.messages['select'+key]);\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart30) + codel30 + linkScript.substring(polStart30); // Add the unassigned task filter from timesheet policy - 1 var po70 = linkScript.indexOf('modalInstance.rendered.then', pol32); var polStart70 = linkScript.indexOf('\n', po70); var code70 = "\ " + scriptCommentStart + "\ // Time sheet policy unassigned task filter\ var policyTaskFilter = scope.data.unassignedTaskFilter;\ if ((policyTaskFilter == null) || (typeof policyTaskFilter == 'undefined') || ('' == '' + policyTaskFilter))\ policyTaskFilter = \"'short_descriptionCONTAINS' + term + '^ORnumberCONTAINS'+ term\";\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart70) + code70 + linkScript.substring(polStart70); // Add the unassigned task filter from timesheet policy - 2 var po80 = linkScript.indexOf('return', polStart70); var polStart80 = linkScript.lastIndexOf('\n', po80); var code80 = "\ " + scriptCommentStart + "\ // Time sheet policy unassigned task filter\ var encodedQueryFunction = Function('\"use strict\";return(function(term, page, scope){return ' + policyTaskFilter + ';})')();\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart80) + code80 + linkScript.substring(polStart80); // Add the unassigned task filter from timesheet policy - 3 var po90 = linkScript.indexOf('sysparm_query', polStart80); var polStart90 = linkScript.lastIndexOf('\n', po90); var polEnd90 = linkScript.indexOf('\n', po90); var code90 = "\ " + scriptCommentStart + "\ // Time sheet policy unassigned task filter\ sysparm_query: encodedQueryFunction(term, page, scope),\ " + scriptCommentEnd + "\ "; linkScript = linkScript.substring(0,polStart90) + code90 + linkScript.substring(polEnd90); // Record the link updates grTimeGrid.link = linkScript; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimeGrid.update(); } // Update the "TimeCardPortalService" angular provider var grTimePortalService = resetAndGetObject('sp_angular_provider', 'name', 'TimeCardPortalService'); if (JSUtil.notNil(grTimePortalService)) { // script var updatedScript = grTimePortalService.script; // Get insert position var pos1 = updatedScript.indexOf('updateTimeCard'); var posStart1 = updatedScript.lastIndexOf('\n', pos1); var posEnd1 = updatedScript.indexOf('\n', pos1); if (pos1 > 0 && posStart1 > 0 && posStart1 < posEnd1) { updatedScript = updatedScript.substring(0,posStart1) + getTimePortalServiceCode1() + updatedScript.substring(posEnd1); } var pos2 = updatedScript.indexOf('updateTimecard'); var posStart2 = updatedScript.indexOf('\n', pos2); if (pos2 > 0 && posStart2 > 0 && posStart2 > pos2) { updatedScript = updatedScript.substring(0,posStart2) + getTimePortalServiceCode2() + updatedScript.substring(posStart2); } grTimePortalService.script = updatedScript; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimePortalService.update(); } // Update the "Time Card Portal - Task Selector" widget var grTimeTaskSelector = resetAndGetObject('sp_widget', 'id', 'timecard-task-selector', 'description'); if (JSUtil.notNil(grTimeTaskSelector)) { // // SCRIPT // var updatedScript = grTimeTaskSelector.script; // Add the code to add secondary fields to cards var pos20 = updatedScript.indexOf('data.ids'); var posStart20 = updatedScript.lastIndexOf('\n', pos20); var code20 = "\ " + scriptCommentStart + "\ // Add secondary fields to cards\ var addedFields = options.addedSecondaryFields;\ if (JSUtil.notNil(addedFields)) {\ for (var cardTable in addedFields) {\ // Make a copy for pm_project_task\ data.layout[cardTable] = JSON.parse(JSON.stringify(data.layout));\ // Add fields\ for (var f = 0; f < addedFields[cardTable].length; f++) {\ data.layout[cardTable].secondary_fields.splice(addedFields[cardTable][f].index, 0, addedFields[cardTable][f].field);\ }\ }\ }\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart20) + code20 + updatedScript.substring(posStart20); // Add the code to filter selected categories, and to get selector table (not sure if we need it, added to be sure) var pos10 = updatedScript.indexOf('choiceList.removeNone'); var pos11 = updatedScript.indexOf('}', pos10); var posStart10 = updatedScript.indexOf('\n', pos11); var code10 = '\n\ ' + scriptCommentStart + '\ // Get allowed task selector tables\ data.selectorTables = timesheetPolicy.selectorTables();\ // Get allowed categories and filter the list\ var policyCategories = timesheetPolicy.categories();\ if (Object.keys(policyCategories).length > 0) {\ data.categories = data.categories.filter(function(category) {\ return policyCategories[category.value];\ });\ }\ ' + scriptCommentEnd + '\ '; updatedScript = updatedScript.substring(0,posStart10) + code10 + updatedScript.substring(posStart10); // Add the code to filter allowed tables - part 1 // (not sure if we need it, added to be sure) var pos30 = updatedScript.indexOf('sp.getRecordElements'); var pos31 = updatedScript.indexOf('var labelGr', pos30); var posStart30 = updatedScript.indexOf('\n', pos31); var code30 = "\n\ " + scriptCommentStart + "\ // Filter allowed task selector tables\ var selectorIsNull = ((data.selectorTables == null) || (typeof data.selectorTables == 'undefined') || ('' == '' + data.selectorTables));\ if (selectorIsNull || data.selectorTables.indexOf(record.sys_class_name) >=0) {\ " + scriptCommentEnd + "\ "; updatedScript = updatedScript.substring(0,posStart30) + code30 + updatedScript.substring(posStart30); // Add the code to filter allowed tables - part 2 // (not sure if we need it, added to be sure) var pos40 = updatedScript.indexOf('data.taskFilters', posStart30); var pos41 = updatedScript.indexOf('count++', pos40); var pos42 = updatedScript.indexOf('}', pos41); var posStart40 = updatedScript.indexOf('\n', pos42); var code40 = '\n\ ' + scriptCommentStart + '\ // end - Filter allowed task selector tables\ }\ ' + scriptCommentEnd + '\ '; updatedScript = updatedScript.substring(0,posStart40) + code40 + updatedScript.substring(posStart40); // Record script updates grTimeTaskSelector.script = updatedScript; // Sleep a while to give enough time to the update set versions to be recorded in the proper chronological order gs.sleep(1000); grTimeTaskSelector.update(); } }; enablePersonDays();