summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-01-22 17:47:32 +0100
committerChristophe Simonis <chs@odoo.com>2018-01-22 17:47:32 +0100
commitfa1f2132e79f2ac2314a8b417d5d35d6571bd41b (patch)
tree3789a8335dd5089998de3c5c6a391b2d95a85187 /addons
parentdb27489fe756b24647e993a22340705625839698 (diff)
parent950d9b2369046f2e3199d144df8edbd0e63f8a2f (diff)
[MERGE] forward port branch 11.0 up to 950d9b23690
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_invoice.py19
-rw-r--r--addons/account/models/company.py3
-rw-r--r--addons/account/views/account_portal_templates.xml2
-rw-r--r--addons/account/views/report_invoice.xml14
-rw-r--r--addons/account/views/res_config_settings_views.xml4
-rw-r--r--addons/board/static/tests/dashboard_tests.js52
-rw-r--r--addons/calendar/views/calendar_views.xml6
-rw-r--r--addons/google_drive/models/google_drive.py4
-rw-r--r--addons/google_drive/static/src/js/gdrive.js139
-rw-r--r--addons/google_drive/static/tests/gdrive_test.js112
-rw-r--r--addons/google_drive/views/google_drive_templates.xml5
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml6
-rw-r--r--addons/hr_holidays/wizard/hr_holidays_summary_department.py2
-rw-r--r--addons/hr_payroll/models/hr_payslip.py1
-rw-r--r--addons/hr_timesheet/models/project.py17
-rw-r--r--addons/hr_timesheet/security/hr_timesheet_security.xml4
-rw-r--r--addons/hr_timesheet/tests/test_timesheet.py48
-rw-r--r--addons/iap/models/iap.py10
-rw-r--r--addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py21
-rw-r--r--addons/l10n_fr_certification/models/account.py8
-rw-r--r--addons/l10n_fr_pos_cert/models/pos.py8
-rw-r--r--addons/mail/models/mail_message.py30
-rw-r--r--addons/mail/models/res_users.py8
-rw-r--r--addons/mail/static/src/js/chatter.js2
-rw-r--r--addons/mail/static/src/js/systray.js4
-rw-r--r--addons/mail/static/src/js/utils.js18
-rw-r--r--addons/mail/static/tests/client_action_mobile_tests.js105
-rw-r--r--addons/mail/static/tests/client_action_tests.js88
-rw-r--r--addons/mail/views/mail_templates.xml7
-rw-r--r--addons/maintenance/models/maintenance.py9
-rw-r--r--addons/mass_mailing/models/mass_mailing.py5
-rw-r--r--addons/mrp/models/mrp_unbuild.py31
-rw-r--r--addons/mrp/models/stock_move.py6
-rw-r--r--addons/mrp/views/mrp_unbuild_views.xml2
-rw-r--r--addons/mrp/wizard/mrp_product_produce.py32
-rw-r--r--addons/payment/controllers/portal.py5
-rw-r--r--addons/payment_authorize/models/payment.py2
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js2
-rw-r--r--addons/phone_validation/tools/phone_validation.py3
-rw-r--r--addons/phone_validation/views/res_company_view.xml4
-rw-r--r--addons/product/views/product_views.xml9
-rw-r--r--addons/product_expiry/models/production_lot.py2
-rw-r--r--addons/project/models/project.py10
-rw-r--r--addons/project_timesheet_holidays/models/hr_holidays.py7
-rw-r--r--addons/purchase/models/account_invoice.py4
-rw-r--r--addons/purchase/models/purchase.py8
-rw-r--r--addons/purchase/security/purchase_security.xml4
-rw-r--r--addons/sale/models/analytic.py14
-rw-r--r--addons/sale/report/sale_report_templates.xml7
-rw-r--r--addons/sms/wizard/send_sms.py34
-rw-r--r--addons/stock/models/stock_location.py4
-rw-r--r--addons/stock/models/stock_move.py14
-rw-r--r--addons/stock/models/stock_move_line.py70
-rw-r--r--addons/stock/views/stock_picking_views.xml2
-rw-r--r--addons/stock_landed_costs/models/stock_landed_cost.py6
-rw-r--r--addons/stock_picking_batch/wizard/stock_picking_to_batch_views.xml2
-rw-r--r--addons/test_mail/tests/test_mail_message.py68
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js5
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js1
-rw-r--r--addons/web/static/src/js/fields/field_utils.js5
-rw-r--r--addons/web/static/src/js/services/data_manager.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js23
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js7
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js8
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js16
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js69
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js1
-rw-r--r--addons/web/static/tests/views/form_tests.js4
-rw-r--r--addons/web/static/tests/views/kanban_tests.js147
-rw-r--r--addons/web/tests/test_js.py2
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js3
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js66
-rw-r--r--addons/website_form/static/src/js/website_form.js7
-rw-r--r--addons/website_sale/static/src/js/website_sale_validate.js5
-rw-r--r--addons/website_sale/views/templates.xml1
-rw-r--r--addons/website_sale_delivery/static/src/js/website_sale_delivery.js2
-rw-r--r--addons/website_slides/static/src/js/slides.js8
-rw-r--r--addons/website_slides/views/website_slides.xml5
78 files changed, 1111 insertions, 391 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index dacbc7e2df1..b89c22d529f 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -350,7 +350,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,
@@ -998,8 +998,6 @@ class AccountInvoice(models.Model):
'invoice_id': self.id,
'analytic_tag_ids': analytic_tag_ids
}
- if line['account_analytic_id']:
- move_line_dict['analytic_line_ids'] = [(0, 0, line._get_analytic_line())]
res.append(move_line_dict)
return res
@@ -1445,21 +1443,6 @@ class AccountInvoiceLine(models.Model):
_description = "Invoice Line"
_order = "invoice_id,sequence,id"
- @api.multi
- def _get_analytic_line(self):
- ref = self.invoice_id.number
- return {
- 'name': self.name,
- 'date': self.invoice_id.date_invoice,
- 'account_id': self.account_analytic_id.id,
- 'unit_amount': self.quantity,
- 'amount': self.price_subtotal_signed,
- 'product_id': self.product_id.id,
- 'product_uom_id': self.uom_id.id,
- 'general_account_id': self.account_id.id,
- 'ref': ref,
- }
-
@api.one
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index ed821367846..d73dbf82c64 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -174,7 +174,6 @@ Best Regards,'''))
def setting_init_fiscal_year_action(self):
""" Called by the 'Fiscal Year Opening' button of the setup bar."""
company = self.env.user.company_id
- company.create_op_move_if_non_existant()
new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id})
view_id = self.env.ref('account.setup_financial_year_opening_form').id
@@ -265,7 +264,7 @@ Best Regards,'''))
default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1)
if not default_journal:
- raise UserError(_("No miscellaneous journal could be found. Please create one before proceeding."))
+ raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding."))
self.account_opening_move_id = self.env['account.move'].create({
'name': _('Opening Journal Entry'),
diff --git a/addons/account/views/account_portal_templates.xml b/addons/account/views/account_portal_templates.xml
index 2ff82b4bc3a..51ac3563c2e 100644
--- a/addons/account/views/account_portal_templates.xml
+++ b/addons/account/views/account_portal_templates.xml
@@ -179,7 +179,7 @@
</td>
</tr>
<tr t-foreach="range(max(5-len(invoice.invoice_line_ids),0))" t-as="l">
- <td>&amp;nbsp;</td>
+ <td t-translation="off">&amp;nbsp;</td>
<td class="hidden"></td>
<td></td>
<td></td>
diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml
index e39b1605d3c..270c792c606 100644
--- a/addons/account/views/report_invoice.xml
+++ b/addons/account/views/report_invoice.xml
@@ -88,7 +88,7 @@
</td>
</tr>
<tr t-foreach="range(max(5-len(o.invoice_line_ids),0))" t-as="l">
- <td>&amp;nbsp;</td>
+ <td t-translation="off">&amp;nbsp;</td>
<td class="hidden"></td>
<td></td>
<td></td>
@@ -112,12 +112,6 @@
<t t-foreach="o._get_tax_amount_by_group()" t-as="amount_by_group">
<tr style="border-bottom:1px solid #dddddd;">
<t t-if="len(o.tax_line_ids) == 1 and o.amount_untaxed == amount_by_group[2]">
- <td><span t-esc="amount_by_group[0]"/></td>
- <td class="text-right">
- <span t-esc="amount_by_group[3]"/>
- </td>
- </t>
- <t t-else="">
<td>
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>
@@ -125,6 +119,12 @@
</span>
</td>
<td class="text-right">
+ <span t-esc="amount_by_group[3]"/>
+ </td>
+ </t>
+ <t t-else="">
+ <td><span t-esc="amount_by_group[0]"/></td>
+ <td class="text-right">
<span t-esc="amount_by_group[3]" />
</td>
</t>
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index 22726578d1b..7a878d265c7 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -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>
diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index 21067b32c44..ab0feaf2c96 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -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();
+});
+
});
diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml
index fd5971a353d..c9bca3b52cf 100644
--- a/addons/calendar/views/calendar_views.xml
+++ b/addons/calendar/views/calendar_views.xml
@@ -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)]}">
diff --git a/addons/google_drive/models/google_drive.py b/addons/google_drive/models/google_drive.py
index 3e7235c2d75..cf5e8c55345 100644
--- a/addons/google_drive/models/google_drive.py
+++ b/addons/google_drive/models/google_drive.py
@@ -9,6 +9,7 @@ import werkzeug.urls
from odoo import api, fields, models
from odoo.exceptions import RedirectWarning, UserError
+from odoo.tools import pycompat
from odoo.tools.safe_eval import safe_eval
from odoo.tools.translate import _
@@ -153,6 +154,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, pycompat.string_types):
+ 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: '<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();
+ });
+});
+
+});
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 @@
</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>
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index d3b8d74f00e..28a852af5df 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -27,13 +27,13 @@
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<form string="Expenses" class="o_expense_form">
- <header>
+ <header invisible="context.get('expense_adding_line')">
<button name="submit_expenses" states="draft" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit"/>
<field name="state" widget="statusbar" statusbar_visible="draft,reported,done"/>
<button name="view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
</header>
<sheet>
- <div class="oe_button_box">
+ <div class="oe_button_box" invisible="context.get('expense_adding_line')">
<button name="action_get_attachment_view"
class="oe_stat_button"
icon="fa-book"
@@ -419,7 +419,7 @@
</group>
<notebook>
<page string="Expense Lines">
- <field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'not_delete': True, 'reload_on_button': True, 'no_create': True}">
+ <field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'not_delete': True, 'reload_on_button': True, 'no_create': True}" context="{'expense_adding_line': True}">
<tree decoration-danger="is_refused">
<field name="date"/>
<field name="name"/>
diff --git a/addons/hr_holidays/wizard/hr_holidays_summary_department.py b/addons/hr_holidays/wizard/hr_holidays_summary_department.py
index 82a398c3c4c..1d8cf868b46 100644
--- a/addons/hr_holidays/wizard/hr_holidays_summary_department.py
+++ b/addons/hr_holidays/wizard/hr_holidays_summary_department.py
@@ -31,4 +31,4 @@ class HolidaysSummaryDept(models.TransientModel):
'model': 'hr.department',
'form': data
}
- return self.env.ref('hr_holidays.action_report_holidayssummary').report_action(departments, data=datas)
+ return self.env.ref('hr_holidays.action_report_holidayssummary').with_context(from_transient_model=True).report_action(departments, data=datas)
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/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml
index 9a75f999347..332d4ec08e1 100644
--- a/addons/hr_timesheet/security/hr_timesheet_security.xml
+++ b/addons/hr_timesheet/security/hr_timesheet_security.xml
@@ -48,7 +48,7 @@
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
- <field name="perm_read" eval="1"/>
+ <field name="perm_read" eval="0"/>
</record>
<record id="timesheet_rule_portal" model="ir.rule">
@@ -68,4 +68,4 @@
</data>
-</odoo> \ No newline at end of file
+</odoo>
diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py
index 0c9fadc6a90..f5ad8f40076 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()
# customer partner
self.partner = self.env['res.partner'].create({
@@ -69,6 +69,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']
@@ -134,3 +137,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/iap/models/iap.py b/addons/iap/models/iap.py
index 4f24f68af99..6f24205f8d4 100644
--- a/addons/iap/models/iap.py
+++ b/addons/iap/models/iap.py
@@ -72,6 +72,11 @@ def jsonrpc(url, method='call', params=None):
#----------------------------------------------------------
# Helpers for proxy
#----------------------------------------------------------
+class IapTransaction(object):
+
+ def __init__(self):
+ self.credit = None
+
@contextlib.contextmanager
def charge(env, key, account_token, credit, description=None, credit_template=None):
"""
@@ -107,7 +112,9 @@ def charge(env, key, account_token, credit, description=None, credit_template=No
e.args = (json.dumps(arguments),)
raise e
try:
- yield
+ transaction = IapTransaction()
+ transaction.credit = credit
+ yield transaction
except Exception as e:
params = {
'token': transaction_token,
@@ -119,6 +126,7 @@ def charge(env, key, account_token, credit, description=None, credit_template=No
params = {
'token': transaction_token,
'key': key,
+ 'credit_to_capture': transaction.credit,
}
r = jsonrpc(endpoint + '/iap/1/capture', params=params) # noqa
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..c80ccc983a5 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",
@@ -76,7 +76,7 @@ class HrContract(models.Model):
default=lambda self: self.get_attribute('eco_checks', 'default_value'),
help="Yearly amount the employee receives in the form of eco vouchers.")
- @api.depends('holidays', 'wage')
+ @api.depends('holidays', 'wage', 'final_yearly_costs')
def _compute_wage_with_holidays(self):
for contract in self:
if contract.holidays > 20.0:
@@ -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/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py
index ff7acf2678e..208b5238967 100644
--- a/addons/l10n_fr_certification/models/account.py
+++ b/addons/l10n_fr_certification/models/account.py
@@ -36,13 +36,13 @@ class AccountMove(models.Model):
_('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.'))
#build and return the hash
- return self._compute_hash(prev_move.l10n_fr_hash if prev_move else '')
+ return self._compute_hash(prev_move.l10n_fr_hash if prev_move else u'')
def _compute_hash(self, previous_hash):
""" Computes the hash of the browse_record given as self, based on the hash
of the previous record in the company's securisation sequence given as parameter"""
self.ensure_one()
- hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
+ hash_string = sha256((previous_hash + self.l10n_fr_string_to_hash).encode('utf-8'))
return hash_string.hexdigest()
def _compute_string_to_hash(self):
@@ -63,7 +63,7 @@ class AccountMove(models.Model):
values[k] = _getattrstring(line, field)
#make the json serialization canonical
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
- move.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
+ move.l10n_fr_string_to_hash = dumps(values, sort_keys=True,
ensure_ascii=True, indent=None,
separators=(',',':'))
@@ -118,7 +118,7 @@ class AccountMove(models.Model):
if not moves:
raise UserError(_('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.') % self.env.user.company_id.name)
- previous_hash = ''
+ previous_hash = u''
start_move_info = []
for move in moves:
if move.l10n_fr_hash != move._compute_hash(previous_hash=previous_hash):
diff --git a/addons/l10n_fr_pos_cert/models/pos.py b/addons/l10n_fr_pos_cert/models/pos.py
index f3a35a5a878..d74641e3c88 100644
--- a/addons/l10n_fr_pos_cert/models/pos.py
+++ b/addons/l10n_fr_pos_cert/models/pos.py
@@ -79,13 +79,13 @@ class pos_order(models.Model):
_('An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order.'))
#build and return the hash
- return self._compute_hash(prev_order.l10n_fr_hash if prev_order else '')
+ return self._compute_hash(prev_order.l10n_fr_hash if prev_order else u'')
def _compute_hash(self, previous_hash):
""" Computes the hash of the browse_record given as self, based on the hash
of the previous record in the company's securisation sequence given as parameter"""
self.ensure_one()
- hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
+ hash_string = sha256((previous_hash + self.l10n_fr_string_to_hash).encode('utf-8'))
return hash_string.hexdigest()
def _compute_string_to_hash(self):
@@ -108,7 +108,7 @@ class pos_order(models.Model):
values[k] = _getattrstring(line, field)
#make the json serialization canonical
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
- order.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
+ order.l10n_fr_string_to_hash = dumps(values, sort_keys=True,
ensure_ascii=True, indent=None,
separators=(',',':'))
@@ -155,7 +155,7 @@ class pos_order(models.Model):
if not orders:
raise UserError(_('There isn\'t any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS') % self.env.user.company_id.name)
- previous_hash = ''
+ previous_hash = u''
start_order_info = []
for order in orders:
if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
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/models/res_users.py b/addons/mail/models/res_users.py
index 3d6518010e6..86fc989d129 100644
--- a/addons/mail/models/res_users.py
+++ b/addons/mail/models/res_users.py
@@ -125,7 +125,7 @@ class Users(models.Model):
@api.model
def activity_user_count(self):
- query = """SELECT m.name, count(*), act.res_model as model,
+ query = """SELECT m.id, count(*), act.res_model as model,
CASE
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue'
@@ -134,16 +134,18 @@ class Users(models.Model):
FROM mail_activity AS act
JOIN ir_model AS m ON act.res_model_id = m.id
WHERE user_id = %s
- GROUP BY m.name, states, act.res_model;
+ GROUP BY m.id, states, act.res_model;
"""
self.env.cr.execute(query, [self.env.uid])
activity_data = self.env.cr.dictfetchall()
+ model_ids = [a['id'] for a in activity_data]
+ model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()}
user_activities = {}
for activity in activity_data:
if not user_activities.get(activity['model']):
user_activities[activity['model']] = {
- 'name': activity['name'],
+ 'name': model_names[activity['id']],
'model': activity['model'],
'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module),
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
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/systray.js b/addons/mail/static/src/js/systray.js
index 91bbdbe8275..2006ae297ef 100644
--- a/addons/mail/static/src/js/systray.js
+++ b/addons/mail/static/src/js/systray.js
@@ -3,7 +3,6 @@ odoo.define('mail.systray', function (require) {
var config = require('web.config');
var core = require('web.core');
-var framework = require('web.framework');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var Widget = require('web.Widget');
@@ -175,6 +174,9 @@ var ActivityMenu = Widget.extend({
return self._rpc({
model: 'res.users',
method: 'activity_user_count',
+ kwargs: {
+ context: session.user_context,
+ },
}).then(function (data) {
self.activities = data;
self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 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 <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])];
- }
- result = text.match(/(.*@.*)/);
- if (result) {
- return [_.str.trim(result[1]), _.str.trim(result[1])];
+ 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];
}
- return [text, false];
}
// Replaces textarea text into html text (add <p>, <a>)
diff --git a/addons/mail/static/tests/client_action_mobile_tests.js b/addons/mail/static/tests/client_action_mobile_tests.js
new file mode 100644
index 00000000000..e781ae34fd3
--- /dev/null
+++ b/addons/mail/static/tests/client_action_mobile_tests.js
@@ -0,0 +1,105 @@
+odoo.define('mail.client_action_mobile_tests', function (require) {
+"use strict";
+
+var session = require('web.session');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+
+var ChatAction = require('mail.chat_client_action');
+var chat_manager = require('mail.chat_manager');
+
+QUnit.module('mail', {}, function () {
+
+QUnit.module('Discuss client action in mobile', {
+ beforeEach: function () {
+ this.data = {
+ 'mail.message': {
+ fields: {},
+ },
+ };
+ this.createChatAction = function (params) {
+ var Parent = Widget.extend({
+ do_push_state: function () {},
+ });
+ var parent = new Parent();
+ testUtils.addMockEnvironment(parent, {
+ data: this.data,
+ archs: {
+ 'mail.message,false,search': '<search/>',
+ },
+ session: params.session || {},
+ intercepts: params.intercepts || {},
+ });
+ var chatAction = new ChatAction(parent, params);
+ chatAction.set_cp_bus(new Widget());
+ chatAction.appendTo($('#qunit-fixture'));
+
+ return chatAction;
+ };
+
+ },
+});
+
+QUnit.test('mobile basic rendering', function (assert) {
+ // This is a very basic first test for the client action. However, with
+ // the chat_manager, it is hard to override RPCs (for instance, the
+ // /mail/client_action route is always called when the test suite is
+ // launched), and we must wait for this RPC to be done before starting to
+ // test the interface. This should be refactored to facilitate the testing.
+ assert.expect(9);
+ var done = assert.async();
+
+ var rpc = session.rpc;
+
+ var chatAction = this.createChatAction({
+ id: 1,
+ context: {},
+ params: {},
+ intercepts: {
+ get_session: function (ev) {
+ ev.data.callback({});
+ },
+ },
+ session: {
+ rpc: function (route, args) {
+ if (args.method === 'message_fetch') {
+ return $.when([]);
+ }
+ return rpc.apply(this, arguments);
+ },
+ },
+ });
+
+ chat_manager.is_ready.then(function () {
+ // test basic rendering in mobile
+ assert.strictEqual(chatAction.$('.o_mail_chat_mobile_control_panel').length, 1,
+ "should have rendered a control panel");
+ assert.strictEqual(chatAction.$('.o_mail_chat_content .o_mail_no_content').length, 1,
+ "should display the no content message");
+ assert.strictEqual(chatAction.$('.o_mail_mobile_tabs').length, 1,
+ "should have rendered the tabs");
+ assert.ok(chatAction.$('.o_mail_mobile_tab[data-type=channel_inbox]').hasClass('active'),
+ "should be in inbox tab");
+ assert.strictEqual(chatAction.$('.o_mail_chat_mobile_inbox_buttons:visible').length, 1,
+ "inbox/starred buttons should be visible");
+ assert.ok(chatAction.$('.o_mail_chat_mobile_inbox_buttons .o_channel_inbox_item[data-type=channel_inbox]').hasClass('btn-primary'),
+ "should be in inbox");
+
+ // move to DMs tab
+ chatAction.$('.o_mail_mobile_tab[data-type=dm]').click();
+ assert.ok(chatAction.$('.o_mail_mobile_tab[data-type=dm]').hasClass('active'),
+ "should be in DMs tab");
+ assert.strictEqual(chatAction.$('.o_mail_chat_content .o_mail_no_content').length, 0,
+ "should display the no content message");
+ chatAction.$('.o_mail_chat_button_dm').click(); // click to add a channel
+ assert.strictEqual(chatAction.$('.o_mail_add_channel input:visible').length, 1,
+ "should display the input to add a channel");
+
+ chatAction.destroy();
+ done();
+ });
+});
+
+});
+
+});
diff --git a/addons/mail/static/tests/client_action_tests.js b/addons/mail/static/tests/client_action_tests.js
deleted file mode 100644
index 378328037fd..00000000000
--- a/addons/mail/static/tests/client_action_tests.js
+++ /dev/null
@@ -1,88 +0,0 @@
-odoo.define('mail.client_action_test', function (require) {
-"use strict";
-
-var testUtils = require('web.test_utils');
-var Widget = require('web.Widget');
-
-var ChatAction = require("mail.chat_client_action");
-
-QUnit.module('mail', {}, function () {
-
-QUnit.module('Discuss client action', {
- beforeEach: function () {
- this.data = {
- 'mail.message': {
- fields: {},
- },
- };
- this.createChatAction = function (params) {
- var parent = new Widget();
- testUtils.addMockEnvironment(parent, {
- data: this.data,
- archs: {
- 'mail.message,false,search': '<search/>',
- },
- config: {device: {isMobile: true}},
- });
- var chatAction = new ChatAction(parent, params);
- chatAction.set_cp_bus(new Widget());
- chatAction.appendTo($('#qunit-fixture'));
-
- return chatAction;
- };
-
- },
-});
-
-QUnit.skip('mobile basic rendering', function (assert) {
- // Unfortunately, this test is skipped for now because there is no way to
- // execute the whole test suite in mobile (it only works test by test), so
- // as the client action include for mobile is rejected when we are not in
- // mobile, it isn't possible to test it
- // Moreover, RPCs done by the chat_manager (e.g. message_fetch) should be
- // properly mocked.
- assert.expect(11);
-
- var parent = new Widget();
- testUtils.addMockEnvironment(parent, {
- data: this.data,
- archs: {
- 'mail.message,false,search': '<search/>',
- },
- config: {device: {isMobile: true}},
- });
-
- var params = {
- id: 1,
- context: {},
- params: {},
- };
- var chatAction = this.createChatAction(params);
-
- // test basic rendering in mobile
- assert.equal(chatAction.$(".o_mail_chat_mobile_control_panel").length, 1, "Mobile control panel created");
- assert.equal(chatAction.$(".o_mail_mobile_tab").length, 4, "Four mobile tabs created");
- assert.equal(chatAction.$('.o_mail_chat_content').length, 1, "One default chat content pane created");
- assert.equal(chatAction.$(".o_mail_chat_tab_pane").length, 3, "Three mobile tab panes created");
-
- // Inbox
- assert.equal(chatAction.activeMobileTab, "channel_inbox", "'channel_inbox' is default active tab");
- assert.ok(chatAction.$(".o_channel_inbox_item:nth(0)").hasClass("btn-primary"), "Showing 'Inbox'");
-
- // Starred
- chatAction.$(".o_channel_inbox_item[data-type='channel_starred']").click();
- assert.ok(chatAction.$(".o_channel_inbox_item:nth(1)").hasClass("btn-primary"), "Clicked on 'Starred'");
-
- assert.ok(chatAction.$(".o_mail_chat_content").is(":visible"), "Default main content pane visible");
-
- chatAction.$(".o_mail_mobile_tab[data-type='dm']").click();
- assert.equal(chatAction.activeMobileTab, "dm", "After click on 'Conversation', is now active tab");
-
- assert.ok(!chatAction.$(".o_mail_chat_content").is(":visible"), "none", "'Main' content pane is invisible");
- assert.ok(chatAction.$(".o_mail_chat_tab_pane:nth(0)").is(":visible"), "'Conversation' pane is visible");
-
- chatAction.destroy();
-});
-
-});
-});
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index 8c4ab97754b..31bbcf1ee47 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -43,13 +43,18 @@
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
- <script type="text/javascript" src="/mail/static/tests/client_action_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
</xpath>
</template>
+ <template id="qunit_mobile_suite" name="mail_mobile_tests" inherit_id="web.qunit_mobile_suite">
+ <xpath expr="//t[@t-set='head']" position="inside">
+ <script type="text/javascript" src="/mail/static/tests/client_action_mobile_tests.js"></script>
+ </xpath>
+ </template>
+
<template id="message_origin_link">
<p>This <t t-esc="self._description.lower()"/> has been <span t-if="edit">modified</span><span t-if="not edit">created</span> from:
<t t-foreach="origin" t-as="o">
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
@@ -214,6 +214,15 @@ class MaintenanceEquipment(models.Model):
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
the kanban view, even if they are empty.
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 05647bc366d..8dd42b61b67 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -585,14 +585,15 @@ class MassMailing(models.Model):
self.ensure_one()
mass_mailing_copy = self.copy()
if mass_mailing_copy:
+ context = dict(self.env.context)
+ context['form_view_initial_mode'] = 'edit'
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'res_id': mass_mailing_copy.id,
- 'context': self.env.context,
- 'flags': {'initial_mode': 'edit'},
+ 'context': context,
}
return False
diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py
index 6c87ed8614e..0ff3db0a337 100644
--- a/addons/mrp/models/mrp_unbuild.py
+++ b/addons/mrp/models/mrp_unbuild.py
@@ -88,6 +88,10 @@ class MrpUnbuild(models.Model):
if self.product_id.tracking != 'none' and not self.lot_id.id:
raise UserError(_('Should have a lot for the finished product'))
+ if self.mo_id:
+ if self.mo_id.state != 'done':
+ raise UserError(_('You cannot unbuild a undone manufacturing order.'))
+
consume_move = self._generate_consume_moves()[0]
produce_moves = self._generate_produce_moves()
@@ -156,12 +160,31 @@ class MrpUnbuild(models.Model):
def _generate_produce_moves(self):
moves = self.env['stock.move']
for unbuild in self:
- factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
- boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id)
- for line, line_data in lines:
- moves += unbuild._generate_move_from_bom_line(line, line_data['qty'])
+ if unbuild.mo_id:
+ raw_moves = unbuild.mo_id.move_raw_ids.filtered(lambda move: move.state == 'done')
+ factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
+ for raw_move in raw_moves:
+ moves += unbuild._generate_move_from_raw_moves(raw_move, factor)
+ else:
+ factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
+ boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id)
+ for line, line_data in lines:
+ moves += unbuild._generate_move_from_bom_line(line, line_data['qty'])
return moves
+ def _generate_move_from_raw_moves(self, raw_move, factor):
+ return self.env['stock.move'].create({
+ 'name': self.name,
+ 'date': self.create_date,
+ 'product_id': raw_move.product_id.id,
+ 'product_uom_qty': raw_move.product_uom_qty * factor,
+ 'product_uom': raw_move.product_uom.id,
+ 'procure_method': 'make_to_stock',
+ 'location_dest_id': self.location_dest_id.id,
+ 'location_id': raw_move.location_dest_id.id,
+ 'unbuild_id': self.id,
+ })
+
def _generate_move_from_bom_line(self, bom_line, quantity):
return self.env['stock.move'].create({
'name': self.name,
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index f9436f66870..73f4c861727 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -195,6 +195,12 @@ class StockMove(models.Model):
move_lines = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
else:
move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id)
+
+ # Sanity check: if the product is a serial number and `lot` is already present in the other
+ # consumed move lines, raise.
+ if lot and self.product_id.tracking == 'serial' and lot in self.move_line_ids.filtered(lambda ml: ml.qty_done).mapped('lot_id'):
+ raise UserError(_('You cannot consume the same serial number twice. Please correct the serial numbers encoded.'))
+
for ml in move_lines:
rounding = ml.product_uom_id.rounding
if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0:
diff --git a/addons/mrp/views/mrp_unbuild_views.xml b/addons/mrp/views/mrp_unbuild_views.xml
index a0f14e9d6d2..d10e18819d4 100644
--- a/addons/mrp/views/mrp_unbuild_views.xml
+++ b/addons/mrp/views/mrp_unbuild_views.xml
@@ -122,7 +122,7 @@
</div>
</group>
<group>
- <field name="mo_id" required="True"/>
+ <field name="mo_id" domain="[('state','=','done')]"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
<field name="has_tracking" invisible="1"/>
diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py
index 33ffb3d48f9..15c1684561e 100644
--- a/addons/mrp/wizard/mrp_product_produce.py
+++ b/addons/mrp/wizard/mrp_product_produce.py
@@ -125,6 +125,8 @@ class MrpProductProduce(models.TransientModel):
raise UserError(_('You need to provide a lot for the finished product'))
existing_move_line = produce_move.move_line_ids.filtered(lambda x: x.lot_id == self.lot_id)
if existing_move_line:
+ if self.product_id.tracking == 'serial':
+ raise UserError(_('You cannot produce the same serial number twice.'))
existing_move_line.product_uom_qty += self.product_qty
existing_move_line.qty_done += self.product_qty
else:
@@ -181,27 +183,25 @@ class MrpProductProduceLine(models.TransientModel):
@api.onchange('lot_id')
def _onchange_lot_id(self):
+ """ When the user is encoding a produce line for a tracked product, we apply some logic to
+ help him. This onchange will automatically switch `qty_done` to 1.0.
+ """
res = {}
if self.product_id.tracking == 'serial':
self.qty_done = 1
return res
- @api.constrains('lot_id')
- def _check_lot_id(self):
- for ml in self:
- if ml.product_id.tracking == 'serial':
- produce_lines_to_check = ml.product_produce_id.produce_line_ids.filtered(lambda l: l.product_id == ml.product_id and l.lot_id)
- message = produce_lines_to_check._check_for_duplicated_serial_numbers()
- if message:
- raise ValidationError(message)
-
- def _check_for_duplicated_serial_numbers(self):
- if self.mapped('lot_id'):
- lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self]
- recorded_serials_counter = Counter(lots_map)
- for (product_id, lot_id), occurrences in recorded_serials_counter.items():
- if occurrences > 1 and lot_id is not False:
- return _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.')
+ @api.onchange('qty_done')
+ def _onchange_qty_done(self):
+ """ When the user is encoding a produce line for a tracked product, we apply some logic to
+ help him. This onchange will warn him if he set `qty_done` to a non-supported value.
+ """
+ res = {}
+ if self.product_id.tracking == 'serial':
+ if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
+ message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
+ res['warning'] = {'title': _('Warning'), 'message': message}
+ return res
@api.onchange('product_id')
def _onchange_product_id(self):
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py
index 265309f8d34..4047ef7d30a 100644
--- a/addons/payment/controllers/portal.py
+++ b/addons/payment/controllers/portal.py
@@ -51,7 +51,10 @@ class WebsitePayment(http.Controller):
'amount': float(amount),
'payment_form': payment_form,
}
- return request.render('payment.pay', values)
+ # TODO: remove return when payment is implemented with refactored payments, i.e. use of
+ # payment.payment_tokens_list in the template.
+ # return request.render('payment.pay', values)
+ return request.render('website.404')
@http.route(['/website_payment/transaction'], type='json', auth="public", website=True)
def transaction(self, reference, amount, currency_id, acquirer_id):
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index a47040be6ad..c354f2e6aa9 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -127,7 +127,7 @@ class PaymentAcquirerAuthorize(models.Model):
for field_name in mandatory_fields:
if not data.get(field_name):
error[field_name] = 'missing'
- if data['cc_expiry'] and datetime.now().strftime('%y%M') > datetime.strptime(data['cc_expiry'], '%M / %y').strftime('%y%M'):
+ if data['cc_expiry'] and datetime.now().strftime('%y%m') > datetime.strptime(data['cc_expiry'], '%m / %y').strftime('%y%m'):
return False
return False if error else True
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 7a6d1447f87..d616ddfe0be 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -92,7 +92,7 @@ odoo.define('payment_stripe.stripe', function(require) {
return false;
}
- var access_token = $("input[name='access_token']").val() || $("input[name='token']").val();
+ var access_token = $("input[name='access_token']").val() || $("input[name='token']").val() || '';
var so_id = $("input[name='return_url']").val().match(/quote\/([0-9]+)/) || undefined;
if (so_id) {
so_id = parseInt(so_id[1]);
diff --git a/addons/phone_validation/tools/phone_validation.py b/addons/phone_validation/tools/phone_validation.py
index 718b0518802..940c4300f26 100644
--- a/addons/phone_validation/tools/phone_validation.py
+++ b/addons/phone_validation/tools/phone_validation.py
@@ -43,11 +43,10 @@ try:
else:
_logger.warning(_('Unable to format %s:\n%s') % number, e)
return number
- if always_international and phone_nbr.country_code != country_phone_code:
+ if always_international or phone_nbr.country_code != country_phone_code:
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
else:
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
-
return phonenumbers.format_number(phone_nbr, phone_fmt)
except ImportError:
diff --git a/addons/phone_validation/views/res_company_view.xml b/addons/phone_validation/views/res_company_view.xml
index d788e5aa7fe..c6401c02cb9 100644
--- a/addons/phone_validation/views/res_company_view.xml
+++ b/addons/phone_validation/views/res_company_view.xml
@@ -1,8 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- The purpose of this view is to move the VAT field at the top of the
- company form. That way, the user will fill it first
--->
<odoo>
<record id="res_company_view_form_phone_validation" model="ir.ui.view">
<field name="name">res.company.view.form.phone_validation</field>
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index 2af34a25e0b..60551814593 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -128,6 +128,15 @@
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<field string="Attributes" name="attribute_line_ids" groups="product.group_product_variant"/>
<field name="pricelist_id" widget="selection" context="{'pricelist': self}" filter_domain="[]" groups="product.group_sale_pricelist"/> <!-- Keep widget=selection on this field to pass numeric `self` value, which is not the case for regular m2o widgets! -->
+ <separator/>
+ <filter string="Late Activities" name="activities_overdue"
+ domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
+ help="Show all records which has next action date is before today"/>
+ <filter string="Today Activities" name="activities_today"
+ domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ <filter string="Future Activities" name="activities_upcoming_all"
+ domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
+ ]"/>
</search>
</field>
</record>
diff --git a/addons/product_expiry/models/production_lot.py b/addons/product_expiry/models/production_lot.py
index 6be9a2865dd..45e465ea146 100644
--- a/addons/product_expiry/models/production_lot.py
+++ b/addons/product_expiry/models/production_lot.py
@@ -44,7 +44,7 @@ class StockProductionLot(models.Model):
# Assign dates according to products data
@api.model
def create(self, vals):
- dates = self._get_dates(vals.get('product_id'))
+ dates = self._get_dates(vals.get('product_id') or self.env.context.get('default_product_id'))
for d in dates:
if not vals.get(d):
vals[d] = dates[d]
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index ca69c79115f..1a9f6db378a 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -512,19 +512,19 @@ class Task(models.Model):
partner_id = fields.Many2one('res.partner',
string='Customer',
default=_get_default_partner)
- manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True)
+ manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False)
company_id = fields.Many2one('res.company',
string='Company',
default=lambda self: self.env['res.company']._company_default_get())
color = fields.Integer(string='Color Index')
- user_email = fields.Char(related='user_id.email', string='User Email', readonly=True)
+ user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False)
attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
help="Attachment that don't come from message.")
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image')
- legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True)
- legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True)
- legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True)
+ legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
+ legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
+ legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
parent_id = fields.Many2one('project.task', string='Parent Task')
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
diff --git a/addons/project_timesheet_holidays/models/hr_holidays.py b/addons/project_timesheet_holidays/models/hr_holidays.py
index 2d245e834d9..633a0b9d6c3 100644
--- a/addons/project_timesheet_holidays/models/hr_holidays.py
+++ b/addons/project_timesheet_holidays/models/hr_holidays.py
@@ -60,7 +60,7 @@ class Holidays(models.Model):
work_hours_data = [item for item in holiday.employee_id.iter_work_hours_count(fields.Datetime.from_string(holiday.date_from), fields.Datetime.from_string(holiday.date_to))]
for index, (day_date, work_hours_count) in enumerate(work_hours_data):
self.env['account.analytic.line'].create({
- 'name': "%s (%s/%s)" % (holiday.name, index + 1, len(work_hours_data)),
+ 'name': "%s (%s/%s)" % (holiday.name or '', index + 1, len(work_hours_data)),
'project_id': holiday_project.id,
'task_id': holiday_task.id,
'account_id': holiday_project.analytic_account_id.id,
@@ -77,6 +77,7 @@ class Holidays(models.Model):
def action_refuse(self):
""" Remove the timesheets linked to the refused holidays """
result = super(Holidays, self).action_refuse()
- self.mapped('timesheet_ids').write({'holiday_id': False})
- self.mapped('timesheet_ids').unlink()
+ timesheets = self.sudo().mapped('timesheet_ids')
+ timesheets.write({'holiday_id': False})
+ timesheets.unlink()
return result
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 4cab61623f0..02d02844a82 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -98,7 +98,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/models/purchase.py b/addons/purchase/models/purchase.py
index a8ae709e023..1a1e65d9611 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -776,10 +776,10 @@ class PurchaseOrderLine(models.Model):
self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
- product_lang = self.product_id.with_context({
- 'lang': self.partner_id.lang,
- 'partner_id': self.partner_id.id,
- })
+ product_lang = self.product_id.with_context(
+ lang=self.partner_id.lang,
+ partner_id=self.partner_id.id,
+ )
self.name = product_lang.display_name
if product_lang.description_purchase:
self.name += '\n' + product_lang.description_purchase
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 @@
<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>
diff --git a/addons/sale/models/analytic.py b/addons/sale/models/analytic.py
index d8bbb2da419..73b9f95d93a 100644
--- a/addons/sale/models/analytic.py
+++ b/addons/sale/models/analytic.py
@@ -128,11 +128,13 @@ class AccountAnalyticLine(models.Model):
raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % sale_order.name)
price = analytic_line._sale_get_invoice_price(sale_order)
- so_line = self.env['sale.order.line'].search([
- ('order_id', '=', sale_order.id),
- ('price_unit', '=', price),
- ('product_id', '=', self.product_id.id)
- ], limit=1)
+ so_line = None
+ if analytic_line.product_id.expense_policy == 'sales_price' and analytic_line.product_id.invoice_policy == 'delivery':
+ so_line = self.env['sale.order.line'].search([
+ ('order_id', '=', sale_order.id),
+ ('price_unit', '=', price),
+ ('product_id', '=', self.product_id.id)
+ ], limit=1)
if not so_line:
# generate a new SO line
@@ -142,7 +144,7 @@ class AccountAnalyticLine(models.Model):
so_line = self.env['sale.order.line'].create(so_line_values)
so_line._compute_tax_id()
else:
- so_line.write({'qty_delivered': analytic_line.unit_amount})
+ so_line.write({'qty_delivered': so_line.qty_delivered + analytic_line.unit_amount})
if so_line: # if so line found or created, then update AAL (this will trigger the recomputation of qty delivered on SO line)
analytic_line.with_context(sale_analytic_norecompute=True).write({'so_line': so_line.id})
diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml
index 350756da6ee..3352f75b048 100644
--- a/addons/sale/report/sale_report_templates.xml
+++ b/addons/sale/report/sale_report_templates.xml
@@ -150,7 +150,10 @@
<t t-foreach="doc._get_tax_amount_by_group()" t-as="amount_by_group">
<tr style="border-bottom:1px solid #dddddd;">
<t t-if="amount_by_group[3] == 1 and doc.amount_untaxed == amount_by_group[2]">
- <td><span t-esc="amount_by_group[0]"/></td>
+ <td>
+ <span t-esc="amount_by_group[0]"/>
+ <span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/></span>
+ </td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/>
@@ -158,7 +161,7 @@
</t>
<t t-else ="">
<td>
- <span t-esc="amount_by_group[0]"/><span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/></span>
+ <span t-esc="amount_by_group[0]"/>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]"
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
index 1856b2f17dd..a637b43133b 100644
--- a/addons/sms/wizard/send_sms.py
+++ b/addons/sms/wizard/send_sms.py
@@ -9,6 +9,16 @@ from odoo.addons.iap.models import iap
_logger = logging.getLogger(__name__)
+try:
+ import phonenumbers
+ _sms_phonenumbers_lib_imported = True
+
+except ImportError:
+ _sms_phonenumbers_lib_imported = False
+ _logger.warning(
+ "The `phonenumbers` Python module is not installed. Try: pip install phonenumbers."
+ )
+
class SendSMS(models.TransientModel):
_name = 'sms.send_sms'
@@ -16,6 +26,27 @@ class SendSMS(models.TransientModel):
recipients = fields.Char('Recipients', required=True)
message = fields.Text('Message', required=True)
+ def _phone_get_country(self, partner):
+ if 'country_id' in partner:
+ return partner.country_id
+ return self.env.user.company_id.country_id
+
+ def _sms_sanitization(self, partner, field_name):
+ number = partner[field_name]
+ if _sms_phonenumbers_lib_imported:
+ country = self._phone_get_country(partner)
+ country_code = country.code if country else None
+ try:
+ phone_nbr = phonenumbers.parse(number, region=country_code, keep_raw_input=True)
+ except phonenumbers.phonenumberutil.NumberParseException:
+ return number
+ if not phonenumbers.is_possible_number(phone_nbr) or not phonenumbers.is_valid_number(phone_nbr):
+ return number
+ phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
+ return phonenumbers.format_number(phone_nbr, phone_fmt).replace(' ', '')
+ else:
+ return number
+
def _get_records(self, model):
if self.env.context.get('active_domain'):
records = model.search(self.env.context.get('active_domain'))
@@ -38,7 +69,8 @@ class SendSMS(models.TransientModel):
phone_numbers = []
no_phone_partners = []
for partner in partners:
- number = partner.mobile
+ default_field_name = 'mobile' if partner.mobile else 'phone'
+ number = self._sms_sanitization(partner, self.env.context('field_name') or default_field_name)
if number:
phone_numbers.append(number)
else:
diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py
index 21e41caa10f..05e4bb2b60d 100644
--- a/addons/stock/models/stock_location.py
+++ b/addons/stock/models/stock_location.py
@@ -183,10 +183,6 @@ class Route(models.Model):
'domain': [('route_ids', 'in', self.ids)],
}
- @api.onchange('warehouse_selectable')
- def _onchange_warehouse_selectable(self):
- if not self.warehouse_selectable:
- self.warehouse_ids = [(5, 0, 0)]
class PushedFlow(models.Model):
_name = "stock.location.path"
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 05b9c9af85f..2ac3bd05415 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1000,8 +1000,16 @@ class StockMove(models.Model):
moves = self.filtered(lambda x: x.state not in ('done', 'cancel'))
moves_todo = self.env['stock.move']
- # Create extra moves where necessary
+
+ # Cancel moves where necessary ; we should do it before creating the extra moves because
+ # this operation could trigger a merge of moves.
for move in moves:
+ if move.quantity_done <= 0:
+ if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0:
+ move._action_cancel()
+
+ # Create extra moves where necessary
+ for move in moves.filtered(lambda m: m.state != 'cancel'):
# Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
# it is possible that the user changed the value before posting the inventory by a value that should be
# rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
@@ -1010,10 +1018,6 @@ class StockMove(models.Model):
# FIXME: move rounding to move line
# rounding = move.product_uom.rounding
# move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
- if move.quantity_done <= 0:
- if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0:
- move._action_cancel()
- continue
moves_todo |= move
moves_todo |= move._create_extra_move()
# Split moves where necessary and move quants
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index 4582c907575..cb9244e4511 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -109,36 +109,49 @@ class StockMoveLine(models.Model):
@api.onchange('lot_name', 'lot_id')
def onchange_serial_number(self):
+ """ When the user is encoding a move line for a tracked product, we apply some logic to
+ help him. This includes:
+ - automatically switch `qty_done` to 1.0
+ - warn if he has already encoded `lot_name` in another move line
+ """
res = {}
if self.product_id.tracking == 'serial':
- self.qty_done = 1
- move_lines_to_check = self._get_similar_move_lines() - self
- message = move_lines_to_check._check_for_duplicated_serial_numbers()
+ if not self.qty_done:
+ self.qty_done = 1
+
+ message = None
+ if self.lot_name or self.lot_id:
+ move_lines_to_check = self._get_similar_move_lines() - self
+ if self.lot_name:
+ counter = Counter(move_lines_to_check.mapped('lot_name'))
+ if counter.get(self.lot_name) and counter[self.lot_name] > 1:
+ message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
+ elif self.lot_id:
+ counter = Counter(move_lines_to_check.mapped('lot_id.id'))
+ if counter.get(self.lot_id.id) and counter[self.lot_id.id] > 1:
+ message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
+
if message:
res['warning'] = {'title': _('Warning'), 'message': message}
return res
+ @api.onchange('qty_done')
+ def _onchange_qty_done(self):
+ """ When the user is encoding a move line for a tracked product, we apply some logic to
+ help him. This onchange will warn him if he set `qty_done` to a non-supported value.
+ """
+ res = {}
+ if self.product_id.tracking == 'serial':
+ if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
+ message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
+ res['warning'] = {'title': _('Warning'), 'message': message}
+ return res
+
@api.constrains('qty_done')
def _check_positive_qty_done(self):
if any([ml.qty_done < 0 for ml in self]):
raise ValidationError(_('You can not enter negative quantities!'))
- @api.constrains('lot_id', 'lot_name', 'qty_done')
- def _check_unique_serial_number(self):
- for ml in self.filtered(lambda ml: ml.move_id.product_id.tracking == 'serial' and (ml.lot_id or ml.lot_name)):
- move_lines_to_check = ml._get_similar_move_lines()
- message = move_lines_to_check._check_for_duplicated_serial_numbers()
- if message:
- raise ValidationError(message)
- if float_compare(ml.qty_done, 1.0, precision_rounding=ml.move_id.product_id.uom_id.rounding) == 1:
- raise UserError(_(
- 'You can only process 1.0 %s for products with unique serial number.') % ml.product_id.uom_id.name)
- if ml.lot_name:
- already_exist = self.env['stock.production.lot'].search(
- [('name', '=', ml.lot_name), ('product_id', '=', ml.product_id.id)])
- if already_exist:
- return _('You have already assigned this serial number to this product. Please correct the serial numbers encoded.')
-
def _get_similar_move_lines(self):
self.ensure_one()
lines = self.env['stock.move.line']
@@ -147,25 +160,6 @@ class StockMoveLine(models.Model):
lines |= picking_id.move_line_ids.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name))
return lines
- def _check_for_duplicated_serial_numbers(self):
- """ This method is used in _check_unique_serial_number and in onchange_serial_number to check that a same serial number is not used twice amongst the recordset passed.
-
- :return: an error message directed to the user if needed else False
- """
- if self.mapped('lot_id'):
- lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self]
- recorded_serials_counter = Counter(lots_map)
- for (product_id, lot_id), occurrences in recorded_serials_counter.items():
- if occurrences > 1 and lot_id is not False:
- return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
- elif self.mapped('lot_name'):
- lots_map = [(ml.product_id.id, ml.lot_name) for ml in self]
- recorded_serials_counter = Counter(lots_map)
- for (product_id, lot_id), occurrences in recorded_serials_counter.items():
- if occurrences > 1 and lot_id is not False:
- return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
- return False
-
@api.model
def create(self, vals):
vals['ordered_qty'] = vals.get('product_uom_qty')
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 82b74d5f32e..7da23c0b44d 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -246,7 +246,7 @@
</group>
<group>
<field name="scheduled_date"/>
- <field name="origin" placeholder="e.g. PO0032" readonly="1"/>
+ <field name="origin" placeholder="e.g. PO0032"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
<div groups="stock.group_tracking_owner" colspan="2" col="2">
<button name="action_assign_owner" string="Assign Owner" type="object" attrs="{'invisible': ['|',('move_line_exist', '=', False),('state', 'not in', ('draft','assigned','confirmed'))]}"
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/stock_picking_batch/wizard/stock_picking_to_batch_views.xml b/addons/stock_picking_batch/wizard/stock_picking_to_batch_views.xml
index 9f994be520b..dc45cf58269 100644
--- a/addons/stock_picking_batch/wizard/stock_picking_to_batch_views.xml
+++ b/addons/stock_picking_batch/wizard/stock_picking_to_batch_views.xml
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<form string="Add pickings to batch">
<separator string="Select a batch"/>
- <field name="batch_id" class="oe_inline"/>
+ <field name="batch_id" class="oe_inline" options="{'no_create_edit': True, 'no_open': True}"/>
<footer>
<button name="attach_pickings" type="object" string="Add to Batch" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 1fe8924dda6..34956afe545 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -274,3 +274,71 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
def test_mail_message_access_create_reply(self):
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
self.env['mail.message'].sudo(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id})
+
+
+ @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") \ No newline at end of file
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 8bb9afca065..2534f8ae90a 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -45,11 +45,6 @@ var AbstractField = Widget.extend({
},
/**
- * fields can extend the context, e.g. binary fields add {bin_size: true}
- */
- context: null,
-
- /**
* An object representing fields to be fetched by the model eventhough not present in the view
* This object contains "field name" as key and an object as value.
* That value object must contain the key "type"
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index f3e66cee421..94facd80ceb 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1094,7 +1094,6 @@ var AbstractFieldBinary = AbstractField.extend({
},
'click .o_clear_file_button': 'on_clear',
}),
- context: { bin_size: true },
init: function (parent, name, record) {
this._super.apply(this, arguments);
this.fields = record.fields;
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 5ac180b1de1..5a11f1b0ba8 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -30,7 +30,7 @@ var _t = core._t;
/**
* Convert binary to bin_size
*
- * @param {string} value base64 representation of the binary (might be already a bin_size!)
+ * @param {string} [value] base64 representation of the binary (might be already a bin_size!)
* @param {Object} [field]
* a description of the field (note: this parameter is ignored)
* @param {Object} [options] additional options (note: this parameter is ignored)
@@ -38,6 +38,9 @@ var _t = core._t;
* @returns {string} bin_size (which is human-readable)
*/
function formatBinary(value, field, options) {
+ if (!value) {
+ return '';
+ }
return utils.binaryToBinsize(value);
}
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 948d3a1c73a..72ec816efdd 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -387,10 +387,6 @@ return core.Class.extend({
attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies;
}
- if (attrs.Widget.prototype.context) {
- attrs.context = attrs.Widget.prototype.context;
- }
-
return attrs;
},
/**
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 a6979776c99..5296c6db7d5 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1854,7 +1854,7 @@ var BasicModel = AbstractModel.extend({
model: record.model,
method: 'read',
args: [[record.res_id], fieldNames],
- context: record.getContext(),
+ context: _.extend({}, record.getContext(), {bin_size: true}),
})
.then(function (result) {
if (result.length === 0) {
@@ -2374,7 +2374,7 @@ var BasicModel = AbstractModel.extend({
});
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
- var def = self._readUngroupedList(list).then(function (list) {
+ var def = self._readUngroupedList(list).then(function () {
return $.when(
self._fetchX2ManysBatched(list),
self._fetchReferencesBatched(list)
@@ -2403,6 +2403,7 @@ var BasicModel = AbstractModel.extend({
var fields = view ? view.fields : fieldInfo.relatedFields;
var viewType = view ? view.type : fieldInfo.viewType;
list = this._applyX2ManyOperations(list);
+ this._sortList(list);
var x2mRecords = [];
// step 1: collect ids
@@ -2640,7 +2641,7 @@ var BasicModel = AbstractModel.extend({
var relRecordAdded = [];
var relRecordUpdated = [];
_.each(list._changes, function (change) {
- if (change.operation === 'ADD') {
+ if (change.operation === 'ADD' && change.id) {
relRecordAdded.push(self.localData[change.id]);
} else if (change.operation === 'UPDATE' && !self.isNew(change.id)) {
// ignore new records that would have been updated
@@ -2699,6 +2700,10 @@ var BasicModel = AbstractModel.extend({
} else if (_.contains(addedIds, list.res_ids[i])) {
// this is a new id (maybe existing in DB, but new in JS)
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
+ if (!relRecord) {
+ commands[fieldName].push(x2ManyCommands.link_to(list.res_ids[i]));
+ continue;
+ }
changes = this._generateChanges(relRecord, options);
if (changes.id) {
// the subrecord already exists in db
@@ -3092,23 +3097,13 @@ var BasicModel = AbstractModel.extend({
id: {type: 'integer'},
}, params.fields);
- // datapoint context is extended by fields widgets
- var context = _.extend({}, params.context);
- if (params.fieldsInfo && params.viewType) {
- _.each(params.fieldsInfo[params.viewType], function (field) {
- if (field.context) {
- _.extend(context, field.context);
- }
- });
- }
-
var dataPoint = {
_cache: type === 'list' ? {} : undefined,
_changes: null,
_domains: {},
_rawChanges: {},
aggregateValues: params.aggregateValues || {},
- context: context,
+ context: params.context,
count: params.count || res_ids.length,
data: data,
domain: params.domain || [],
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index 0e50e37194f..d3553d8db12 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -167,6 +167,13 @@ var KanbanColumn = Widget.extend({
* Adds the quick create record to the top of the column.
*/
addQuickCreate: function () {
+ if (this.folded) {
+ // first open the column, and then add the quick create
+ this.trigger_up('column_toggle_fold', {
+ openQuickCreate: true,
+ });
+ return;
+ }
if (this.quickCreateWidget) {
return;
}
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..26f379d4a39 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;
@@ -388,13 +389,18 @@ var KanbanController = BasicController.extend({
/**
* @private
* @param {OdooEvent} event
+ * @param {boolean} [event.data.openQuickCreate=false] if true, opens the
+ * QuickCreate in the toggled column (it assumes that we are opening it)
*/
_onToggleColumn: function (event) {
var self = this;
var column = event.target;
this.model.toggleGroup(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
- self.renderer.updateColumn(db_id, data);
+ var options = {
+ openQuickCreate: !!event.data.openQuickCreate,
+ };
+ self.renderer.updateColumn(db_id, data, options);
self._updateEnv();
});
},
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index aba59555e93..df6172f0d8b 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -22,6 +22,7 @@ var KanbanModel = BasicModel.extend({
* @returns {Deferred<string>} resolves to the local id of the new record
*/
addRecordToGroup: function (groupID, resId) {
+ var self = this;
var group = this.localData[groupID];
var new_record = this._makeDataPoint({
res_id: resId,
@@ -31,15 +32,16 @@ var KanbanModel = BasicModel.extend({
viewType: group.viewType,
parentID: groupID,
});
- group.data.unshift(new_record.id);
- group.res_ids.unshift(resId);
- group.count++;
-
- // update the res_ids and count of the parent
- this.localData[group.parentID].count++;
- this._updateParentResIDs(group);
return this._fetchRecord(new_record).then(function (result) {
+ group.data.unshift(new_record.id);
+ group.res_ids.unshift(resId);
+ group.count++;
+
+ // update the res_ids and count of the parent
+ self.localData[group.parentID].count++;
+ self._updateParentResIDs(group);
+
return result.id;
});
},
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index ef5139511fe..ff9a9c63621 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -6916,6 +6916,7 @@ QUnit.module('relational_fields', {
if (args.method === 'read' && args.model === 'partner') {
assert.deepEqual(args.kwargs.context, {
active_field: 2,
+ bin_size: true,
someKey: 'some value',
}, "sent context should be correct");
}
@@ -7639,6 +7640,74 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many with multiple pages and sequence field, part2', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+ this.data.partner.onchanges.turtles = function () {};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" invisible="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({value: { turtles: [
+ [5],
+ [1, 1, {turtle_foo: "from onchange id2", partner_ids: [[5]]}],
+ [1, 3, {turtle_foo: "from onchange id3", partner_ids: [[5]]}],
+ ]}});
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ form.$('span[name="delete"]').first().click();
+ assert.strictEqual(form.$('.o_data_row').text(), 'from onchange id2from onchange id3',
+ 'onchange has been properly applied');
+ form.destroy();
+ });
+
+ QUnit.test('new record, with one2many with more default values than limit', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ context: { default_turtles: [1,2,3]},
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
+ 'data has been properly loaded');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
+ 'data has been properly saved');
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 72061f326e0..492dfd4224c 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -74,6 +74,7 @@ QUnit.module('Views', {
mockRPC: function (route, args) {
assert.deepEqual(args.kwargs.context, {
active_field: 2,
+ bin_size: true,
someKey: 'some value',
}, "should have sent the correct context");
return this._super.apply(this, arguments);
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 9f2a9b68698..9e7c89628c4 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -199,7 +199,7 @@ QUnit.module('Views', {
});
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
- assert.expect(1);
+ assert.expect(2);
var form = createView({
View: FormView,
@@ -214,6 +214,8 @@ QUnit.module('Views', {
// field, not sure why. Maybe this test should be modified.
assert.deepEqual(args.args[1], ["foo", "display_name"],
"should only fetch requested fields");
+ assert.deepEqual(args.kwargs.context, {bin_size: true},
+ "bin_size should always be in the context");
return this._super(route, args);
}
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 9e6d6e9e30f..250bb8ef64f 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -33,9 +33,10 @@ QUnit.module('Views', {
state: { string: "State", type: "selection", selection: [["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"]]},
date: {string: "Date Field", type: 'date'},
datetime: {string: "Datetime Field", type: 'datetime'},
+ image: {string: "Image", type: "binary"},
},
records: [
- {id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: []},
+ {id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: [], 'image': 'R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA=='},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]},
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]},
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []},
@@ -440,6 +441,50 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('quick create when first column is folded', function (assert) {
+ assert.expect(6);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+
+ assert.notOk(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
+ "first column should not be folded");
+
+ // fold the first column
+ kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').click();
+
+ assert.ok(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
+ "first column should be folded");
+
+ // click on 'Create' to open the quick create in the first column
+ kanban.$buttons.find('.o-kanban-button-new').click();
+
+ assert.notOk(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
+ "first column should no longer be folded");
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+ assert.strictEqual($quickCreate.length, 1,
+ "should have added a quick create element in first column");
+
+ // fold again the first column
+ kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').click();
+
+ assert.ok(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
+ "first column should be folded");
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "there should be no more quick create");
+
+ kanban.destroy();
+ });
+
QUnit.test('quick create and edit in grouped mode', function (assert) {
assert.expect(6);
@@ -545,6 +590,80 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('quick create several records on slow network', function (assert) {
+ assert.expect(6);
+
+ var self = this;
+ var def;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ return result.then(function (result) {
+ // simulate a dynamic default value for foo based on display_name,
+ // so that we can check that the record has been fetched before
+ // being displayed
+ var newRec = _.findWhere(self.data.partner.records, {id: result[0]});
+ newRec.foo = newRec.display_name;
+ return result;
+ });
+ }
+ if (args.method === 'read') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain one record");
+
+ // click to add a record, and add two in a row (first one will be delayed)
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "the quick create should be open");
+
+ def = $.Deferred();
+ kanban.$('.o_kanban_quick_create input')
+ .val('new partner 1')
+ .trigger($.Event('keypress', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+ var firstDef = def;
+ def = null;
+ kanban.$('.o_kanban_quick_create input')
+ .val('new partner 2')
+ .trigger($.Event('keypress', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should now contain two records");
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record:first').text(),
+ 'new partner 2', "new record should have been fetched");
+
+ firstDef.resolve();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
+ "first column should now contain three records");
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record:first').text(),
+ 'new partner 1', "new record should have been fetched");
+
+ kanban.destroy();
+ });
+
QUnit.test('quick create fail in grouped', function (assert) {
assert.expect(7);
@@ -2339,6 +2458,32 @@ QUnit.module('Views', {
"quick create should have been added in the first column");
}
});
+
+ QUnit.test('test displaying image', function (assert) {
+ assert.expect(2);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="id"/>' +
+ '<field name="image"/>' +
+ '<templates><t t-name="kanban-box"><div>' +
+ '<img t-att-src="kanban_image(\'partner\', \'image\', record.id.raw_value)"/>' +
+ '</div></t></templates>' +
+ '</kanban>',
+ });
+
+ var imageOnRecord = kanban.$('img[src*="/web/image"][src*="&id=1"]');
+ assert.strictEqual(imageOnRecord.length, 1, "partner with image display image");
+
+ var placeholders = kanban.$('img[src$="/web/static/src/img/placeholder.png"]');
+ assert.strictEqual(placeholders.length, this.data.partner.records.length - 1,
+ "partner with image should displaiy");
+
+ kanban.destroy();
+ });
});
});
diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py
index 49f5b8d6dce..ab0eefb360b 100644
--- a/addons/web/tests/test_js.py
+++ b/addons/web/tests/test_js.py
@@ -13,7 +13,7 @@ class WebSuite(odoo.tests.HttpCase):
def test_01_js(self):
# webclient desktop test suite
- self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=300)
+ self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=360)
def test_02_js(self):
# webclient mobile test suite
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
index 6da2ea72ea1..d2bcd29e5ce 100644
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ b/addons/web_editor/static/src/js/editor/rte.js
@@ -374,10 +374,11 @@ var RTEWidget = Widget.extend({
* @param {boolean} internal_history
*/
historyRecordUndo: function ($target, event, internal_history) {
+ $target = $($target);
var rng = range.create();
var $editable = $(rng && rng.sc).closest('.o_editable');
if (!rng || !$editable.length) {
- $editable = $($target).closest('.o_editable');
+ $editable = $target.closest('.o_editable');
rng = range.create($target.closest('*')[0],0);
} else {
rng = $editable.data('range') || rng;
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index d6f9805599e..8ce2ed692da 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -846,37 +846,67 @@ range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
return new range.WrappedRange(sc, so, ec, eo);
};
-// isOnImg: judge whether range is an image node or not
+/**
+ * Returns the image the range is in or matches (if any, false otherwise).
+ *
+ * @todo this implementation may not cover all corner cases but should do the
+ * trick for all reproductible ones
+ * @returns {DOMElement|boolean}
+ */
range.WrappedRange.prototype.isOnImg = function () {
+ // If not a selection but a cursor position, just check if a point's
+ // ancestor is an image or not
+ if (this.sc === this.ec && this.so === this.eo) {
+ return dom.ancestor(this.sc, dom.isImg);
+ }
+
+ var startPoint = {node: this.sc, offset: this.so};
+ var endPoint = {node: this.ec, offset: this.eo};
+
var nb = 0;
var image;
- var startPoint = {node: this.sc.childNodes.length && this.sc.childNodes[this.so] || this.sc};
- startPoint.offset = startPoint.node === this.sc ? this.so : 0;
- var endPoint = {node: this.ec.childNodes.length && this.ec.childNodes[this.eo] || this.ec};
- endPoint.offset = endPoint.node === this.ec ? this.eo : 0;
-
- if (dom.isImg(startPoint.node)) {
- nb ++;
- image = startPoint.node;
- }
+ var textNode;
dom.walkPoint(startPoint, endPoint, function (point) {
- if (!dom.isText(endPoint.node) && point.node === endPoint.node && point.offset === endPoint.offset) {
+ // If the element has children (not a text node and not empty node),
+ // the element cannot be considered as selected (these children will
+ // be processed to determine that)
+ if (dom.hasChildren(point.node)) {
return;
}
- var node = point.node.childNodes.length && point.node.childNodes[point.offset] || point.node;
- var offset = node === point.node ? point.offset : 0;
- var isImg = dom.ancestor(node, dom.isImg);
- if (!isImg && ((!dom.isBR(node) && !dom.isText(node)) || (offset && node.textContent.length !== offset && node.textContent.match(/\S|\u00A0/)))) {
+
+ // Check if an ancestor of the current point is an image
+ var pointImg = dom.ancestor(point.node, dom.isImg);
+ var isText = dom.isText(point.node);
+
+ // Check if a visible element is selected, i.e.
+ // - If an ancestor of the current is an image we did not see yet
+ // - If the point is not in a br or a text (so a node with no children)
+ // - If the point is in a non empty text node we already saw
+ if (pointImg ?
+ (image !== pointImg) :
+ ((!dom.isBR(point.node) && !isText) || (textNode === point.node && point.node.textContent.match(/\S|\u00A0/)))) {
nb++;
}
- if (isImg && image !== isImg) {
- image = isImg;
- nb ++;
+
+ // If an ancestor of the current point is an image, then save it as the
+ // image we are looking for
+ if (pointImg) {
+ image = pointImg;
+ }
+ // If the current point is a text node save it as the last text node
+ // seen (if we see it again, this might mean it is selected)
+ if (isText) {
+ textNode = point.node;
}
});
+
return nb === 1 && image;
};
range.WrappedRange.prototype.deleteContents = function (towrite) {
+ if (this.sc === this.ec && this.so === this.eo) {
+ return this;
+ }
+
var prevBP = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
$(dom.node(prevBP.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
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/static/src/js/website_sale_validate.js b/addons/website_sale/static/src/js/website_sale_validate.js
index 6630044a6a4..a75b24fd0ad 100644
--- a/addons/website_sale/static/src/js/website_sale_validate.js
+++ b/addons/website_sale/static/src/js/website_sale_validate.js
@@ -21,7 +21,10 @@ $(document).ready(function () {
setTimeout(function () { payment_transaction_poll_status(); }, Math.ceil(_poll_nbr / 3) * 1000);
}
else {
- result.message = "<i class='fa fa-warning' />";
+ var $message = $(result.message);
+ $message.find('span:first').prepend($(
+ "<i title='We are waiting the confirmation of the bank or payment provider' class='fa fa-warning' style='margin-right:10px;'>"));
+ result.message = $message.html();
}
}
$('div.oe_website_sale_tx_status').html(result.message);
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index e51ff65a80a..9fd121bd471 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -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&gt;10 and step&lt;50"/>
diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
index 913addf8fb8..bbdece5fe55 100644
--- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
+++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
@@ -11,12 +11,14 @@ odoo.define('website_sale_delivery.checkout', function (require) {
var $amount_delivery = $('#order_delivery span.oe_currency_value');
var $amount_untaxed = $('#order_total_untaxed span.oe_currency_value');
var $amount_tax = $('#order_total_taxes span.oe_currency_value');
+ var $amount_total = $('#order_total span.oe_currency_value');
var $carrier_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge.hidden');
var $compute_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_delivery_compute');
if (result.status === true) {
$amount_delivery.text(result.new_amount_delivery);
$amount_untaxed.text(result.new_amount_untaxed);
$amount_tax.text(result.new_amount_tax);
+ $amount_total.text(result.new_amount_delivery + result.new_amount_untaxed + result.new_amount_tax);
$carrier_badge.children('span').text(result.new_amount_delivery);
$carrier_badge.removeClass('hidden');
$compute_badge.addClass('hidden');
diff --git a/addons/website_slides/static/src/js/slides.js b/addons/website_slides/static/src/js/slides.js
index c4df6643f99..279c256c5ac 100644
--- a/addons/website_slides/static/src/js/slides.js
+++ b/addons/website_slides/static/src/js/slides.js
@@ -7,12 +7,12 @@ var core = require('web.core');
var time = require('web.time');
var Widget = require('web.Widget');
var local_storage = require('web.local_storage');
+var websiteRootInstance = require('website.WebsiteRoot.instance');
var _t = core._t;
var page_widgets = {};
-$(document).ready(function () {
-
+(function () {
var widget_parent = $('body');
@@ -23,7 +23,7 @@ $(document).ready(function () {
// then return fix formate string else timeago
display_str = "";
if (datetime_obj && new Date().getTime() - datetime_obj.getTime() > 7 * 24 * 60 * 60 * 1000) {
- display_str = datetime_obj.toDateString();
+ display_str = moment(datetime_obj).format('ll');
} else {
display_str = moment(datetime_obj).fromNow();
}
@@ -216,7 +216,7 @@ $(document).ready(function () {
});
});
}
-});
+})();
return {
page_widgets: page_widgets,
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 @@
<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 -->