[MERGE] forward port branch 12.0 up to c1c322dd40

This commit is contained in:
Christophe Simonis
2019-04-09 20:43:35 +02:00
20 changed files with 246 additions and 29 deletions
+10 -12
View File
@@ -20,18 +20,6 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="base.group_public" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_show_line_subtotals_tax_excluded'))]"/>
</record>
<record id="base.group_portal" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_show_line_subtotals_tax_excluded'))]"/>
</record>
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_show_line_subtotals_tax_excluded'))]"/>
</record>
<record id="group_account_user" model="res.groups">
<field name="name">Show Full Accounting Features</field>
<field name="category_id" ref="base.module_category_hidden"/>
@@ -75,6 +63,16 @@
<data noupdate="1">
<record id="base.group_public" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_show_line_subtotals_tax_excluded'))]"/>
</record>
<record id="base.group_portal" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_show_line_subtotals_tax_excluded'))]"/>
</record>
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_show_line_subtotals_tax_excluded'))]"/>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('account.group_account_manager'))]"/>
</record>
+4
View File
@@ -51,12 +51,16 @@
<table class="table table-sm o_main_table" name="invoice_line_table">
<thead>
<tr>
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="6"/>
<th class="text-left"><span>Description</span></th>
<th class="d-none text-left"><span>Source Document</span></th>
<th class="text-right"><span>Quantity</span></th>
<th t-attf-class="text-right {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}"><span>Unit Price</span></th>
<th t-if="display_discount" t-attf-class="text-right {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}">
<span>Disc.(%)</span>
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th t-attf-class="text-left {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}"><span>Taxes</span></th>
<th class="text-right">
+2 -2
View File
@@ -123,8 +123,8 @@
}
// Override height of graph. min-height doesn't do the trick
.o_graph_controller {
height: 500px;
.o_graph_svg_container {
height: 250px;
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ from odoo.addons.calendar.models.calendar import get_real_ids
class Partner(models.Model):
_inherit = 'res.partner'
calendar_last_notif_ack = fields.Datetime('Last notification marked as read from base Calendar')
calendar_last_notif_ack = fields.Datetime('Last notification marked as read from base Calendar', default=fields.Datetime.now)
@api.multi
def get_attendee_detail(self, meeting_id):
+17 -3
View File
@@ -13,9 +13,22 @@ class IrAttachment(models.Model):
"""
super(IrAttachment, self)._post_add_create()
for record in self:
record.register_as_main_attachment()
record.register_as_main_attachment(force=False)
def register_as_main_attachment(self):
@api.multi
def unlink(self):
self.remove_as_main_attachment()
super(IrAttachment, self).unlink()
@api.multi
def remove_as_main_attachment(self):
for attachment in self:
related_record = self.env[attachment.res_model].browse(attachment.res_id)
if related_record and hasattr(related_record, 'message_main_attachment_id'):
if related_record.message_main_attachment_id == attachment:
related_record.message_main_attachment_id = False
def register_as_main_attachment(self, force=True):
""" Registers this attachment as the main one of the model it is
attached to.
"""
@@ -24,4 +37,5 @@ class IrAttachment(models.Model):
# message_main_attachment_id field can be empty, that's why we compare to False;
# we are just checking that it exists on the model before writing it
if related_record and hasattr(related_record, 'message_main_attachment_id'):
related_record.message_main_attachment_id = self
if force or not related_record.message_main_attachment_id:
related_record.message_main_attachment_id = self
+4
View File
@@ -81,6 +81,7 @@ var Chatter = Widget.extend({
var nodeOptions = fieldsInfo[mailFields.mail_thread].options || {};
this.hasLogButton = options.display_log_button || nodeOptions.display_log_button;
this.postRefresh = nodeOptions.post_refresh || 'never';
this.reloadOnUploadAttachment = this.postRefresh === 'always';
}
},
/**
@@ -572,6 +573,9 @@ var Chatter = Widget.extend({
* @private
*/
_onReloadAttachmentBox: function () {
if (this.reloadOnUploadAttachment) {
this.trigger_up('reload');
}
this._reloadAttachmentBox();
},
/**
+1 -1
View File
@@ -189,7 +189,7 @@ class MrpProductProduceLine(models.TransientModel):
product_produce_id = fields.Many2one('mrp.product.produce')
product_id = fields.Many2one('product.product', 'Product')
product_tracking = fields.Selection(related="product_id.tracking", readonly=False)
product_tracking = fields.Selection(related="product_id.tracking")
lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number')
qty_to_consume = fields.Float('To Consume', digits=dp.get_precision('Product Unit of Measure'))
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure')
+6 -3
View File
@@ -523,6 +523,7 @@ class ProductTemplate(models.Model):
variants_to_create.append({
'product_tmpl_id': tmpl_id.id,
'attribute_value_ids': [(6, 0, list(value_ids))],
'active': tmpl_id.active,
})
if len(variants_to_create) > 1000:
raise UserError(_(
@@ -962,8 +963,10 @@ class ProductTemplate(models.Model):
if not self.active:
return _("The product template is archived so no combination is possible.")
ptal_stack = [self._get_valid_product_template_attribute_lines()]
combination_stack = [self.env['product.template.attribute.value']]
necessary_values = necessary_values or self.env['product.template.attribute.value']
necessary_attributes = necessary_values.mapped('attribute_id')
ptal_stack = [self.valid_product_template_attribute_line_ids.filtered(lambda ptal: ptal.attribute_id not in necessary_attributes)]
combination_stack = [necessary_values]
# keep going while we have attribute lines to test
while len(ptal_stack):
@@ -972,7 +975,7 @@ class ProductTemplate(models.Model):
if not attribute_lines:
# full combination, if it's possible return it, otherwise skip it
if self._is_combination_possible(combination, parent_combination) and all(v in combination for v in (necessary_values or [])):
if self._is_combination_possible(combination, parent_combination):
yield(combination)
else:
# we have remaining attribute lines to consider
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@@ -423,6 +425,47 @@ class TestProductAttributeValueConfig(TestProductAttributeValueSetup):
self.assertEqual(self.computer._get_closest_possible_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_4 + computer_hdd_2),
computer_ssd_256 + computer_ram_8 + computer_hdd_4)
# Make sure this is not extremely slow:
product_template = self.env['product.template'].create({
'name': 'many combinations',
})
for i in range(10):
# create the attributes
product_attribute = self.env['product.attribute'].create({
'name': "att %s" % i,
'create_variant': 'dynamic',
'sequence': i,
})
for j in range(10):
# create the attribute values
self.env['product.attribute.value'].create([{
'name': "val %s/%s" % (i, j),
'attribute_id': product_attribute.id,
'sequence': j,
}])
# set attribute and attribute values on the template
self.env['product.template.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, product_attribute.value_ids.ids)]
}])
# Get a value in the middle for each attribute to make sure it would
# take time to reach it (if looping one by one like before the fix).
combination = self.env['product.template.attribute.value']
for ptal in product_template.attribute_line_ids:
combination += ptal.product_template_value_ids[5]
started_at = time.time()
self.assertEqual(product_template._get_closest_possible_combination(combination), combination)
elapsed = time.time() - started_at
# It should take around 10ms, but to avoid false positives we check an
# higher value. Before the fix it would take hours.
self.assertLess(elapsed, 0.5)
def test_clear_caches(self):
"""The goal of this test is to make sure the cache is invalidated when
it should be."""
@@ -70,11 +70,15 @@
<table class="table table-sm o_main_table">
<thead>
<tr>
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="5"/>
<th class="text-left">Description</th>
<th class="text-right">Quantity</th>
<th class="text-right">Unit Price</th>
<th t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line">
<span>Disc.(%)</span>
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th class="text-right">Taxes</th>
<th class="text-right">
@@ -423,11 +423,15 @@
<table class="table table-sm" id="sales_order_table">
<thead class="bg-100">
<tr>
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="6"/>
<th class="text-left">Products</th>
<th class="text-right">Quantity</th>
<th t-attf-class="text-right {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}">Unit Price</th>
<th t-if="display_discount" t-attf-class="text-right {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}">
<span>Discount</span>
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="colspan+1"/>
</th>
<th t-attf-class="text-right {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}">Taxes</th>
<th class="text-right" >
+2 -1
View File
@@ -31,7 +31,8 @@ class SaleOrder(models.Model):
@api.onchange('partner_id')
def onchange_partner_id(self):
super(SaleOrder, self).onchange_partner_id()
self.note = self.sale_order_template_id.note or self.note
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
self.note = template.note or self.note
def _compute_line_data_for_template_change(self, line):
return {
@@ -18,7 +18,7 @@ class SaleOrderTemplate(models.Model):
name = fields.Char('Quotation Template', required=True)
sale_order_template_line_ids = fields.One2many('sale.order.template.line', 'sale_order_template_id', 'Lines', copy=True)
note = fields.Text('Terms and conditions')
note = fields.Text('Terms and conditions', translate=True)
sale_order_template_option_ids = fields.One2many('sale.order.template.option', 'sale_order_template_id', 'Optional Products', copy=True)
number_of_days = fields.Integer('Quotation Duration',
help='Number of days for the validity date computation of the quotation')
@@ -15,6 +15,7 @@
<xpath expr="//section[@id='details']//table[@id='sales_order_table']/thead/tr" position="inside">
<th t-if="display_remove">
<!-- TODO: remove in master -->
<t t-set="colspan" t-value="colspan+1"/>
</th>
</xpath>
@@ -6,6 +6,7 @@
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<br/>
<h2>Stock Inventory</h2>
<table class="table table-sm">
+1 -1
View File
@@ -67,7 +67,7 @@
<div name="standard_price_div" class="row ml-0">
<field name="standard_price"
attrs="{'readonly':[('valuation','=','real_time')]}"
widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" class="float-left"/>
widget='monetary' options="{'currency_field': 'cost_currency_id', 'field_digits': True}" class="float-left"/>
<div name="update_cost_price">
<button string="Update Cost" type="action"
name="%(action_view_change_standard_price)d"
+3
View File
@@ -95,6 +95,9 @@ if(!the_form.length) {
}
// checkboxes and radios
the_form.find("input[name^='" + key + "_'][type='checkbox']").each(function(){
$(this).val(value);
});
the_form.find("input[name=" + key + "][type!='text']").each(function(){
$(this).val(value);
});
@@ -428,6 +428,18 @@ ListRenderer.include({
});
}
},
/**
* @override
*/
updateState: function (state, params) {
if (params.noRender) {
// the state changed, but we won't do a re-rendering right now, so
// remove computed modifiers data (as they are obsolete) to force
// them to be recomputed at next (sub-)rendering
this.allModifiersData = [];
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
@@ -478,7 +490,8 @@ ListRenderer.include({
*/
_moveToPreviousLine: function () {
if (this.currentRow > 0) {
this._selectCell(this.currentRow - 1, this.columns.length - 1);
// FORWARD-PORT THIS COMMIT UP TO SAAS-12.2
this._selectCell(this.currentRow - 1, this.columns.length - 1, {inc: -1});
} else {
this.unselectRow().then(this.trigger_up.bind(this, 'add_record'));
}
@@ -692,6 +705,8 @@ ListRenderer.include({
* @param {boolean} [options.force=false] if true, force selecting the cell
* even if seems to be already the selected one (useful after a re-
* rendering, to reset the focus on the correct field)
* @param {integer} [options.inc=1] the increment to use when searching for
* the "next" possible cell (if the cell to select can't be selected)
* @return {Deferred} fails if no cell could be selected
*/
_selectCell: function (rowIndex, fieldIndex, options) {
@@ -715,7 +730,7 @@ ListRenderer.include({
var oldFieldIndex = self.currentFieldIndex;
self.currentFieldIndex = fieldIndex;
fieldIndex = self._activateFieldWidget(record, fieldIndex, {
inc: 1,
inc: options.inc || 1,
wrap: wrap,
event: options && options.event,
});
@@ -868,7 +883,7 @@ ListRenderer.include({
switch (ev.data.direction) {
case 'previous':
if (this.currentFieldIndex > 0) {
this._selectCell(this.currentRow, this.currentFieldIndex - 1, {wrap: false})
this._selectCell(this.currentRow, this.currentFieldIndex - 1, {inc: -1, wrap: false})
.fail(this._moveToPreviousLine.bind(this));
} else {
this._moveToPreviousLine();
+122
View File
@@ -2879,6 +2879,87 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('pressing SHIFT-TAB in editable list with a readonly field [REQUIRE FOCUS]', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="int_field" readonly="1"/>' +
'<field name="qux"/>' +
'</tree>',
});
// start on 'qux', line 3
testUtils.dom.click(list.$('.o_data_row:nth(2) .o_data_cell:nth(2)'));
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(2) .o_data_cell input[name=qux]')[0]);
// Press 'shift-Tab' -> should go to first cell (same line)
$(document.activeElement).trigger({type: 'keydown', which: $.ui.keyCode.TAB, shiftKey: true});
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(2) .o_data_cell input[name=foo]')[0]);
list.destroy();
});
QUnit.test('pressing SHIFT-TAB in editable list with a readonly field in first column [REQUIRE FOCUS]', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="int_field" readonly="1"/>' +
'<field name="foo"/>' +
'<field name="qux"/>' +
'</tree>',
});
// start on 'foo', line 3
testUtils.dom.click(list.$('.o_data_row:nth(2) .o_data_cell:nth(1)'));
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(2) .o_data_cell input[name=foo]')[0]);
// Press 'shift-Tab' -> should go to previous line (last cell)
$(document.activeElement).trigger({type: 'keydown', which: $.ui.keyCode.TAB, shiftKey: true});
assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(1) .o_data_cell input[name=qux]')[0]);
list.destroy();
});
QUnit.test('pressing SHIFT-TAB in editable list with a readonly field in last column [REQUIRE FOCUS]', function (assert) {
assert.expect(4);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="int_field"/>' +
'<field name="foo"/>' +
'<field name="qux" readonly="1"/>' +
'</tree>',
});
// start on 'int_field', line 3
testUtils.dom.click(list.$('.o_data_row:nth(2) .o_data_cell:first'));
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(2) .o_data_cell input[name=int_field]')[0]);
// Press 'shift-Tab' -> should go to previous line ('foo' field)
$(document.activeElement).trigger({type: 'keydown', which: $.ui.keyCode.TAB, shiftKey: true});
assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(1) .o_data_cell input[name=foo]')[0]);
list.destroy();
});
QUnit.test('skip invisible fields when navigating list view with TAB', function (assert) {
assert.expect(2);
@@ -4097,6 +4178,47 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('create record on list with modifiers depending on id', function (assert) {
assert.expect(8);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="id" invisible="1"/>' +
'<field name="foo" attrs="{\'readonly\': [[\'id\',\'!=\',False]]}"/>' +
'<field name="int_field" attrs="{\'invisible\': [[\'id\',\'!=\',False]]}"/>' +
'</tree>',
});
// add a new record
testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
// modifiers should be evaluted to false
assert.containsOnce(list, '.o_selected_row');
assert.doesNotHaveClass(list.$('.o_selected_row .o_data_cell:first'), 'o_readonly_modifier');
assert.doesNotHaveClass(list.$('.o_selected_row .o_data_cell:nth(1)'), 'o_invisible_modifier');
// set a value and save
testUtils.fields.editInput(list.$('.o_selected_row input[name=foo]'), 'some value');
testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
// modifiers should be evaluted to true
assert.hasClass(list.$('.o_data_row:first .o_data_cell:first'), 'o_readonly_modifier');
assert.hasClass(list.$('.o_data_row:first .o_data_cell:nth(1)'), 'o_invisible_modifier');
// edit again the just created record
testUtils.dom.click(list.$('.o_data_row:first .o_data_cell:first'));
// modifiers should be evaluted to true
assert.containsOnce(list, '.o_selected_row');
assert.hasClass(list.$('.o_selected_row .o_data_cell:first'), 'o_readonly_modifier');
assert.hasClass(list.$('.o_selected_row .o_data_cell:nth(1)'), 'o_invisible_modifier');
list.destroy();
});
});
});
+1 -1
View File
@@ -115,7 +115,7 @@ class Currency(models.Model):
fractional_value = int(parts[2] or 0)
lang_code = self.env.context.get('lang') or self.env.user.lang
lang = self.env['res.lang'].search([('code', '=', lang_code)])
lang = self.env['res.lang'].with_context(active_test=False).search([('code', '=', lang_code)])
amount_words = tools.ustr('{amt_value} {amt_word}').format(
amt_value=_num2words(integer_value, lang=lang.iso_code),
amt_word=self.currency_unit_label,