From 1c015283753df0bdc201b2e010a27be398960b03 Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Thu, 1 Mar 2018 11:49:10 +0100 Subject: [PATCH 01/11] [FIX] hr_expense: ensure message_new has only one record (singleton) Before this commit: Expense emails title are parsed to find a product that match the given [CODE]. For example, an email title like '[CARD] Graphic Card $900' will create an expense for the product having 'CARD' as internal reference. Since the search is done with an 'ilike' and with no 'limit', it could return multiple product: all the one containing the CODE in their internal reference. This would throw a singleton error when trying afterward to read product.id. Then, the mail would not go through the mail.thread flow and it would return a Mail System Delivery fail saying the address does not exist. Users would get stuck with this, having no way to understand where the error comes from. Now, we ensure to find only 1 product. To avoid breaking the ilike behavior that could be used by users only typing the first caracters of a long CODE, we still use ilike but force exact match if there is multiple results. Closes #23419 opw-1816340 --- addons/hr_expense/models/hr_expense.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index e83b9ca66a8..53c64c4a5ac 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -348,7 +348,8 @@ class HrExpense(models.Model): product = default_product else: expense_description = expense_description.replace(product_code.group(), '') - product = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product + products = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product + product = products.filtered(lambda p: p.default_code == product_code.group(1)) or products[0] pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?' # Match the last occurence of a float in the string From 2f46f3bdb2e1e1baf69e9a548975813d5fe61d31 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 7 Mar 2018 16:11:39 +0100 Subject: [PATCH 02/11] [FIX] hr_payroll: generate payslips sets the employee company In a multi-company environment, if a user in company A created a Payslips batch for an employee in the company B and used the `generate payslips` button, the created payslip was set with the user company (A) while it must be the employee company (B). opw-1820138 --- addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py index 251eec34b18..64cf6e331b9 100644 --- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py +++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py @@ -46,6 +46,7 @@ class hr_payslip_employees(osv.osv_memory): 'date_from': from_date, 'date_to': to_date, 'credit_note': credit_note, + 'company_id': emp.company_id.id, } slip_ids.append(slip_pool.create(cr, uid, res, context=context)) slip_pool.compute_sheet(cr, uid, slip_ids, context=context) From d594fece4ab12500228f32fb9007a8f573149ac0 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 7 Mar 2018 16:32:17 +0100 Subject: [PATCH 03/11] [FIX] point_of_sale: return product When a user returns a tracked product in the POS, it is not possible to register the serial number. In case a customer returns 2 units of a tracked product, the following error occurs: 'Cannot set the done quantity from this stock move...' Since the serial number were not set, `qty_done` remains equal to zero and `pack_lots` is empty. Therefore, we are trying to set a zero value, which is actually useless. opw-1817695 --- addons/point_of_sale/models/pos_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index ce11da16f09..03d8c5e00e0 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -793,7 +793,7 @@ class PosOrder(models.Model): 'location_dest_id': move.location_dest_id.id, 'lot_id': lot_id, }) - if not pack_lots: + if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding): move.quantity_done = qty_done return has_wrong_lots From 9db108c106bf1b64443a827b37d28b17286ea6c6 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 7 Mar 2018 18:47:56 +0100 Subject: [PATCH 04/11] [FIX] delivery: remove extra unsourced file --- addons/delivery/security/delivery_security.xml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 addons/delivery/security/delivery_security.xml diff --git a/addons/delivery/security/delivery_security.xml b/addons/delivery/security/delivery_security.xml deleted file mode 100644 index ae588bc245c..00000000000 --- a/addons/delivery/security/delivery_security.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Delivery multi-company - - - ['|',('product_id.company_id','=',user.company_id.id),('product_id.company_id','=',False)] - - - - From 9fdbbb816cfd6b8a81ab807244033d16de78107d Mon Sep 17 00:00:00 2001 From: len-odoo Date: Wed, 7 Mar 2018 12:01:34 +0100 Subject: [PATCH 05/11] [FIX] website_sale: display prices using public user pricelist on the shop When a pricelist was set on the public user that was different from that of the website, the prices were displayed using the public user default currency, but the currency symbol displayed was the website default currency. The issue was that in the template, the currency was accessed as a computed, related field of website while the price was obtained using a function call. In the first case the field was obtained using sudo, and thus the call would not get the public user pricelist. opw-1817761 --- addons/website_sale/models/website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 5de0f524fc7..9f43a6791d8 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -14,7 +14,7 @@ class Website(models.Model): _inherit = 'website' pricelist_id = fields.Many2one('product.pricelist', compute='_compute_pricelist_id', string='Default Pricelist') - currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Default Currency') + currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', related_sudo=False, string='Default Currency') salesperson_id = fields.Many2one('res.users', string='Salesperson') salesteam_id = fields.Many2one('crm.team', string='Sales Channel') pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids", From 1048fc6ed5e57c96fe1ada4529654c10d98a1246 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 8 Mar 2018 08:14:58 +0100 Subject: [PATCH 06/11] [FIX] purchase, stock_dropshipping: fix tests Astonishingly, some people really need a green runbot to work. --- addons/purchase/test/fifo_price.yml | 2 +- addons/stock_dropshipping/test/lifo_price.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/purchase/test/fifo_price.yml b/addons/purchase/test/fifo_price.yml index 29a6fea4e08..89029799ab7 100644 --- a/addons/purchase/test/fifo_price.yml +++ b/addons/purchase/test/fifo_price.yml @@ -3,7 +3,7 @@ - !python {model: res.company}: | company = self.env.ref('base.main_company') - company.currency_id = self.env.ref('base.EUR') + self.env.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.env.ref("base.EUR").id, company.id]) - Set a product as using fifo price - diff --git a/addons/stock_dropshipping/test/lifo_price.yml b/addons/stock_dropshipping/test/lifo_price.yml index 3cff45c24fa..1d34b828f4d 100644 --- a/addons/stock_dropshipping/test/lifo_price.yml +++ b/addons/stock_dropshipping/test/lifo_price.yml @@ -2,7 +2,7 @@ Set the company currency as EURO for the sake of repeatibility - !python {model: res.company, id: base.main_company}: | - self.write({'currency_id':ref('base.EUR')}) + self.env.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.env.ref("base.EUR").id, self.id]) - Set product category removal strategy as LIFO @@ -116,4 +116,4 @@ Check standard price became 80 euro - !python {model: product.product, id: product_lifo_icecream}: | - assert self.standard_price == 80.0, 'Price should have been 80 euro' \ No newline at end of file + assert self.standard_price == 80.0, 'Price should have been 80 euro' From 9f226528bd69968b662a87a19086dc3b14091101 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Mon, 5 Mar 2018 15:48:16 +0100 Subject: [PATCH 07/11] [FIX] stock_account: Force accounting date on validated inventory adjustment When the inventory adjustement has been validated, changing accounting_date has no impact on the stock moves created by the inventory adjustement. This behavior cannot be changed in stable version. opw:1817813 --- addons/stock_account/views/stock_account_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index bd817ab8bd3..cea114b735c 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -8,7 +8,7 @@ - + From b4f8ca0a34c9d30a1bbbf4dfced5848d200cb66c Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Wed, 7 Mar 2018 18:07:49 +0100 Subject: [PATCH 08/11] [FIX] base: fix dev mode for inherits view Before this commit, model that create ir_ui_view via _inheritS was ignored in --dev xml The arch_fs was not set. Because it was not ir.ui.view, and because xml_id was suffixed by _ir_ui_view. Now we manage the case of inherits. So if model model.name inherits ir_ui_view class MyClass(models.Model): _name = 'model.name' _inherits = {'ir.ui.view': 'view_id'} view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade") ... content will be read from xml in dev mode. parsing the xml file for xml_id pok_ir_ui_view and pok. The orm by default will add this suffix. The 'dynamic' view created when we update the groups is a special case, which we don't want the arch_fs for dev mode because the xml only contains by default and an empty container and btw the imd was wrong since we had the res.group model a xml_id into the _inverse_arch. The page create via a template (method new_page) will automatically remove the arch_fs since we don't want to see the empty container if we are in dev_mode. --- addons/website/models/website.py | 4 ++ odoo/addons/base/ir/ir_ui_view.py | 66 +++++++++++++++---------------- odoo/addons/base/res/res_users.py | 6 ++- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 64b9934c126..1cc7472f8e3 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -119,6 +119,10 @@ class Website(models.Model): 'arch': template_record.arch.replace(template, key), 'name': name, }) + + if view.arch_fs: + view.arch_fs = False + if ispage: page = self.env['website.page'].create({ 'url': page_url, diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index c3ca7e7ce35..386abda72f8 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -115,33 +115,31 @@ def _hasclass(context, *cls): def get_view_arch_from_file(filename, xmlid): doc = etree.parse(filename) - node = None - for n in doc.xpath('//*[@id="%s"]' % (xmlid)): - if n.tag in ('template', 'record'): - node = n - break - if node is None: - # fallback search on template with implicit module name - for n in doc.xpath('//*[@id="%s"]' % (xmlid.split('.')[1])): - if n.tag in ('template', 'record'): - node = n - break - if node is not None: - if node.tag == 'record': - field = node.find('field[@name="arch"]') - _fix_multiple_roots(field) - inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()]) - return field.text + inner - elif node.tag == 'template': - # The following dom operations has been copied from convert.py's _tag_template() - if not node.get('inherit_id'): - node.set('t-name', xmlid) - node.tag = 't' - else: - node.tag = 'data' - node.attrib.pop('id', None) - return etree.tostring(node, encoding='unicode') - _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid) + + xmlid_search = (xmlid, xmlid.split('.')[1]) + + # when view is created from model with inheritS of ir_ui_view, the xml id has been suffixed by '_ir_ui_view' + suffix = '_ir_ui_view' + if xmlid.endswith(suffix): + xmlid_search += (xmlid.rsplit(suffix, 1)[0], xmlid.split('.')[1].rsplit(suffix, 1)[0]) + + for node in doc.xpath('//*[%s]' % ' or '.join(["@id='%s'" % _id for _id in xmlid_search])): + if node.tag in ('template', 'record'): + if node.tag == 'record': + field = node.find('field[@name="arch"]') + _fix_multiple_roots(field) + inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()]) + return field.text + inner + elif node.tag == 'template': + # The following dom operations has been copied from convert.py's _tag_template() + if not node.get('inherit_id'): + node.set('t-name', xmlid) + node.tag = 't' + else: + node.tag = 'data' + node.attrib.pop('id', None) + return etree.tostring(node, encoding='unicode') + _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid_search) return None def add_text_before(node, text): @@ -258,14 +256,12 @@ actual arch. data = dict(arch_db=view.arch) if 'install_mode_data' in self._context: imd = self._context['install_mode_data'] - if '.' not in imd['xml_id']: - imd['xml_id'] = '%s.%s' % (imd['module'], imd['xml_id']) - if self._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']): - # we store the relative path to the resource instead of the absolute path, if found - # (it will be missing e.g. when importing data-only modules using base_import_module) - path_info = get_resource_from_path(imd['xml_file']) - if path_info: - data['arch_fs'] = '/'.join(path_info[0:2]) + + # we store the relative path to the resource instead of the absolute path, if found + # (it will be missing e.g. when importing data-only modules using base_import_module) + path_info = get_resource_from_path(imd['xml_file']) + if path_info: + data['arch_fs'] = '/'.join(path_info[0:2]) view.write(data) @api.depends('arch') diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index 53a3438b21a..ac65e3bf1d2 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -752,7 +752,11 @@ class GroupsView(models.Model): xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace") xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS")) xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode") - view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False}) + + new_context = dict(view._context) + new_context.pop('install_mode_data', None) # don't set arch_fs for this computed view + new_context['lang'] = None + view.with_context(new_context).write({'arch': xml_content}) def get_application_groups(self, domain): """ Return the non-share groups that satisfy ``domain``. """ From 622399c796756827783b1b58017bc61479019747 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Thu, 8 Mar 2018 09:35:22 +0100 Subject: [PATCH 09/11] [FIX] purchase: Changing the price of a confirmed PO before the receipt Steps to reproduce the bug: - Set your company in anglo-saxon - Create a product P in a category which is in real price and perpetual - Create a PO with P at 500$ and validate it - Change the unit price of P in the PO line and set 100$ - Receive the product Bug: The two journal entries generated for the goods were created with the price unit 500$ instead of 100$. Fix: Now the right price is taken into account to generate the journal entries. opw:1813946 --- addons/purchase/models/stock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index 4f10e405165..e3fa460f4f1 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -45,7 +45,8 @@ class StockMove(models.Model): order = self.purchase_line_id.order_id #if the currency of the PO is different than the company one, the price_unit on the move must be reevaluated #(was created at the rate of the PO confirmation, but must be valuated at the rate of stock move execution) - if order.currency_id != self.company_id.currency_id: + #also change when the unit price on the PO line changes in the meantime + if order.currency_id != self.company_id.currency_id or self.purchase_line_id.price_unit != self.price_unit: #we don't pass the move.date in the compute() for the currency rate on purpose because # 1) get_price_unit() is supposed to be called only through move.action_done(), # 2) the move hasn't yet the correct date (currently it is the expected date, after From 41af298d9d8a68be29cee57a4352e49d269594b7 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 8 Mar 2018 11:11:44 +0100 Subject: [PATCH 10/11] [FIX] hr_payroll: use defined variable Oversight of previous forward-port --- addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py index 715fb3cb1c6..d9a59cba3a0 100644 --- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py +++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py @@ -35,7 +35,7 @@ class HrPayslipEmployees(models.TransientModel): 'date_from': from_date, 'date_to': to_date, 'credit_note': run_data.get('credit_note'), - 'company_id': emp.company_id.id, + 'company_id': employee.company_id.id, } payslips += self.env['hr.payslip'].create(res) payslips.compute_sheet() From 3809fc891f5b4794493b6ad4ed4bc4000c9a416e Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 7 Mar 2018 09:23:24 +0100 Subject: [PATCH 11/11] [FIX] web,mail: always reload widget mail_activity Before this rev., the field widget 'mail_activity' kept an internal cache of fetched activities (like the 'mail_thread' and 'mail_followers'). However, for activities, this cache may cause issues. For instance, create a new lead in 'My pipeline', schedule a 'Meeting' activity on that lead (it opens the Calendar view so that real meetings can be created), create a meeting 2 weeks from now, go back to the lead form view: the activity still diplays that the meeting is today (when the view is refreshed, the correct date is displayed). This rev. removes the internal cache for this field widget, as the activities can easily be updated in business flows (unlike messages or followers). This rev. also fixes a bug in the BasicModel, where the res_ids of a list datapoint was altered in place to return a list of x2many commands instead of the list of ids when being stringified. This caused a traceback when opening a lead with activities, as soon as the internal cache had been removed. --- addons/mail/static/src/js/activity.js | 147 ++++++++++-------- addons/mail/static/tests/chatter_tests.js | 125 ++++++++++++++- .../static/src/js/views/basic/basic_model.js | 2 +- 3 files changed, 206 insertions(+), 68 deletions(-) diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 59b32f32023..5da6abd0eb1 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -2,7 +2,7 @@ odoo.define('mail.Activity', function (require) { "use strict"; var AbstractField = require('web.AbstractField'); -var concurrency = require('web.concurrency'); +var BasicModel = require('web.BasicModel'); var core = require('web.core'); var field_registry = require('web.field_registry'); var time = require('web.time'); @@ -11,6 +11,63 @@ var utils = require('mail.utils'); var QWeb = core.qweb; var _t = core._t; +/** + * Fetches activities and postprocesses them. + * + * This standalone function performs an RPC, but to do so, it needs an instance + * of a widget that implements the _rpc() function. + * + * @todo i'm not very proud of the widget instance given in arguments, we should + * probably try to do it a better way in the future. + * + * @param {Widget} self a widget instance that can perform RPCs + * @param {Array} ids the ids of activities to read + * @return {Deferred} resolved with the activities + */ +function _readActivities(self, ids) { + if (!ids.length) { + return $.when([]); + } + return self._rpc({ + model: 'mail.activity', + method: 'read', + args: [ids], + }).then(function (activities) { + // convert create_date and date_deadline to moments + _.each(activities, function (activity) { + activity.create_date = moment(time.auto_str_to_date(activity.create_date)); + activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline)); + }); + + // sort activities by due date + activities = _.sortBy(activities, 'date_deadline'); + + return activities; + }); +} + +BasicModel.include({ + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Fetches the activities displayed by the activity field widget in form + * views. + * + * @private + * @param {Object} record - an element from the localData + * @param {string} fieldName + * @return {Deferred} resolved with the activities + */ + _fetchSpecialActivity: function (record, fieldName) { + var localID = (record._changes && fieldName in record._changes) ? + record._changes[fieldName] : + record.data[fieldName]; + return _readActivities(this, this.localData[localID].res_ids); + }, +}); + /** * Set the 'label_delay' entry in activity data according to the deadline date * @param {Array} activities list of activity Object @@ -44,12 +101,6 @@ var setDelayLabel = function(activities){ }; var AbstractActivityField = AbstractField.extend({ - // inherited - init: function () { - this._super.apply(this, arguments); - this.activities = []; - }, - // private _markActivityDone: function (id, feedback) { return this._rpc({ @@ -59,33 +110,6 @@ var AbstractActivityField = AbstractField.extend({ kwargs: {feedback: feedback}, }); }, - _readActivities: function () { - var self = this; - var missing_ids = _.difference(this.value.res_ids, _.pluck(this.activities, 'id')); - var fetch_def; - if (missing_ids.length) { - fetch_def = this._rpc({ - model: 'mail.activity', - method: 'read', - args: [missing_ids], - }); - } - return $.when(fetch_def).then(function (results) { - // convert create_date and date_deadline to moments - _.each(results, function (activity) { - activity.create_date = moment(time.auto_str_to_date(activity.create_date)); - activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline)); - }); - - // filter out activities that are no longer linked to this record - self.activities = _.filter(self.activities.concat(results || []), function (activity) { - return _.contains(self.value.res_ids, activity.id); - }); - - // sort activities by due date - self.activities = _.sortBy(self.activities, 'date_deadline'); - }); - }, _scheduleActivity: function (id, previous_activity_type_id, callback) { var action = { type: 'ir.actions.act_window', @@ -115,41 +139,37 @@ var Activity = AbstractActivityField.extend({ 'click .o_activity_unlink': '_onUnlinkActivity', 'click .o_activity_done': '_onMarkActivityDone', }, + specialData: '_fetchSpecialActivity', // inherited init: function () { this._super.apply(this, arguments); - this.dp = new concurrency.DropPrevious(); + this.activities = this.record.specialData[this.name]; }, _render: function () { - // note: the rendering of this widget is asynchronous as it needs to - // fetch the details of the linked activities - var self = this; - var fetch_def = this.dp.add(this._readActivities()); - return fetch_def.then(function () { - _.each(self.activities, function (activity) { - if (activity.note) { - activity.note = utils.parse_and_transform(activity.note, utils.add_link); - } - }); - var activities = setDelayLabel(self.activities); - if (activities.length) { - var nbActivities = _.countBy(activities, 'state'); - self.$el.html(QWeb.render('mail.activity_items', { - activities: activities, - nbPlannedActivities: nbActivities.planned, - nbTodayActivities: nbActivities.today, - nbOverdueActivities: nbActivities.overdue, - date_format: time.getLangDateFormat(), - datetime_format: time.getLangDatetimeFormat(), - })); - } else { - self.$el.empty(); + _.each(this.activities, function (activity) { + if (activity.note) { + activity.note = utils.parse_and_transform(activity.note, utils.add_link); } }); + var activities = setDelayLabel(this.activities); + if (activities.length) { + var nbActivities = _.countBy(activities, 'state'); + this.$el.html(QWeb.render('mail.activity_items', { + activities: activities, + nbPlannedActivities: nbActivities.planned, + nbTodayActivities: nbActivities.today, + nbOverdueActivities: nbActivities.overdue, + date_format: time.getLangDateFormat(), + datetime_format: time.getLangDatetimeFormat(), + })); + } else { + this.$el.empty(); + } }, _reset: function (record) { this._super.apply(this, arguments); + this.activities = this.record.specialData[this.name]; // the mail widgets being persistent, one need to update the res_id on reset this.res_id = record.res_id; }, @@ -184,9 +204,6 @@ var Activity = AbstractActivityField.extend({ }); return this.do_action(action, { on_close: function () { - // remove the edited activity from the array of fetched activities to - // force a reload of that activity - self.activities = _.reject(self.activities, {id: activity_id}); self._reload({activity: true, thread: true}); }, }); @@ -300,10 +317,10 @@ var KanbanActivity = AbstractActivityField.extend({ _renderDropdown: function () { var self = this; this.$('.o_activity').html(QWeb.render("mail.KanbanActivityLoading")); - return this._readActivities().then(function () { + return _readActivities(this, this.value.res_ids).then(function (activities) { self.$('.o_activity').html(QWeb.render("mail.KanbanActivityDropdown", { selection: self.selection, - records: _.groupBy(setDelayLabel(self.activities), 'state'), + records: _.groupBy(setDelayLabel(activities), 'state'), uid: self.getSession().uid, })); }); @@ -316,7 +333,9 @@ var KanbanActivity = AbstractActivityField.extend({ // handlers _onButtonClicked: function (event) { event.preventDefault(); - this._renderDropdown(); + if (!this.$el.hasClass('open')) { + this._renderDropdown(); + } }, _onMarkActivityDone: function (event) { event.stopPropagation(); diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 9cf98987778..5c986273ad7 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -368,12 +368,12 @@ QUnit.test('kanban activity widget with an activity', function (assert) { // click on the activity button to re-open dropdown $record.find('.o_activity_btn').click(); - assert.strictEqual(rpcCount, 2, 'no RPC should be done as the activities are now in cache'); + assert.strictEqual(rpcCount, 3, 'should have reloaded the activities'); // mark activity as done $record.find('.o_mark_as_done').click(); $record = kanban.$('.o_kanban_record').first(); // the record widget has been reset - assert.strictEqual(rpcCount, 4, 'should have done an RPC to mark activity as done, and a read'); + assert.strictEqual(rpcCount, 5, 'should have done an RPC to mark activity as done, and a read'); assert.ok($record.find('.o_mail_activity .o_activity_color_default:not(.o_activity_color_today)').length, "activity widget should have been updated correctly"); assert.strictEqual($record.find('.o_mail_activity.open').length, 1, @@ -732,6 +732,125 @@ QUnit.test('chatter: Attachment viewer', function (assert) { form.destroy(); }); +QUnit.test('form activity widget: read RPCs', function (assert) { + // Records of model 'mail.activity' may be updated in business flows (e.g. + // the date of a 'Meeting' activity is updated when the associated meeting + // is dragged&dropped in the Calendar view). Because of that, the activities + // must be reloaded when the form is reloaded (the widget can't keep an + // internal cache). + assert.expect(6); + this.data.partner.records[0].activity_ids = [1]; + this.data.partner.records[0].activity_state = 'today'; + this.data['mail.activity'].records = [{ + id: 1, + display_name: "An activity", + date_deadline: moment().format("YYYY-MM-DD"), // now + state: "today", + user_id: 2, + activity_type_id: 2, + }]; + + var nbReads = 0; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '' + + '
' + + '
', + res_id: 2, + mockRPC: function (route, args) { + if (args.method === 'read' && args.model === 'mail.activity') { + nbReads++; + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(nbReads, 1, "should have read the activities"); + assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 1, + "should display an activity"); + assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(), + 'Today', "the activity should be today"); + + form.$buttons.find('.o_form_button_edit').click(); + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(nbReads, 1, "should not have re-read the activities"); + + // simulate a date change, and a reload of the form view + var tomorrow = moment().add(1, 'day').format("YYYY-MM-DD"); + this.data['mail.activity'].records[0].date_deadline = tomorrow; + form.reload(); + + assert.strictEqual(nbReads, 2, "should have re-read the activities"); + assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(), + 'Tomorrow', "the activity should be tomorrow"); + + form.destroy(); +}); + +QUnit.test('form activity widget on a new record', function (assert) { + assert.expect(0); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '' + + '
' + + '
', + mockRPC: function (route, args) { + if (args.method === 'read' && args.model === 'mail.activity') { + throw new Error("should not do a read on mail.activity"); + } + return this._super.apply(this, arguments); + }, + }); + + form.destroy(); +}); + +QUnit.test('form activity widget with another x2many field in view', function (assert) { + assert.expect(1); + + this.data.partner.fields.m2m = {string: "M2M", type: 'many2many', relation: 'partner'}; + + this.data.partner.records[0].m2m = [2]; + this.data.partner.records[0].activity_ids = [1]; + this.data.partner.records[0].activity_state = 'today'; + this.data['mail.activity'].records = [{ + id: 1, + display_name: "An activity", + date_deadline: moment().format("YYYY-MM-DD"), // now + state: "today", + user_id: 2, + activity_type_id: 2, + }]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '
' + + '' + + '
' + + '', + res_id: 2, + }); + + assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 1, + "should display an activity"); + + form.destroy(); +}); + QUnit.test('form activity widget: schedule next activity', function (assert) { assert.expect(5); this.data.partner.records[0].activity_ids = [1]; @@ -767,7 +886,7 @@ QUnit.test('form activity widget: schedule next activity', function (assert) { "the feedback should be sent correctly"); return $.when(); } - if (args.method === 'read' && checkReadArgs) { + if (args.method === 'read' && args.model === 'partner' && checkReadArgs) { assert.deepEqual(args.args[1], ['activity_ids', 'message_ids', 'display_name'], "should only read the mail fields"); } diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index c7f61edd541..c8bd4ae030f 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -3047,7 +3047,7 @@ var BasicModel = AbstractModel.extend({ if (field.type === 'one2many' || field.type === 'many2many') { var ids; if (!context[fieldName] || _.isArray(context[fieldName])) { // no dataPoint created yet - ids = context[fieldName] || []; + ids = context[fieldName] ? context[fieldName].slice(0) : []; } else { relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]); ids = relDataPoint.res_ids.slice(0);