[MERGE] forward port branch 11.0 up to 8dade01e2c
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -232,6 +234,20 @@ var DocumentViewer = Widget.extend({
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-6
@@ -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):
|
||||
|
||||
+1
-1
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user