summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/models/account_move.py2
-rw-r--r--addons/account_check_printing/models/account_payment.py2
-rw-r--r--addons/mail/models/ir_model.py6
-rw-r--r--addons/mail/models/mail_activity.py2
-rw-r--r--addons/mail/models/res_users.py13
-rw-r--r--addons/mail/static/src/js/chat_window.js6
-rw-r--r--addons/mail/static/src/js/composer.js6
-rw-r--r--addons/mail/static/src/js/document_viewer.js16
-rw-r--r--addons/mail/static/src/js/thread.js1
-rw-r--r--addons/mail/static/src/less/composer.less1
-rw-r--r--addons/mail/static/src/xml/thread.xml2
-rw-r--r--addons/mail/static/tests/chat_window_tests.js78
-rw-r--r--addons/mrp/models/mrp_workorder.py22
-rw-r--r--addons/mrp/wizard/mrp_product_produce.py7
-rw-r--r--addons/stock/models/stock_move.py2
-rw-r--r--addons/stock/models/stock_quant.py20
-rw-r--r--addons/stock/tests/test_move.py41
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js5
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js31
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js10
-rw-r--r--addons/web/static/src/js/views/form/form_view.js8
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js70
-rw-r--r--addons/website_sale_comparison/views/website_sale_comparison_template.xml2
-rw-r--r--addons/website_slides/models/slides.py2
-rw-r--r--odoo/api.py8
-rw-r--r--odoo/models.py13
-rwxr-xr-xsetup/package.py2
27 files changed, 328 insertions, 50 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)
diff --git a/odoo/api.py b/odoo/api.py
index 3f55669023e..d1b1872e7f5 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -1014,6 +1014,14 @@ class Cache(object):
]
return model.browse(ids)
+ def get_missing_ids(self, records, field):
+ """ Return the ids of ``records`` that have no value for ``field``. """
+ key = field.cache_key(records)
+ field_cache = self._data[field]
+ for record_id in records._ids:
+ if key not in field_cache.get(record_id, ()):
+ yield record_id
+
def copy(self, records, env):
""" Copy the cache of ``records`` to ``env``. """
src = records
diff --git a/odoo/models.py b/odoo/models.py
index c1f239e5918..32a0b4ed7d5 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -4784,12 +4784,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
(:class:`Field` instance), including ``self``.
Return at most ``limit`` records.
"""
- ids0 = self._prefetch[self._name]
- ids1 = set(self.env.cache.get_records(self, field)._ids)
- recs = self.browse([it for it in ids0 if it and it not in ids1])
- if limit and len(recs) > limit:
- recs = self + (recs - self)[:(limit - len(self))]
- return recs
+ recs = self.browse(self._prefetch[self._name])
+ ids = [self.id]
+ for record_id in self.env.cache.get_missing_ids(recs - self, field):
+ ids.append(record_id)
+ if limit and limit <= len(ids):
+ break
+ return self.browse(ids)
@api.model
def refresh(self):
diff --git a/setup/package.py b/setup/package.py
index eeea18aabe1..dd15b84e224 100755
--- a/setup/package.py
+++ b/setup/package.py
@@ -181,7 +181,7 @@ class KVM(object):
self.login = login
def timeout(self,signum,frame):
- logging.warning("vm timeout kill",self.pid)
+ logging.warning("vm timeout kill (pid: {})".format(self.pid))
os.kill(self.pid,15)
def start(self):