[MERGE] forward port branch 11.0 up to bfe88c50e2

This commit is contained in:
Christophe Simonis
2018-03-08 14:06:54 +01:00
14 changed files with 256 additions and 122 deletions
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="delivery_comp_rule" model="ir.rule">
<field name="name">Delivery multi-company</field>
<field name="model_id" ref="model_delivery_carrier"/>
<field name="global" eval="True"/>
<field name="domain_force"> ['|',('product_id.company_id','=',user.company_id.id),('product_id.company_id','=',False)]</field>
<field name="active" eval="False"/>
</record>
</data>
</odoo>
+2 -1
View File
@@ -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
@@ -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()
+83 -64
View File
@@ -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<Array>} 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<Array>} 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();
+122 -3
View File
@@ -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: '<form string="Partners">' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
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: '<form string="Partners">' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
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: '<form string="Partners">' +
'<field name="m2m" widget="many2many_tags"/>' +
'<div class="oe_chatter">' +
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
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");
}
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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({
@@ -19,7 +19,7 @@
<field name="inherit_id" ref="stock.view_inventory_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="accounting_date"/>
<field name="accounting_date" attrs="{'readonly':[('state','!=', 'draft')]}"/>
</xpath>
</field>
</record>
@@ -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({
@@ -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);
+4
View File
@@ -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,
+1 -1
View File
@@ -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",
+31 -35
View File
@@ -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')
+5 -1
View File
@@ -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``. """