diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 27e9dcdd7e6..7cf32537e81 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -346,7 +346,7 @@ class AccountInvoice(models.Model):
payment_move_line_ids = fields.Many2many('account.move.line', string='Payment Move Lines', compute='_compute_payments', store=True)
user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
readonly=True, states={'draft': [('readonly', False)]},
- default=lambda self: self.env.user)
+ default=lambda self: self.env.user, copy=False)
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position',
readonly=True, states={'draft': [('readonly', False)]})
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True,
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index f61cf860d84..2b48b3c37a2 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -168,6 +168,10 @@
+
diff --git a/addons/google_drive/models/google_drive.py b/addons/google_drive/models/google_drive.py
index 3e7235c2d75..784b60ebf57 100644
--- a/addons/google_drive/models/google_drive.py
+++ b/addons/google_drive/models/google_drive.py
@@ -153,6 +153,9 @@ class GoogleDrive(models.Model):
a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it)
:return: the config id and config name
'''
+ # TO DO in master: fix my signature and my model
+ if isinstance(res_model, basestring):
+ res_model = self.env['ir.model'].search([('model', '=', res_model)]).id
if not res_id:
raise UserError(_("Creating google drive may only be done by one at a time."))
# check if a model is configured with a template
diff --git a/addons/google_drive/static/src/js/gdrive.js b/addons/google_drive/static/src/js/gdrive.js
index b7d2542bb7a..def3e0d76a4 100644
--- a/addons/google_drive/static/src/js/gdrive.js
+++ b/addons/google_drive/static/src/js/gdrive.js
@@ -1,78 +1,109 @@
-odoo.define('google_drive.google_drive', function (require) {
+odoo.define('google_drive.sidebar', function (require) {
"use strict";
-var data = require('web.data');
+/**
+ * The purpose of this file is to include the Sidebar widget to add Google
+ * Drive related items.
+ */
+
var Sidebar = require('web.Sidebar');
+
Sidebar.include({
- init: function () {
- var self = this;
- var ids;
- this._super.apply(this, arguments);
- var view = self.getParent();
- var result;
- if (view.fields_view && view.fields_view.type === "form") {
- ids = [];
- view.on("load_record", self, function (r) {
- ids = [r.id];
- self.add_gdoc_items(view, r.id);
- });
+ // TO DO: clean me in master
+ /**
+ * @override
+ */
+ start: function () {
+ var def;
+ if (this.options.viewType === "form") {
+ def = this._addGoogleDocItems(this.env.model, this.env.activeIds[0]);
}
+ return $.when(def).then(this._super.bind(this));
},
- add_gdoc_items: function (view, res_id) {
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} model
+ * @param {integer} resID
+ * @returns {Deferred}
+ */
+ _addGoogleDocItems: function (model, resID) {
var self = this;
+ if (!resID) {
+ return $.when();
+ }
var gdoc_item = _.indexOf(_.pluck(self.items.other, 'classname'), 'oe_share_gdoc');
if (gdoc_item !== -1) {
self.items.other.splice(gdoc_item, 1);
}
- if (res_id) {
- view.sidebar_eval_context().done(function (context) {
- var ds = new data.DataSet(this, 'google.drive.config', context);
- ds.call('get_google_drive_config', [view.dataset.model, res_id, context]).done(function (r) {
- if (!_.isEmpty(r)) {
- _.each(r, function (res) {
- var already_there = false;
- for (var i = 0;i < self.items.other.length;i++){
- if (self.items.other[i].classname === "oe_share_gdoc" && self.items.other[i].label.indexOf(res.name) > -1){
- already_there = true;
- break;
- }
- }
- if (!already_there){
- self.add_items('other', [{
- label: res.name,
- config_id: res.id,
- res_id: res_id,
- res_model: view.dataset.model,
- callback: self.on_google_doc,
- classname: 'oe_share_gdoc'
- },
- ]);
- }
- });
+ return this._rpc({
+ args: [this.env.model, resID],
+ context: this.env.context,
+ method: 'get_google_drive_config',
+ model: 'google.drive.config',
+ }).then(function (r) {
+ if (!_.isEmpty(r)) {
+ _.each(r, function (res) {
+ var already_there = false;
+ for (var i = 0; i < self.items.other.length; i++) {
+ var item = self.items.other[i];
+ if (item.classname === 'oe_share_gdoc' && item.label.indexOf(res.name) > -1) {
+ already_there = true;
+ break;
+ }
+ }
+ if (!already_there) {
+ self._addItems('other', [{
+ callback: self._onGoogleDocItemClicked.bind(self, res.id, resID),
+ classname: 'oe_share_gdoc',
+ config_id: res.id,
+ label: res.name,
+ res_id: resID,
+ res_model: model,
+ }]);
}
});
- });
- }
+ }
+ });
},
- on_google_doc: function (doc_item) {
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {integer} configID
+ * @param {integer} resID
+ */
+ _onGoogleDocItemClicked: function (configID, resID) {
var self = this;
- var domain = [['id', '=', doc_item.config_id]];
+ var domain = [['id', '=', configID]];
var fields = ['google_drive_resource_id', 'google_drive_client_id'];
this._rpc({
+ args: [domain, fields],
+ method: 'search_read',
+ model: 'google.drive.config',
+ }).then(function (configs) {
+ self._rpc({
+ args: [configID, resID, configs[0].google_drive_resource_id],
+ context: self.env.context,
+ method: 'get_google_drive_url',
model: 'google.drive.config',
- method: 'search_read',
- args: [domain, fields],
- })
- .then(function (configs) {
- var ds = new data.DataSet(self, 'google.drive.config');
- ds.call('get_google_drive_url', [doc_item.config_id, doc_item.res_id,configs[0].google_drive_resource_id, self.dataset.context]).done(function (url) {
- if (url){
- window.open(url, '_blank');
- }
- });
+ }).then(function (url) {
+ if (url){
+ window.open(url, '_blank');
+ }
});
+ });
},
-});
});
+
+return Sidebar;
+});
diff --git a/addons/google_drive/static/tests/gdrive_test.js b/addons/google_drive/static/tests/gdrive_test.js
new file mode 100644
index 00000000000..75548822cce
--- /dev/null
+++ b/addons/google_drive/static/tests/gdrive_test.js
@@ -0,0 +1,112 @@
+odoo.define('google_drive.gdrive_integration', function (require) {
+"use strict";
+//rebuild
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+var GoogleDriveSideBar = require('google_drive.sidebar');
+
+var createView = testUtils.createView;
+
+/*
+ * @override
+ * Avoid breaking other tests because of the new route
+ * that the module introduces
+ */
+var _addGoogleDocItemsOriginal = GoogleDriveSideBar.prototype._addGoogleDocItems;
+
+var _addGoogleDocItemsMocked = function (model, resID) {
+ return $.when();
+};
+
+GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked;
+
+QUnit.module('gdrive_integration', {
+ beforeEach: function () {
+ // For our test to work, the _addGoogleDocItems function needs to be the original
+ GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsOriginal;
+
+ this.data = {
+ partner: {
+ fields: {
+ display_name: {string: "Displayed name", type: "char", searchable: true},
+ },
+ records: [{
+ id: 1,
+ display_name: "Locomotive Breath",
+ }],
+ },
+ 'google.drive.config': {
+ fields: {
+ model_id: {string: 'Model', type: 'int'},
+ name: {string: 'Name', type: 'char'},
+ google_drive_resource_id: {string: 'Resource ID', type: 'char'},
+ },
+ records: [{
+ id: 27,
+ name: 'Cyberdyne Systems',
+ model_id: 1,
+ google_drive_resource_id: 'T1000',
+ }],
+ },
+ 'ir.attachment': {
+ fields: {
+ name: {string: 'Name', type:'char'}
+ },
+ records: [],
+ }
+ };
+ },
+
+ afterEach: function() {
+ GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked;
+ }
+
+}, function () {
+ QUnit.module('Google Drive Sidebar');
+
+ QUnit.test('rendering of the google drive attachments in Sidebar', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '
',
+ res_id: 1,
+ viewOptions: {sidebar: true},
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_config') {
+ assert.deepEqual(args.args, ['partner', 1],
+ 'The route to get google drive config should have been called');
+ return $.when([{id: 27, name: 'Cyberdyne Systems'}]);
+ }
+ if (route === '/web/dataset/call_kw/google.drive.config/search_read'){
+ return $.when([{google_drive_resource_id: "T1000",
+ google_drive_client_id: "cyberdyne.org",
+ id: 1}]);
+ }
+ if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_url') {
+ assert.deepEqual(args.args, [27, 1, 'T1000'],
+ 'The route to get the Google url should have been called');
+ // We don't return anything useful, otherwise it will open a new tab
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ var google_action = form.sidebar.$('.oe_share_gdoc');
+
+ assert.strictEqual(google_action.length, 1,
+ 'The button to the google action should be present');
+
+ // Trigger opening of the dynamic link
+ google_action.find('a:first').click();
+
+ form.destroy();
+ });
+});
+
+});
diff --git a/addons/google_drive/views/google_drive_templates.xml b/addons/google_drive/views/google_drive_templates.xml
index 1906a27696b..9f4725702e0 100644
--- a/addons/google_drive/views/google_drive_templates.xml
+++ b/addons/google_drive/views/google_drive_templates.xml
@@ -8,4 +8,9 @@
+
+
+
+
+
diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py
index fdf93d99951..726e5efe375 100644
--- a/addons/hr_payroll/models/hr_payslip.py
+++ b/addons/hr_payroll/models/hr_payslip.py
@@ -102,6 +102,7 @@ class HrPayslip(models.Model):
def refund_sheet(self):
for payslip in self:
copied_payslip = payslip.copy({'credit_note': True, 'name': _('Refund: ') + payslip.name})
+ copied_payslip.compute_sheet()
copied_payslip.action_payslip_done()
formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False)
treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False)
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index 4202e49bc45..af27e3094a4 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -58,4 +58,19 @@ class Task(models.Model):
if context.get('default_parent_id', False):
vals['parent_id'] = context.pop('default_parent_id', None)
task = super(Task, self.with_context(context)).create(vals)
- return task
\ No newline at end of file
+ return task
+
+ @api.multi
+ def write(self, values):
+ result = super(Task, self).write(values)
+ # reassign project_id on related timesheet lines
+ if 'project_id' in values:
+ project_id = values.get('project_id')
+ # a timesheet must have an analytic account (and a project)
+ if not project_id:
+ raise UserError(_('This task must have a project since they are linked to timesheets.'))
+ self.sudo().mapped('timesheet_ids').write({
+ 'project_id': project_id,
+ 'account_id': self.env['project.project'].browse(project_id).sudo().analytic_account_id.id
+ })
+ return result
\ No newline at end of file
diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py
index e27ee1a6534..0cc92853fbc 100644
--- a/addons/hr_timesheet/tests/test_timesheet.py
+++ b/addons/hr_timesheet/tests/test_timesheet.py
@@ -2,13 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
-from odoo.exceptions import AccessError
+from odoo.exceptions import AccessError, UserError
-class TestTimesheet(TransactionCase):
+class TestCommonTimesheet(TransactionCase):
def setUp(self):
- super(TestTimesheet, self).setUp()
+ super(TestCommonTimesheet, self).setUp()
self.project_customer = self.env['project.project'].create({
'name': 'Project X',
@@ -59,6 +59,9 @@ class TestTimesheet(TransactionCase):
'user_id': self.user_manager.id,
})
+
+class TestTimesheet(TestCommonTimesheet):
+
def test_log_timesheet(self):
""" Test when log timesheet : check analytic account, user and employee are correctly set. """
Timesheet = self.env['account.analytic.line']
@@ -115,3 +118,42 @@ class TestTimesheet(TransactionCase):
'employee_id': self.empl_employee2.id,
})
self.assertEquals(timesheet1.user_id, self.user_employee2, 'Changing timesheet employee should change the related user')
+
+ def test_transfert_project(self):
+ """ Test transfert task with timesheet to another project """
+ Timesheet = self.env['account.analytic.line']
+ # create a second project
+ self.project_customer2 = self.env['project.project'].create({
+ 'name': 'Project NUMBER DEUX',
+ 'allow_timesheets': True,
+ })
+ # employee 1 log some timesheet on task 1
+ Timesheet.create({
+ 'project_id': self.project_customer.id,
+ 'task_id': self.task1.id,
+ 'name': 'my first timesheet',
+ 'unit_amount': 4,
+ })
+
+ timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
+ timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
+ self.assertEquals(timesheet_count1, 1, "One timesheet in project 1")
+ self.assertEquals(timesheet_count2, 0, "No timesheet in project 2")
+ self.assertEquals(len(self.task1.timesheet_ids), 1, "The timesheet should be linked to task 1")
+
+ # change project of task 1
+ self.task1.write({
+ 'project_id': self.project_customer2.id
+ })
+
+ timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
+ timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
+ self.assertEquals(timesheet_count1, 0, "No timesheet in project 1")
+ self.assertEquals(timesheet_count2, 1, "One timesheet in project 2")
+ self.assertEquals(len(self.task1.timesheet_ids), 1, "The timesheet should be linked to task 1")
+
+ # it is forbidden to set a task with timesheet without project
+ with self.assertRaises(UserError):
+ self.task1.write({
+ 'project_id': False
+ })
diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
index 8ecca4ee5d4..7e5fa858cf1 100644
--- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
+++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
@@ -67,7 +67,7 @@ class HrContract(models.Model):
help="Number of days of paid leaves the employee gets per year.")
holidays_editable = fields.Boolean(string="Editable Leaves", default=True)
holidays_compensation = fields.Monetary(compute='_compute_holidays_compensation', string="Holidays Compensation")
- wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', string="Wage update with holidays retenues")
+ wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', inverse='_inverse_wage_with_holidays', string="Wage update with holidays retenues")
additional_net_amount = fields.Monetary(string="Net Supplements",
help="Monthly net amount the employee receives.")
retained_net_amount = fields.Monetary(sting="Net Retained",
@@ -85,6 +85,23 @@ class HrContract(models.Model):
else:
contract.wage_with_holidays = contract.wage
+ def _inverse_wage_with_holidays(self):
+ for contract in self:
+ if contract.holidays > 20.0:
+ remaining_for_gross = contract.wage_with_holidays * (13.0 + 13.0 * 0.3507 + 0.92)
+ yearly_cost = remaining_for_gross \
+ + 12.0 * contract.representation_fees \
+ + 12.0 * contract.fuel_card \
+ + 12.0 * contract.internet \
+ + 12.0 * (contract.mobile + contract.mobile_plus) \
+ + 12.0 * contract.transport_employer_cost \
+ + (1.326 / 1.05 * 12.0) * contract.commission_on_target \
+ + 220.0 * contract.meal_voucher_paid_by_employer
+ contract.final_yearly_costs = yearly_cost / (1.0 - (contract.holidays - 20.0) / 231.0)
+ contract.wage = contract._get_gross_from_employer_costs(contract.final_yearly_costs)
+ else:
+ contract.wage = contract.wage_with_holidays
+
@api.depends('transport_mode', 'company_car_total_depreciated_cost',
'public_transport_reimbursed_amount', 'others_reimbursed_amount')
def _compute_transport_employer_cost(self):
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index e7f487b6e6e..9fa6701be16 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -148,7 +148,7 @@ class Message(models.Model):
given, restrict to messages written in one of those channels. """
partner_id = self.env.user.partner_id.id
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
- if domain is None and delete_mode:
+ if not domain and delete_mode:
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
args = [(partner_id,)]
if channel_ids:
@@ -468,6 +468,7 @@ class Message(models.Model):
- if author_id == pid, uid is the author, OR
- uid belongs to a notified channel, OR
- uid is in the specified recipients, OR
+ - uid has a notification on the message, OR
- uid have read access to the related document is model, res_id
- otherwise: remove the id
"""
@@ -495,17 +496,22 @@ class Message(models.Model):
# check read access rights before checking the actual rules on the given ids
super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read')
- self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, partner_rel.res_partner_id, channel_partner.channel_id as channel_id
+ self._cr.execute("""
+ SELECT DISTINCT m.id, m.model, m.res_id, m.author_id,
+ COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
+ channel_partner.channel_id as channel_id
FROM "%s" m
LEFT JOIN "mail_message_res_partner_rel" partner_rel
- ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s)
+ ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
+ LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
+ ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
ON channel_rel.mail_message_id = m.id
LEFT JOIN "mail_channel" channel
ON channel.id = channel_rel.mail_channel_id
LEFT JOIN "mail_channel_partner" channel_partner
- ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s)
- WHERE m.id = ANY (%%s)""" % self._table, (pid, pid, ids,))
+ ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
+ WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
if author_id == pid:
author_ids.add(id)
@@ -533,6 +539,7 @@ class Message(models.Model):
- read: if
- author_id == pid, uid is the author OR
- uid is in the recipients (partner_ids) OR
+ - uid has been notified (needaction) OR
- uid is member of a listern channel (channel_ids.partner_ids) OR
- uid have read access to the related document if model, res_id
- otherwise: raise
@@ -583,17 +590,22 @@ class Message(models.Model):
message_values = dict((res_id, {}) for res_id in self.ids)
if operation in ['read', 'write']:
- self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, partner_rel.res_partner_id, channel_partner.channel_id as channel_id
+ self._cr.execute("""
+ SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
+ COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
+ channel_partner.channel_id as channel_id
FROM "%s" m
LEFT JOIN "mail_message_res_partner_rel" partner_rel
- ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s)
+ ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
+ LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
+ ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
ON channel_rel.mail_message_id = m.id
LEFT JOIN "mail_channel" channel
ON channel.id = channel_rel.mail_channel_id
LEFT JOIN "mail_channel_partner" channel_partner
- ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s)
- WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.partner_id.id, self.env.user.partner_id.id, self.ids,))
+ ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
+ WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
message_values[mid] = {
'model': rmod,
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index c8496d62ef4..9d7c357131c 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -258,7 +258,7 @@ var Chatter = Widget.extend(chat_mixin, {
var suggested_partners = [];
var thread_recipients = result[self.context.default_res_id];
_.each(thread_recipients, function (recipient) {
- var parsed_email = utils.parse_email(recipient[1]);
+ var parsed_email = recipient[1] && utils.parse_email(recipient[1]);
suggested_partners.push({
checked: true,
partner_id: recipient[0],
diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js
index 6364be77eb5..49d1705eccd 100644
--- a/addons/mail/static/src/js/utils.js
+++ b/addons/mail/static/src/js/utils.js
@@ -100,15 +100,17 @@ function inline (node, transform_children) {
// Parses text to find email: Tagada
-> [Tagada, address@mail.fr] or False
function parse_email (text) {
- var result = text.match(/(.*)<(.*@.*)>/);
- if (result) {
- return [_.str.trim(result[1]), _.str.trim(result[2])];
+ if (text){
+ var result = text.match(/(.*)<(.*@.*)>/);
+ if (result) {
+ return [_.str.trim(result[1]), _.str.trim(result[2])];
+ }
+ result = text.match(/(.*@.*)/);
+ if (result) {
+ return [_.str.trim(result[1]), _.str.trim(result[1])];
+ }
+ return [text, false];
}
- result = text.match(/(.*@.*)/);
- if (result) {
- return [_.str.trim(result[1]), _.str.trim(result[1])];
- }
- return [text, false];
}
// Replaces textarea text into html text (add , )
diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py
index 8e2264ad6d3..7a65fc755e4 100644
--- a/addons/mail/tests/test_mail_features.py
+++ b/addons/mail/tests/test_mail_features.py
@@ -24,7 +24,6 @@ class TestMailFeatures(TestMail):
self.env['res.partner'].create({'name': 'Actual Partner'})
self.assertTrue(record.name)
-
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_needaction(self):
# needaction use Inbox notification
diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py
index 566a038d9ef..64b4c16ecbd 100644
--- a/addons/mail/tests/test_mail_message.py
+++ b/addons/mail/tests/test_mail_message.py
@@ -288,3 +288,70 @@ class TestMailMessageAccess(TestMail):
msg_cnt = len(self.test_pigs.message_ids)
self.test_pigs.message_post(body='Hi!', subject='test')
self.assertEqual(len(self.test_pigs.message_ids), msg_cnt + 1)
+
+ @mute_logger('openerp.addons.mail.models.mail_mail')
+ def test_mark_all_as_read(self):
+ self.user_employee.notification_type = 'inbox'
+ emp_partner = self.user_employee.partner_id.sudo(self.user_employee.id)
+
+ group_private = self.env['mail.channel'].with_context({
+ 'mail_create_nolog': True,
+ 'mail_create_nosubscribe': True,
+ 'mail_channel_noautofollow': True,
+ }).create({
+ 'name': 'Private',
+ 'description': 'Private James R.',
+ 'public': 'private',
+ 'alias_name': 'private',
+ 'alias_contact': 'followers'}
+ ).with_context({'mail_create_nosubscribe': False})
+
+ # mark all as read clear needactions
+ group_private.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[emp_partner.id])
+ emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+ na_count = emp_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
+
+ # mark all as read also clear inaccessible needactions
+ new_msg = group_private.message_post(body='Zest', message_type='comment', subtype='mail.mt_comment', partner_ids=[emp_partner.id])
+ needaction_accessible = len(emp_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
+
+ new_msg.sudo().partner_ids = self.env['res.partner']
+ emp_partner.env['mail.message'].search([['needaction', '=', True]])
+ needaction_length = len(emp_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_length, 1, "message should still be readable when notified")
+
+ na_count = emp_partner.get_needaction_count()
+ self.assertEqual(na_count, 1, "message not accessible is currently still counted")
+
+ emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+ na_count = emp_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
+
+ @mute_logger('openerp.addons.mail.models.mail_mail')
+ def test_mark_all_as_read_share(self):
+ self.user_portal.notification_type = 'inbox'
+ portal_partner = self.user_portal.partner_id.sudo(self.user_portal.id)
+
+ # mark all as read clear needactions
+ self.group_pigs.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[portal_partner.id])
+ portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+ na_count = portal_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
+
+ # mark all as read also clear inaccessible needactions
+ new_msg = self.group_pigs.message_post(body='Zest', message_type='comment', subtype='mail.mt_comment', partner_ids=[portal_partner.id])
+ needaction_accessible = len(portal_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
+
+ new_msg.sudo().partner_ids = self.env['res.partner']
+ needaction_length = len(portal_partner.env['mail.message'].search([['needaction', '=', True]]))
+ self.assertEqual(needaction_length, 1, "message should still be readable when notified")
+
+ na_count = portal_partner.get_needaction_count()
+ self.assertEqual(na_count, 1, "message not accessible is currently still counted")
+
+ portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
+ na_count = portal_partner.get_needaction_count()
+ self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index 0afbbc71e1b..d46b688ca79 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -213,6 +213,15 @@ class MaintenanceEquipment(models.Model):
self.message_subscribe_users(user_ids=[vals['owner_user_id']])
return super(MaintenanceEquipment, self).write(vals)
+ @api.model
+ def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None):
+ """ mail.thread override so user_id which has no special access allowance is not
+ automatically subscribed.
+ """
+ if auto_follow_fields is None:
+ auto_follow_fields = []
+ return super(MaintenanceEquipment, self)._message_get_auto_subscribe_fields(updated_fields, auto_follow_fields)
+
@api.model
def _read_group_category_ids(self, categories, domain, order):
""" Read group customization in order to display all the categories in
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 195829b82e2..b059e825628 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -95,7 +95,9 @@ class AccountInvoice(models.Model):
def _onchange_currency_id(self):
if self.currency_id:
for line in self.invoice_line_ids.filtered(lambda r: r.purchase_line_id):
- line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice).compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
+ if line.purchase_id.currency_id != self.currency_id:
+ line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice)\
+ .compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
@api.onchange('invoice_line_ids')
def _onchange_origin(self):
diff --git a/addons/purchase/security/purchase_security.xml b/addons/purchase/security/purchase_security.xml
index 9327210daf9..8b4f7f2227d 100644
--- a/addons/purchase/security/purchase_security.xml
+++ b/addons/purchase/security/purchase_security.xml
@@ -61,7 +61,7 @@
Portal Purchase Orders
- [('message_partner_ids','child_of',[user.commercial_partner_id.id])]
+ ['|', ('message_partner_ids','child_of',[user.commercial_partner_id.id]),('partner_id', 'child_of', [user.commercial_partner_id.id])]
@@ -72,7 +72,7 @@
Portal Purhcase Orders Line
- [('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id])]
+ ['|',('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id]),('order_id.partner_id','child_of',[user.commercial_partner_id.id])]
diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py
index f61014caf0c..a1eb43c76c6 100644
--- a/addons/stock_landed_costs/models/stock_landed_cost.py
+++ b/addons/stock_landed_costs/models/stock_landed_cost.py
@@ -171,9 +171,13 @@ class LandedCost(models.Model):
val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id})
self.env['stock.valuation.adjustment.lines'].create(val_line_values)
total_qty += val_line_values.get('quantity', 0.0)
- total_cost += val_line_values.get('former_cost', 0.0)
total_weight += val_line_values.get('weight', 0.0)
total_volume += val_line_values.get('volume', 0.0)
+
+ former_cost = val_line_values.get('former_cost', 0.0)
+ # round this because former_cost on the valuation lines is also rounded
+ total_cost += tools.float_round(former_cost, precision_digits=digits[1]) if digits else former_cost
+
total_line += 1
for line in cost.cost_lines:
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 9e97ec40686..5a7cb536f43 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -207,6 +207,7 @@ var KanbanController = BasicController.extend({
* @param {OdooEvent} event
*/
_onButtonClicked: function (event) {
+ event.stopPropagation();
var self = this;
var attrs = event.data.attrs;
var record = event.data.record;
diff --git a/addons/website_form/static/src/js/website_form.js b/addons/website_form/static/src/js/website_form.js
index aa540650473..47ba66c244b 100644
--- a/addons/website_form/static/src/js/website_form.js
+++ b/addons/website_form/static/src/js/website_form.js
@@ -22,7 +22,7 @@ odoo.define('website_form.animation', function (require) {
start: function () {
var self = this;
- qweb.add_template('/website_form/static/src/xml/website_form.xml');
+ this.templates_loaded = ajax.loadXML('/website_form/static/src/xml/website_form.xml', qweb);
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
// Initialize datetimepickers
@@ -233,7 +233,10 @@ odoo.define('website_form.animation', function (require) {
if (status !== 'success') { // Restore send button behavior if result is an error
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
}
- this.$target.find('#o_website_form_result').replaceWith(qweb.render("website_form.status_" + status));
+ var $result = this.$('#o_website_form_result');
+ this.templates_loaded.done(function () {
+ $result.replaceWith(qweb.render("website_form.status_" + status));
+ });
},
});
});
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 9248e27fcef..3f05085554f 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -684,6 +684,7 @@
+
diff --git a/addons/website_slides/views/website_slides.xml b/addons/website_slides/views/website_slides.xml
index 1d51e1aabb9..dd6d9938113 100644
--- a/addons/website_slides/views/website_slides.xml
+++ b/addons/website_slides/views/website_slides.xml
@@ -112,11 +112,6 @@
Upload
-
-
- Upload
-
-
diff --git a/doc/cla/individual/nguyenductamlhp.md b/doc/cla/individual/nguyenductamlhp.md
new file mode 100644
index 00000000000..898f465c52b
--- /dev/null
+++ b/doc/cla/individual/nguyenductamlhp.md
@@ -0,0 +1,11 @@
+Vietnam, 2018-01-16,
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Nguyen Duc Tam nguyenductamlhp@gmail.com https://github.com/nguyenductamlhp