[MERGE] forward port branch 11.0 up to 8dade01e2c

This commit is contained in:
Christophe Simonis
2018-05-04 16:10:50 +02:00
27 changed files with 328 additions and 50 deletions
-2
View File
@@ -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
-6
View File
@@ -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'
+1 -1
View File
@@ -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',
+8 -5
View File
@@ -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()}
+5 -1
View File
@@ -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
*/
+6
View File
@@ -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
+1
View File
@@ -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%;
+1 -1
View File
@@ -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);
+20 -2
View File
@@ -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
+4 -3
View File
@@ -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')
+1 -1
View File
@@ -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):
+14 -6
View File
@@ -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
+41
View File
@@ -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>
+2
View File
@@ -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)
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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):