[FIX] web,hr_timesheet,partner_autocomplete: make multi company timesheet uom work

This commit's primary objective is to ensure that the timesheet uom is working in accordance
with the company settings in a multi company environment.

Prior to this commit:

 - The timesheet preferences sent to the front end were always those of the default
   company of the user.
 - The timesheet related widgets initialisation process (adding the correct ones in
   the fieldRegistry) was performed before the front end treatment of cids and coockies
   which prevented applying the front end selected company settings.

After this commit:

 - A dictionnary is used in the session in order to structure the companies info.
 - The company timesheet preferences are sent to the front end through the company dict.
 - The uom info is sent to the front end through the session.
 - The timesheet uom is now managed from the frontend and is now in sync with the settings.
 - Timesheet widgets are initialised during the AbstractWebClient init and are in sync with
   the multicompany front end settings

task-2168337
Closes: #66551
This commit is contained in:
Laurent Stukkens (LTU)
2021-03-04 10:34:32 +01:00
parent c815ba7d40
commit 3e847fc8f4
11 changed files with 212 additions and 126 deletions
+29 -6
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import api, models
from odoo.http import request
class Http(models.AbstractModel):
@@ -9,13 +10,35 @@ class Http(models.AbstractModel):
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.
widget to apply, depending on the current company.
"""
result = super(Http, self).session_info()
if self.env.user.has_group('base.group_user'):
company = self.env.company
encoding_uom = company.timesheet_encode_uom_id
company_ids = request.env.user.company_ids
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
for company in company_ids:
result["user_companies"]["allowed_companies"][company.id].update({
"timesheet_uom_id": company.timesheet_encode_uom_id.id,
"timesheet_uom_factor": company.project_time_mode_id._compute_quantity(
1.0,
company.timesheet_encode_uom_id,
round=False
),
})
result["uom_ids"] = self.get_timesheet_uoms()
return result
@api.model
def get_timesheet_uoms(self):
company_ids = request.env.user.company_ids
uom_ids = company_ids.mapped('timesheet_encode_uom_id') | \
company_ids.mapped('project_time_mode_id')
return {
uom.id:
{
'id': uom.id,
'name': uom.name,
'rounding': uom.rounding,
'timesheet_widget': uom.timesheet_widget,
} for uom in uom_ids
}
@@ -12,10 +12,14 @@ odoo.define('hr_timesheet.GraphView', function (require) {
_processData: function (originIndex, rawData) {
this._super.apply(this, arguments);
const session = this.getSession();
if (this.chart.measure === 'unit_amount' && session.timesheet_uom_factor !== 1) {
const currentCompanyId = session.user_context.allowed_company_ids[0];
const currentCompany = session.user_companies.allowed_companies[currentCompanyId];
const currentCompanyTimesheetUOMFactor = currentCompany.timesheet_uom_factor || 1;
if (this.chart.measure === 'unit_amount' && currentCompanyTimesheetUOMFactor !== 1) {
// recalculate the Duration values according to the timesheet_uom_factor
this.chart.dataPoints.forEach(function (dataPt) {
dataPt.value *= session.timesheet_uom_factor;
dataPt.value *= currentCompanyTimesheetUOMFactor;
});
}
},
@@ -11,16 +11,26 @@ const fieldRegistry = require('web.field_registry');
require('web._field_registry');
const session = require('web.session');
const AbstractWebClient = require('web.AbstractWebClient');
const TimesheetUOMMultiCompanyMixin = {
init: function(parent, name, record, options) {
this._super(parent, name, record, options);
const currentCompanyId = session.user_context.allowed_company_ids[0];
const currentCompany = session.user_companies.allowed_companies[currentCompanyId];
this.currentCompanyTimesheetUOMFactor = currentCompany.timesheet_uom_factor || 1;
}
};
/**
* 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.
**/
const FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({
const FieldTimesheetFactor = basicFields.FieldFloatFactor.extend(TimesheetUOMMultiCompanyMixin).extend({
formatType: 'float_factor',
/**
* Override init to tweak options depending on the session_info
* Override init to tweak options depending on the session info
*
* @constructor
* @override
@@ -29,10 +39,8 @@ const FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({
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;
}
this.nodeOptions.factor = this.currentCompanyTimesheetUOMFactor;
this.parseOptions.factor = this.currentCompanyTimesheetUOMFactor;
},
});
@@ -43,10 +51,10 @@ const FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({
* native widget, and the 'factor' is forced to be the UoM timesheet
* conversion.
**/
const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({
const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend(TimesheetUOMMultiCompanyMixin).extend({
formatType: 'float_factor',
/**
* Override init to tweak options depending on the session_info
* Override init to tweak options depending on the session info
*
* @constructor
* @override
@@ -66,7 +74,7 @@ const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({
if (!hasRange) {
this.nodeOptions.range = [0.00, 1.00, 0.50];
}
this.nodeOptions.factor = session.timesheet_uom_factor;
this.nodeOptions.factor = this.currentCompanyTimesheetUOMFactor;
},
});
@@ -74,91 +82,92 @@ const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({
/**
* Extend float time widget
*/
const FieldTimesheetTime = basicFields.FieldFloatTime.extend({
const FieldTimesheetTime = basicFields.FieldFloatTime.extend(TimesheetUOMMultiCompanyMixin).extend({
init: function () {
this._super.apply(this, arguments);
if (session.timesheet_uom_factor) {
this.nodeOptions.factor = session.timesheet_uom_factor;
this.parseOptions.factor = session.timesheet_uom_factor;
}
this.nodeOptions.factor = this.currentCompanyTimesheetUOMFactor;
this.parseOptions.factor = this.currentCompanyTimesheetUOMFactor;
}
});
AbstractWebClient.include({
init: function () {
this._super(...arguments);
/**
* 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'.
**/
const widgetName = this.currentCompanyTimesheetUOM &&
this.currentCompanyTimesheetUOM.timesheet_widget ||
'float_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'.
**/
const widgetName = 'timesheet_uom' in session ?
session.timesheet_uom.timesheet_widget : 'float_factor';
let FieldTimesheetUom = null;
let FieldTimesheetUom = null;
if (widgetName === 'float_toggle') {
FieldTimesheetUom = FieldTimesheetToggle;
} else if (widgetName === 'float_time') {
FieldTimesheetUom = FieldTimesheetTime;
} else {
FieldTimesheetUom = (
fieldRegistry.get(widgetName) &&
fieldRegistry.get(widgetName).extend({ })
) || FieldTimesheetFactor;
}
fieldRegistry.add('timesheet_uom', FieldTimesheetUom);
if (widgetName === 'float_toggle') {
FieldTimesheetUom = FieldTimesheetToggle;
} else if (widgetName === 'float_time') {
FieldTimesheetUom = FieldTimesheetTime;
} else {
FieldTimesheetUom = (
fieldRegistry.get(widgetName) &&
fieldRegistry.get(widgetName).extend({})
) || FieldTimesheetFactor;
}
fieldRegistry.add('timesheet_uom', FieldTimesheetUom);
// widget timesheet_uom_no_toggle is the same as timesheet_uom but without toggle.
// We can modify easly huge amount of days.
let FieldTimesheetUomWithoutToggle = null;
if (widgetName === 'float_toggle') {
FieldTimesheetUomWithoutToggle = FieldTimesheetFactor;
} else {
FieldTimesheetUomWithoutToggle = FieldTimesheetTime;
}
fieldRegistry.add('timesheet_uom_no_toggle', FieldTimesheetUomWithoutToggle);
// widget timesheet_uom_no_toggle is the same as timesheet_uom but without toggle.
// We can modify easly huge amount of days.
let FieldTimesheetUomWithoutToggle = null;
if (widgetName === 'float_toggle') {
FieldTimesheetUomWithoutToggle = FieldTimesheetFactor;
} else {
FieldTimesheetUomWithoutToggle = FieldTimesheetTime;
}
fieldRegistry.add('timesheet_uom_no_toggle', FieldTimesheetUomWithoutToggle);
// bind the formatter and parser method, and tweak the options
const _tweak_options = function(options) {
if (!_.contains(options, 'factor')) {
options.factor = session.timesheet_uom_factor;
}
return options;
};
// bind the formatter and parser method, and tweak the options
const _tweak_options = (options) => {
if (!_.contains(options, 'factor')) {
options.factor = this.currentCompanyTimesheetUOMFactor;
}
return options;
};
fieldUtils.format.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || {});
const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.format.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || { });
const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.parse.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || {});
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
return parser(value, field, options);
};
fieldUtils.parse.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || { });
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
return parser(value, field, options);
};
fieldUtils.format.timesheet_uom_no_toggle = function(value, field, options) {
options = _tweak_options(options || {});
const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.format.timesheet_uom_no_toggle = function(value, field, options) {
options = _tweak_options(options || { });
const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.parse.timesheet_uom_no_toggle = function(value, field, options) {
options = _tweak_options(options || {});
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
return parser(value, field, options);
};
fieldUtils.parse.timesheet_uom_no_toggle = function(value, field, options) {
options = _tweak_options(options || { });
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
return parser(value, field, options);
};
},
});
return {
FieldTimesheetUom,
FieldTimesheetFactor,
FieldTimesheetTime,
FieldTimesheetToggle
FieldTimesheetToggle,
};
});
@@ -8,7 +8,7 @@ return AbstractWebClient.include({
start: function () {
if (session.iap_company_enrich) {
const current_company_id = session.user_companies.current_company[0];
const current_company_id = session.user_companies.current_company;
this._rpc({
model: 'res.company',
method: 'iap_enrich_auto',
+9 -1
View File
@@ -60,7 +60,15 @@ class Http(models.AbstractModel):
}
session_info.update({
# current_company should be default_company
"user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]},
"user_companies": {
'current_company': user.company_id.id,
'allowed_companies': {
comp.id: {
'id': comp.id,
'name': comp.name,
} for comp in user.company_ids
},
},
"currencies": self.get_currencies(),
"show_effect": True,
"display_switch_company_menu": user.has_group('base.group_multi_company') and len(user.company_ids) > 1,
@@ -104,6 +104,19 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
this.set('title_part', {"zopenerp": "Odoo"});
this.env = env;
this.env.bus.on('set_title_part', this, this._onSetTitlePart);
// Only manage the multi company state if a session exists (in order not to interfere during js tests)
if (session.uid && Number.isInteger(session.uid) && session.uid > 0) {
this._manageMultiCompanyState();
}
if (session.user_context &&
session.user_context.allowed_company_ids &&
session.user_context.allowed_company_ids.length) {
this.currentCompanyId = session.user_context.allowed_company_ids[0];
const currentCompany = session.user_companies.allowed_companies[this.currentCompanyId];
const currentCompanyTimesheetUOMId = currentCompany.timesheet_uom_id || false;
this.currentCompanyTimesheetUOMFactor = currentCompany.timesheet_uom_factor || 1;
this.currentCompanyTimesheetUOM = currentCompanyTimesheetUOMId && session.uom_ids[currentCompanyTimesheetUOMId] || false;
}
},
/**
* @override
@@ -120,28 +133,8 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
this.on("change:title_part", this, this._title_changed);
this._title_changed();
var state = $.bbq.getState();
// If not set on the url, retrieve cids from the local storage
// of from the default company on the user
var current_company_id = session.user_companies.current_company[0]
if (!state.cids) {
state.cids = utils.get_cookie('cids') !== null ? utils.get_cookie('cids') : String(current_company_id);
}
// If a key appears several times in the hash, it is available in the
// bbq state as an array containing all occurrences of that key
const cids = Array.isArray(state.cids) ? state.cids[0] : state.cids;
let stateCompanyIDS = cids.split(',').map(cid => parseInt(cid, 10));
var userCompanyIDS = _.map(session.user_companies.allowed_companies, function(company) {return company[0]});
// Check that the user has access to all the companies
if (!_.isEmpty(_.difference(stateCompanyIDS, userCompanyIDS))) {
state.cids = String(current_company_id);
stateCompanyIDS = [current_company_id]
}
// Update the user context with this configuration
session.user_context.allowed_company_ids = stateCompanyIDS;
$.bbq.pushState(state);
// Update favicon
$("link[type='image/x-icon']").attr('href', '/web/image/res.company/' + String(stateCompanyIDS[0]) + '/favicon/')
$("link[type='image/x-icon']").attr('href', '/web/image/res.company/' + String(this.currentCompanyId) + '/favicon/');
return session.is_bound
.then(function () {
@@ -237,6 +230,28 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
});
this.$el.children().remove();
},
_manageMultiCompanyState: function () {
let state = $.bbq.getState();
// If not set on the url, retrieve cids from the local storage
// or from the default company on the user
const current_company_id = session.user_companies.current_company;
if (!state.cids) {
state.cids = utils.get_cookie('cids') !== null ? utils.get_cookie('cids') : String(current_company_id);
}
// If a key appears several times in the hash, it is available in the
// bbq state as an array containing all occurrences of that key
const cids = Array.isArray(state.cids) ? state.cids[0] : state.cids;
let stateCompanyIDS = cids.split(',').map(cid => parseInt(cid, 10));
const userCompanyIDS = Object.keys(session.user_companies.allowed_companies).map(id => parseInt(id, 10));
// Check that the user has access to all the companies
if (!_.isEmpty(_.difference(stateCompanyIDS, userCompanyIDS))) {
state.cids = String(current_company_id);
stateCompanyIDS = [current_company_id];
}
// Update the user context with this configuration
session.user_context.allowed_company_ids = stateCompanyIDS;
$.bbq.pushState(state);
},
// --------------------------------------------------------------
// Window title handling
// --------------------------------------------------------------
@@ -3710,10 +3710,10 @@ var BasicModel = AbstractModel.extend({
// in general, the actual "company_id" field of the form should be used for m2o domains, not this fallback
let current_company_id;
if (session.user_context.allowed_company_ids) {
current_company_id = session.user_context.allowed_company_ids[0];
current_company_id = Object.keys(session.user_context.allowed_company_ids)[0];
} else {
current_company_id = session.user_companies ?
session.user_companies.current_company[0] :
session.user_companies.current_company :
false;
}
return Object.assign(
@@ -38,15 +38,12 @@ var SwitchCompanyMenu = Widget.extend({
* @override
*/
willStart: function () {
var self = this;
this.allowed_company_ids = String(session.user_context.allowed_company_ids)
.split(',')
.map(function (id) {return parseInt(id);});
this.user_companies = session.user_companies.allowed_companies;
this.current_company = this.allowed_company_ids[0];
this.current_company_name = _.find(session.user_companies.allowed_companies, function (company) {
return company[0] === self.current_company;
})[1];
this.current_company_name = session.user_companies.allowed_companies[this.current_company]['name'];
return this._super.apply(this, arguments);
},
+7 -7
View File
@@ -1734,11 +1734,11 @@
</span>
</a>
<div class="dropdown-menu dropdown-menu-right" role="menu">
<t t-foreach="widget.user_companies" t-as="company">
<div class="dropdown-item d-flex py-0 px-0" data-menu="company" t-att-data-company-id="company[0]">
<t t-set="is_allowed" t-value="widget.allowed_company_ids.includes(company[0])"/>
<t t-set="is_current" t-value="company[0] === widget.current_company"/>
<div role="menuitemcheckbox" t-att-aria-checked="is_allowed" t-att-aria-label="company[1]" tabindex="0" class="ml-auto pl-3 pr-3 border border-top-0 border-left-0 border-bottom-0 toggle_company o_py">
<t t-foreach="_.values(widget.user_companies)" t-as="company">
<div class="dropdown-item d-flex py-0 px-0" data-menu="company" t-att-data-company-id="company.id">
<t t-set="is_allowed" t-value="widget.allowed_company_ids.includes(company.id)"/>
<t t-set="is_current" t-value="company.id === widget.current_company"/>
<div role="menuitemcheckbox" t-att-aria-checked="is_allowed" t-att-aria-label="company.name" tabindex="0" class="ml-auto pl-3 pr-3 border border-top-0 border-left-0 border-bottom-0 toggle_company o_py">
<span style="height: 2rem;">
<t t-if="is_allowed">
<i class="fa fa-fw fa-check-square pt-2"></i>
@@ -1751,12 +1751,12 @@
<div role="button" t-att-aria-pressed="is_current" aria-label="Switch to this company" tabindex="0" class="d-flex flex-grow-1 align-items-center py-0 log_into pl-3 o_py" t-att-style="is_current ? 'background-color: lightgrey;' : ''">
<t t-if="is_allowed">
<span class='mr-3 company_label'>
<t t-esc="company[1]"/>
<t t-esc="company.name"/>
</span>
</t>
<t t-if="!is_allowed">
<span class='mr-3 company_label text-muted'>
<t t-esc="company[1]"/>
<t t-esc="company.name"/>
</span>
</t>
</div>
@@ -47,17 +47,47 @@ QUnit.module('widgets', {
beforeEach: async function () {
this.session_mock_multi = {
user_companies: {
current_company: [1, "Company 1"],
allowed_companies: [[1, "Company 1"], [2, "Company 2"], [3, "Company 3"]],
current_company: 1,
allowed_companies: {
1: {
id: 1,
name: 'Company 1',
},
2: {
id: 2,
name: 'Company 2',
},
3: {
id: 3,
name: 'Company 3',
},
},
},
user_context: {
allowed_company_ids: [1, 3],
},
user_context: {allowed_company_ids: [1, 3]},
};
this.session_mock_single = {
user_companies: {
current_company: [1, "Company 1"],
allowed_companies: [[1, "Company 1"], [2, "Company 2"], [3, "Company 3"]],
current_company: 1,
allowed_companies: {
1: {
id: 1,
name: 'Company 1',
},
2: {
id: 2,
name: 'Company 2',
},
3: {
id: 3,
name: 'Company 3',
},
},
},
user_context: {
allowed_company_ids: [1],
},
user_context: {allowed_company_ids: [1]},
};
},