summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-rw-r--r--addons/account/report/account_aged_partner_balance.py17
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js1
-rw-r--r--addons/account/views/account_invoice_view.xml1
-rw-r--r--addons/account/views/account_view.xml6
-rw-r--r--addons/board/static/src/js/board_view.js21
-rw-r--r--addons/board/static/tests/dashboard_tests.js92
-rw-r--r--addons/calendar/i18n/fr.po297
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml18
-rw-r--r--addons/mail/models/mail_tracking_value.py4
-rw-r--r--addons/mail/views/mail_templates.xml2
-rw-r--r--addons/maintenance/models/maintenance.py4
-rw-r--r--addons/mass_mailing/static/src/scss/themes/theme_default.scss1
-rw-r--r--addons/mrp/models/mrp_workorder.py4
-rw-r--r--addons/mrp/report/mrp_production_templates.xml2
-rw-r--r--addons/mrp/report/mrp_report_bom_structure.py9
-rw-r--r--addons/point_of_sale/static/src/js/models.js3
-rw-r--r--addons/point_of_sale/static/src/js/screens.js8
-rw-r--r--addons/product_margin/models/product_product.py2
-rw-r--r--addons/purchase/models/purchase.py2
-rw-r--r--addons/purchase_mrp/models/purchase_mrp.py3
-rw-r--r--addons/purchase_requisition/models/purchase_requisition.py4
-rw-r--r--addons/sale/static/src/js/sale.js4
-rw-r--r--addons/sale_stock/models/sale_order.py2
-rw-r--r--addons/sale_timesheet/views/hr_timesheet_templates.xml2
-rw-r--r--addons/stock/i18n/de.po4
-rw-r--r--addons/stock/i18n/fr.po4
-rw-r--r--addons/stock/models/stock_inventory.py4
-rw-r--r--addons/stock/models/stock_move_line.py2
-rw-r--r--addons/stock/views/stock_picking_views.xml1
-rw-r--r--addons/stock/wizard/product_replenish.py3
-rw-r--r--addons/stock_account/views/stock_account_views.xml6
-rw-r--r--addons/stock_dropshipping/models/sale.py6
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js103
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js64
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js4
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js2
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js1
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js6
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js6
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js122
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js84
-rw-r--r--addons/web/static/tests/helpers/mock_server.js4
-rw-r--r--addons/web/static/tests/helpers/test_utils.js6
-rw-r--r--addons/web/static/tests/views/calendar_tests.js8
-rw-r--r--addons/web/static/tests/views/kanban_model_tests.js7
-rw-r--r--addons/web/static/tests/views/kanban_tests.js77
-rw-r--r--addons/web/static/tests/views/list_tests.js24
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js3
-rw-r--r--addons/web_editor/static/src/scss/web_editor.ui.scss3
-rw-r--r--addons/website_links/static/src/js/website_links_charts.js3
-rw-r--r--addons/website_sale/controllers/main.py17
-rw-r--r--addons/website_sale_delivery/controllers/main.py11
-rw-r--r--addons/website_sale_delivery/static/src/js/website_sale_delivery.js13
-rw-r--r--addons/website_sale_options/views/website_sale_options_templates.xml7
55 files changed, 830 insertions, 288 deletions
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 8748a03ff34..78e10cc7a20 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -17,17 +17,28 @@ class ReportAgedPartnerBalance(models.AbstractModel):
# Do an invoice and a payment and unreconcile. The amount will be nullified
# By default, the partner wouldn't appear in this report.
# The context key allow it to appear
+ # In case of a period_length of 30 days as of 2019-02-08, we want the following periods:
+ # Name Stop Start
+ # 1 - 30 : 2019-02-07 - 2019-01-09
+ # 31 - 60 : 2019-01-08 - 2018-12-10
+ # 61 - 90 : 2018-12-09 - 2018-11-10
+ # 91 - 120 : 2018-11-09 - 2018-10-11
+ # +120 : 2018-10-10
ctx = self._context
periods = {}
start = datetime.strptime(date_from, "%Y-%m-%d") - relativedelta(days=1)
for i in range(5)[::-1]:
stop = start - relativedelta(days=period_length)
+ period_name = str((5-(i+1)) * period_length + 1) + '-' + str((5-i) * period_length)
+ period_stop = (start - relativedelta(days=1)).strftime('%Y-%m-%d')
+ if i == 0:
+ period_name = '+' + str(4 * period_length)
periods[str(i)] = {
- 'name': (i!=0 and (str((5-(i+1)) * period_length) + '-' + str((5-i) * period_length)) or ('+'+str(4 * period_length))),
- 'stop': start.strftime('%Y-%m-%d'),
+ 'name': period_name,
+ 'stop': period_stop,
'start': (i!=0 and stop.strftime('%Y-%m-%d') or False),
}
- start = stop - relativedelta(days=1)
+ start = stop
res = []
total = []
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 03d5d9ac8a3..91b567730b7 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -44,6 +44,7 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
var defs = [this._super.apply(this, arguments)];
this.time = Date.now();
this.$progress = this.$('.progress');
+ this.clickStatementName = this._initialState.bank_statement_id ? true : false;
if (this._initialState.bank_statement_id) {
var def = this.model.makeRecord("account.bank.statement", [{
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 8345b94ac58..d352e2b8fa4 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -72,6 +72,7 @@
<field name="currency_id" invisible="1"/>
</group>
<group>
+ <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field domain="[('company_id', '=', parent.company_id)]" name="account_id" groups="account.group_account_user"/>
<field name="invoice_line_tax_ids" context="{'type':parent.type}" domain="[('type_tax_use','!=','none'),('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'no_create': True}"/>
<field domain="[('company_id', '=', parent.company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 1ebb1835507..3902d2e5737 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -423,8 +423,10 @@
</div>
<group>
<group>
- <field name="bank_acc_number"/>
- <field name="bank_id"/>
+ <field name="company_partner_id" invisible="1"/>
+ <field name="bank_account_id"/>
+ <field name="bank_acc_number" attrs="{'readonly': [('bank_account_id', '!=', False)]}"/>
+ <field name="bank_id" attrs="{'readonly': [('bank_account_id', '!=', False)]}"/>
</group>
<group>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
diff --git a/addons/board/static/src/js/board_view.js b/addons/board/static/src/js/board_view.js
index b2d57903a3d..60abe892045 100644
--- a/addons/board/static/src/js/board_view.js
+++ b/addons/board/static/src/js/board_view.js
@@ -9,6 +9,8 @@ var Domain = require('web.Domain');
var FormController = require('web.FormController');
var FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
+var pyUtils = require('web.pyeval'); // do not forwardport this to 12.0
+var session = require('web.session');
var viewRegistry = require('web.view_registry');
var _t = core._t;
@@ -236,7 +238,6 @@ var BoardRenderer = FormRenderer.extend({
*/
_createController: function (params) {
var self = this;
- var context = params.context.eval();
return this._rpc({
route: '/web/action/load',
params: {action_id: params.actionID}
@@ -246,18 +247,24 @@ var BoardRenderer = FormRenderer.extend({
// the action does not exist anymore
return $.when();
}
+ // tz and lang are saved in the custom view
+ // override the language to take the current one
+ var rawContext = new Context(params.context, action.context, {lang: session.user_context.lang});
+ var context = pyUtils.eval('context', rawContext);
+ var domain = params.domain || pyUtils.eval('domain', action.domain || '[]', action.context);
+ var viewType = params.viewType || action.views[0][1];
var view = _.find(action.views, function (descr) {
- return descr[1] === params.viewType;
- }) || [false, params.viewType];
+ return descr[1] === viewType;
+ }) || [false, viewType];
return self.loadViews(action.res_model, context, [view])
.then(function (viewsInfo) {
- var viewInfo = viewsInfo[params.viewType];
- var View = viewRegistry.get(params.viewType);
+ var viewInfo = viewsInfo[viewType];
+ var View = viewRegistry.get(viewType);
var view = new View(viewInfo, {
action: action,
context: context,
- domain: params.domain,
- groupBy: context.group_by,
+ domain: domain,
+ groupBy: context.group_by || [],
modelName: action.res_model,
hasSelectors: false,
});
diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index e777b7b36b7..2293e1c87e6 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -6,6 +6,7 @@ var BoardView = require('board.BoardView');
var ListController = require('web.ListController');
var testUtils = require('web.test_utils');
var ListRenderer = require('web.ListRenderer');
+var pyeval = require('web.pyeval');
var createActionManager = testUtils.createActionManager;
var createView = testUtils.createView;
@@ -191,6 +192,57 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
form.destroy();
});
+QUnit.test('can render an action without view_mode attribute', function (assert) {
+ // The view_mode attribute is automatically set to the 'action' nodes when
+ // the action is added to the dashboard using the 'Add to dashboard' button
+ // in the searchview. However, other dashboard views can be written by hand
+ // (see openacademy tutorial), and in this case, we don't want hardcode
+ // action's params (like context or domain), as the dashboard can directly
+ // retrieve them from the action. Same applies for the view_type, as the
+ // first view of the action can be used, by default.
+ assert.expect(2);
+
+ var form = createView({
+ View: BoardView,
+ model: 'board',
+ data: this.data,
+ arch: '<form string="My Dashboard">' +
+ '<board style="2-1">' +
+ '<column>' +
+ '<action string="ABC" name="51" context="{\'a\': 1}"></action>' +
+ '</column>' +
+ '</board>' +
+ '</form>',
+ archs: {
+ 'partner,4,list':
+ '<tree string="Partner"><field name="foo"/></tree>',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/board/static/src/img/layout_1-1-1.png') {
+ return $.when();
+ }
+ if (route === '/web/action/load') {
+ return $.when({
+ context: '{"b": 2}',
+ domain: '[["foo", "=", "yop"]]',
+ res_model: 'partner',
+ views: [[4, 'list'], [false, 'form']],
+ });
+ }
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [['foo', '=', 'yop']],
+ "should use the domain of the action");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.oe_action:contains(ABC) .o_list_view').length, 1,
+ "the list view (first view of action) should have been rendered correctly");
+
+ form.destroy();
+});
+
QUnit.test('can sort a sub list', function (assert) {
assert.expect(2);
@@ -691,4 +743,44 @@ QUnit.test('save to dashboard actions with flag keepSearchView', function (asser
actionManager.destroy();
});
+QUnit.test("Views should be loaded in the user's language", function (assert) {
+ assert.expect(2);
+ var form = createView({
+ View: BoardView,
+ model: 'board',
+ data: this.data,
+ session: {user_context: {lang: 'fr_FR'}},
+ arch: '<form string="My Dashboard">' +
+ '<board style="2-1">' +
+ '<column>' +
+ '<action context="{\'lang\': \'en_US\'}" view_mode="list" string="ABC" name="51" domain="[]"></action>' +
+ '</column>' +
+ '</board>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'load_views') {
+ assert.deepEqual(pyeval.eval('context', args.kwargs.context), {lang: 'fr_FR'},
+ 'The views should be loaded with the correct context');
+ }
+ if (route === "/web/dataset/search_read") {
+ assert.deepEqual(args.context, {lang: 'fr_FR'},
+ 'The data should be loaded with the correct context');
+ }
+ if (route === '/web/action/load') {
+ return $.when({
+ res_model: 'partner',
+ views: [[4, 'list']],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ 'partner,4,list':
+ '<list string="Partner"><field name="foo"/></list>',
+ },
+ });
+
+ form.destroy();
+});
+
});
diff --git a/addons/calendar/i18n/fr.po b/addons/calendar/i18n/fr.po
index 13fdc967623..0da3f13c630 100644
--- a/addons/calendar/i18n/fr.po
+++ b/addons/calendar/i18n/fr.po
@@ -302,73 +302,87 @@ msgid ""
" </table>\n"
"</div>"
msgstr ""
-"<div summary=\"o_mail_template\" "
-"style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top "
-"/100%;color:#777777\"><table cellspacing=\"0\" cellpadding=\"0\" "
-"style=\"width:600px;border-"
-"collapse:collapse;background:inherit;color:inherit\"><tbody><tr><td "
-"valign=\"center\" width=\"200\" style=\"padding:10px 10px 10px 5px;font-"
-"size: 12px\"><img src=\"/logo.png\" style=\"padding: 0px; margin: 0px; "
-"height: auto; width: 80px;\" alt=\"${user.company_id.name}\"></td><td "
-"valign=\"center\" align=\"right\" width=\"340\" style=\"padding:10px 10px "
-"10px 5px; font-size: 12px;\"><p><a "
-"href=\"/calendar/meeting/accept?db=${'dbname' in ctx and ctx['dbname'] or "
-"''}&amp;token=${object.access_token}&amp;action=${'action_id' in ctx and "
-"ctx['action_id'] or ''}&amp;id=${object.event_id.id}\" style=\"padding: 5px "
-"10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-"
-"color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: "
-"0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: "
-"pointer; white-space: nowrap; background-image: none; background-color: "
-"#875A7B; border: 1px solid #875A7B; border-radius:3px\"></a><a "
-"href=\"/calendar/meeting/decline?db=${'dbname' in ctx and ctx['dbname'] or "
-"'' }&amp;token=${object.access_token}&amp;action=${'action_id' in ctx and "
-"ctx['action_id'] or ''}&amp;id=${object.event_id.id}\" style=\"padding: 5px "
-"10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-"
-"color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: "
-"0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: "
-"pointer; white-space: nowrap; background-image: none; background-color: "
-"#875A7B; border: 1px solid #875A7B; border-radius:3px\"></a><a "
-"href=\"/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or "
-"''}&amp;token=${object.access_token}&amp;action=${'action_id' in ctx and "
-"ctx['action_id'] or ''}&amp;id=${object.event_id.id}\" style=\"padding: 5px "
-"10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-"
-"color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: "
-"0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: "
-"pointer; white-space: nowrap; background-image: none; background-color: "
-"#875A7B; border: 1px solid #875A7B; border-"
-"radius:3px\"></a></p></td></tr></tbody></table><table cellspacing=\"0\" "
-"cellpadding=\"0\" style=\"width:600px;border-"
-"collapse:collapse;background:inherit;color:inherit\"><tbody><tr><td "
-"valign=\"top\" style=\"width:600px; padding:10px 10px 10px 5px;\"><div><hr "
-"width=\"100%\" style=\"background-color:rgb(204,204,204);border:medium "
-"none;clear:both;display:block;font-size:0px;min-height:1px;line-"
-"height:0;margin:15px "
-"auto;padding:0\"></div></td></tr></tbody></table></div><div "
-"style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top "
-"/100%;color:#777777\"><table cellspacing=\"0\" cellpadding=\"0\" "
-"style=\"width:600px;border-"
-"collapse:collapse;background:inherit;color:inherit\"><tbody><tr><td "
-"style=\"padding:10px 10px 10px 5px;font-size: 14px;\"><p style=\"font-size: "
-"20px; text-align: center;\"><strong></strong></p><p><strong></strong><br "
-"/></p><table style=\"margin-top: 20px;\"><tr><td><div style=\"border-top-"
-"left-radius:3px;border-top-right-radius:3px;font-size:12px;border-"
-"collapse:separate;text-align:center;font-"
-"weight:bold;color:#ffffff;width:130px;min-height: 18px;background:#875A7B"
-";padding-top: 4px;border:1px solid #875A7B;\"></div><div style=\"font-"
-"size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F"
-";background-color: #F8F8F8;width: 130px;border:1px solid "
-"#875A7B;\"></div><div style='font-size:12px;text-align:center;font-"
-"weight:bold;color:#ffffff;background-color:#875A7B'></div><div style"
-"=\"border-collapse:separate;color: #5F5F5F;text-align:center;width: 130px"
-";font-size:12px;border-bottom-right-radius:3px;font-weight:bold;border:1px "
-"solid #875A7B;border-bottom-left-radius:3px;\"></div></td><td "
-"width=\"20px;\"/><td><p></p><ul><li><ul><li><div style=\"display:inline-"
-"block; border-radius: 50%; width:10px; "
-"height:10px;background:${colors[attendee.state] or 'white'};\"></div><span "
-"style=\"margin-left:5px\"></span><span style=\"margin-"
-"left:5px\"></span></li></ul></li></ul></td></tr></table></td></tr></tbody></table></div><li><a"
-" target=\"_blank\" "
-"href=\"http://maps.google.com/maps?oi=map&amp;q=${object.event_id.location}\"></a></li><li></li><li></li>"
+"\n"
+"<div summary=\"o_mail_template\" style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top /100%;color:#777777\">\n"
+"% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}\n"
+" <table cellspacing=\"0\" cellpadding=\"0\" style=\"width:600px;border-collapse:collapse;background:inherit;color:inherit\">\n"
+" <tbody><tr>\n"
+" <td valign=\"center\" width=\"200\" style=\"padding:10px 10px 10px 5px;font-size: 12px\">\n"
+" <img src=\"/logo.png\" style=\"padding: 0px; margin: 0px; height: auto; width: 80px;\" alt=\"${user.company_id.name}\">\n"
+" </td>\n"
+" <td valign=\"center\" align=\"right\" width=\"340\" style=\"padding:10px 10px 10px 5px; font-size: 12px;\">\n"
+" <p>\n"
+" <a href=\"/calendar/meeting/accept?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}\" style=\"padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px\">Accepter</a>\n"
+" <a href=\"/calendar/meeting/decline?db=${'dbname' in ctx and ctx['dbname'] or '' }&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}\" style=\"padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px\">Refuser</a>\n"
+" <a href=\"/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}\" style=\"padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px\">Voir</a>\n"
+" </p>\n"
+" </td>\n"
+" </tr></tbody>\n"
+" </table>\n"
+" <table cellspacing=\"0\" cellpadding=\"0\" style=\"width:600px;border-collapse:collapse;background:inherit;color:inherit\">\n"
+" <tbody><tr>\n"
+" <td valign=\"top\" style=\"width:600px; padding:10px 10px 10px 5px;\">\n"
+" <div>\n"
+" <hr width=\"100%\" style=\"background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0;margin:15px auto;padding:0\">\n"
+" </div>\n"
+" </td>\n"
+" </tr></tbody>\n"
+" </table>\n"
+"</div>\n"
+"<div style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top /100%;color:#777777\">\n"
+" <table cellspacing=\"0\" cellpadding=\"0\" style=\"width:600px;border-collapse:collapse;background:inherit;color:inherit\">\n"
+" <tbody><tr>\n"
+" <td style=\"padding:10px 10px 10px 5px;font-size: 14px;\">\n"
+" <p style=\"font-size: 20px; text-align: center;\">Invitation à l'événement <strong>${object.event_id.name}</strong></p>\n"
+" <p>\n"
+" <strong>Cher/chère ${object.common_name}</strong>,<br />\n"
+" ${object.event_id.user_id.partner_id.name} vous a invité(e) à la réunion ${object.event_id.name} de ${object.event_id.user_id.company_id.name}.</p>\n"
+" <table style=\"margin-top: 20px;\"><tr>\n"
+" <td>\n"
+" <div style=\"border-top-left-radius:3px;border-top-right-radius:3px;font-size:12px;border-collapse:separate;text-align:center;font-weight:bold;color:#ffffff;width:130px;min-height: 18px;background:#875A7B;padding-top: 4px;border:1px solid #875A7B;\">\n"
+" ${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}\n"
+" </div>\n"
+" <div style=\"font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #F8F8F8;width: 130px;border:1px solid #875A7B;\">\n"
+" ${object.event_id.get_interval('day', tz=object.partner_id.tz if not object.event_id.allday else None)}\n"
+" </div>\n"
+" <div style='font-size:12px;text-align:center;font-weight:bold;color:#ffffff;background-color:#875A7B'>${object.event_id.get_interval('month', tz=object.partner_id.tz if not object.event_id.allday else None)}</div>\n"
+" <div style=\"border-collapse:separate;color: #5F5F5F;text-align:center;width: 130px;font-size:12px;border-bottom-right-radius:3px;font-weight:bold;border:1px solid #875A7B;border-bottom-left-radius:3px;\">${not object.event_id.allday and object.event_id.get_interval('time', tz=object.partner_id.tz) or ''}</div>\n"
+" </td>\n"
+" <td width=\"20px;\"/>\n"
+" <td>\n"
+" <p>Détails de l'événement</p>\n"
+" <ul>\n"
+" % if object.event_id.location:\n"
+" <li>Lieu : ${object.event_id.location}\n"
+" (<a target=\"_blank\" href=\"http://maps.google.com/maps?oi=map&q=${object.event_id.location}\">Afficher la carte</a>)\n"
+" </li>\n"
+" % endif\n"
+" % if object.event_id.description :\n"
+" <li>Description : ${object.event_id.description}</li>\n"
+" % endif\n"
+" % if not object.event_id.allday and object.event_id.duration\n"
+" <li>Durée : ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}</li>\n"
+" % endif\n"
+" <li>Participants\n"
+" <ul>\n"
+" % for attendee in object.event_id.attendee_ids:\n"
+" <li>\n"
+" <div style=\"display:inline-block; border-radius: 50%; width:10px; height:10px;background:${colors[attendee.state] or 'white'};\"></div>\n"
+" % if attendee.common_name != object.common_name:\n"
+" <span style=\"margin-left:5px\">${attendee.common_name}</span>\n"
+" % else:\n"
+" <span style=\"margin-left:5px\">Vous</span>\n"
+" % endif\n"
+" </li>\n"
+" % endfor\n"
+" </ul></li>\n"
+" </ul>\n"
+" </td>\n"
+" </tr></table>\n"
+" </td>\n"
+" </tr></tbody>\n"
+" </table>\n"
+"</div>"
#. module: calendar
#: model:mail.template,body_html:calendar.calendar_template_meeting_reminder
@@ -456,73 +470,88 @@ msgid ""
" </table>\n"
"</div>"
msgstr ""
-"<div summary=\"o_mail_template\" "
-"style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top "
-"/100%;color:#777777\"><table cellspacing=\"0\" cellpadding=\"0\" "
-"style=\"width:600px;border-"
-"collapse:collapse;background:inherit;color:inherit\"><tbody><tr><td "
-"valign=\"center\" width=\"200\" style=\"padding:10px 10px 10px 5px;font-"
-"size: 12px\"><img src=\"/logo.png\" style=\"padding: 0px; margin: 0px; "
-"height: auto; width: 80px;\" alt=\"${user.company_id.name}\"></td><td "
-"valign=\"center\" align=\"right\" width=\"340\" style=\"padding:10px 10px "
-"10px 5px; font-size: 12px;\"><p><a "
-"href=\"/calendar/meeting/accept?db=${'dbname' in ctx and ctx['dbname'] or "
-"''}&amp;token=${object.access_token}&amp;action=${'action_id' in ctx and "
-"ctx['action_id'] or ''}&amp;id=${object.event_id.id}\" style=\"padding: 5px "
-"10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-"
-"color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: "
-"0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: "
-"pointer; white-space: nowrap; background-image: none; background-color: "
-"#875A7B; border: 1px solid #875A7B; border-radius:3px\"></a><a "
-"href=\"/calendar/meeting/decline?db=${'dbname' in ctx and ctx['dbname'] or "
-"'' }&amp;token=${object.access_token}&amp;action=${'action_id' in ctx and "
-"ctx['action_id'] or ''}&amp;id=${object.event_id.id}\" style=\"padding: 5px "
-"10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-"
-"color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: "
-"0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: "
-"pointer; white-space: nowrap; background-image: none; background-color: "
-"#875A7B; border: 1px solid #875A7B; border-radius:3px\"></a><a "
-"href=\"/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or "
-"''}&amp;token=${object.access_token}&amp;action=${'action_id' in ctx and "
-"ctx['action_id'] or ''}&amp;id=${object.event_id.id}\" style=\"padding: 5px "
-"10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-"
-"color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: "
-"0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: "
-"pointer; white-space: nowrap; background-image: none; background-color: "
-"#875A7B; border: 1px solid #875A7B; border-"
-"radius:3px\"></a></p></td></tr></tbody></table><table cellspacing=\"0\" "
-"cellpadding=\"0\" style=\"width:600px;border-"
-"collapse:collapse;background:inherit;color:inherit\"><tbody><tr><td "
-"valign=\"top\" style=\"width:600px; padding:10px 10px 10px 5px;\"><div><hr "
-"width=\"100%\" style=\"background-color:rgb(204,204,204);border:medium "
-"none;clear:both;display:block;font-size:0px;min-height:1px;line-"
-"height:0;margin:15px "
-"auto;padding:0\"></div></td></tr></tbody></table></div><div "
-"style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top "
-"/100%;color:#777777\"><table cellspacing=\"0\" cellpadding=\"0\" "
-"style=\"width:600px;border-"
-"collapse:collapse;background:inherit;color:inherit\"><tbody><tr><td "
-"style=\"padding:10px 10px 10px 5px;font-size: 14px;\"><p style=\"font-size: "
-"20px; text-align: center;\"><strong></strong></p><p><strong></strong><br "
-"/></p><table style=\"margin-top: 20px;\"><tr><td><div style=\"border-top-"
-"left-radius:3px;border-top-right-radius:3px;font-size:12px;border-"
-"collapse:separate;text-align:center;font-"
-"weight:bold;color:#ffffff;width:130px;min-height: 18px;background:#875A7B"
-";padding-top: 4px;\"></div><div style=\"font-size:48px;min-height:auto;font-"
-"weight:bold;text-align:center;color: #5F5F5F;background-color: "
-"#F8F8F8;width: 130px;border:1px solid #875A7B;\"></div><div style='font-"
-"size:12px;text-align:center;font-weight:bold;color:#ffffff;background-"
-"color:#875A7B'></div><div style=\"border-collapse:separate;color: #5F5F5F"
-";text-align:center;width: 130px;font-size:12px;border-bottom-right-"
-"radius:3px;font-weight:bold;border:1px solid #875A7B;border-bottom-left-"
-"radius:3px;\"></div></td><td "
-"width=\"20px;\"/><td><p></p><ul><li><ul><li><div style=\"display:inline-"
-"block; border-radius: 50%; width:10px; "
-"height:10px;background:${colors[attendee.state] or 'white'};\"></div><span "
-"style=\"margin-left:5px\"></span><span style=\"margin-"
-"left:5px\"></span></li></ul></li></ul></td></tr></table></td></tr></tbody></table></div><li><a"
-" target=\"_blank\" "
-"href=\"http://maps.google.com/maps?oi=map&amp;q=${object.event_id.location}\"></a></li><li></li><li></li>"
+"\n"
+"<div summary=\"o_mail_template\" style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top /100%;color:#777777\">\n"
+"% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}\n"
+" <table cellspacing=\"0\" cellpadding=\"0\" style=\"width:600px;border-collapse:collapse;background:inherit;color:inherit\">\n"
+" <tbody><tr>\n"
+" <td valign=\"center\" width=\"200\" style=\"padding:10px 10px 10px 5px;font-size: 12px\">\n"
+" <img src=\"/logo.png\" style=\"padding: 0px; margin: 0px; height: auto; width: 80px;\" alt=\"${user.company_id.name}\">\n"
+" </td>\n"
+" <td valign=\"center\" align=\"right\" width=\"340\" style=\"padding:10px 10px 10px 5px; font-size: 12px;\">\n"
+" <p>\n"
+" <a href=\"/calendar/meeting/accept?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}\" style=\"padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px\">Accepter</a>\n"
+" <a href=\"/calendar/meeting/decline?db=${'dbname' in ctx and ctx['dbname'] or '' }&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}\" style=\"padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px\">Refuser</a>\n"
+" <a href=\"/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}\" style=\"padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; background-image: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px\">Voir</a>\n"
+" </p>\n"
+" </td>\n"
+" </tr></tbody>\n"
+" </table>\n"
+" <table cellspacing=\"0\" cellpadding=\"0\" style=\"width:600px;border-collapse:collapse;background:inherit;color:inherit\">\n"
+" <tbody><tr>\n"
+" <td valign=\"top\" style=\"width:600px; padding:10px 10px 10px 5px;\">\n"
+" <div>\n"
+" <hr width=\"100%\" style=\"background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0;margin:15px auto;padding:0\">\n"
+" </div>\n"
+" </td>\n"
+" </tr></tbody>\n"
+" </table>\n"
+"</div>\n"
+"<div style=\"padding:0px;width:600px;margin:auto;background: #FFFFFF repeat top /100%;color:#777777\">\n"
+" <table cellspacing=\"0\" cellpadding=\"0\" style=\"width:600px;border-collapse:collapse;background:inherit;color:inherit\">\n"
+" <tbody><tr>\n"
+" <td style=\"padding:10px 10px 10px 5px;font-size: 14px;\">\n"
+" <p style=\"font-size: 20px; text-align: center;\">Rappel pour l'événement <strong>${object.event_id.name}</strong></p>\n"
+" <p>\n"
+" <strong>Cher/chère ${object.common_name}</strong>,<br />\n"
+" Il s'agit d'un rappel pour l'événement ci-dessous :\n"
+" </p>\n"
+" <table style=\"margin-top: 20px;\"><tr>\n"
+" <td>\n"
+" <div style=\"border-top-left-radius:3px;border-top-right-radius:3px;font-size:12px;border-collapse:separate;text-align:center;font-weight:bold;color:#ffffff;width:130px;min-height: 18px;background:#875A7B;padding-top: 4px;\">\n"
+" ${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}\n"
+" </div>\n"
+" <div style=\"font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #F8F8F8;width: 130px;border:1px solid #875A7B;\">\n"
+" ${object.event_id.get_interval('day', tz=object.partner_id.tz if not object.event_id.allday else None)}\n"
+" </div>\n"
+" <div style='font-size:12px;text-align:center;font-weight:bold;color:#ffffff;background-color:#875A7B'>${object.event_id.get_interval('month', tz=object.partner_id.tz if not object.event_id.allday else None)}</div>\n"
+" <div style=\"border-collapse:separate;color: #5F5F5F;text-align:center;width: 130px;font-size:12px;border-bottom-right-radius:3px;font-weight:bold;border:1px solid #875A7B;border-bottom-left-radius:3px;\">${not object.event_id.allday and object.event_id.get_interval('time', tz=object.partner_id.tz) or ''}</div>\n"
+" </td>\n"
+" <td width=\"20px;\"/>\n"
+" <td>\n"
+" <p>Détails de l'événement</p>\n"
+" <ul>\n"
+" % if object.event_id.location:\n"
+" <li>Lieu : ${object.event_id.location}\n"
+" (<a target=\"_blank\" href=\"http://maps.google.com/maps?oi=map&q=${object.event_id.location}\">Afficher la carte</a>)\n"
+" </li>\n"
+" % endif\n"
+" % if object.event_id.description :\n"
+" <li>Description : ${object.event_id.description}</li>\n"
+" % endif\n"
+" % if not object.event_id.allday and object.event_id.duration\n"
+" <li>Durée : ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}</li>\n"
+" % endif\n"
+" <li>Participants\n"
+" <ul>\n"
+" % for attendee in object.event_id.attendee_ids:\n"
+" <li>\n"
+" <div style=\"display:inline-block; border-radius: 50%; width:10px; height:10px;background:${colors[attendee.state] or 'white'};\"></div>\n"
+" % if attendee.common_name != object.common_name:\n"
+" <span style=\"margin-left:5px\">${attendee.common_name}</span>\n"
+" % else:\n"
+" <span style=\"margin-left:5px\">Vous</span>\n"
+" % endif\n"
+" </li>\n"
+" % endfor\n"
+" </ul></li>\n"
+" </ul>\n"
+" </td>\n"
+" </tr></table>\n"
+" </td>\n"
+" </tr></tbody>\n"
+" </table>\n"
+"</div>"
#. module: calendar
#: model:mail.template,subject:calendar.calendar_template_meeting_reminder
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index 13c33451b67..d33781a84f1 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -66,7 +66,7 @@
</div>
<group>
<group>
- <field name="product_id" context="{'default_can_be_expensed': 1}"/>
+ <field name="product_id" context="{'default_can_be_expensed': 1, 'tree_view_ref': 'hr_expense.product_product_expense_tree_view'}"/>
<field name="unit_amount" required="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<label for="quantity"/>
<div>
@@ -317,6 +317,22 @@
</field>
</record>
+ <record id="product_product_expense_tree_view" model="ir.ui.view">
+ <field name="name">product.product.expense.tree</field>
+ <field name="model">product.product</field>
+ <field eval="50" name="priority"/>
+ <field name="arch" type="xml">
+ <tree string="Product Variants">
+ <field name="default_code"/>
+ <field name="name"/>
+ <field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
+ <field name="standard_price"/>
+ <field name="uom_id" options="{'no_open': True, 'no_create': True}" groups="product.group_uom"/>
+ <field name="barcode"/>
+ </tree>
+ </field>
+ </record>
+
<record id="hr_expense_product" model="ir.actions.act_window">
<field name="name">Expense Products</field>
<field name="res_model">product.product</field>
diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py
index 5fbe63d6a14..24d3aff9207 100644
--- a/addons/mail/models/mail_tracking_value.py
+++ b/addons/mail/models/mail_tracking_value.py
@@ -60,8 +60,8 @@ class MailTracking(models.Model):
values.update({
'old_value_integer': initial_value and initial_value.id or 0,
'new_value_integer': new_value and new_value.id or 0,
- 'old_value_char': initial_value and initial_value.name_get()[0][1] or '',
- 'new_value_char': new_value and new_value.name_get()[0][1] or ''
+ 'old_value_char': initial_value and initial_value.sudo().name_get()[0][1] or '',
+ 'new_value_char': new_value and new_value.sudo().name_get()[0][1] or ''
})
else:
tracked = False
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index 7283014052e..a22019d5aba 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -80,7 +80,7 @@
<template id="message_user_assigned">
<p style="margin: 0px;">
- <span>Dear <t t-esc="object.user_id.name"/>,</span><br />
+ <span>Dear <t t-esc="object.user_id.sudo().name"/>,</span><br />
<span style="margin-top: 8px;">You have been assigned to the <t t-esc="object._description.lower()"/> <t t-esc="object.name_get()[0][1]"/>.</span>
</p>
<p style="margin-top: 24px; margin-bottom: 16px;">
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index b1a95bb495e..de52bb74514 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -348,8 +348,8 @@ class MaintenanceRequest(models.Model):
res = super(MaintenanceRequest, self).write(vals)
if vals.get('owner_user_id') or vals.get('technician_user_id'):
self._add_followers()
- if self.stage_id.done and 'stage_id' in vals:
- self.write({'close_date': fields.Date.today()})
+ if 'stage_id' in vals:
+ self.filtered(lambda m: m.stage_id.done).write({'close_date': fields.Date.today()})
self.activity_feedback(['maintenance.mail_act_maintenance_request'])
if 'schedule_date' in vals:
self.activity_update()
diff --git a/addons/mass_mailing/static/src/scss/themes/theme_default.scss b/addons/mass_mailing/static/src/scss/themes/theme_default.scss
index 704ea7656d7..d29af0935d4 100644
--- a/addons/mass_mailing/static/src/scss/themes/theme_default.scss
+++ b/addons/mass_mailing/static/src/scss/themes/theme_default.scss
@@ -87,7 +87,6 @@ td {
}
.btn.btn-primary {
- -webkit-user-select: none;
color: $o-mm-def-btn-text;
border: none;
text-decoration: none;
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 8fc24657f33..599dd356998 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -307,8 +307,10 @@ class MrpWorkorder(models.Model):
if self.product_id.tracking != 'none':
qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
move._generate_consumed_move_line(qty_to_add, self.final_lot_id)
- else:
+ elif len(move._get_move_lines()) < 2:
move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
+ else:
+ move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding))
# Transfer quantities from temporary to final move lots or make them final
for move_line in self.active_move_line_ids:
diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml
index 1e27b00e224..ba13c3f771c 100644
--- a/addons/mrp/report/mrp_production_templates.xml
+++ b/addons/mrp/report/mrp_production_templates.xml
@@ -35,7 +35,7 @@
<div class="col-xs-3">
<strong>Quantity to Produce:</strong><br/>
<span t-field="o.product_qty"/>
- <span t-field="o.product_uom_id.name" groups="product.group_uom"/>
+ <span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
</div>
<div class="col-xs-3">
<strong>Printing date:</strong><br/>
diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py
index 08aedeee44c..5734ce2a6b4 100644
--- a/addons/mrp/report/mrp_report_bom_structure.py
+++ b/addons/mrp/report/mrp_report_bom_structure.py
@@ -45,7 +45,7 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def get_operations(self, bom_id=False, qty=0, level=0):
bom = self.env['mrp.bom'].browse(bom_id)
- lines = self._get_operation_line(bom.routing_id, qty, level)
+ lines = self._get_operation_line(bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
values = {
'bom_id': bom_id,
'currency': self.env.user.company_id.currency_id,
@@ -91,7 +91,7 @@ class ReportBomStructure(models.AbstractModel):
product = self.env['product.product'].browse(int(product_id))
else:
product = bom.product_id or bom.product_tmpl_id.product_variant_id
- operations = self._get_operation_line(bom.routing_id, (bom_quantity / bom.product_qty), 0)
+ operations = self._get_operation_line(bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
lines = {
'bom': bom,
'bom_qty': bom_quantity,
@@ -119,7 +119,7 @@ class ReportBomStructure(models.AbstractModel):
continue
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
if line.child_bom_id:
- factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) * line.child_bom_id.product_qty
+ factor = float_round(line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty, precision_rounding=1, rounding_method='UP')
sub_total = self._get_price(line.child_bom_id, factor)
else:
sub_total = price
@@ -143,7 +143,8 @@ class ReportBomStructure(models.AbstractModel):
operations = []
total = 0.0
for operation in routing.operation_ids:
- duration_expected = qty * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
+ operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
+ duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
operations.append({
'level': level or 0,
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index df20d976efd..ed0794d2cff 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1509,6 +1509,7 @@ exports.Orderline = Backbone.Model.extend({
// when we add an new orderline we want to merge it with the last line to see reduce the number of items
// in the orderline. This returns true if it makes sense to merge the two
can_be_merged_with: function(orderline){
+ var price = parseFloat(round_di(this.price || 0, this.pos.dp['Product Price']).toFixed(this.pos.dp['Product Price']));
if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged
return false;
}else if(!this.get_unit() || !this.get_unit().is_pos_groupable){
@@ -1517,7 +1518,7 @@ exports.Orderline = Backbone.Model.extend({
return false;
}else if(this.get_discount() > 0){ // we don't merge discounted orderlines
return false;
- }else if(this.price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
+ }else if(price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
return false;
}else if(this.product.tracking == 'lot') {
return false;
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index f0623933788..75030e95acc 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -34,6 +34,7 @@ var core = require('web.core');
var rpc = require('web.rpc');
var utils = require('web.utils');
var field_utils = require('web.field_utils');
+var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
var QWeb = core.qweb;
var _t = core._t;
@@ -1678,6 +1679,13 @@ var PaymentScreenWidget = ScreenWidget.extend({
// also called explicitly to handle some keydown events that
// do not generate keypress events.
this.keyboard_handler = function(event){
+ // On mobile Chrome BarcodeEvents relies on an invisible
+ // input being filled by a barcode device. Let events go
+ // through when this input is focused.
+ if (BarcodeEvents.$barcodeInput && BarcodeEvents.$barcodeInput.is(":focus")) {
+ return;
+ }
+
var key = '';
if (event.type === "keypress") {
diff --git a/addons/product_margin/models/product_product.py b/addons/product_margin/models/product_product.py
index 46adfc5118b..9fc94ea6310 100644
--- a/addons/product_margin/models/product_product.py
+++ b/addons/product_margin/models/product_product.py
@@ -107,7 +107,7 @@ class ProductProduct(models.Model):
select
sum(l.price_unit * l.quantity)/nullif(sum(l.quantity),0) as avg_unit_price,
sum(l.quantity) as num_qty,
- sum(l.quantity * (l.price_subtotal/(nullif(l.quantity,0)))) as total,
+ sum(l.quantity * (l.price_subtotal_signed/(nullif(l.quantity,0)))) as total,
sum(l.quantity * pt.list_price) as sale_expected
from account_invoice_line l
left join account_invoice i on (l.invoice_id = i.id)
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 51e32a48ff5..7b43110aa48 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -421,7 +421,7 @@ class PurchaseOrderLine(models.Model):
taxes = line.product_id.supplier_taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.taxes_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_id) if fpos else taxes
- @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity')
+ @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity', 'invoice_lines.uom_id')
def _compute_qty_invoiced(self):
for line in self:
qty = 0.0
diff --git a/addons/purchase_mrp/models/purchase_mrp.py b/addons/purchase_mrp/models/purchase_mrp.py
index b498f4a63e5..ebfb1f0bc10 100644
--- a/addons/purchase_mrp/models/purchase_mrp.py
+++ b/addons/purchase_mrp/models/purchase_mrp.py
@@ -21,7 +21,8 @@ class PurchaseOrderLine(models.Model):
# In the case of a kit, we need to check if all components are shipped. Since the BOM might
# have changed, we don't compute the quantities but verify the move state.
if bom:
- bom_delivered = all([move.state == 'done' for move in self.move_ids])
+ moves = self.move_ids.filtered(lambda m: m.picking_id and m.picking_id.state != 'cancel')
+ bom_delivered = all([move.state == 'done' for move in moves])
if bom_delivered:
return self.product_qty
else:
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
index 3adfb19c426..c8862d15c4a 100644
--- a/addons/purchase_requisition/models/purchase_requisition.py
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -44,8 +44,8 @@ class PurchaseRequisition(models.Model):
def _get_picking_in(self):
pick_in = self.env.ref('stock.picking_type_in')
- if not pick_in:
- company = self.env['res.company']._company_default_get('purchase.requisition')
+ company = self.env['res.company']._company_default_get('purchase.requisition')
+ if not pick_in or pick_in.sudo().warehouse_id.company_id.id != company.id:
pick_in = self.env['stock.picking.type'].search(
[('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')],
limit=1,
diff --git a/addons/sale/static/src/js/sale.js b/addons/sale/static/src/js/sale.js
index 289d26ecae3..1121a38501a 100644
--- a/addons/sale/static/src/js/sale.js
+++ b/addons/sale/static/src/js/sale.js
@@ -21,8 +21,8 @@ KanbanRecord.include({
ev.preventDefault();
this.$target_input = $('<input>');
- this.$('.o_kanban_primary_bottom').html(this.$target_input);
- this.$('.o_kanban_primary_bottom').prepend(_t("Set an invoicing target: "));
+ this.$('.o_kanban_primary_bottom:last').html(this.$target_input);
+ this.$('.o_kanban_primary_bottom:last').prepend(_t("Set an invoicing target: "));
this.$target_input.focus();
var self = this;
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index 608d42214c3..5eebc48be63 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -166,7 +166,7 @@ class SaleOrderLine(models.Model):
qty = 0.0
for move in line.move_ids.filtered(lambda r: r.state == 'done' and not r.scrapped):
if move.location_dest_id.usage == "customer":
- if not move.origin_returned_move_id:
+ if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund):
qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
elif move.location_dest_id.usage != "customer" and move.to_refund:
qty -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index 5503066c3c3..ef6edee2828 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -188,7 +188,7 @@
<td>
<div t-if="repartition_employee_max" class="progress" t-att-style="'width: ' + str(repartition_employee[employee_id]['total'] / repartition_employee_max * 100) +'%'">
- <t t-set="total" t-value="repartition_employee[employee_id]['total']" />
+ <t t-set="total" t-value="repartition_employee[employee_id]['total'] or 1.0" />
<t t-set="billable_fixed" t-value="repartition_employee[employee_id]['billable_fixed']" />
<t t-set="billable_time" t-value="repartition_employee[employee_id]['billable_time']" />
<t t-set="non_billable" t-value="repartition_employee[employee_id]['non_billable']" />
diff --git a/addons/stock/i18n/de.po b/addons/stock/i18n/de.po
index 9c9561985c2..ac8a58779b4 100644
--- a/addons/stock/i18n/de.po
+++ b/addons/stock/i18n/de.po
@@ -5808,7 +5808,9 @@ msgstr ""
msgid ""
"The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has"
" been created."
-msgstr "<a href=# data-oe-model=stock.picking data-oe-id=%d></a>"
+msgstr ""
+"Der Lieferrückstand <a href=# data-oe-model=stock.picking data-oe-"
+"id=%d>%s</a> wurde erstellt."
#. module: stock
#: sql_constraint:stock.location:0
diff --git a/addons/stock/i18n/fr.po b/addons/stock/i18n/fr.po
index 99959ce0cbf..b8eebce3ff3 100644
--- a/addons/stock/i18n/fr.po
+++ b/addons/stock/i18n/fr.po
@@ -5860,7 +5860,9 @@ msgstr ""
msgid ""
"The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has"
" been created."
-msgstr "<a href=# data-oe-model=stock.picking data-oe-id=%d></a>"
+msgstr ""
+"Le reliquat <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> a été"
+" créé."
#. module: stock
#: sql_constraint:stock.location:0
diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py
index c8da3f66a51..a623ebcea3a 100644
--- a/addons/stock/models/stock_inventory.py
+++ b/addons/stock/models/stock_inventory.py
@@ -213,7 +213,7 @@ class Inventory(models.Model):
def _get_inventory_lines_values(self):
# TDE CLEANME: is sql really necessary ? I don't think so
locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])])
- domain = ' location_id in %s'
+ domain = ' location_id in %s AND active = TRUE'
args = (tuple(locations.ids),)
vals = []
@@ -254,6 +254,8 @@ class Inventory(models.Model):
self.env.cr.execute("""SELECT product_id, sum(quantity) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
FROM stock_quant
+ LEFT JOIN product_product
+ ON product_product.id = stock_quant.product_id
WHERE %s
GROUP BY product_id, location_id, lot_id, package_id, partner_id """ % domain, args)
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index 81ebfdf39ac..da855e4752a 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -182,6 +182,8 @@ class StockMoveLine(models.Model):
ml = super(StockMoveLine, self).create(vals)
if ml.state == 'done':
+ if 'qty_done' in vals:
+ ml.move_id.product_uom_qty = ml.move_id.quantity_done
if ml.product_id.type == 'product':
Quant = self.env['stock.quant']
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 6c499b84df8..3f40edd791f 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -327,6 +327,7 @@
<field name="group_id" groups="base.group_no_one"/>
<field name="priority" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
</group>
+ <field name="note" string="Note" placeholder="Add an internal note that will be printed on the Picking Operations sheet"/>
</group>
</page>
</notebook>
diff --git a/addons/stock/wizard/product_replenish.py b/addons/stock/wizard/product_replenish.py
index ee426cb5939..133d901c04f 100644
--- a/addons/stock/wizard/product_replenish.py
+++ b/addons/stock/wizard/product_replenish.py
@@ -4,6 +4,7 @@
import datetime
from odoo import api, fields, models
from odoo.exceptions import UserError
+from odoo.tools.misc import clean_context
class ProductReplenish(models.TransientModel):
@@ -51,7 +52,7 @@ class ProductReplenish(models.TransientModel):
uom_reference = self.product_id.uom_id
self.quantity = self.product_uom_id._compute_quantity(self.quantity, uom_reference)
try:
- self.env['procurement.group'].run(
+ self.env['procurement.group'].with_context(clean_context(self.env.context)).run(
self.product_id,
self.quantity,
uom_reference,
diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml
index ecf114226fb..4a38664ec38 100644
--- a/addons/stock_account/views/stock_account_views.xml
+++ b/addons/stock_account/views/stock_account_views.xml
@@ -75,7 +75,7 @@
<tree>
<field name="display_name"/>
<field name="qty_at_date"/>
- <field name="uom_id" groups="product.group_uom"/>
+ <field name="uom_id" groups="uom.group_uom"/>
<field name="currency_id" invisible="1"/>
<field name="stock_value" sum="Stock Valuation" widget="monetary"/>
<field name="cost_method" invisible="1"/>
@@ -104,7 +104,7 @@
<field name="ref" />
<field name="product_id" />
<field name="quantity" />
- <field name="product_uom_id" groups="product.group_uom" />
+ <field name="product_uom_id" groups="uom.group_uom" />
<field name="balance" string="Value"/>
</tree>
</field>
@@ -121,7 +121,7 @@
<field name="state" invisible="1"/>
<field name="product_id"/>
<field name="product_uom_qty" string="Quantity"/>
- <field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
+ <field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
<field name="value" sum="Stock Valuation"/>
</tree>
</field>
diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py
index 81f975cbce6..b527701a049 100644
--- a/addons/stock_dropshipping/models/sale.py
+++ b/addons/stock_dropshipping/models/sale.py
@@ -11,9 +11,11 @@ class SaleOrderLine(models.Model):
@api.multi
def _get_qty_procurement(self):
- if not self.move_ids.filtered(lambda r: r.state != 'cancel') and self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
+ # People without purchase rights should be able to do this operation
+ purchase_lines_sudo = self.sudo().purchase_line_ids
+ if not self.move_ids.filtered(lambda r: r.state != 'cancel') and purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty = 0.0
- for po_line in self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
+ for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
return qty
else:
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index c98d25989e9..9f2e9431bb1 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -434,6 +434,10 @@ var FieldDate = InputField.extend({
this._super.apply(this, arguments);
// use the session timezone when formatting dates
this.formatOptions.timezone = true;
+ this.datepickerOptions = _.defaults(
+ this.nodeOptions.datepicker || {},
+ {defaultDate: this.value}
+ );
},
/**
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
@@ -498,13 +502,7 @@ var FieldDate = InputField.extend({
* @private
*/
_makeDatePicker: function () {
- return new datepicker.DateWidget(
- this,
- _.defaults(
- this.nodeOptions.datepicker || {},
- {defaultDate: this.value}
- )
- );
+ return new datepicker.DateWidget(this, this.datepickerOptions);
},
/**
@@ -522,6 +520,17 @@ var FieldDate = InputField.extend({
var FieldDateTime = FieldDate.extend({
supportedFieldTypes: ['datetime'],
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ if (this.value) {
+ var offset = this.getSession().getTZOffset(this.value);
+ var displayedValue = this.value.clone().add(offset, 'minutes');
+ this.datepickerOptions.defaultDate = displayedValue;
+ }
+ },
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -552,10 +561,8 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_makeDatePicker: function () {
- var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
- return new datepicker.DateTimeWidget(this, {defaultDate: value});
+ return new datepicker.DateTimeWidget(this, this.datepickerOptions);
},
-
/**
* Set the datepicker to the right value rather than the default one.
*
@@ -952,6 +959,9 @@ var HandleWidget = AbstractField.extend({
var FieldEmail = InputField.extend({
className: 'o_field_email',
+ events: _.extend({}, InputField.prototype.events, {
+ 'click': '_onClick',
+ }),
prefix: 'mailto',
supportedFieldTypes: ['char'],
@@ -992,7 +1002,21 @@ var FieldEmail = InputField.extend({
this.$el.text(this.value)
.addClass('o_form_uri o_text_overflow')
.attr('href', this.prefix + ':' + this.value);
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prevent the URL click from opening the record (when used on a list).
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick: function (ev) {
+ ev.stopPropagation();
+ },
});
var FieldPhone = FieldEmail.extend({
@@ -1019,6 +1043,9 @@ var FieldPhone = FieldEmail.extend({
var UrlWidget = InputField.extend({
className: 'o_field_url',
+ events: _.extend({}, InputField.prototype.events, {
+ 'click': '_onClick',
+ }),
supportedFieldTypes: ['char'],
/**
@@ -1060,7 +1087,21 @@ var UrlWidget = InputField.extend({
.addClass('o_form_uri o_text_overflow')
.attr('target', '_blank')
.attr('href', this.value);
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prevent the URL click from opening the record (when used on a list).
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick: function (ev) {
+ ev.stopPropagation();
+ },
});
var AbstractFieldBinary = AbstractField.extend({
@@ -1322,19 +1363,23 @@ var FieldPdfViewer = FieldBinaryFile.extend({
},
/**
* @private
+ * @param {string} [fileURI] file URI if specified
* @returns {string} the pdf viewer URI
*/
- _getURI: function () {
- var queryObj = {
- model: this.model,
- field: this.name,
- id: this.res_id,
- };
+ _getURI: function (fileURI) {
var page = this.recordData[this.name + '_page'] || 1;
- var queryString = $.param(queryObj);
- var url = encodeURIComponent('/web/image?' + queryString);
+ if (!fileURI) {
+ var queryObj = {
+ model: this.model,
+ field: this.name,
+ id: this.res_id,
+ };
+ var queryString = $.param(queryObj);
+ fileURI = '/web/image?' + queryString
+ }
+ fileURI = encodeURIComponent(fileURI);
var viewerURL = '/web/static/lib/pdfjs/web/viewer.html?file=';
- return viewerURL + url + '#page=' + page;
+ return viewerURL + fileURI + '#page=' + page;
},
/**
* @private
@@ -1378,14 +1423,16 @@ var FieldPdfViewer = FieldBinaryFile.extend({
*/
on_file_change: function (ev) {
this._super.apply(this, arguments);
+ var files = ev.target.files;
+ if (!files || files.length === 0) {
+ return;
+ }
+ // TOCheck: is there requirement to fallback on FileReader if browser don't support URL
+ var fileURI = URL.createObjectURL(files[0]);
if (this.PDFViewerApplication) {
- var files = ev.target.files;
- if (!files || files.length === 0) {
- return;
- }
- var file = files[0];
- // TOCheck: is there requirement to fallback on FileReader if browser don't support URL
- this.PDFViewerApplication.open(URL.createObjectURL(file), 0);
+ this.PDFViewerApplication.open(fileURI, 0);
+ } else {
+ this.$('.o_pdfview_iframe').attr('src', this._getURI(fileURI));
}
},
/**
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 25722cd3db9..7ae9e0addf3 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -2377,10 +2377,12 @@ var FieldSelectionBadge = FieldSelection.extend({
* a FieldMany2one for its value.
* Its intern representation is similar to the many2one (a datapoint with a
* `name_get` as data).
+ * Note that there is some logic to support char field because of one use in our
+ * codebase, but this use should be removed along with this note.
*/
var FieldReference = FieldMany2One.extend({
specialData: "_fetchSpecialReference",
- supportedFieldTypes: ['char', 'reference'],
+ supportedFieldTypes: ['reference'],
template: 'FieldReference',
events: _.extend({}, FieldMany2One.prototype.events, {
'change select': '_onSelectionChange',
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 10283c308f6..9b5dec6d169 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -904,27 +904,51 @@ var BasicModel = AbstractModel.extend({
route: '/web/dataset/resequence',
params: params,
})
- .then(function () {
- var offset = options.offset ? options.offset : 0;
- var old_data = data.data.slice();
- data.data = _.sortBy(data.data, function (d) {
- if (_.contains(resIDs, self.localData[d].res_id)) {
- return _.indexOf(resIDs, self.localData[d].res_id) + offset;
- } else {
- return _.indexOf(old_data, d);
- }
- });
- data.res_ids = [];
- _.each(data.data, function (d) {
- var dataPoint = self.localData[d];
- if (dataPoint.type === 'record') {
- data.res_ids.push(dataPoint.res_id);
- } else {
- data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ .then(function (wasResequenced) {
+ if (!wasResequenced) {
+ // the field on which the resequence was triggered does not
+ // exist, so no resequence happened server-side
+ return $.when();
+ }
+ var field = params.field ? params.field : 'sequence';
+
+ return self._rpc({
+ model: modelName,
+ method: 'read',
+ args: [resIDs, [field]],
+ }).then(function (records) {
+ if (data.data.length) {
+ var dataType = self.localData[data.data[0]].type;
+ if (dataType === 'record') {
+ _.each(data.data, function (dataPoint) {
+ var recordData = self.localData[dataPoint].data;
+ var inRecords = _.findWhere(records, {id: recordData.id});
+ if (inRecords) {
+ recordData[field] = inRecords[field];
+ }
+ });
+ data.data = _.sortBy(data.data, function (d) {
+ return self.localData[d].data[field];
+ });
+ }
+ if (dataType === 'list') {
+ data.data = _.sortBy(data.data, function (d) {
+ return _.indexOf(resIDs, self.localData[d].res_id)
+ });
+ }
}
- });
- self._updateParentResIDs(data);
- return parentID;
+ data.res_ids = [];
+ _.each(data.data, function (d) {
+ var dataPoint = self.localData[d];
+ if (dataPoint.type === 'record') {
+ data.res_ids.push(dataPoint.res_id);
+ } else {
+ data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ }
+ });
+ self._updateParentResIDs(data);
+ return parentID;
+ })
});
},
/**
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 2c4ed711223..e1befc9daf1 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -378,6 +378,10 @@ var BasicView = AbstractView.extend({
if (!(dependency_name in fields)) {
fields[dependency_name] = dependency_dict;
}
+
+ if (fv.fields && !(dependency_name in fv.fields)) {
+ fv.fields[dependency_name] = dependency_dict;
+ }
}
}
return false;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 4b4050b67cc..06043f69846 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -77,7 +77,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
self.model.get(recordID),
{
mode: 'edit',
- can_create: false,
+ attrs: {can_create: false},
attrs: {
'placeholder': _.str.sprintf(_t("Add %s"), self.title),
},
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index f1e89698e97..46429189e12 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -948,7 +948,6 @@ var FormRenderer = BasicRenderer.extend({
*/
_onNavigationMove: function (ev) {
ev.stopPropagation();
-
var index;
if (ev.data.direction === "next") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target);
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 30bafaab117..654617c9847 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -450,10 +450,12 @@ var KanbanRecord = Widget.extend({
ischild = false;
}
var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== "tooltip");
+ var testLinkWithHref = elem.nodeName.toLowerCase() === 'a' && elem.href;
if (ischild) {
children.push(elem);
- if (test_event) {
- // do not trigger global click if one child has a click event registered
+ if (test_event || testLinkWithHref) {
+ // Do not trigger global click if one child has a click
+ // event registered (or it is a link with href)
trigger = false;
}
}
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 8f87b36c874..d3f4d134229 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -22,8 +22,6 @@ var DateWidget = Widget.extend({
init: function(parent, options) {
this._super.apply(this, arguments);
- var l10n = _t.database.parameters;
-
this.name = parent.name;
this.options = _.defaults(options || {}, {
format : this.type_of_date === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(),
@@ -51,12 +49,8 @@ var DateWidget = Widget.extend({
*/
start: function() {
this.$input = this.$('input.o_datepicker_input');
- this.$input.focus(function(e) {
- e.stopImmediatePropagation();
- });
this.$input.datetimepicker(this.options);
this.picker = this.$input.data('DateTimePicker');
- this.$input.click(this.picker.toggle.bind(this.picker));
this._setReadonly(false);
},
/**
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 6b828397ba8..34a2302cd18 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -10,6 +10,7 @@ var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var session = require('web.session');
var testUtils = require('web.test_utils');
+var field_registry = require('web.field_registry');
var createView = testUtils.createView;
var DebouncedField = basicFields.DebouncedField;
@@ -1653,6 +1654,49 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test("pdf_viewer: upload rendering", function (assert) {
+ assert.expect(6);
+
+ testUtils.patch(field_registry.map.pdf_viewer, {
+ on_file_change: function (ev) {
+ ev.target = {files: [new Blob()]};
+ this._super.apply(this, arguments);
+ },
+ _getURI: function (fileURI) {
+ var res = this._super.apply(this, arguments);
+ assert.step('_getURI');
+ assert.ok(_.str.startsWith(fileURI, 'blob:'));
+ this.PDFViewerApplication = {
+ open: function (URI) {
+ assert.step('open');
+ assert.ok(_.str.startsWith(URI, 'blob:'));
+ },
+ };
+ return 'about:blank';
+ },
+ });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="document" widget="pdf_viewer"/>' +
+ '</form>',
+ });
+
+ // first upload initialize iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI']);
+ // second upload call pdfjs method inside iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI', 'open']);
+
+ testUtils.unpatch(field_registry.map.pdf_viewer);
+ form.destroy();
+ });
+
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
@@ -1852,6 +1896,40 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('image fields in x2many list are loaded correctly', function (assert) {
+ assert.expect(2);
+
+ this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
+ this.data.partner_type.records[0].image = 'product_image';
+ this.data.partner.records[0].timmy = [12];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="image" widget="image"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === 'data:image/png;base64,product_image') {
+ assert.ok(true, "The list's image should have been fetched");
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ 'There should be one record in the many2many');
+
+ form.destroy();
+ });
+
QUnit.test('image fields with required attribute', function (assert) {
assert.expect(2);
@@ -2148,7 +2226,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('date field value should not set on first click', function (assert) {
- assert.expect(2);
+ assert.expect(3);
var form = createView({
View: FormView,
@@ -2158,10 +2236,10 @@ QUnit.module('basic_fields', {
res_id: 4,
});
form.$buttons.find('.o_form_button_edit').click();
- form.$('.o_datepicker_input').click();
+ assert.strictEqual($('.bootstrap-datetimepicker-widget').length, 1, "there should be a datepicker (autofocus)");
assert.strictEqual(form.$('.o_datepicker_input').val(), '', "date field's input should be empty on first click");
$('.day:contains(22)').click();
- form.$('.o_datepicker_input').click(); // Open Datepicker second time
+ form.$('.o_datepicker_input').focus(); // Open Datepicker second time
assert.strictEqual($('.day.active').text(), '22', 'datepicker should be highlight with 22nd day of month');
form.destroy();
});
@@ -2199,8 +2277,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- // click on the input and select another value
- form.$('.o_datepicker_input').click();
+ // select another value
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
assert.strictEqual($('.day.active').data('day'), '02/03/2017', 'datepicker should be highlight February 3');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
@@ -2283,8 +2360,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(list.$('input.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- // click on the input and select another value
- list.$('input.o_datepicker_input').click();
+ // select another value
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2401,8 +2477,7 @@ QUnit.module('basic_fields', {
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString,
'the datetime should be correct in edit mode');
- // click on the input and select 22 February at 8:23:33
- form.$('.o_datepicker_input').click();
+ // select 22 February at 8:23:33
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2500,8 +2575,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString,
'the date should be correct in edit mode');
- // click on the input and select 22 February at 8:23:33
- list.$('input.o_datepicker_input').click();
+ // select 22 February at 8:23:33
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2659,6 +2733,32 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('datepicker option: daysOfWeekDisabled', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
+ this.data.partner.fields.datetime.required = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="datetime" ' +
+ 'options=\'{"datepicker": {"daysOfWeekDisabled": [0, 6]}}\'/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_create').click();
+ assert.ok($('.day:last-child(),.day:nth-child(2)').hasClass('disabled'),
+ 'first and last days must be disabled');
+ assert.notOk($('.day:not(:last-child()):not(:nth-child(2))').hasClass('disabled'),
+ 'other days must stay clickable');
+
+ form.destroy();
+ });
+
QUnit.module('FieldMonetary');
QUnit.test('monetary field in form view', function (assert) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 2763933bae7..20008a92c21 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -9819,6 +9819,37 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('click on URL should not open the record', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="display_name" widget="email"/>' +
+ '<field name="turtle_foo" widget="url"/>' +
+ '</tree>' +
+ '<form></form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$('.o_email_cell a').click();
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+
+ form.$('.o_url_cell a').click();
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+ form.destroy();
+ });
+
QUnit.test('create and edit on m2o in o2m, and press ESCAPE', function (assert) {
assert.expect(4);
@@ -12952,6 +12983,59 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form><group><field name="foo"/><field name="bar"/></group></form>'
+ },
+ });
+
+ // add a new line
+ form.$el.find('.o_field_x2many_list_row_add>a').click();
+
+ // open the field turtle_trululu (one2many)
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+ form.$el.find('.o_input_dropdown>input').click();
+
+ concurrency.delay(0).then(function () {
+ // click create and edit
+ $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter').click();
+
+ // hit escape immediately
+ var escapeKey = $.ui.keyCode.ESCAPE;
+ $(document.activeElement).trigger(
+ $.Event('keydown', {which: escapeKey, keyCode: escapeKey}));
+
+ assert.ok('did not crash');
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ done();
+ });
+ });
+
});
});
});
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 6a6dd62f2df..e0b59ae5119 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -904,10 +904,14 @@ var MockServer = Class.extend({
var offset = args.offset ? Number(args.offset) : 0;
var field = args.field ? args.field : 'sequence';
var records = this.data[args.model].records;
+ if (!(field in this.data[args.model].fields)) {
+ return false;
+ }
for (var i in args.ids) {
var record = _.findWhere(records, {id: args.ids[i]});
record[field] = Number(i) + offset;
}
+ return true;
},
/**
* Simulate a 'search_count' operation
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index beb39687364..db40c2b5049 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -218,6 +218,12 @@ function createAsyncView(params) {
controlPanel.appendTo($web_client);
var $content = $('<div>').addClass('o_content').appendTo($web_client);
+ if (params.interceptsPropagate) {
+ _.each(params.interceptsPropagate, function (cb, name) {
+ intercept(widget, name, cb, true);
+ });
+ }
+
return view.getController(widget).then(function (view) {
// override the view's 'destroy' so that it calls 'destroy' on the widget
// instead, as the widget is the parent of the view and the mockServer.
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 0a7ca319c2f..71dd1e155c3 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -645,14 +645,14 @@ QUnit.module('Views', {
"should display the datetime from the date with the timezone");
// use datepicker to enter a date: 12/13/2016 08:00:00
- $('.o_field_widget[name="start"] input').trigger('click');
+ $('.o_field_widget[name="start"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click');
// use datepicker to enter a date: 12/13/2016 10:00:00
- $('.o_field_widget[name="stop"] input').trigger('click');
+ $('.o_field_widget[name="stop"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click');
@@ -894,14 +894,14 @@ QUnit.module('Views', {
"should display the datetime from the date with the timezone");
// use datepicker to enter a date: 12/13/2016 08:00:00
- $('.o_field_widget[name="start"] input').trigger('click');
+ $('.o_field_widget[name="start"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click');
// use datepicker to enter a date: 12/13/2016 10:00:00
- $('.o_field_widget[name="stop"] input').trigger('click');
+ $('.o_field_widget[name="stop"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click');
diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js
index 1a3391c720d..20eb2918d04 100644
--- a/addons/web/static/tests/views/kanban_model_tests.js
+++ b/addons/web/static/tests/views/kanban_model_tests.js
@@ -192,6 +192,8 @@ QUnit.module('Views', {
var done = assert.async();
assert.expect(8);
+ this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
+ this.data.partner.fields.sequence = {string: "Sequence", type: "integer"};
this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37});
var nbReseq = 0;
@@ -204,7 +206,7 @@ QUnit.module('Views', {
if (nbReseq === 1) { // resequencing columns
assert.deepEqual(args.ids, [41, 37],
"ids should be correct");
- assert.strictEqual(args.model, 'product_id',
+ assert.strictEqual(args.model, 'product',
"model should be correct");
} else if (nbReseq === 2) { // resequencing records
assert.deepEqual(args.ids, [3, 1],
@@ -212,7 +214,6 @@ QUnit.module('Views', {
assert.strictEqual(args.model, 'partner',
"model should be correct");
}
- return $.when();
}
return this._super.apply(this, arguments);
},
@@ -229,7 +230,7 @@ QUnit.module('Views', {
"first group should be res_id 37");
// resequence columns
- return model.resequence('product_id', [41, 37], stateID);
+ return model.resequence('product', [41, 37], stateID);
})
.then(function (stateID) {
var state = model.get(stateID);
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 7680d8877b1..9db68d8a26e 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -1335,6 +1335,72 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('Do not open record when clicking on `a` with `href`', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records = [
+ { id: 1, foo: 'yop' },
+ ];
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<field name="foo"/>' +
+ '<div>' +
+ '<a class="o_test_link" href="#">test link</a>' +
+ '</div>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ intercepts: {
+ // when clicking on a record in kanban view,
+ // it switches to form view.
+ switch_view: function () {
+ throw new Error("should not switch view");
+ },
+ },
+ });
+
+ var $record = kanban.$('.o_kanban_record:not(.o_kanban_ghost)');
+ assert.strictEqual($record.length, 1,
+ "should display a kanban record");
+
+ var $testLink = $record.find('a');
+ assert.strictEqual($testLink.length, 1,
+ "should contain a link in the kanban record");
+ assert.ok(!!$testLink[0].href,
+ "link inside kanban record should have non-empty href");
+
+ // Mocked views prevent accessing a link with href. This is intented
+ // most of the time, but not in this test which specifically needs to
+ // let the browser access a link with href.
+ kanban.$el.off('click', 'a');
+ // Prevent the browser default behaviour when clicking on anything.
+ // This includes clicking on a `<a>` with `href`, so that it does not
+ // change the URL in the address bar.
+ // Note that we should not specify a click listener on 'a', otherwise
+ // it may influence the kanban record global click handler to not open
+ // the record.
+ $(document.body).on('click.o_test', function (ev) {
+ assert.notOk(ev.isDefaultPrevented(),
+ "should not prevented browser default behaviour beforehand");
+ assert.strictEqual(ev.target, $testLink[0],
+ "should have clicked on the test link in the kanban record");
+ ev.preventDefault();
+ });
+
+ $testLink.click();
+
+ $(document.body).off('click.o_test');
+ kanban.destroy();
+ });
+
QUnit.test('can drag and drop a record from one column to the next', function (assert) {
assert.expect(9);
@@ -1731,7 +1797,7 @@ QUnit.module('Views', {
});
QUnit.test('delete a column in grouped on m2o', function (assert) {
- assert.expect(33);
+ assert.expect(36);
testUtils.patch(KanbanRenderer, {
_renderGrouped: function () {
@@ -2697,6 +2763,7 @@ QUnit.module('Views', {
QUnit.test('resequence columns in grouped by m2o', function (assert) {
assert.expect(7);
+ this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
var kanban = createView({
@@ -2710,12 +2777,6 @@ QUnit.module('Views', {
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
- mockRPC: function (route) {
- if (route === '/web/dataset/resequence') {
- return $.when();
- }
- return this._super.apply(this, arguments);
- },
intercepts: {
env_updated: function (event) {
assert.deepEqual(event.data.env.ids, envIDs,
@@ -2739,7 +2800,7 @@ QUnit.module('Views', {
kanban.update({}, {reload: false}); // re-render without reloading
assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5,
- "first column should be id 5 before resequencing");
+ "first column should be id 5 after resequencing");
kanban.destroy();
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 9f476a6f1c9..05f7495e312 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -3085,10 +3085,10 @@ QUnit.module('Views', {
foo: {
fields: {int_field: {string: "int_field", type: "integer", sortable: true}},
records: [
- {id: 1, int_field: 0},
- {id: 2, int_field: 1},
- {id: 3, int_field: 2},
- {id: 4, int_field: 3},
+ {id: 1, int_field: 11},
+ {id: 2, int_field: 12},
+ {id: 3, int_field: 13},
+ {id: 4, int_field: 14},
]
}
};
@@ -3107,14 +3107,15 @@ QUnit.module('Views', {
assert.deepEqual(args, {
model: "foo",
ids: [4, 3],
- offset: 2,
+ offset: 13,
field: "int_field",
});
}
if (moves === 1) {
assert.deepEqual(args, {
model: "foo",
- ids: [1, 4, 2, 3],
+ ids: [4, 2],
+ offset: 12,
field: "int_field",
});
}
@@ -3122,14 +3123,15 @@ QUnit.module('Views', {
assert.deepEqual(args, {
model: "foo",
ids: [2, 4],
- offset: 1,
+ offset: 12,
field: "int_field",
});
}
if (moves === 3) {
assert.deepEqual(args, {
model: "foo",
- ids: [1, 4, 2, 3],
+ ids: [4, 2],
+ offset: 12,
field: "int_field",
});
}
@@ -3196,7 +3198,6 @@ QUnit.module('Views', {
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2, 3],
"should write the sequence in correct order");
- return $.when();
}
return this._super.apply(this, arguments);
},
@@ -3326,13 +3327,16 @@ QUnit.module('Views', {
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
+ var _super = this._super.bind(this);
assert.strictEqual(args.offset, 1,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2, 3],
"should write the sequence in correct order");
- return $.when(def);
+ return $.when(def).then(function () {
+ return _super(route, args);
+ });
}
return this._super.apply(this, arguments);
},
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 097f3d3b12f..d452dfe07b8 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -754,6 +754,7 @@ registry.colorpicker = SnippetOption.extend({
*/
_onColorResetButtonClick: function () {
this.$target.removeClass(this.classes);
+ self.$target.trigger('content_changed');
this.$el.find('.colorpicker button.selected').removeClass('selected');
},
});
@@ -898,7 +899,7 @@ registry.background = SnippetOption.extend({
* @param {string} value
*/
_setCustomBackground: function (value) {
- this.__customImageSrc = this._getSrcFromCssValue(value);
+ this.__customImageSrc = value;
this.background(false, this.__customImageSrc);
this.$target.addClass('oe_custom_bg');
this._setActive();
diff --git a/addons/web_editor/static/src/scss/web_editor.ui.scss b/addons/web_editor/static/src/scss/web_editor.ui.scss
index 179f814c4b3..24ec59e8f61 100644
--- a/addons/web_editor/static/src/scss/web_editor.ui.scss
+++ b/addons/web_editor/static/src/scss/web_editor.ui.scss
@@ -1037,6 +1037,9 @@ body.editor_enable {
left: 0;
}
}
+ .btn {
+ -webkit-user-select: initial;
+ }
}
#web_editor_inside_iframe {
diff --git a/addons/website_links/static/src/js/website_links_charts.js b/addons/website_links/static/src/js/website_links_charts.js
index 75876b55c35..98aebba58a4 100644
--- a/addons/website_links/static/src/js/website_links_charts.js
+++ b/addons/website_links/static/src/js/website_links_charts.js
@@ -61,6 +61,7 @@ if (!$('.o_website_links_chart').length) {
var chart = nv.models.lineChart()
.x(function (d) { return getDate(d); })
.y(function (d) { return getNbClicks(d); })
+ .margin({top: 10, right: 60, bottom: 60, left: 60})
.showYAxis(true)
.showXAxis(true);
@@ -74,7 +75,7 @@ if (!$('.o_website_links_chart').length) {
chart.yAxis
.tickFormat(d3.format("d"))
- .ticks(chart_data[0]['values'].length - 1);
+ .ticks(Math.min(chart_data[0]['values'].length - 1, 10));
d3.select(self.$element + ' svg')
.datum(chart_data)
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index c5a89397e47..e1ba0e62572 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -1070,3 +1070,20 @@ class WebsiteSale(http.Controller):
states=[(st.id, st.name, st.code) for st in country.get_website_sale_states(mode=mode)],
phone_code=country.phone_code
)
+
+ @http.route(['/shop/update_carrier'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
+ def update_eshop_carrier(self, **post):
+ results = {}
+ if hasattr(self, '_update_website_sale_delivery'):
+ results.update(self._update_website_sale_delivery(**post))
+
+ if hasattr(self, '_update_website_sale_coupon'):
+ results.update(self._update_website_sale_coupon(**post))
+
+ return results
+
+ def _format_amount(self, amount, currency):
+ fmt = "%.{0}f".format(currency.decimal_places)
+ lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
+ return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
+ .replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py
index 52522fa8117..a7b7d41998b 100644
--- a/addons/website_sale_delivery/controllers/main.py
+++ b/addons/website_sale_delivery/controllers/main.py
@@ -55,8 +55,7 @@ class WebsiteSaleDelivery(WebsiteSale):
values['delivery_action_id'] = request.env.ref('delivery.action_delivery_carrier_form').id
return values
- @http.route(['/shop/update_carrier'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
- def update_eshop_carrier(self, **post):
+ def _update_website_sale_delivery(self, **post):
order = request.website.sale_get_order()
carrier_id = int(post['carrier_id'])
currency = order.currency_id
@@ -70,10 +69,4 @@ class WebsiteSaleDelivery(WebsiteSale):
'new_amount_tax': self._format_amount(order.amount_tax, currency),
'new_amount_total': self._format_amount(order.amount_total, currency),
}
-
- def _format_amount(self, amount, currency):
- fmt = "%.{0}f".format(currency.decimal_places)
- lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
-
- return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
- .replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
+ return {}
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 971e604d1d1..79c70becf98 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
@@ -3,6 +3,8 @@ odoo.define('website_sale_delivery.checkout', function (require) {
require('web.dom_ready');
var ajax = require('web.ajax');
+ var core = require('web.core');
+ var _t = core._t;
/* Handle interactive carrier choice + cart update */
var $pay_button = $('#o_payment_form_pay');
@@ -14,6 +16,17 @@ odoo.define('website_sale_delivery.checkout', function (require) {
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');
+ var $discount = $('#order_discounted');
+
+ if ($discount && result.new_amount_order_discounted) {
+ // Cross module without bridge
+ // Update discount of the order
+ $discount.find('.oe_currency_value').text(result.new_amount_order_discounted);
+
+ // We are in freeshipping, so every carrier is Free
+ $('#delivery_carrier .badge').text(_t('Free'));
+ }
+
if (result.status === true) {
$amount_delivery.text(result.new_amount_delivery);
$amount_untaxed.text(result.new_amount_untaxed);
diff --git a/addons/website_sale_options/views/website_sale_options_templates.xml b/addons/website_sale_options/views/website_sale_options_templates.xml
index 90c51ec14f2..2914cb862ac 100644
--- a/addons/website_sale_options/views/website_sale_options_templates.xml
+++ b/addons/website_sale_options/views/website_sale_options_templates.xml
@@ -92,17 +92,16 @@
<input type="hidden" class="product_id" t-attf-name="optional-product-#{product_index}" t-att-value="product.product_variant_id.id if len(product.product_variant_ids) == 1 else '0'"/>
<span t-field="product.image_small" t-options='{"widget": "image"}'/>
</td>
- <td class='td-product_name'>
+ <td class='td-product_name' colspan="2">
<div class="pull-left">
<strong class="media-heading" t-field="product.name"/>
<div class="text-muted small" t-field="product.description_sale"/>
</div>
- </td>
- <td class="td-qty">
- <div class="pull-right">
+ <div class="pull-right">
<t t-call="website_sale.variants"/>
</div>
</td>
+
<td class="td-price">
<span class="text-danger oe_default_price oe_optional"
t-field="product.lst_price"