[MERGE] forward port branch 12.0 up to b82df99d7a

This commit is contained in:
Christophe Simonis
2019-07-25 20:01:05 +02:00
22 changed files with 501 additions and 36 deletions
+12 -9
View File
@@ -84,15 +84,18 @@ class AccountInvoiceSend(models.TransientModel):
# This should ideally be fixed in mail_compose_message, so when a fix is made there this whole commit should be reverted.
# basically self.body (which could be manually edited) extracts self.template_id,
# which is then not translated for each customer.
active_ids = self.env.context.get('active_ids', self.res_id)
active_records = self.env[self.model].browse(active_ids)
langs = active_records.mapped('partner_id.lang')
default_lang = self.env.context.get('lang', 'en_US')
for lang in (set(langs) or [default_lang]):
active_ids_lang = active_records.filtered(lambda r: r.partner_id.lang == lang).ids
self_lang = self.with_context(active_ids=active_ids_lang, lang=lang)
self_lang.onchange_template_id()
self_lang._send_email()
if self.composition_mode == 'mass_mail' and self.template_id:
active_ids = self.env.context.get('active_ids', self.res_id)
active_records = self.env[self.model].browse(active_ids)
langs = active_records.mapped('partner_id.lang')
default_lang = self.env.context.get('lang', 'en_US')
for lang in (set(langs) or [default_lang]):
active_ids_lang = active_records.filtered(lambda r: r.partner_id.lang == lang).ids
self_lang = self.with_context(active_ids=active_ids_lang, lang=lang)
self_lang.onchange_template_id()
self_lang._send_email()
else:
self._send_email()
if self.is_print:
return self._print_document()
return {'type': 'ir.actions.act_window_close'}
@@ -14,6 +14,7 @@
<field name="invoice_ids" invisible="1"/>
<field name="email_from" invisible="1" />
<field name="mail_server_id" invisible="1"/>
<field name="reply_to" invisible="1"/>
<div name="option_print">
<field name="is_print" />
<b><label for="is_print"/></b>
+6 -1
View File
@@ -383,7 +383,7 @@ var BasicActivity = AbstractField.extend({
container: $markDoneBtn,
title : _t("Feedback"),
html: true,
trigger:'click',
trigger: 'manual',
placement: 'right', // FIXME: this should work, maybe a bug in the popper lib
content : function () {
var $popover = $(QWeb.render('mail.activity_feedback_form', {
@@ -400,6 +400,11 @@ var BasicActivity = AbstractField.extend({
$popover.find('#activity_feedback').focus();
self._bindPopoverFocusout($(this));
}).popover('show');
} else {
var popover = $markDoneBtn.data('bs.popover');
if ($('#' + popover.tip.id).length === 0) {
popover.show();
}
}
},
/**
+67
View File
@@ -1858,6 +1858,73 @@ QUnit.test('form activity widget: schedule next activity', function (assert) {
form.destroy();
});
QUnit.test('form activity widget: do not close activity popover on click', async function (assert) {
assert.expect(3);
this.data.partner.records[0].activity_ids = [1];
this.data.partner.records[0].activity_state = 'today';
this.data['mail.activity'].records = [{
id: 1,
display_name: "An activity",
date_deadline: moment().format("YYYY-MM-DD"), // now
state: "today",
user_id: 2,
create_user_id: 2,
activity_type_id: 2,
}];
const form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: `<form string="Partners">
<sheet>
<field name="foo"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="activity_ids" widget="mail_activity"/>
</div>
</form>`,
res_id: 2,
mockRPC(route, args) {
if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') {
return $.when();
}
return this._super.apply(this, arguments);
},
intercepts: {
do_action(ev) {
ev.data.options.on_close();
},
},
});
//Schedule next activity
await testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=1]'));
for (let i = 0; i < 2; i++) {
// click twice in feedback area
await testUtils.dom.click(form.$('.o_mail_activity_feedback.popover textarea'));
const $popover = form.$('.o_mail_activity_feedback.popover');
const $PopoverInDom = form.$(`#${$popover.attr('id')}`);
assert.isVisible(
$PopoverInDom,
"the feedback popover should still be visible on click");
}
await concurrency.delay(250); // wait for popover transition
const $popover = form.$('.o_mail_activity_feedback.popover');
if (!$popover) {
throw new Error("popover should be in DOM");
}
const $PopoverInDom = form.$(`#${$popover.attr('id')}`);
assert.isVisible(
$PopoverInDom,
"the feedback popover should still be visible on click");
form.destroy();
});
QUnit.test('form activity widget: edit next activity', function (assert) {
assert.expect(3);
@@ -306,6 +306,7 @@ class MailComposer(models.TransientModel):
'attachment_ids': [attach.id for attach in self.attachment_ids],
'author_id': self.author_id.id,
'email_from': self.email_from,
'reply_to': self.reply_to,
'record_name': self.record_name,
'no_auto_thread': self.no_auto_thread,
'mail_server_id': self.mail_server_id.id,
+3 -1
View File
@@ -538,10 +538,12 @@ class ProductTemplate(models.Model):
# deleted
valid_value_ids = tmpl_id.valid_product_attribute_value_wnva_ids
valid_attribute_ids = tmpl_id.valid_product_attribute_wnva_ids
seen_attributes = set(p.attribute_value_ids for p in tmpl_id.product_variant_ids if p.active)
for product_id in tmpl_id.product_variant_ids:
if product_id._has_valid_attributes(valid_attribute_ids, valid_value_ids):
if not product_id.active:
if not product_id.active and product_id.attribute_value_ids not in seen_attributes:
variants_to_activate += product_id
seen_attributes.add(product_id.attribute_value_ids)
else:
variants_to_unlink += product_id
+283
View File
@@ -534,3 +534,286 @@ class TestVariantsImages(common.TestProductCommon):
self.assertEqual(self.template.image, self.images['red'])
self.assertEqual(self.variants[0].image_raw_original, False)
self.assertEqual(self.variants[0].image, self.images['red'])
class TestVariantsArchive(common.TestProductCommon):
"""Once a variant is used on orders/invoices, etc, they can't be unlinked.
As a result, updating attributes on a product template would simply archive the variants instead.
We make sure that at each update, we have the correct number of active and inactive records.
In these tests, we use the commands sent by the JS framework to the ORM when using the interface
(even though using 3 commands would simplify the code compared to 2 commands).
Note that: we don't overly test the ids of archived / modified variants.
In some cases _create_product_variants reuse existing variants and in other create new ones,
in a way that is not very predictable (without reading the code).
We do not enshrine the current behaviour in the tests (yet) as it could change with only moderate functional impact.
Consequently, if it is intentional the number of archived variants could change,
in that case update the test; it is there to make sure that a fix is not going to break anything silently.
The most important is to keep correct numbers of active variants.
"""
def setUp(self):
res = super(TestVariantsArchive, self).setUp()
attribute_values = {
'color': ['white', 'black'],
'size': ['s', 'm'],
}
self.attributes = {}
for attr, attr_values in attribute_values.items():
attribute = self.env['product.attribute'].create({'name': attr})
values = [self.env['product.attribute.value'].create({'name': name, 'attribute_id': attribute.id})
for name in attr_values]
self.attributes.update({attr: {'a': attribute, 'v': values}})
attribute_line_ids = [(0, 0, {
'attribute_id': attr['a'].id,
'value_ids': [(6, 0, [v.id for v in attr['v']])],
}) for attr in self.attributes.values()]
self.template = self.env['product.template'].create({
'name': 'consume product',
'attribute_line_ids': attribute_line_ids,
})
return res
def test_update_variant_unlink(self):
"""Variants are not used anywhere, so removing an attribute line would
unlink the variants and create new ones. Nothing too fancy here.
"""
lines = self.template.attribute_line_ids
self.assertEqual(len(self.template.product_variant_ids), 4)
# we keep lines[0], remove and copy lines[1]
line_copy_command = {'attribute_id': lines[1].attribute_id.id, 'value_ids': [(6, False, lines[1].value_ids.mapped('id'))]}
self.template.write({'attribute_line_ids': [(4, lines[0].id), (2, lines[1].id)]})
self.assertEqual(len(self.template.product_variant_ids), len(lines[0].value_ids),
"Since we only kept line[0], we should have as many variants as it has values")
archived_products = self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)])
self.assertFalse(archived_products,
"Since they are used nowhere, there should not be any archived variant.")
# we re-add the line we just removed, so we should get new variants
self.template.write({
'attribute_line_ids': [
(4, lines[0].id),
(0, 'virtual', line_copy_command),
]})
self.assertEqual(len(self.template.product_variant_ids), 4)
def test_update_variant_archive_1_value(self):
"""We do the same operations on the template as in test_update_variant_unlink,
except we simulate that the variants can't be unlinked.
It follows that variants should be archived instead, so the results should all be different from previous test.
In this test we have a line that have only one possible value:
this is handled differently than the case where we have more than one,
since in principle it does not add new variants.
"""
# we create a new template with a line with only one attribute.
# Since this does not create any additional variants, the behaviour is a little bit different:
# it tries to add the new attribute to existing variants if possible
attribute_line_ids = [(0, 0, {
'attribute_id': attr['a'].id,
'value_ids': [(6, 0, [v.id for v in (attr['v'] if attr['a'].display_name == 'color' else attr['v'][:1])])],
}) for attr in self.attributes.values()]
template = self.env['product.template'].create({
'name': 'consume product',
'attribute_line_ids': attribute_line_ids,
})
# create a patch to make as if one variant was undeletable
# (e.g. present in a field with ondelete=restrict)
Product = self.env['product.product']
no_remove = template.product_variant_ids[0].id
def unlink(self):
if self.id == no_remove:
raise Exception('just')
else:
return super(Product.__class__, self).unlink()
Product._patch_method('unlink', unlink)
variants_ids_history = []
self.assertEqual(len(template.product_variant_ids), 2)
variants_ids_history.append(template.product_variant_ids.ids)
lines = template.attribute_line_ids
# we keep lines[0], remove and copy lines[1], which is the one with only one possible value
line_copy_command = {'attribute_id': lines[1].attribute_id.id,
'value_ids': [(6, False, lines[1].value_ids.mapped('id'))]}
template.write({'attribute_line_ids': [(4, lines[0].id), (2, lines[1].id)]})
self.assertEqual(len(lines[0].value_ids), 2)
self.assertEqual(len(template.product_variant_ids), len(lines[0].value_ids),
"Since we only kept line[0], we should have as many variants as it has values")
variants_ids_history.append(template.product_variant_ids.ids)
archived_products_1 = self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', template.id)])
self.assertEqual(len(archived_products_1), 1,
"One variant should have been unlinked, not the other.")
# we re-add the line we just removed, so we should get new variants
template.write({
'attribute_line_ids': [
(4, lines[0].id),
(0, 'virtual', line_copy_command),
]})
self.assertEqual(len(template.product_variant_ids), 2)
variants_ids_history.append(template.product_variant_ids.ids)
archived_products_2 = self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', template.id)])
self.assertEqual(len(archived_products_2), 1,
"We did not reactivate this variant.")
# tests on ids, see the doc_string
self.assertEqual(archived_products_1, archived_products_2)
self.assertEqual(len(set(variants_ids_history[0] + variants_ids_history[1])), 4,
"After the first update, we have new active variants.")
self.assertEqual(len(set(variants_ids_history[1] + variants_ids_history[2])), 2,
"After the second update, we kept the same active variants.")
Product._revert_method('unlink')
def test_update_variant_archive_2_value(self):
"""We do the same operations on the template as in test_update_variant_unlink,
except we simulate that the variants can't be unlinked.
It follows that variants should be archived instead, so the results should all be different from previous test.
"""
Product = self.env['product.product']
def unlink(slef):
raise Exception('just')
Product._patch_method('unlink', unlink)
variants_ids_history = []
self.assertEqual(len(self.template.product_variant_ids), 4)
variants_ids_history.append(self.template.product_variant_ids.ids)
lines = self.template.attribute_line_ids
# we keep lines[0], remove and copy lines[1]
line_copy_command = {'attribute_id': lines[1].attribute_id.id,
'value_ids': [(6, False, lines[1].value_ids.mapped('id'))]}
line_second_copy_command = {'attribute_id': lines[1].attribute_id.id,
'value_ids': [(6, False, lines[1].value_ids[:1].mapped('id'))]}
self.template.write({'attribute_line_ids': [(4, lines[0].id), (2, lines[1].id)]})
self.assertEqual(len(self.template.product_variant_ids), len(lines[0].value_ids),
"Since we only kept line[0], we should have as many variants as it has values")
variants_ids_history.append(self.template.product_variant_ids.ids)
archived_products = []
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)]))
self.assertEqual(len(archived_products[0]), 4,
"Since they can't be unlinked, all variants should be archived.")
# we re-add the line we just removed, so we should get new variants
self.template.write({
'attribute_line_ids': [
(4, lines[0].id),
(0, 'virtual', line_copy_command),
]})
self.assertEqual(len(self.template.product_variant_ids), 4)
variants_ids_history.append(self.template.product_variant_ids.ids)
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)]))
self.assertEqual(len(archived_products[1]), 2,
"Since they can't be unlinked, all previous variants should be archived.")
# we redo the whole remove/readd to check
lines = self.template.attribute_line_ids
self.template.write({'attribute_line_ids': [(4, lines[0].id), (2, lines[1].id)]})
self.assertEqual(len(self.template.product_variant_ids), 2)
variants_ids_history.append(self.template.product_variant_ids.ids)
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)]))
self.template.write({
'attribute_line_ids': [
(4, lines[0].id),
(0, 'virtual', line_copy_command),
]})
self.assertEqual(len(self.template.product_variant_ids), 4)
variants_ids_history.append(self.template.product_variant_ids.ids)
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)]))
self.assertEqual(len(archived_products[3]), 2)
# now we test when we try to add the value back on existing products
lines = self.template.attribute_line_ids
self.template.write({'attribute_line_ids': [(4, lines[0].id), (2, lines[1].id)]})
self.assertEqual(len(self.template.product_variant_ids), 2)
variants_ids_history.append(self.template.product_variant_ids.ids)
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)]))
self.assertEqual(len(archived_products[4]), 4)
# this time we only add one of the two attributes we've been removing
self.template.write({
'attribute_line_ids': [
(4, lines[0].id),
(0, 'virtual', line_second_copy_command),
]})
self.assertEqual(len(self.template.product_variant_ids), 2)
variants_ids_history.append(self.template.product_variant_ids.ids)
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', self.template.id)]))
self.assertEqual(len(archived_products[5]), 4)
Product._revert_method('unlink')
def test_update_variant_archive_3_value(self):
"""In this one, we have unique values for one lines, and one line with two values.
We first remove the unique line to populate archived variants, then remove the two values line.
Finally, we re-add athe two lines, bringing us back to the first situation:
we should not have any more than two variants (rest should be archived)
"""
attribute_line_ids = [(0, 0, {
'attribute_id': attr['a'].id,
'value_ids': [(6, 0, [v.id for v in (attr['v'] if attr['a'].display_name == 'color' else attr['v'][:1])])],
}) for attr in self.attributes.values()]
template = self.env['product.template'].create({
'name': 'consume product',
'attribute_line_ids': attribute_line_ids,
})
Product = self.env['product.product']
def unlink(slef):
raise Exception('just')
Product._patch_method('unlink', unlink)
variants_ids_history = []
self.assertEqual(len(template.product_variant_ids), 2)
variants_ids_history.append(template.product_variant_ids.ids)
lines = template.attribute_line_ids
template.write({'attribute_line_ids': [(4, lines[0].id), (2, lines[1].id)]})
self.assertEqual(len(template.product_variant_ids), len(lines[0].value_ids),
"Since we only kept line[0], we should have as many variants as it has values")
variants_ids_history.append(template.product_variant_ids.ids)
archived_products = []
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', template.id)]))
self.assertEqual(len(archived_products[0]), 2,
"Since they can't be unlinked, all variants should be archived.")
template.write({'attribute_line_ids': [(2, lines[0].id)]})
self.assertEqual(len(template.product_variant_ids), 1)
variants_ids_history.append(template.product_variant_ids.ids)
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', template.id)]))
self.assertEqual(len(archived_products[1]), 4,
"Since they can't be unlinked, all previous variants should be archived.")
# we re-add everything:
template.write({'attribute_line_ids': attribute_line_ids})
archived_products.append(self.env['product.product'].search(
[('active', '=', False), ('product_tmpl_id', '=', template.id)]))
self.assertEqual(len(template.product_variant_ids), 2)
self.assertEqual(len(archived_products[2]), 3,
"We should have re-activated one variant only.")
Product._revert_method('unlink')
+1 -1
View File
@@ -169,7 +169,7 @@
placeholder="Name, TIN, Email, or Reference"
/>
<field name="partner_ref"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="currency_id" groups="base.group_multi_currency" force_save="1"/>
</group>
<group>
<field name="date_order"/>
+5 -1
View File
@@ -72,7 +72,11 @@ class SaleOrder(models.Model):
price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit:
discount = (line.price_unit - price) / line.price_unit * 100
price = line.price_unit
# negative discounts (= surcharge) are included in the display price
if discount < 0:
discount = 0
else:
price = line.price_unit
else:
price = line.price_unit
@@ -53,7 +53,7 @@
<div>
<field name="product_uom_qty" class="oe_inline"/>
</div>
<field name="price_unit" invisible="1"/>
<field name="price_unit"/>
<label for="discount" groups="sale.group_discount_per_so_line"/>
<div groups="sale.group_discount_per_so_line">
<field name="discount" class="oe_inline"/> %%
@@ -90,7 +90,7 @@
attrs="{'required': [('display_type', '=', False)]}"
/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="price_unit" invisible="1"/>
<field name="price_unit"/>
</tree>
</field>
</page>
@@ -101,7 +101,7 @@
<field name="name"/>
<field name="quantity"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="price_unit" invisible="1"/>
<field name="price_unit"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
</tree>
</field>
+9 -6
View File
@@ -1053,6 +1053,7 @@ class StockMove(models.Model):
def _prepare_extra_move_vals(self, qty):
vals = {
'procure_method': 'make_to_stock',
'origin_returned_move_id': self.origin_returned_move_id.id,
'product_uom_qty': qty,
'picking_id': self.picking_id.id,
@@ -1079,13 +1080,17 @@ class StockMove(models.Model):
rounding_method='HALF-UP')
extra_move_vals = self._prepare_extra_move_vals(extra_move_quantity)
extra_move = self.copy(default=extra_move_vals)
if extra_move.picking_id:
merge_into_self = all(self[field] == extra_move[field] for field in self._prepare_merge_moves_distinct_fields())
if merge_into_self and extra_move.picking_id:
extra_move = extra_move._action_confirm(merge_into=self)
return extra_move
else:
extra_move = extra_move._action_confirm()
# link it to some move lines. We don't need to do it for move since they should be merged.
if self.exists() and not self.picking_id:
if not merge_into_self:
for move_line in self.move_line_ids.filtered(lambda ml: ml.qty_done):
if float_compare(move_line.qty_done, extra_move_quantity, precision_rounding=rounding) <= 0:
# move this move line to our extra move
@@ -1102,7 +1107,7 @@ class StockMove(models.Model):
extra_move_quantity -= extra_move_quantity
if extra_move_quantity == 0.0:
break
return extra_move
return extra_move | self
def _unreserve_initial_demand(self, new_move):
pass
@@ -1123,9 +1128,7 @@ class StockMove(models.Model):
for move in moves:
if move.state == 'cancel' or move.quantity_done <= 0:
continue
# extra move will not be merged in mrp
if not move.picking_id:
moves_todo |= move
moves_todo |= move._create_extra_move()
# Split moves where necessary and move quants
+4 -3
View File
@@ -501,7 +501,7 @@ class StockMoveLine(models.Model):
if quantity > available_quantity:
# We now have to find the move lines that reserved our now unavailable quantity. We
# take care to exclude ourselves and the move lines were work had already been done.
oudated_move_lines_domain = [
outdated_move_lines_domain = [
('move_id.state', 'not in', ['done', 'cancel']),
('product_id', '=', product_id.id),
('lot_id', '=', lot_id.id if lot_id else False),
@@ -511,14 +511,15 @@ class StockMoveLine(models.Model):
('product_qty', '>', 0.0),
('id', 'not in', ml_to_ignore.ids),
]
oudated_candidates = self.env['stock.move.line'].search(oudated_move_lines_domain)
current_picking_first = lambda cand: cand.picking_id != self.move_id.picking_id
outdated_candidates = self.env['stock.move.line'].search(outdated_move_lines_domain).sorted(current_picking_first)
# As the move's state is not computed over the move lines, we'll have to manually
# recompute the moves which we adapted their lines.
move_to_recompute_state = self.env['stock.move']
rounding = self.product_uom_id.rounding
for candidate in oudated_candidates:
for candidate in outdated_candidates:
if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0:
quantity -= candidate.product_qty
move_to_recompute_state |= candidate.move_id
+21
View File
@@ -185,6 +185,27 @@ class TestPickShip(TestStockCommon):
# the client picking should not be assigned anymore, as we returned partially what we took
self.assertEqual(picking_client.state, 'confirmed')
def test_mto_moves_extra_qty(self):
""" Ensure that a move in MTO will support an extra quantity. The extra
move should be created in MTS and should not be merged in the initial
move if it's in MTO. It should also avoid to trigger the rules.
"""
picking_pick, picking_client = self.create_pick_ship()
stock_location = self.env['stock.location'].browse(self.stock_location)
self.productA.write({'route_ids': [(4, self.env.ref('stock.route_warehouse0_mto').id)]})
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
picking_pick.action_assign()
picking_pick.move_lines[0].move_line_ids[0].qty_done = 15.0
picking_pick.action_done()
self.assertEqual(picking_pick.state, 'done')
self.assertEqual(picking_client.state, 'assigned')
picking_client.move_lines[0].move_line_ids[0].qty_done = 15.0
picking_client.move_lines._action_done()
self.assertEqual(len(picking_client.move_lines), 2)
self.assertEqual(picking_client.move_lines.mapped('procure_method'), ['make_to_order', 'make_to_stock'])
self.assertEqual(picking_client.move_lines.mapped('product_uom_qty'), [10.0, 5.0])
def test_mto_moves_return_extra(self):
picking_pick, picking_client = self.create_pick_ship()
stock_location = self.env['stock.location'].browse(self.stock_location)
+3 -3
View File
@@ -251,10 +251,10 @@ class IrUiView(models.Model):
if called_view and called_view not in views_to_return:
views_to_return += self._views_get(called_view, options=options, bundles=bundles)
extensions = self._view_get_inherited_children(view, options)
if not options:
# only active children
extensions = extensions.filtered(lambda view: view.active)
return views_to_return
extensions = self._view_get_inherited_children(view, options)
# Keep options in a deterministic order regardless of their applicability
for extension in extensions.sorted(key=lambda v: v.id):
@@ -358,6 +358,8 @@ var SnippetEditor = Widget.extend({
ev.preventDefault();
this.trigger_up('cover_will_change');
this.trigger_up('snippet_will_be_cloned', {$target: this.$target});
var $clone = this.$target.clone(false);
this.trigger_up('request_history_undo_record', {$target: this.$target});
@@ -372,7 +374,7 @@ var SnippetEditor = Widget.extend({
}
},
});
this.trigger_up('snippet_cloned', {$target: $clone});
this.trigger_up('snippet_cloned', {$target: $clone, $origin: this.$target});
$clone.trigger('content_changed');
},
/**
+1 -1
View File
@@ -13,6 +13,6 @@ class WebsiteRedirect(models.Model):
redirect_type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, default="301")
url_from = fields.Char('Redirect From', required=True)
url_to = fields.Char('Redirect To', required=True)
website_id = fields.Many2one('website', 'Website')
website_id = fields.Many2one('website', 'Website', ondelete='cascade')
active = fields.Boolean(default=True)
sequence = fields.Integer()
+22 -1
View File
@@ -21,6 +21,7 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, {
content_will_be_destroyed: '_onContentWillBeDestroyed',
content_was_recreated: '_onContentWasRecreated',
snippet_will_be_cloned: '_onSnippetWillBeCloned',
snippet_cloned: '_onSnippetCloned',
snippet_dropped: '_onSnippetDropped',
edition_will_stopped: '_onEditionWillStop',
@@ -203,9 +204,22 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
});
this.editModeEnable = false;
},
/**
* Called when a snippet is about to be cloned in the page. Notifies the
* WebsiteRoot that is should destroy the animations for this snippet.
*
* @private
* @param {OdooEvent} ev
*/
_onSnippetWillBeCloned: function (ev) {
this.trigger_up('animation_stop_demand', {
$target: ev.data.$target,
});
},
/**
* Called when a snippet is cloned in the page. Notifies the WebsiteRoot
* that is should start the animations for this snippet.
* that is should start the animations for this snippet and the snippet it
* was cloned from.
*
* @private
* @param {OdooEvent} ev
@@ -215,6 +229,13 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
editableMode: true,
$target: ev.data.$target,
});
// TODO: remove in saas-12.5, undefined $origin will restart #wrapwrap
if (ev.data.$origin) {
this.trigger_up('animation_start_demand', {
editableMode: true,
$target: ev.data.$origin,
});
}
},
/**
* Called when a snippet is dropped in the page. Notifies the WebsiteRoot
+14
View File
@@ -610,6 +610,20 @@ class TestCowViewSaving(common.TransactionCase):
'arch': '<div position="inside">, sub ext</div>',
'key': 'II',
})
# B
# |
# I
# |
# II
# First, test that children of inactive children are not returned (not multiwebsite related)
self.inherit_view.active = False
views = View.get_related_views('B')
self.assertEqual(views.mapped('key'), ['B', 'I'], "As 'I' is inactive, 'II' (its own child) should not be returned.")
self.inherit_view.active = True
# Second, test multi-website
self.inherit_view.with_context(website_id=1).write({'name': 'Extension'}) # Trigger cow on hierarchy
View.create({
'name': 'II2',
+7 -3
View File
@@ -349,9 +349,11 @@ class WebsiteSale(ProductConfiguratorController):
@http.route(['/shop/pricelist'], type='http', auth="public", website=True, sitemap=False)
def pricelist(self, promo, **post):
redirect = post.get('r', '/shop/cart')
pricelist = request.env['product.pricelist'].sudo().search([('code', '=', promo)], limit=1)
if not pricelist or (pricelist and not request.website.is_pricelist_available(pricelist.id)):
return request.redirect("%s?code_not_available=1" % redirect)
# empty promo code is used to reset/remove pricelist (see `sale_get_order()`)
if promo:
pricelist = request.env['product.pricelist'].sudo().search([('code', '=', promo)], limit=1)
if (not pricelist or (pricelist and not request.website.is_pricelist_available(pricelist.id))):
return request.redirect("%s?code_not_available=1" % redirect)
request.website.sale_get_order(code=promo)
return request.redirect(redirect)
@@ -685,6 +687,8 @@ class WebsiteSale(ProductConfiguratorController):
if mode[1] == 'billing':
order.partner_id = partner_id
order.onchange_partner_id()
# This is the *only* thing that the front end user will see/edit anyway when choosing billing address
order.partner_invoice_id = partner_id
if not kw.get('use_same'):
kw['callback'] = kw.get('callback') or \
(not order.only_services and (mode[0] == 'edit' and '/shop/checkout' or '/shop/address'))
+11
View File
@@ -0,0 +1,11 @@
Canada, 2019-07-22
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
David Dufresne 27902736+ddufresne@users.noreply.github.com https://github.com/ddufresne
+7 -2
View File
@@ -17,7 +17,7 @@ its sub-called methods.
.. code:: python
from odoo.tools.profiler import profile
from odoo.tools.misc import profile
[...]
@profile('/temp/prof.profile')
@api.multi
@@ -37,10 +37,15 @@ A tool called *xdot* will display the resulting graph:
xdot /temp/prof.xdot
The profiler can be also used without saving data in a file:
Log a method
============
Another profiler can be used to log statistics on a method:
.. code:: python
from odoo.tools.profiler import profile
[...]
@profile
@api.model
def mymethod(...):
+17
View File
@@ -1155,6 +1155,23 @@ class GroupsView(models.Model):
return res
class ModuleCategory(models.Model):
_inherit = "ir.module.category"
@api.multi
def write(self, values):
res = super().write(values)
if "name" in values:
self.env["res.groups"]._update_user_groups_view()
return res
@api.multi
def unlink(self):
res = super().unlink()
self.env["res.groups"]._update_user_groups_view()
return res
class UsersView(models.Model):
_inherit = 'res.users'