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)] - - - - diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index a4ed9baff81..3e96acb2bb7 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -416,7 +416,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 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 73a304daa2e..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,6 +35,7 @@ class HrPayslipEmployees(models.TransientModel): 'date_from': from_date, 'date_to': to_date, 'credit_note': run_data.get('credit_note'), + 'company_id': employee.company_id.id, } payslips += self.env['hr.payslip'].create(res) payslips.compute_sheet() diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 37903e3861f..4bb81489789 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', @@ -116,41 +140,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; }, @@ -196,9 +216,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}); }, }); @@ -312,10 +329,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, })); }); @@ -328,7 +345,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 16bc619e8c7..fa5182a10a1 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -351,12 +351,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, @@ -707,6 +707,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]; @@ -746,7 +865,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/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 19d0b32d58f..bde7996800d 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 diff --git a/addons/purchase/tests/test_fifo_price.py b/addons/purchase/tests/test_fifo_price.py index 6673a37059c..00f15a5c20d 100644 --- a/addons/purchase/tests/test_fifo_price.py +++ b/addons/purchase/tests/test_fifo_price.py @@ -14,7 +14,8 @@ class TestFifoPrice(TestPurchase): self._load('stock_account', 'test', 'stock_valuation_account.xml') # Set the company currency as EURO for the sake of repeatibility - self.env.ref('base.main_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, self.env.ref('base.main_company')]) # Set a product as using fifo price product_icecream = self.env['product.product'].create({ diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index bd9f7f523f8..70c1d04e9da 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -19,7 +19,7 @@ - + diff --git a/addons/stock_dropshipping/tests/test_lifo_price.py b/addons/stock_dropshipping/tests/test_lifo_price.py index d4d9cc3523c..a27796d860c 100644 --- a/addons/stock_dropshipping/tests/test_lifo_price.py +++ b/addons/stock_dropshipping/tests/test_lifo_price.py @@ -18,7 +18,8 @@ class TestLifoPrice(common.TransactionCase): self._load('stock_account', 'test', 'stock_valuation_account.xml') # Set the company currency as EURO for the sake of repeatibility - self.env.ref('base.main_company').write({'currency_id': self.env.ref('base.EUR').id}) + self.env.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", + [self.env.ref("base.EUR").id, self.env.ref('base.main_company')]) # Set product category removal strategy as LIFO product_category_001 = self.env['product.category'].create({ 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 36ae061cdb3..21259671505 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -3048,7 +3048,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); diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 1f758bf7086..2458acf91f1 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/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", diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 02199375f87..5a53839b628 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/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/models/res_users.py b/odoo/addons/base/models/res_users.py index b0dd4a8ff78..abb25a4bc2e 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -751,7 +751,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``. """