From 28d79fc9396a07afadcf0e6a57a611ad25591c76 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Tue, 2 Oct 2018 15:33:26 +0000 Subject: [PATCH 1/6] [FIX] mrp: pdf preview shown before saved In the current code, we only show a PDF previewer if a PDF is already saved. When the PDF is changed we see the preview if it was previously saved also. This seems to be the expected behavior, but this changeset changes it so we could always show a preview. opw-1888399 closes #27393 --- addons/mrp/static/src/js/mrp.js | 38 ++++++++++++++---------- addons/mrp/static/tests/mrp_tests.js | 44 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/addons/mrp/static/src/js/mrp.js b/addons/mrp/static/src/js/mrp.js index f1815650024..0c91ba4d89f 100644 --- a/addons/mrp/static/src/js/mrp.js +++ b/addons/mrp/static/src/js/mrp.js @@ -38,19 +38,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 @@ -94,14 +98,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/mrp/static/tests/mrp_tests.js b/addons/mrp/static/tests/mrp_tests.js index 3acca104dad..29426169653 100644 --- a/addons/mrp/static/tests/mrp_tests.js +++ b/addons/mrp/static/tests/mrp_tests.js @@ -1,6 +1,7 @@ odoo.define('mrp.tests', function (require) { "use strict"; +var field_registry = require('web.field_registry'); var FormView = require('web.FormView'); var testUtils = require("web.test_utils"); @@ -86,6 +87,49 @@ QUnit.module('mrp', { 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: + '
' + + '' + + '', + }); + + // 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("bullet_state: basic rendering", function (assert) { assert.expect(2); From 798d21ee3f197c2aaa4b990574bee7133df764c5 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Thu, 27 Sep 2018 11:42:13 +0000 Subject: [PATCH 2/6] [FIX] website_sale_options: on webshop, display attributes Before this commit, when the names and values of the attributes on an optional product were too long (and it did not take that much) The display was broken into lines After this commit, the container of the attributes works in standalone and won't break but, on very small devices, it is not ideal either OPW 1888416 closes odoo/odoo#27279 --- .../views/website_sale_options_templates.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 f7f505f5640..f7b0166fe0d 100644 --- a/addons/website_sale_options/views/website_sale_options_templates.xml +++ b/addons/website_sale_options/views/website_sale_options_templates.xml @@ -90,17 +90,16 @@ - +
- - -
+
+ Date: Mon, 8 Oct 2018 15:26:02 +0000 Subject: [PATCH 3/6] [FIX] stock_account: float division by zero Steps to reproduce: - Create a stockable product P with 2 quantity on hand - Choose a delivery order in state 'done', unlock it and add a line with P - Try to set 2 as done quantity and confirm the wizard Bug: A ZeroDivisionError(float division by zero) was raised. opw:1886735 closes odoo/odoo#27547 --- addons/stock/models/stock_move_line.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 81fda382d91..e965ea28d0b 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -185,6 +185,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') From b732ab3dc9698e65d55402c4a3ee499ced6af59b Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Tue, 25 Sep 2018 23:39:18 +0000 Subject: [PATCH 4/6] [FIX] base: fix dev mode for get_view_arch_from_file and inherits Before this commits, dev mode crash in some case (see below) when you declare a website_page without field name='arch'. After this commit, we follow the view_id (limit: only into the same file) Contactus works luckily before this commit, because compute field xml_id on View return the first ir_model_data (order by name asc by default). website.contactus is BEFORE website.contactus_page_ir_ui_view -- while -- website.bs_debug_page_view is AFTER website.contactus_page_ir_ui_view closes odoo/odoo#27234 --- odoo/addons/base/ir/ir_ui_view.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index f181b1786b2..40df3341055 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -127,6 +127,13 @@ def get_view_arch_from_file(filename, xmlid): if node.tag in ('template', 'record'): if node.tag == 'record': field = node.find('field[@name="arch"]') + if field is None: + if node.find('field[@name="view_id"]') is not None: + view_id = node.find('field[@name="view_id"]').attrib.get('ref') + ref_id = '%s%s' % ('.' not in view_id and xmlid.split('.')[0] + '.' or '', view_id) + return get_view_arch_from_file(filename, ref_id) + else: + return None _fix_multiple_roots(field) inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()]) return field.text + inner From 078b31dc7f67f3893bcbe5a3b76936490add0e38 Mon Sep 17 00:00:00 2001 From: svs-odoo Date: Tue, 2 Oct 2018 13:08:18 +0000 Subject: [PATCH 5/6] [FIX] web: Make datetimepicker configurable in datetime fields The datetimepicker option was introduced to be able to customize the datetime picker widget in date/datetime fields. However, due to the way the _makeDatePicker function was coded, it did not work in datetime fields. Thank to Yajo for the initial fix closes odoo/odoo#27541 --- .../web/static/src/js/fields/basic_fields.js | 27 ++++++++++++------- .../static/tests/fields/basic_fields_tests.js | 27 +++++++++++++++++++ doc/reference/javascript_reference.rst | 2 +- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 71aab9f7992..13c5de86491 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -404,6 +404,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. @@ -468,13 +472,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); }, /** @@ -492,6 +490,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 //-------------------------------------------------------------------------- @@ -522,10 +531,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. * diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 1133bed08b5..eab68f08207 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2556,6 +2556,33 @@ 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:'
' + + '' + + '', + res_id: 1, + }); + + form.$buttons.find('.o_form_button_create').click(); + form.$('.o_datepicker_input.o_input').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/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 053f0fe6bbb..d7fb8de399a 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -1348,7 +1348,7 @@ order. .. code-block:: xml - + - monetary (FieldMonetary) This is the default field type for fields of type 'monetary'. It is used to From a69f117bbf5031bde6c2d353a2adcfb399bf466a Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 31 Aug 2018 08:54:06 +0000 Subject: [PATCH 6/6] [FIX] board: handle actions with no view_mode 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 to 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. Before this rev., the second usecase wasn't handled, and it crashed when no view_mode was specified. This rev. also ensure that the context and domain are correctly retrieved from the action. Closes #24088 closes odoo/odoo#27517 --- addons/board/static/src/js/dashboard.js | 20 +++++--- addons/board/static/tests/dashboard_tests.js | 51 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index d14d85df28b..b1cbe86d5fa 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -9,6 +9,7 @@ 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 viewRegistry = require('web.view_registry'); var _t = core._t; @@ -243,7 +244,6 @@ FormRenderer.include({ */ _createController: function (params) { var self = this; - var context = params.context.eval(); return this._rpc({ route: '/web/action/load', params: {action_id: params.actionID} @@ -253,18 +253,22 @@ FormRenderer.include({ // the action does not exist anymore return $.when(); } + var rawContext = new Context(params.context, action.context); + 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 self.loadViews(action.res_model, params.context, [view]) + return descr[1] === viewType; + }) || [false, viewType]; + return self.loadViews(action.res_model, rawContext, [view]) // use context instead of rawContext when forwardported to > 11.0 .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 8d30922fc8b..08a22237a93 100644 --- a/addons/board/static/tests/dashboard_tests.js +++ b/addons/board/static/tests/dashboard_tests.js @@ -188,6 +188,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: FormView, // replace by BoardView when forwarported to saas-11.3 + model: 'board', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + archs: { + 'partner,4,list': + '', + }, + 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);