From 772bebede53ce75f4cc8682c8502920610d1d66f Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Mon, 9 Apr 2018 16:46:57 +0200 Subject: [PATCH 1/8] [MOV] project,hr_timesheet: move project_time_mode_id field 'project_time_mode_id' is the field used to determined the UoM to use when logging timesheets. It is not used in project alone, but in timesheet. Moving it make sense, and does not cause any problems. For now, no UI allow the end user to change it: it is set with the Hours UoM (hardcoded). This is a technical move to prepare the feature that will allow people timesheeting in Days or Hours. Task #39079 --- addons/hr_timesheet/models/__init__.py | 1 + addons/hr_timesheet/models/project.py | 42 +++++++++++++++++++ .../models/res_company.py | 0 .../models/res_config_settings.py | 5 +++ .../views/res_config_settings_views.xml | 11 +++++ addons/project/models/__init__.py | 1 - addons/project/models/project.py | 42 ------------------- addons/project/models/res_config_settings.py | 5 --- .../views/res_config_settings_views.xml | 2 +- 9 files changed, 60 insertions(+), 49 deletions(-) rename addons/{project => hr_timesheet}/models/res_company.py (100%) diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py index b8bfb2333b9..4b66a96b171 100644 --- a/addons/hr_timesheet/models/__init__.py +++ b/addons/hr_timesheet/models/__init__.py @@ -4,6 +4,7 @@ from . import analytic_account from . import hr from . import hr_timesheet +from . import res_company from . import res_config_settings from . import project from . import res_users diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 7976df1defb..ed555772fb3 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from lxml import etree + from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError @@ -151,3 +153,43 @@ class Task(models.Model): 'company_id': account_id.company_id.id, }) return result + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + # read uom as admin to avoid access rights issues, e.g. for portal/share users, + # this should be safe (no context passed to avoid side-effects) + obj_tm = self.env.user.company_id.project_time_mode_id + tm = obj_tm and obj_tm.name or 'Hours' + + res = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + + # read uom as admin to avoid access rights issues, e.g. for portal/share users, + # this should be safe (no context passed to avoid side-effects) + obj_tm = self.env.user.company_id.project_time_mode_id + # using get_object to get translation value + uom_hour = self.env.ref('uom.product_uom_hour', False) + if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id: + return res + + eview = etree.fromstring(res['arch']) + + # if the project_time_mode_id is not in hours (so in days), display it as a float field + def _check_rec(eview): + if eview.attrib.get('widget', '') == 'float_time': + eview.set('widget', 'float') + for child in eview: + _check_rec(child) + return True + + _check_rec(eview) + + res['arch'] = etree.tostring(eview, encoding='unicode') + + # replace reference of 'Hours' to 'Day(s)' + for f in res['fields']: + # TODO this NOT work in different language than english + # the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days' + # but string 'Initially Planned Days' is not available in translation + if 'Hours' in res['fields'][f]['string']: + res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name) + return res diff --git a/addons/project/models/res_company.py b/addons/hr_timesheet/models/res_company.py similarity index 100% rename from addons/project/models/res_company.py rename to addons/hr_timesheet/models/res_company.py diff --git a/addons/hr_timesheet/models/res_config_settings.py b/addons/hr_timesheet/models/res_config_settings.py index 7200bc5c21f..164b23b4f71 100644 --- a/addons/hr_timesheet/models/res_config_settings.py +++ b/addons/hr_timesheet/models/res_config_settings.py @@ -9,3 +9,8 @@ class ResConfigSettings(models.TransientModel): module_project_timesheet_synchro = fields.Boolean("Awesome Timesheet") module_project_timesheet_holidays = fields.Boolean("Leaves") + project_time_mode_id = fields.Many2one( + 'uom.uom', related='company_id.project_time_mode_id', string='Project Time Unit', + help="This will set the unit of measure used in projects and tasks.\n" + "If you use the timesheet linked to projects, don't " + "forget to setup the right unit of measure in your employees.") diff --git a/addons/hr_timesheet/views/res_config_settings_views.xml b/addons/hr_timesheet/views/res_config_settings_views.xml index 19d21fac97c..39f98a5374d 100644 --- a/addons/hr_timesheet/views/res_config_settings_views.xml +++ b/addons/hr_timesheet/views/res_config_settings_views.xml @@ -13,6 +13,17 @@

Timesheet Apps

+
+
+
+
diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index 081d29b8d4e..5a94820d26e 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -3,7 +3,6 @@ from . import project from . import res_config_settings -from . import res_company from . import res_partner from . import res_users from . import digest diff --git a/addons/project/models/project.py b/addons/project/models/project.py index c5c5370d78a..bddeff7d218 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import timedelta -from lxml import etree from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import UserError, AccessError, ValidationError @@ -620,47 +619,6 @@ class Task(models.Model): if not task._check_recursion(): raise ValidationError(_('Error! You cannot create recursive hierarchy of task(s).')) - # Override view according to the company definition - @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - # read uom as admin to avoid access rights issues, e.g. for portal/share users, - # this should be safe (no context passed to avoid side-effects) - obj_tm = self.env.user.company_id.project_time_mode_id - tm = obj_tm and obj_tm.name or 'Hours' - - res = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - - # read uom as admin to avoid access rights issues, e.g. for portal/share users, - # this should be safe (no context passed to avoid side-effects) - obj_tm = self.env.user.company_id.project_time_mode_id - # using get_object to get translation value - uom_hour = self.env.ref('uom.product_uom_hour', False) - if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id: - return res - - eview = etree.fromstring(res['arch']) - - # if the project_time_mode_id is not in hours (so in days), display it as a float field - def _check_rec(eview): - if eview.attrib.get('widget', '') == 'float_time': - eview.set('widget', 'float') - for child in eview: - _check_rec(child) - return True - - _check_rec(eview) - - res['arch'] = etree.tostring(eview, encoding='unicode') - - # replace reference of 'Hours' to 'Day(s)' - for f in res['fields']: - # TODO this NOT work in different language than english - # the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days' - # but string 'Initially Planned Days' is not available in translation - if 'Hours' in res['fields'][f]['string']: - res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name) - return res - @api.model def get_empty_list_help(self, help): tname = _("task") diff --git a/addons/project/models/res_config_settings.py b/addons/project/models/res_config_settings.py index bf8f890d968..c6815aaae90 100644 --- a/addons/project/models/res_config_settings.py +++ b/addons/project/models/res_config_settings.py @@ -10,8 +10,3 @@ class ResConfigSettings(models.TransientModel): module_project_forecast = fields.Boolean(string="Forecasts") group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project") group_project_rating = fields.Boolean("Use Rating on Project", implied_group='project.group_project_rating') - project_time_mode_id = fields.Many2one( - 'uom.uom', related='company_id.project_time_mode_id', string='Project Time Unit', - help="This will set the unit of measure used in projects and tasks.\n" - "If you use the timesheet linked to projects, don't " - "forget to setup the right unit of measure in your employees.") diff --git a/addons/project/views/res_config_settings_views.xml b/addons/project/views/res_config_settings_views.xml index da472468e96..d052efeac6f 100644 --- a/addons/project/views/res_config_settings_views.xml +++ b/addons/project/views/res_config_settings_views.xml @@ -50,7 +50,7 @@

Time Management

-
+
From 6b4062ea63c628c81caecfca21968820b17d0573 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Mon, 9 Apr 2018 16:50:28 +0200 Subject: [PATCH 2/8] [IMP] project,hr_timesheet: module dependencies For historical reason, project depended on product. Probably to use UoM. Now, UoM is extracted in 'uom' module, and project does not use UoM anymore (only with timesheet). Removing the 'product' dependency requires to make hr_timesheet depending of uom, since this one really use UoM mecanism. Task #39079 --- addons/hr_timesheet/__manifest__.py | 2 +- addons/hr_timesheet/security/ir.model.access.csv | 2 -- addons/project/__manifest__.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index cac0188cc03..401608701bc 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -20,7 +20,7 @@ It is completely integrated with the cost accounting module. It allows you to se up a management by affair. """, 'website': 'https://www.odoo.com/page/timesheet-mobile-app', - 'depends': ['hr', 'analytic', 'project'], + 'depends': ['hr', 'analytic', 'project', 'uom'], 'data': [ 'security/hr_timesheet_security.xml', 'security/ir.model.access.csv', diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index 63bc0cace28..c6776df9364 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -6,8 +6,6 @@ access_account_analytic_account_user,account.analytic.account,analytic.model_acc access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1 access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0 access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 -access_product_product_user,product.product.timesheet.user,product.model_product_product,hr_timesheet.group_hr_timesheet_user,1,1,1,0 -access_product_template_timesheet_user,product.template.timesheet.user,product.model_product_template,hr_timesheet.group_hr_timesheet_user,1,1,1,0 access_uom_uom_hr_timesheet,uom.uom.timesheet.user,uom.model_uom_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_project,project.project.timesheet.user,model_project_project,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_task,project.task.timesheet.user,model_project_task,hr_timesheet.group_hr_timesheet_user,1,1,0,0 diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 43c81ed1cae..a2d56f1624f 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -10,7 +10,6 @@ 'summary': 'Organize and schedule your projects ', 'depends': [ 'base_setup', - 'product', 'mail', 'portal', 'rating', From 1f72644c445826ae7263e9b0f4737ffab99a778e Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Wed, 9 May 2018 09:40:02 +0200 Subject: [PATCH 3/8] [IMP] web: float_factor and float_toggle widgets This commit provides new js widgets for list, kanban and form views on float field. 1/ 'float_factor' displays the normal float field, but takes a conversion factor as option. The displays value is the normal one multiplied by the factor. You can use the widget like 2/ 'float_toggle' displays a button (in edit mode) looping on a range of given value at each click. A conversion factor is optional (default will be 1) to display a converted value, but sent to non converted one to the server. You can use the widget like 'range' and 'factor' are put in the options. In simple read mode, the widget display the value as a traditionnal text (like a normal char field). This commit provides tests and documentation for those 2 widgets. Task #39079 --- addons/web/static/src/js/core/utils.js | 20 +++ .../web/static/src/js/fields/basic_fields.js | 140 +++++++++++++++++- .../static/src/js/fields/field_registry.js | 2 + .../web/static/src/js/fields/field_utils.js | 29 ++++ addons/web/static/src/scss/fields.scss | 5 + .../static/tests/fields/basic_fields_tests.js | 84 +++++++++++ doc/reference/javascript_reference.rst | 20 +++ 7 files changed, 299 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index 3068a6528c9..f4031fccec9 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -511,6 +511,26 @@ var utils = { return Object.freeze(obj); }, + /** + * Find the closest value of the given one in the provided array + * + * @param {Number} num + * @param {Array} arr + * @returns {Number|undefined} + */ + closestNumber: function (num, arr) { + var curr = arr[0]; + var diff = Math.abs (num - curr); + for (var val = 0; val < arr.length; val++) { + var newdiff = Math.abs (num - arr[val]); + if (newdiff < diff) { + diff = newdiff; + curr = arr[val]; + } + } + return curr; + }, + }; return utils; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index f209925f37c..399647d1dbe 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -898,6 +898,142 @@ var FieldFloatTime = FieldFloat.extend({ formatType: 'float_time', }); +var FieldFloatFactor = FieldFloat.extend({ + supportedFieldTypes: ['float'], + className: 'o_field_float_factor', + formatType: 'float_factor', + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + // default values + if (!this.nodeOptions.factor){ + this.nodeOptions.factor = 1; + } + // use as format and parse options + this.parseOptions = this.nodeOptions; + } +}); + +/** + * The goal of this widget is to replace the input field by a button containing a + * range of possible values (given in the options). Each click allows the user to loop + * in the range. The purpose here is to restrict the field value to a predefined selection. + * Also, the widget support the factor conversion as the *float_factor* widget (Range values + * should be the result of the conversion). + **/ +var FieldFloatToggle = AbstractField.extend({ + supportedFieldTypes: ['float'], + formatType: 'float_factor', + className: 'o_field_float_toggle', + tagName: 'span', + events: { + click: '_onClick' + }, + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + + this.formatType = 'float_factor'; + + if (this.mode === 'edit') { + this.tagName = 'button'; + } + + // we don't inherit Float Field + if (this.attrs.digits) { + this.nodeOptions.digits = JSON.parse(this.attrs.digits); + } + // default values + if (!this.nodeOptions.factor){ + this.nodeOptions.factor = 1; + } + if (!this.nodeOptions.range){ + this.nodeOptions.range = [0.0, 0.5, 1.0]; + } + + // use as format and parse options + this.parseOptions = this.nodeOptions; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Get the display value but in real type to use it in calculations + * + * @private + * @returns {float} The current formatted value + */ + _getDisplayedValue: function () { + return parseFloat(this._formatValue(this.value)); + }, + /** + * Formats the HTML input tag for edit mode and stores selection status. + * + * @override + * @private + */ + _renderEdit: function () { + // Keep a reference to the input so $el can become something else + // without losing track of the actual input. + this.$el.text(this._formatValue(this.value)); + }, + /** + * Resets the content to the formated value in readonly mode. + * + * @override + * @private + */ + _renderReadonly: function () { + this.$el.text(this._formatValue(this.value)); + }, + /** + * Get the next value in the range, from the current one. If the current + * one is not in the range, the next value of the closest one will be chosen. + * + * @private + * @returns {number} The next formatted value in the range + */ + _nextValue: function () { + var range = this.nodeOptions.range; + var val = utils.closestNumber(this._getDisplayedValue(), range); + var index = _.indexOf(range, val); + if (index !== -1) { + if (index + 1 < range.length) { + return range[index + 1]; + } + } + return range[0]; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Clicking on the button triggers the change of value; the next one of + * the range will be displayed. + * + * @private + * @param {OdooEvent} ev + */ + _onClick: function(ev) { + if (this.mode === 'edit') { + ev.stopPropagation(); // only stop propagation in edit mode + var next_val = this._nextValue(); + this._setValue(next_val.toString()); // will be parsed in _setValue + } + }, + +}); + var FieldPercentage = FieldFloat.extend({ formatType:'percentage', }); @@ -1102,7 +1238,7 @@ var CopyClipboard = { $clipboardBtn.tooltip({title: _t('Copied !'), trigger: 'manual', placement: 'right'}); this.clipboard = new ClipboardJS($clipboardBtn[0], { text: function (_) { - return self.value.trim(); + return self.value.trim(); }, // Container added because of Bootstrap modal that give the focus to another element. // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc) @@ -2757,6 +2893,8 @@ return { FieldDomain: FieldDomain, FieldFloat: FieldFloat, FieldFloatTime: FieldFloatTime, + FieldFloatFactor: FieldFloatFactor, + FieldFloatToggle: FieldFloatToggle, FieldPercentage : FieldPercentage, FieldInteger: FieldInteger, FieldMonetary: FieldMonetary, diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index 20af2a01064..df8ffc9dd5b 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -53,6 +53,8 @@ registry .add('statinfo', basic_fields.StatInfo) .add('percentpie', basic_fields.FieldPercentPie) .add('float_time', basic_fields.FieldFloatTime) + .add('float_factor', basic_fields.FieldFloatFactor) + .add('float_toggle', basic_fields.FieldFloatToggle) .add('progressbar', basic_fields.FieldProgressBar) .add('toggle_button', basic_fields.FieldToggleBoolean) .add('dashboard_graph', basic_fields.JournalDashboardGraph) diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index 989f99c5fef..0e9dc063834 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -173,6 +173,21 @@ function formatFloat(value, field, options) { return formatted.join(l10n.decimal_point); } + +/** + * Returns a string representing a float value, from a float converted with a + * factor. + * + * @param {number} value + * @param {number} [options.factor] + * Conversion factor, default value is 1.0 + * @returns {string} + */ +function formatFloatFactor(value, field, options) { + var factor = options.factor || 1; + return formatFloat(value * factor, field, options); +} + /** * Returns a string representing a time value, from a float. The idea is that * we sometimes want to display something like 1:45 instead of 1.75, or 0:15 @@ -528,6 +543,18 @@ function parseMonetary(value, field, options) { return parseFloat(values[0] === currency.symbol ? values[1] : values[0]); } +/** + * Parse a String containing float and unconvert it with a conversion factor + * + * @param {number} [options.factor] + * Conversion factor, default value is 1.0 + */ +function parseFloatFactor(value, field, options) { + var parsed = parseFloat(value); + var factor = options.factor || 1.0; + return parsed / factor; +} + function parseFloatTime(value) { var factor = 1; if (value[0] === '-') { @@ -596,6 +623,7 @@ return { date: formatDate, datetime: formatDateTime, float: formatFloat, + float_factor: formatFloatFactor, float_time: formatFloatTime, html: _.identity, // todo integer: formatInteger, @@ -615,6 +643,7 @@ return { date: parseDate, // todo datetime: parseDateTime, // todo float: parseFloat, + float_factor: parseFloatFactor, float_time: parseFloatTime, html: _.identity, // todo integer: parseInteger, diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss index 5f14493c613..9a1e904fdc2 100644 --- a/addons/web/static/src/scss/fields.scss +++ b/addons/web/static/src/scss/fields.scss @@ -429,3 +429,8 @@ } } + +button.o_field_float_toggle { + width: 100%; + text-align: center; +} diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 030d173dbeb..1855b032bf3 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -3264,6 +3264,90 @@ QUnit.module('basic_fields', { }); + QUnit.module('FieldFloatFactor'); + + QUnit.test('float_factor field in form view', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 16.4 / 2 = 8.2 + assert.strictEqual(args.args[1].qux, 4.6, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 5, + }); + assert.strictEqual(form.$('.o_field_widget').first().text(), '4.55', // 9.1 / 0.5 + 'The formatted value should be displayed properly.'); + + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('input').val(), '4.55', + 'The value should be rendered correctly in the input.'); + + form.$('input').val('2.3').trigger('input'); + + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('.o_field_widget').first().text(), '2.30', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + QUnit.module('FieldFloatToggle'); + + QUnit.test('float_toggle field in form view', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 1.000 / 0.125 = 8 + assert.strictEqual(args.args[1].qux, 8, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + }); + assert.strictEqual(form.$('.o_field_widget').first().text(), '0.056', + 'The formatted time value should be displayed properly.'); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('button.o_field_float_toggle').text(), '0.056', + 'The value should be rendered correctly on the button.'); + + form.$('button.o_field_float_toggle').click(); // clicking will make the next value 1, since 0 was the closest of 0.056 + + assert.strictEqual(form.$('button.o_field_float_toggle').text(), '1.000', + 'The value should be rendered correctly on the button.'); + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_field_widget').first().text(), '1.000', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + QUnit.module('PhoneWidget'); QUnit.test('phone field in form view on extra small screens', function (assert) { diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 6cbfd3563dd..14647955afa 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -1418,6 +1418,26 @@ order. - Supported field types: *float* +- float_factor (FieldFloatFactor) + This widget aims to display properly a float value that converted using a factor + given in its options. So, for example, the value saved in database is 0.5 and the + factor is 3, the widget value should be formatted as 1.5. + + - Supported field types: *float* + +- float_toggle (FieldFloatToggle) + The goal of this widget is to replace the input field by a button containing a + range of possible values (given in the options). Each click allows the user to loop + in the range. The purpose here is to restrict the field value to a predefined selection. + Also, the widget support the factor conversion as the *float_factor* widget (Range values + should be the result of the conversion). + + - Supported field types: *float* + + .. code-block:: xml + + + - boolean (FieldBoolean) This is the default field type for fields of type *boolean*. From 70941ab9b1386549925b612ed00f8712b087e010 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Wed, 27 Jun 2018 14:58:30 +0200 Subject: [PATCH 4/8] [IMP] hr_timesheet: force timesheet view for action When coming from the employee form view, the user can see the timesheet. This commit forces the views to use in this action. Task #39079 --- addons/hr_timesheet/views/hr_views.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/addons/hr_timesheet/views/hr_views.xml b/addons/hr_timesheet/views/hr_views.xml index b8c94984d69..630e45bcd5f 100644 --- a/addons/hr_timesheet/views/hr_views.xml +++ b/addons/hr_timesheet/views/hr_views.xml @@ -13,6 +13,20 @@ } + + + tree + + + + + + + form + + + + hr.employee.form.timesheet hr.employee From 55ddb7aa66c84d3abc2a0f5067eacd50c665bf23 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Mon, 28 May 2018 17:04:15 +0200 Subject: [PATCH 5/8] [IMP] hr_timesheet: float_time widget for pivot view As timesheet duration (unit_amount) is already stored in hours in database, the pivot reporting view should use the float_time widget, even when encoding in days. Task #39079 --- addons/hr_timesheet/views/hr_timesheet_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index b6522f559e7..ed87b26d582 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -70,7 +70,7 @@ - +
From 3592dbd03508a233a0e7e6b23131f9f544f1e001 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Mon, 30 Jul 2018 08:38:03 +0200 Subject: [PATCH 6/8] [IMP] uom: prevent time uom deletion Time UoM are almost Master Data for timesheet modules. Some user remove those before installing timesheet modules, making it impossible and crashing (as some field required a time UoM to be set at installation). Since we don't want to hardcode xmlids of 'Hours' and 'Days' time UoM, we rather prevent the deletion of such UoM in the system. The UoM can be desactivate to avoid having plenty of them in system in use. Task #39079 --- addons/uom/models/uom_uom.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/addons/uom/models/uom_uom.py b/addons/uom/models/uom_uom.py index 151ae10aa47..41c2271da0d 100644 --- a/addons/uom/models/uom_uom.py +++ b/addons/uom/models/uom_uom.py @@ -22,6 +22,12 @@ class UoMCategory(models.Model): ('uom_category_unique_type', 'UNIQUE(measure_type)', 'You can have only one category per measurement type.'), ] + @api.multi + def unlink(self): + if self.filtered(lambda categ: categ.measure_type == 'time'): + raise UserError(_("You cannot delete this UoM Category as it is used by the system.")) + return super(UoMCategory, self).unlink() + class UoM(models.Model): _name = 'uom.uom' @@ -102,6 +108,12 @@ class UoM(models.Model): values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0 return super(UoM, self).write(values) + @api.multi + def unlink(self): + if self.filtered(lambda uom: uom.measure_type == 'time'): + raise UserError(_("You cannot delete this UoM as it is used by the system. You should rather archive it.")) + return super(UoM, self).unlink() + @api.model def name_create(self, name): """ The UoM category and factor are required, so we'll have to add temporary values From 83820a898f972f0addaf5ee579718d9601e6a35b Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Wed, 9 May 2018 16:04:56 +0200 Subject: [PATCH 7/8] [IMP] hr_timesheet: encoding in any UoM This commit provides a mecanism to allow user to log their timesheets using any time UoM they want. Everything will still be stored in hours in the database (in the UoM selected with project_mode_time_id field of the company). The chosen encoding UoM will provide a widget to help the encoding (float_toggle for Days, float_time for Hours, ...). The widget will use the UoM rate conversion. Task #39079 --- addons/hr_timesheet/__manifest__.py | 1 + .../hr_timesheet/data/hr_timesheet_data.xml | 12 ++- addons/hr_timesheet/models/__init__.py | 2 + addons/hr_timesheet/models/hr_timesheet.py | 11 +++ addons/hr_timesheet/models/ir_http.py | 21 +++++ addons/hr_timesheet/models/project.py | 42 --------- addons/hr_timesheet/models/res_company.py | 13 +++ .../models/res_config_settings.py | 4 + addons/hr_timesheet/models/uom.py | 10 +++ .../static/src/js/timesheet_uom.js | 86 +++++++++++++++++++ addons/hr_timesheet/views/assets.xml | 8 ++ .../hr_timesheet/views/hr_timesheet_views.xml | 16 ++-- addons/hr_timesheet/views/project_views.xml | 2 +- .../views/res_config_settings_views.xml | 18 +++- 14 files changed, 194 insertions(+), 52 deletions(-) create mode 100644 addons/hr_timesheet/models/ir_http.py create mode 100644 addons/hr_timesheet/models/uom.py create mode 100644 addons/hr_timesheet/static/src/js/timesheet_uom.js create mode 100644 addons/hr_timesheet/views/assets.xml diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index 401608701bc..520b5470cdd 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -24,6 +24,7 @@ up a management by affair. 'data': [ 'security/hr_timesheet_security.xml', 'security/ir.model.access.csv', + 'views/assets.xml', 'views/hr_timesheet_views.xml', 'views/res_config_settings_views.xml', 'views/project_views.xml', diff --git a/addons/hr_timesheet/data/hr_timesheet_data.xml b/addons/hr_timesheet/data/hr_timesheet_data.xml index 303736ccca8..594c0ca9745 100644 --- a/addons/hr_timesheet/data/hr_timesheet_data.xml +++ b/addons/hr_timesheet/data/hr_timesheet_data.xml @@ -8,9 +8,19 @@ + + + float_toggle + + + + float_time + + + - \ No newline at end of file + diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py index 4b66a96b171..f6758089f5c 100644 --- a/addons/hr_timesheet/models/__init__.py +++ b/addons/hr_timesheet/models/__init__.py @@ -4,7 +4,9 @@ from . import analytic_account from . import hr from . import hr_timesheet +from . import ir_http from . import res_company from . import res_config_settings from . import project from . import res_users +from . import uom diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index f46a5464015..da01b442fad 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from lxml import etree +import json + from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -46,6 +49,10 @@ class AccountAnalyticLine(models.Model): for line in self: line.department_id = line.employee_id.department_id + # ---------------------------------------------------- + # ORM overrides + # ---------------------------------------------------- + @api.model def create(self, values): # compute employee only for timesheet lines, makes no sense for other lines @@ -70,6 +77,10 @@ class AccountAnalyticLine(models.Model): self.filtered(lambda t: t.project_id)._timesheet_postprocess(values) return result + # ---------------------------------------------------- + # Business Methods + # ---------------------------------------------------- + def _timesheet_preprocess(self, vals): """ Deduce other field values from the one given. Overrride this to compute on the fly some field that can not be computed fields. diff --git a/addons/hr_timesheet/models/ir_http.py b/addons/hr_timesheet/models/ir_http.py new file mode 100644 index 00000000000..a0694d71d1b --- /dev/null +++ b/addons/hr_timesheet/models/ir_http.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Http(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + """ The widget 'timesheet_uom' needs to know which UoM conversion factor and which javascript + widget to apply, depending on th ecurrent company. + """ + result = super(Http, self).session_info() + + company = self.env.user.company_id + encoding_uom = company.timesheet_encode_uom_id + + result['timesheet_uom'] = encoding_uom.read(['name', 'rounding', 'timesheet_widget'])[0] + result['timesheet_uom_factor'] = company.project_time_mode_id._compute_quantity(1.0, encoding_uom, round=False) # convert encoding uom into stored uom to get conversion factor + return result diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index ed555772fb3..7976df1defb 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from lxml import etree - from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError @@ -153,43 +151,3 @@ class Task(models.Model): 'company_id': account_id.company_id.id, }) return result - - @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - # read uom as admin to avoid access rights issues, e.g. for portal/share users, - # this should be safe (no context passed to avoid side-effects) - obj_tm = self.env.user.company_id.project_time_mode_id - tm = obj_tm and obj_tm.name or 'Hours' - - res = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - - # read uom as admin to avoid access rights issues, e.g. for portal/share users, - # this should be safe (no context passed to avoid side-effects) - obj_tm = self.env.user.company_id.project_time_mode_id - # using get_object to get translation value - uom_hour = self.env.ref('uom.product_uom_hour', False) - if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id: - return res - - eview = etree.fromstring(res['arch']) - - # if the project_time_mode_id is not in hours (so in days), display it as a float field - def _check_rec(eview): - if eview.attrib.get('widget', '') == 'float_time': - eview.set('widget', 'float') - for child in eview: - _check_rec(child) - return True - - _check_rec(eview) - - res['arch'] = etree.tostring(eview, encoding='unicode') - - # replace reference of 'Hours' to 'Day(s)' - for f in res['fields']: - # TODO this NOT work in different language than english - # the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days' - # but string 'Initially Planned Days' is not available in translation - if 'Hours' in res['fields'][f]['string']: - res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name) - return res diff --git a/addons/hr_timesheet/models/res_company.py b/addons/hr_timesheet/models/res_company.py index afad6d1b54e..2c77e7e83cf 100644 --- a/addons/hr_timesheet/models/res_company.py +++ b/addons/hr_timesheet/models/res_company.py @@ -16,8 +16,21 @@ class ResCompany(models.Model): uom = self.env['uom.uom'].search([('measure_type', '=', 'time')], limit=1) return uom + @api.model + def _default_timesheet_encode_uom_id(self): + uom = self.env.ref('uom.product_uom_hour', raise_if_not_found=False) + if not uom: + uom = self.env['uom.uom'].search([('measure_type', '=', 'time'), ('uom_type', '=', 'reference')], limit=1) + if not uom: + uom = self.env['uom.uom'].search([('measure_type', '=', 'time')], limit=1) + return uom + project_time_mode_id = fields.Many2one('uom.uom', string='Project Time Unit', default=_default_project_time_mode_id, domain=[('measure_type', '=', 'time')], help="This will set the unit of measure used in projects and tasks.\n" "If you use the timesheet linked to projects, don't " "forget to setup the right unit of measure in your employees.") + timesheet_encode_uom_id = fields.Many2one('uom.uom', string="Timesheet Encoding Unit", + default=_default_timesheet_encode_uom_id, domain=[('measure_type', '=', 'time')], required=True, + help="""This will set the unit of measure used to encode timesheet. This will simply provide tools + and widgets to help the encoding. All reporting will still be expressed in hours (default value).""") diff --git a/addons/hr_timesheet/models/res_config_settings.py b/addons/hr_timesheet/models/res_config_settings.py index 164b23b4f71..73d57d048fa 100644 --- a/addons/hr_timesheet/models/res_config_settings.py +++ b/addons/hr_timesheet/models/res_config_settings.py @@ -14,3 +14,7 @@ class ResConfigSettings(models.TransientModel): help="This will set the unit of measure used in projects and tasks.\n" "If you use the timesheet linked to projects, don't " "forget to setup the right unit of measure in your employees.") + timesheet_encode_uom_id = fields.Many2one('uom.uom', string="Encoding Unit", + related='company_id.timesheet_encode_uom_id', + help="""This will set the unit of measure used to encode timesheet. This will simply provide tools + and widgets to help the encoding. All reporting will still be expressed in hours (default value).""") diff --git a/addons/hr_timesheet/models/uom.py b/addons/hr_timesheet/models/uom.py new file mode 100644 index 00000000000..885fc9de387 --- /dev/null +++ b/addons/hr_timesheet/models/uom.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Uom(models.Model): + _inherit = 'uom.uom' + + timesheet_widget = fields.Char("Widget", help="Widget used in the webclient when this unit is the one used to encode timesheets.") diff --git a/addons/hr_timesheet/static/src/js/timesheet_uom.js b/addons/hr_timesheet/static/src/js/timesheet_uom.js new file mode 100644 index 00000000000..74fd96b5412 --- /dev/null +++ b/addons/hr_timesheet/static/src/js/timesheet_uom.js @@ -0,0 +1,86 @@ +odoo.define('hr_timesheet.timesheet_uom', function (require) { +'use strict'; + +var AbstractField = require('web.AbstractField'); +var basicFields = require('web.basic_fields'); +var fieldRegistry = require('web.field_registry'); +var session = require('web.session'); + +/** + * Extend the float factor widget to set default value for timesheet + * use case. The 'factor' is forced to be the UoM timesheet + * conversion from the session info. + **/ +var FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({ + formatType: 'float_factor', + /** + * Override init to tweak options depending on the session_info + * + * @constructor + * @override + */ + init: function(parent, name, record, options) { + this._super(parent, name, record, options); + + // force factor in format and parse options + if (session.timesheet_uom_factor) { + this.nodeOptions.factor = session.timesheet_uom_factor; + this.parseOptions.factor = session.timesheet_uom_factor; + } + }, +}); + + +/** + * Extend the float toggle widget to set default value for timesheet + * use case. The 'range' is different from the default one of the + * native widget, and the 'factor' is forced to be the UoM timesheet + * conversion. + **/ +var FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({ + formatType: 'float_factor', + /** + * Override init to tweak options depending on the session_info + * + * @constructor + * @override + */ + init: function(parent, name, record, options) { + options = options || {}; + var fieldsInfo = record.fieldsInfo[options.viewType || 'default']; + var attrs = options.attrs || (fieldsInfo && fieldsInfo[name]) || {}; + + var hasRange = _.contains(_.keys(attrs.options || {}), 'range'); + + this._super(parent, name, record, options); + + // Set the timesheet widget options: the range can be customized + // by setting the option on the field in the view. The factor + // is forced to be the UoM conversion factor. + if (!hasRange) { + this.nodeOptions.range = [0.00, 1.00, 0.50]; + } + this.nodeOptions.factor = session.timesheet_uom_factor; + }, +}); + + +/** + * Binding depending on Company Preference + * + * determine wich widget will be the timesheet one. + * Simply match the 'timesheet_uom' widget key with the correct + * implementation (float_time, float_toggle, ...). The default + * value will be 'float_factor'. +**/ +var FieldTimesheetUom = FieldTimesheetFactor; +var widgetName = 'timesheet_uom' in session ? + session.timesheet_uom.timesheet_widget : 'float_factor'; +var FieldTimesheetUom = widgetName === 'float_toggle' ? + FieldTimesheetToggle : (fieldRegistry.get(widgetName) || FieldTimesheetFactor); + +fieldRegistry.add('timesheet_uom', FieldTimesheetUom); + +return FieldTimesheetUom; +}); + diff --git a/addons/hr_timesheet/views/assets.xml b/addons/hr_timesheet/views/assets.xml new file mode 100644 index 00000000000..bc14570e60e --- /dev/null +++ b/addons/hr_timesheet/views/assets.xml @@ -0,0 +1,8 @@ + + + + diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index ed87b26d582..298c8537542 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -43,8 +43,8 @@ - - + + @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + @@ -184,7 +184,7 @@ - Duration: + Duration:
@@ -201,7 +201,9 @@ account.analytic.line tree,form [('project_id', '!=', False), ('user_id', '=', uid)] - {"search_default_week":1} + { + "search_default_week":1, + }

@@ -256,7 +258,7 @@ 'search_default_week':1, 'search_default_groupby_employee':1, 'search_default_groupby_project':1, - 'search_default_groupby_task':1 + 'search_default_groupby_task':1, } diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index a7b8bf0a74f..caa69689a42 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -87,7 +87,7 @@ - + diff --git a/addons/hr_timesheet/views/res_config_settings_views.xml b/addons/hr_timesheet/views/res_config_settings_views.xml index 39f98a5374d..4be15949746 100644 --- a/addons/hr_timesheet/views/res_config_settings_views.xml +++ b/addons/hr_timesheet/views/res_config_settings_views.xml @@ -11,7 +11,7 @@

-

Timesheet Apps

+

Time Encoding

@@ -24,6 +24,22 @@
+
+
+
+
From cce8e002f5085b290bd4142c259f3f9f9a5e7225 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Mon, 13 Aug 2018 11:12:09 +0200 Subject: [PATCH 8/8] [IMP] hr_timesheet: label on unit_amount We want the UoM name (encoding timesheet) to be displayed in the label of unit_amount field. To do so, the field_view_get is extended to change the label on the field decorated with the `timesheet_uom` widget, in the view arch If a string attribute is set on the field, it is prior; so we only add label if no one is set to avoid breaking view inheritance. Task #39079 --- addons/hr_timesheet/models/hr_timesheet.py | 18 ++++++++++++++++++ addons/hr_timesheet/models/project.py | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index da01b442fad..6e2ceffa36e 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -77,6 +77,24 @@ class AccountAnalyticLine(models.Model): self.filtered(lambda t: t.project_id)._timesheet_postprocess(values) return result + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + """ Set the correct label for `unit_amount`, depending on company UoM """ + result = super(AccountAnalyticLine, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + result['arch'] = self._apply_timesheet_label(result['arch']) + return result + + @api.model + def _apply_timesheet_label(self, view_arch): + doc = etree.XML(view_arch) + encoding_uom = self.env.user.company_id.timesheet_encode_uom_id + # Here, we select only the unit_amount field having no string set to give priority to + # custom inheretied view stored in database. Even if normally, no xpath can be done on + # 'string' attribute. + for node in doc.xpath("//field[@name='unit_amount'][@widget='timesheet_uom'][not(@string)]"): + node.set('string', _('Duration (%s)') % (encoding_uom.name)) + return etree.tostring(doc, encoding='unicode') + # ---------------------------------------------------- # Business Methods # ---------------------------------------------------- diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 7976df1defb..8ab95b05cab 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -151,3 +151,10 @@ class Task(models.Model): 'company_id': account_id.company_id.id, }) return result + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + """ Set the correct label for `unit_amount`, depending on company UoM """ + result = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) + result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch']) + return result