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``. """