diff options
Diffstat (limited to 'addons')
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 " -"''}&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\"></a><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\"></a><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\"></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&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 " -"''}&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\"></a><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\"></a><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\"></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&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" |
