diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-05-04 16:10:50 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-05-04 16:10:50 +0200 |
| commit | ac63dfc8f48fa6451b4ef94e24e784f252b84f8e (patch) | |
| tree | 768b741e1a34a94b95977d8ff869143aab73617e /addons | |
| parent | 62705aee69460457c1b9257ac30363754c60c973 (diff) | |
| parent | 8dade01e2c5b560864291fb3c437ccf5999aee12 (diff) | |
[MERGE] forward port branch 11.0 up to 8dade01e2c5
Diffstat (limited to 'addons')
24 files changed, 312 insertions, 43 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a6288da6a22..8e974ef5b26 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1054,8 +1054,6 @@ class AccountMoveLine(models.Model): raise UserError(_('Entries are not of the same account!')) if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'): raise UserError(_('The account %s (%s) is not marked as reconcilable !') % (all_accounts[0].name, all_accounts[0].code)) - if len(partners) > 1: - raise UserError(_('The partner has to be the same on all lines for receivable and payable accounts!')) #reconcile everything that can be remaining_moves = self.auto_reconcile_lines() diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 61ed7176ca1..3f29d9bf10b 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -34,7 +34,7 @@ class AccountRegisterPayments(models.TransientModel): res = super(AccountRegisterPayments, self)._prepare_payment_vals(invoices) if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'): res.update({ - 'check_amount_in_words': self.check_amount_in_words, + 'check_amount_in_words': self.currency_id.amount_to_text(res['amount']) if self.multi else self.check_amount_in_words, }) return res diff --git a/addons/mail/models/ir_model.py b/addons/mail/models/ir_model.py index 04529610f0e..0b029f3e1a1 100644 --- a/addons/mail/models/ir_model.py +++ b/addons/mail/models/ir_model.py @@ -52,12 +52,6 @@ class IrModel(models.Model): model_class._inherit = parents + ['mail.thread'] return model_class - def unlink(self): - # Delete followers for models that will be unlinked. - query = "DELETE FROM mail_followers WHERE res_model IN %s" - self.env.cr.execute(query, [tuple(self.mapped('model'))]) - return super(IrModel, self).unlink() - class IrModelField(models.Model): _inherit = 'ir.model.fields' diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 47e0c37408f..209a05999be 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -88,7 +88,7 @@ class MailActivity(models.Model): summary = fields.Char('Summary') note = fields.Html('Note') feedback = fields.Html('Feedback') - date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today) + date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today) # description user_id = fields.Many2one( 'res.users', 'Assigned to', diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index f397d19e506..c03c80cbb3b 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -128,16 +128,19 @@ class Users(models.Model): def activity_user_count(self): query = """SELECT m.id, count(*), act.res_model as model, CASE - WHEN now()::date - act.date_deadline::date = 0 Then 'today' - WHEN now()::date - act.date_deadline::date > 0 Then 'overdue' - WHEN now()::date - act.date_deadline::date < 0 Then 'planned' + WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today' + WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue' + WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned' END AS states FROM mail_activity AS act JOIN ir_model AS m ON act.res_model_id = m.id - WHERE user_id = %s + WHERE user_id = %(user_id)s GROUP BY m.id, states, act.res_model; """ - self.env.cr.execute(query, [self.env.uid]) + self.env.cr.execute(query, { + 'today': fields.Date.context_today(self), + 'user_id': self.env.uid, + }) activity_data = self.env.cr.dictfetchall() model_ids = [a['id'] for a in activity_data] model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()} diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index 9f09eb390b0..9e080f3a248 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -16,7 +16,8 @@ var HEIGHT_FOLDED = '34px'; return Widget.extend({ template: "mail.ChatWindow", custom_events: { - escape_pressed: '_onEscapePressed' + escape_pressed: '_onEscapePressed', + document_viewer_closed: '_onDocumentViewerClose', }, events: { 'click .o_chat_composer': '_onComposerClick', @@ -182,6 +183,9 @@ return Widget.extend({ } this.focus_input(); }, + _onDocumentViewerClose: function (ev) { + this.focus_input(); + }, /** * @private */ diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index 3a7b9c320e1..8354fe610b5 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -565,6 +565,9 @@ var BasicComposer = Widget.extend({ on_click_add_attachment: function () { this.$('input.o_input_file').click(); this.$input.focus(); + // set ignoreEscape to avoid escape_pressed event when file selector dialog is opened + // when user press escape to cancel file selector dialog then escape_pressed event should not be trigerred + this.ignoreEscape = true; }, setState: function (state) { @@ -635,6 +638,8 @@ var BasicComposer = Widget.extend({ if (this.mention_manager.is_open()) { event.stopPropagation(); this.mention_manager.reset_suggestions(); + } else if (this.ignoreEscape) { + this.ignoreEscape = false; } else { this.trigger_up("escape_pressed"); } @@ -871,6 +876,7 @@ var BasicComposer = Widget.extend({ * @param {MouseEvent} event */ _onAttachmentView: function (event) { + event.stopPropagation(); var activeAttachmentID = $(event.currentTarget).data('id'); var attachments = this.get('attachment_ids'); if (activeAttachmentID) { diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js index 0aac950cf5b..d7c43b2d4f6 100644 --- a/addons/mail/static/src/js/document_viewer.js +++ b/addons/mail/static/src/js/document_viewer.js @@ -25,6 +25,7 @@ var DocumentViewer = Widget.extend({ 'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox 'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE 'keydown': '_onKeydown', + 'keyup': '_onKeyUp', 'mousedown .o_viewer_img': '_onStartDrag', 'mousemove .o_viewer_content': '_onDrag', 'mouseup .o_viewer_content': '_onEndDrag' @@ -152,6 +153,7 @@ var DocumentViewer = Widget.extend({ _onClose: function (e) { e.preventDefault(); this.$el.modal('hide'); + this.trigger_up('document_viewer_closed'); }, /** * When popup close complete destroyed modal even DOM footprint too @@ -233,6 +235,20 @@ var DocumentViewer = Widget.extend({ } }, /** + * Close popup on ESCAPE keyup + * + * @private + * @param {KeyEvent} e + */ + _onKeyUp: function (e) { + switch (e.which) { + case $.ui.keyCode.ESCAPE: + e.preventDefault(); + this._onClose(e); + break; + } + }, + /** * @private * @param {MouseEvent} e */ diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 97b6bf4906f..f05134891e1 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -371,6 +371,7 @@ var Thread = Widget.extend({ * @param {MouseEvent} event */ _onAttachmentView: function (event) { + event.stopPropagation(); var activeAttachmentID = $(event.currentTarget).data('id'); if (activeAttachmentID) { var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID); diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index 66b754c6d97..976473f6e1e 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -336,6 +336,7 @@ } .o_modal_fullscreen { + z-index: @o-chat-window-zindex + 1; // To overlap chat window .o_viewer_content { width: 100%; height: 100%; diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index eef37598164..30d1f85c071 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -76,7 +76,7 @@ </t> <t t-name="DocumentViewer"> - <div class="modal o_modal_fullscreen" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog" aria-hidden="true"> <t t-call="DocumentViewer.Content"/> <t t-if="widget.attachment.length != 1"> diff --git a/addons/mail/static/tests/chat_window_tests.js b/addons/mail/static/tests/chat_window_tests.js index ab5a9f03538..05bbc418e70 100644 --- a/addons/mail/static/tests/chat_window_tests.js +++ b/addons/mail/static/tests/chat_window_tests.js @@ -61,6 +61,84 @@ QUnit.module('mail', { parent.destroy(); }); + QUnit.test('open document viewer and close using ESCAPE key should reset focus to chat window', function (assert) { + assert.expect(6); + + function createParent(params) { + var widget = new Widget(); + + testUtils.addMockEnvironment(widget, params); + return widget; + } + var messages = [{ + attachment_ids: [{ + filename: 'image1.jpg', + id:1, + mimetype: 'image/jpeg', + name: 'Test Image 1', + url: '/web/content/1?download=true' + }], + author_id: ["1", "John Doe"], + body: "A message", + date: moment("2016-12-20 09:35:40"), + displayed_author: "John Doe", + id: 1, + is_note: false, + is_starred: false, + model: 'partner', + res_id: 2 + }]; + var parent = createParent({ + mockRPC: function (route, args) { + if(_.str.contains(route, '/mail/attachment/preview/') || + _.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){ + var canvas = document.createElement('canvas'); + return $.when(canvas.toDataURL()); + } + return this._super.apply(this, arguments); + }, + data: {}, + }); + + var chatWindow = new ChatWindow(parent, 1, "user", false, messages.length, {}); + chatWindow.appendTo($('#qunit-fixture')); + chatWindow.render(messages); + + testUtils.intercept(chatWindow, 'get_messages', function(event) { + event.stopPropagation(); + var requested_msgs = _.filter(messages, function (msg) { + return _.contains(event.data.options.ids, msg.id); + }); + event.data.callback($.when(requested_msgs)); + }, true); + + testUtils.intercept(chatWindow, 'get_bus', function(event) { + event.stopPropagation(); + event.data.callback(new Bus()); + }, true); + + chatWindow.on('document_viewer_closed', null, function () { + assert.ok(true, "chat window should trigger a close document viewer event"); + }); + assert.strictEqual(chatWindow.$('.o_thread_message .o_attachment').length, 1, + "there should be three attachment on message"); + // click on first image attachement + chatWindow.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first().click(); + // check focus is on document viewer popup and then press escape to close it + assert.strictEqual(document.activeElement, $('.o_modal_fullscreen')[0], "Modal popup should have focus"); + assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[src*="/web/image/1?unique=1"]').length, 1, + "Modal popup should open with first image src"); + // trigger ESCAPE keyup on document viewer popup + var upKeyEvent = jQuery.Event("keyup", {which: 27}); + $('.o_modal_fullscreen').trigger(upKeyEvent); + assert.strictEqual(document.activeElement, chatWindow.$input[0], + "input should be focused"); + var upKeyEvent = jQuery.Event( "keyup", {which: 27}); + chatWindow.$('.o_composer_input').trigger(upKeyEvent); + assert.strictEqual(chatWindow.folded, false, "Closed chat Window"); + parent.destroy(); + }); + QUnit.test('chat window\'s input can still be focused when the UI is blocked', function (assert) { assert.expect(2); diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 28541fcb6c0..eef3b9a7de9 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -290,6 +290,18 @@ class MrpWorkorder(models.Model): self.final_lot_id = self.env['stock.production.lot'].search([('use_next_on_work_order_id', '=', self.id)], order='create_date, id', limit=1) + def _get_byproduct_move_line(self, by_product_move, quantity): + return { + 'move_id': by_product_move.id, + 'product_id': by_product_move.product_id.id, + 'product_uom_qty': quantity, + 'product_uom_id': by_product_move.product_uom.id, + 'qty_done': quantity, + 'workorder_id': self.id, + 'location_id': by_product_move.location_id.id, + 'location_dest_id': by_product_move.location_dest_id.id, + } + @api.multi def record_production(self): self.ensure_one() @@ -367,8 +379,14 @@ class MrpWorkorder(models.Model): if not self.next_work_order_id: for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))): - if by_product_move.has_tracking == 'none': - by_product_move.quantity_done += self.qty_producing * by_product_move.unit_factor + if by_product_move.has_tracking != 'serial': + values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor) + self.env['stock.move.line'].create(values) + elif by_product_move.has_tracking == 'serial': + qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id) + for i in range(0, int(float_round(qty_todo, precision_digits=0))): + values = self._get_byproduct_move_line(by_product_move, 1) + self.env['stock.move.line'].create(values) # Update workorder quantity produced self.qty_produced += self.qty_producing diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 7e960e94056..047b575816e 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -38,7 +38,8 @@ class MrpProductProduce(models.TransientModel): if 'produce_line_ids' in fields: lines = [] for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel') and x.bom_line_id): - qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty + qty_to_consume = float_round(todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty, + precision_rounding=move.product_uom.rounding, rounding_method="UP") for move_line in move.move_line_ids: if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: break @@ -176,9 +177,9 @@ class MrpProductProduceLine(models.TransientModel): product_produce_id = fields.Many2one('mrp.product.produce') product_id = fields.Many2one('product.product', 'Product') lot_id = fields.Many2one('stock.production.lot', 'Lot') - qty_to_consume = fields.Float('To Consume') + qty_to_consume = fields.Float('To Consume', digits=dp.get_precision('Product Unit of Measure')) product_uom_id = fields.Many2one('product.uom', 'Unit of Measure') - qty_done = fields.Float('Done') + qty_done = fields.Float('Done', digits=dp.get_precision('Product Unit of Measure')) move_id = fields.Many2one('stock.move') @api.onchange('lot_id') diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 67370c0a30d..cdf318c3b51 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -902,7 +902,7 @@ class StockMove(models.Model): assigned_moves = self.env['stock.move'] partially_available_moves = self.env['stock.move'] for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']): - if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\ + if move.location_id.should_bypass_reservation()\ or move.product_id.type == 'consu': # create the move line(s) but do not impact quants if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots): diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index bfd6e52acf4..c1765c3f083 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -253,13 +253,21 @@ class StockQuant(models.Model): self = self.sudo() rounding = product_id.uom_id.rounding quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) - available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) - if float_compare(quantity, 0, precision_rounding=rounding) > 0 and float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: - raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name')))) - elif float_compare(quantity, 0, precision_rounding=rounding) < 0 and float_compare(abs(quantity), sum(quants.mapped('reserved_quantity')), precision_rounding=rounding) > 0: - raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name')))) - reserved_quants = [] + + if float_compare(quantity, 0, precision_rounding=rounding) > 0: + # if we want to reserve + available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) + if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: + raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name')))) + elif float_compare(quantity, 0, precision_rounding=rounding) < 0: + # if we want to unreserve + available_quantity = sum(quants.mapped('reserved_quantity')) + if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0: + raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name')))) + else: + return reserved_quants + for quant in quants: if float_compare(quantity, 0, precision_rounding=rounding) > 0: max_quantity_on_quant = quant.quantity - quant.reserved_quantity diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 32e815b7af3..df57065b8d7 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -1120,6 +1120,47 @@ class StockMove(TransactionCase): for quant in quants: self.assertEqual(quant.reserved_quantity, 0) + def test_unreserve_6(self): + """ In a situation with a negative and a positive quant, reserve and unreserve. + """ + q1 = self.env['stock.quant'].create({ + 'product_id': self.product1.id, + 'location_id': self.stock_location.id, + 'quantity': -10, + 'reserved_quantity': 0, + }) + + q2 = self.env['stock.quant'].create({ + 'product_id': self.product1.id, + 'location_id': self.stock_location.id, + 'quantity': 30.0, + 'reserved_quantity': 10.0, + }) + + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0) + + move1 = self.env['stock.move'].create({ + 'name': 'test_unreserve_6', + 'location_id': self.stock_location.id, + 'location_dest_id': self.customer_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10.0, + }) + move1._action_confirm() + move1._action_assign() + self.assertEqual(move1.state, 'assigned') + self.assertEqual(len(move1.move_line_ids), 1) + self.assertEqual(move1.move_line_ids.product_qty, 10) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0) + self.assertEqual(q2.reserved_quantity, 20) + + move1._do_unreserve() + self.assertEqual(move1.state, 'confirmed') + self.assertEqual(len(move1.move_line_ids), 0) + self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0) + self.assertEqual(q2.reserved_quantity, 10) + def test_link_assign_1(self): """ Test the assignment mechanism when two chained stock moves try to move one unit of an untracked product. diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 4ba65e6d258..a12377e9b5b 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1294,10 +1294,7 @@ var FieldOne2Many = FieldX2Many.extend({ var self = this; var id = ev.data.id; - var onSaved = function (record, hasChanged) { - if (!hasChanged) { - return; - } + var onSaved = function (record) { if (_.some(self.value.data, {id: record.id})) { // the record already exists in the relation, so trigger an // empty 'UPDATE' operation when the user clicks on 'Save' in 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 22a7a7a0188..3936ff4112c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1054,7 +1054,13 @@ var BasicModel = AbstractModel.extend({ addFieldsInfo: function (recordID, viewInfo) { var record = this.localData[recordID]; record.fields = _.extend({}, record.fields, viewInfo.fields); - record.fieldsInfo = _.extend({}, record.fieldsInfo, viewInfo.fieldsInfo); + // complete the given fieldsInfo with the fields of the main view, so + // that those field will be reloaded if a reload is triggered by the + // secondary view + var fieldsInfo = _.mapObject(viewInfo.fieldsInfo, function (fieldsInfo) { + return _.defaults({}, fieldsInfo, record.fieldsInfo[record.viewType]); + }); + record.fieldsInfo = _.extend({}, record.fieldsInfo, fieldsInfo); }, /** * For list resources, this freezes the current records order. @@ -1993,7 +1999,8 @@ var BasicModel = AbstractModel.extend({ */ _fetchRecord: function (record, options) { var self = this; - var fieldNames = options && options.fieldNames || record.getFieldNames(); + options = options || {}; + var fieldNames = options.fieldNames || record.getFieldNames(options); fieldNames = _.uniq(fieldNames.concat(['display_name'])); return this._rpc({ model: record.model, @@ -2493,8 +2500,9 @@ var BasicModel = AbstractModel.extend({ _fetchX2Manys: function (record, options) { var self = this; var defs = []; - var fieldNames = options && options.fieldNames || record.getFieldNames(); - var viewType = options && options.viewType || record.viewType; + options = options || {}; + var fieldNames = options.fieldNames || record.getFieldNames(options); + var viewType = options.viewType || record.viewType; _.each(fieldNames, function (fieldName) { var field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { @@ -3017,11 +3025,15 @@ var BasicModel = AbstractModel.extend({ * default view type. * * @param {Object} element an element from the localData + * @param {Object} [options] + * @param {Object} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record * @returns {string[]} the list of field names */ - _getFieldNames: function (element) { + _getFieldNames: function (element, options) { var fieldsInfo = element.fieldsInfo; - return Object.keys(fieldsInfo && fieldsInfo[element.viewType] || {}); + var viewType = options && options.viewType || element.viewType; + return Object.keys(fieldsInfo && fieldsInfo[viewType] || {}); }, /** * Evaluate the record evaluation context. This method is supposed to be @@ -3501,13 +3513,16 @@ var BasicModel = AbstractModel.extend({ * @see _fetchRecord @see _makeDefaultRecord * * @param {Object} record - * @param {Object} record + * @param {Object} [options] + * @param {Object} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record * @returns {Deferred<Object>} resolves to the finished resource */ _postprocess: function (record, options) { var self = this; var defs = []; - _.each(record.getFieldNames(), function (name) { + + _.each(record.getFieldNames(options), function (name) { var field = record.fields[name]; var fieldInfo = record.fieldsInfo[record.viewType][name] || {}; var options = fieldInfo.options || {}; diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 4c73fde121b..2cc52e4937b 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -214,6 +214,16 @@ var FormController = BasicController.extend({ return changedFields; }); }, + /** + * Overrides to force the viewType to 'form', so that we ensure that the + * correct fields are reloaded (this is only useful for one2many form views). + * + * @override + */ + update: function (params, options) { + params = _.extend({viewType: 'form'}, params); + return this._super(params, options); + }, //-------------------------------------------------------------------------- // Private diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js index 10fc9a12b20..d8693586590 100644 --- a/addons/web/static/src/js/views/form/form_view.js +++ b/addons/web/static/src/js/views/form/form_view.js @@ -73,6 +73,14 @@ var FormView = BasicView.extend({ _.each(this.loadParams.fieldsInfo.form, function (attrs, fieldName) { var field = fields[fieldName]; + if (!field) { + // when a one2many record is opened in a form view, the fields + // of the main one2many view (list or kanban) are added to the + // fieldsInfo of its form view, but those fields aren't in the + // loadParams.fields, as they are not displayed in the view, so + // we can ignore them. + return; + } if (field.type !== 'one2many' && field.type !== 'many2many') { return; } diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 29d3dad6056..b2e41a981e3 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -5699,7 +5699,7 @@ QUnit.module('relational_fields', { rpcCount++; if (args.method === 'write') { assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], { - int_field: 123, product_id: 41 + foo: false, int_field: 123, product_id: 41, }]]); } return this._super(route, args); @@ -8857,6 +8857,74 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many form view with action button', function (assert) { + // once the action button is clicked, the record is reloaded (via the + // on_close handler, executed because the python method does not return + // any action, or an ir.action.act_window_close) ; this test ensures that + // it reloads the fields of the opened view (i.e. the form in this case). + // See https://github.com/odoo/odoo/issues/24189 + assert.expect(7); + + var data = this.data; + data.partner.records[0].p = [2]; + + var form = createView({ + View: FormView, + model: 'partner', + data: data, + res_id: 1, + arch: '<form string="Partners">' + + '<field name="p">' + + '<tree><field name="display_name"/></tree>' + + '<form>' + + '<button type="action" string="Set Timmy"/>' + + '<field name="timmy"/>' + + '</form>' + + '</field>' + + '</form>', + archs: { + 'partner_type,false,list': '<tree><field name="display_name"/></tree>', + }, + intercepts: { + execute_action: function (ev) { + data.partner.records[1].display_name = 'new name'; + data.partner.records[1].timmy = [12]; + ev.data.on_closed(); + }, + }, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_data_row').length, 1, + "there should be one record in the one2many"); + assert.strictEqual(form.$('.o_data_cell').text(), 'second record', + "initial display_name of o2m record should be correct"); + + // open one2many record in form view + form.$('.o_data_cell:first').click(); + assert.strictEqual($('.modal .o_form_view').length, 1, + "should have opened the form view in a dialog"); + assert.strictEqual($('.modal .o_form_view .o_data_row').length, 0, + "there should be no record in the many2many"); + + // click on the action button + $('.modal .o_form_view button').click(); + assert.strictEqual($('.modal .o_data_row').length, 1, + "fields in the o2m form view should have been read"); + assert.strictEqual($('.modal .o_data_cell').text(), 'gold', + "many2many subrecord should have been fetched"); + + // save the dialog + $('.modal .modal-footer .btn-primary').click(); + + assert.strictEqual(form.$('.o_data_cell').text(), 'new name', + "fields in the o2m list view should have been read as well"); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { diff --git a/addons/website_sale_comparison/views/website_sale_comparison_template.xml b/addons/website_sale_comparison/views/website_sale_comparison_template.xml index f472c5db811..e69065fa910 100644 --- a/addons/website_sale_comparison/views/website_sale_comparison_template.xml +++ b/addons/website_sale_comparison/views/website_sale_comparison_template.xml @@ -57,7 +57,7 @@ <template id='recommended_product' inherit_id="website_sale.recommended_products" name="Alternative Products"> <xpath expr="//h3" position="replace"> - <t t-set="ids" t-value="str(product.id) + ','+ ','.join([str(a) for a in product.alternative_product_ids.ids])"></t> + <t t-set="ids" t-value="str(product.product_variant_id.id) + ','+ ','.join([str(a) for a in product.alternative_product_ids.mapped('product_variant_id.id')])"/> <div><span class='h3'>Suggested alternatives: </span><a class="btn btn-primary" t-attf-href="/shop/compare/?products=#{ids}"><i class="fa fa-exchange"></i> Compare</a></div> </xpath> </template> diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 1689e8070c9..24795760727 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -348,6 +348,8 @@ class Slide(models.Model): def _get_embed_code(self): base_url = request and request.httprequest.url_root or self.env['ir.config_parameter'].sudo().get_param('web.base.url') + if base_url[-1] == '/': + base_url = base_url[:-1] for record in self: if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']): slide_url = base_url + url_for('/slides/embed/%s?page=1' % record.id) |
