[MERGE] forward port branch saas-16 up to f99c4f2b48
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -168,6 +168,10 @@
|
||||
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
||||
<field name="currency_exchange_journal_id"/>
|
||||
</div>
|
||||
<div class="row mt16">
|
||||
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
||||
<field name="currency_exchange_journal_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -402,5 +402,57 @@ QUnit.test('non-existing action in a dashboard', function (assert) {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('clicking on a kanban\'s button should trigger the action', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'board',
|
||||
data: this.data,
|
||||
arch: '<form string="My Dashboard">' +
|
||||
'<board style="2-1">' +
|
||||
'<column>' +
|
||||
'<action name="149" string="Partner" view_mode="kanban" id="action_0_1"></action>' +
|
||||
'</column>' +
|
||||
'</board>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner,false,kanban':
|
||||
'<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="foo"/>' +
|
||||
'</div>' +
|
||||
'<div><button name="sitting_on_a_park_bench" type="object">Eying little girls with bad intent</button>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
},
|
||||
intercepts: {
|
||||
execute_action: function (event) {
|
||||
var data = event.data;
|
||||
assert.strictEqual(data.env.model, 'partner', "should have correct model");
|
||||
assert.strictEqual(data.action_data.name, 'sitting_on_a_park_bench',
|
||||
"should call correct method");
|
||||
}
|
||||
},
|
||||
|
||||
mockRPC: function (route) {
|
||||
if (route === '/board/static/src/img/layout_1-1-1.png') {
|
||||
return $.when();
|
||||
}
|
||||
if (route === '/web/action/load') {
|
||||
return $.when({res_model: 'partner', view_mode: 'kanban', views: [[false, 'kanban']]});
|
||||
}
|
||||
if (route === '/web/dataset/search_read') {
|
||||
return $.when({records: [{foo: 'aqualung'}]});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
form.$('.o_kanban_test').find('button:first').click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -122,10 +122,10 @@
|
||||
<field name="stop" attrs="{'invisible': True}"/>
|
||||
<field name="id" attrs="{'invisible': True}"/>
|
||||
|
||||
<field name="start_date" string="Starting at" attrs="{'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||
<field name="stop_date" string="Ending at" attrs="{'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||
|
||||
<field name="start_datetime" string="Starting at" attrs="{'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||
<field name="stop_datetime" invisible="1"/>
|
||||
<label for="duration" attrs="{'invisible': [('allday','=',True)]}"/>
|
||||
<div attrs="{'invisible': [('allday','=',True)]}">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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: '<form string="Partners">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</form>',
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -8,4 +8,9 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="google_drive tests" inherit_id="web.qunit_suite">
|
||||
<xpath expr="//t[@t-set='head']" position="inside">
|
||||
<script type="text/javascript" src="/google_drive/static/tests/gdrive_test.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
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
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -100,15 +100,17 @@ function inline (node, transform_children) {
|
||||
|
||||
// Parses text to find email: Tagada <address@mail.fr> -> [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 <p>, <a>)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<record id="portal_purchase_order_user_rule" model="ir.rule">
|
||||
<field name="name">Portal Purchase Orders</field>
|
||||
<field name="model_id" ref="purchase.model_purchase_order"/>
|
||||
<field name="domain_force">[('message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
|
||||
<field name="domain_force">['|', ('message_partner_ids','child_of',[user.commercial_partner_id.id]),('partner_id', 'child_of', [user.commercial_partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
@@ -72,7 +72,7 @@
|
||||
<record id="portal_purchase_order_line_rule" model="ir.rule">
|
||||
<field name="name">Portal Purhcase Orders Line</field>
|
||||
<field name="model_id" ref="purchase.model_purchase_order_line"/>
|
||||
<field name="domain_force">[('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
|
||||
<field name="domain_force">['|',('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id]),('order_id.partner_id','child_of',[user.commercial_partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -684,6 +684,7 @@
|
||||
</template>
|
||||
|
||||
<template id="wizard_checkout" name="Wizard Checkout">
|
||||
<t t-set="website_sale_order" t-value="website.sale_get_order()"/>
|
||||
<ul class="wizard pull-right hidden-xs">
|
||||
<li t-att-class="step==10 and 'text-primary' or 'text-muted'" id="step10">
|
||||
<t t-set="step_review_order" t-value="step>10 and step<50"/>
|
||||
|
||||
@@ -112,11 +112,6 @@
|
||||
<i class="fa fa-cloud-upload"></i> Upload
|
||||
</a>
|
||||
</t>
|
||||
<t t-if="channel.can_upload and is_public_user">
|
||||
<a class="btn btn-primary" title="Upload Presentation" t-attf-href="/web/login?redirect=/slides/#{slug(channel)}">
|
||||
<i class="fa fa-cloud-upload"></i> Upload
|
||||
</a>
|
||||
</t>
|
||||
</h3>
|
||||
</div>
|
||||
<!-- Search box -->
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user