[IMP] website_sale: allow using alternative products in dynamic snippet

With this commit we replace the current way to show alternative
products with a new option on the dynamic snippets.

Instead of having alternative products show up on all products if the
option is enabled, it will now be on a per product (template that is) basis
using the dynamic product snippet.

TaskId-2695187

closes odoo/odoo#85189

Related: odoo/upgrade#3277
Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
William Braeckman
2022-07-21 00:49:46 +02:00
parent c91c2712a1
commit 57a1776c36
10 changed files with 115 additions and 125 deletions
+17
View File
@@ -110,6 +110,16 @@ model.env.context['product_template_id'] = request.params.get('productTemplateId
response = DynamicFilter._get_products('recently_sold_with', model.env.context)
</field>
</record>
<record id="dynamic_snippet_alternative_products" model="ir.actions.server">
<field name="name">Alternative Products</field>
<field name="model_id" ref="model_product_product"/>
<field name="state">code</field>
<field name="code">
DynamicFilter = model.env['website.snippet.filter']
model.env.context['product_template_id'] = request.params.get('productTemplateId')
response = DynamicFilter._get_products('alternative_products', model.env.context)
</field>
</record>
<!-- Dynamic Filter -->
<record id="dynamic_filter_newest_products" model="website.snippet.filter">
<field name="filter_id" ref="website_sale.dynamic_snippet_newest_products_filter"/>
@@ -143,6 +153,13 @@ response = DynamicFilter._get_products('recently_sold_with', model.env.context)
<field name="name">Products Recently Sold With Product</field>
<field name="product_cross_selling">True</field>
</record>
<record id="dynamic_filter_cross_selling_alternative_products" model="website.snippet.filter">
<field name="action_server_id" ref="website_sale.dynamic_snippet_alternative_products"/>
<field name="field_names">display_name,description_sale,image_512</field>
<field name="limit" eval="16"/>
<field name="name">Alternative Products</field>
<field name="product_cross_selling">True</field>
</record>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>sale.order</value>
@@ -382,6 +382,10 @@ class ProductTemplate(models.Model):
return self.website_ribbon_id
return self.product_tag_ids.ribbon_id[:1] or self.product_variant_ids.additional_product_tag_ids.ribbon_id[:1]
@api.model
def _get_alternative_product_filter(self):
return self.env.ref('website_sale.dynamic_filter_cross_selling_alternative_products').id
# ---------------------------------------------------------
# Rating Mixin API
# ---------------------------------------------------------
@@ -157,3 +157,22 @@ class WebsiteSnippetFilter(models.Model):
])
products = self.env['product.product'].with_context(display_default_code=False).search(domain, limit=limit)
return products
def _get_products_alternative_products(self, website, limit, domain, context):
products = self.env['product.product']
current_id = context.get('product_template_id')
if not current_id:
return products
current_template = self.env['product.template'].browse(int(current_id))
if current_template.exists():
excluded_products = website.sale_get_order().order_line.product_id
excluded_products |= current_template.product_variant_ids
included_products = current_template.alternative_product_ids.product_variant_ids
products = included_products - excluded_products
if products:
domain = expression.AND([
domain,
[('id', 'in', products.ids)],
])
products = self.env['product.product'].with_context(display_default_code=False).search(domain, limit=limit)
return products
@@ -766,4 +766,31 @@ options.registry.WebsiteSaleProductAttribute = options.Class.extend({
return this._super(methodName, params);
},
});
// Disable "Shown on Mobile" option if for dynamic product snippets
options.registry.MobileVisibility.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
async _computeVisibility() {
return await this._super(...arguments)
&& !this.$target.hasClass('s_dynamic_snippet_products');
},
});
// Disable save for alternative products snippet
options.registry.SnippetSave.include({
/**
* @override
*/
async _computeVisibility() {
return await this._super(...arguments)
&& !this.$target.hasClass('o_wsale_alternative_products');
}
})
});
@@ -81,10 +81,19 @@ const DynamicSnippetProducts = DynamicSnippetCarousel.extend({
if (productNames) {
const nameDomain = [];
for (const productName of productNames.split(',')) {
// Ignore empty names
if (!productName.length) {
continue;
}
// Search on name, internal reference and barcode.
if (nameDomain.length) {
nameDomain.unshift('|');
}
nameDomain.push(['name', 'ilike', productName]);
nameDomain.push(...[
'|', '|', ['name', 'ilike', productName],
['default_code', '=', productName],
['barcode', '=', productName],
]);
}
searchDomain.push(...nameDomain);
}
@@ -6,6 +6,10 @@ const s_dynamic_snippet_carousel_options = require('website.s_dynamic_snippet_ca
var wUtils = require('website.utils');
const alternativeSnippetRemovedOptions = [
'filter_opt', 'product_category_opt', 'product_tag_opt', 'product_names_opt',
]
const dynamicSnippetProductsOptions = s_dynamic_snippet_carousel_options.extend({
/**
@@ -23,11 +27,22 @@ const dynamicSnippetProductsOptions = s_dynamic_snippet_carousel_options.extend(
this.contextualFilterDomain.push(['product_cross_selling', '=', false]);
}
this.productCategories = {};
this.isAlternativeProductSnippet = this.$target.hasClass('o_wsale_alternative_products');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @override
*/
_computeWidgetVisibility(widgetName, params) {
if (this.isAlternativeProductSnippet && alternativeSnippetRemovedOptions.includes(widgetName)) {
return false;
}
return this._super(...arguments);
},
/**
* Fetches product categories.
* @private
@@ -32,7 +32,7 @@
data-select-data-attribute=""/>
<we-input string="Product names" class="o_we_large" data-name="product_names_opt"
data-attribute-name="productNames" data-no-preview="true" data-select-data-attribute=""
placeholder="e.g. lamp,bin" title="Comma-separated list of parts of product names"/>
placeholder="e.g. lamp,bin" title="Comma-separated list of parts of product names, barcodes or internal reference"/>
</t>
</xpath>
</template>
+22 -30
View File
@@ -724,6 +724,28 @@
</t>
</template>
<template id="alternative_products" name="Alternative Products" inherit_id="website_sale.product" active="True">
<div itemprop="description" position="after">
<div class="oe_structure oe_structure_solo oe_unremovable oe_unmovable" id="oe_structure_website_sale_recommended_products" t-ignore="true" t-if="product.alternative_product_ids">
<section data-snippet="s_dynamic_snippet_products"
class="oe_unmovable oe_unremovable s_dynamic_snippet_products o_wsale_alternative_products s_dynamic pt32 pb32 o_colored_level s_product_product_borderless_1 d-none"
data-name="Alternative Products" style="background-image: none;" t-att-data-filter-id="product._get_alternative_product_filter()"
data-template-key="website_sale.dynamic_filter_template_product_product_borderless_1" data-product-category-id="all" data-number-of-elements="4"
data-number-of-elements-small-devices="1" data-number-of-records="16" data-carousel-interval="5000" data-original-title="" title="">
<div class="container o_not_editable">
<div class="css_non_editable_mode_hidden">
<div class="missing_option_warning alert alert-info rounded-0 fade show d-none d-print-none o_default_snippet_text">
Your Dynamic Snippet will be displayed here...
This message is displayed because youy did not provide both a filter and a template to use.
</div>
</div>
<div class="dynamic_snippet_template"></div>
</div>
</section>
</div>
</div>
</template>
<template id="product_custom_text" inherit_id="website_sale.product" active="True" name="Terms and Conditions" priority="21">
<xpath expr="//div[@id='o_product_terms_and_share']" position="inside">
<p class="text-muted mb-0">
@@ -761,36 +783,6 @@
</xpath>
</template>
<template id="recommended_products" inherit_id="website_sale.product" name="Alternative Products">
<xpath expr="//div[@id='product_full_description']" position="after">
<t t-set="alternative_products" t-value="product._get_website_alternative_product()"/>
<div t-if="alternative_products" class="container mt32">
<h3>Alternative Products:</h3>
<div class="row mt16" style="">
<t t-foreach="alternative_products" t-as="alt_product">
<div class="col-lg-2" t-att-data-publish="alt_product.website_published and 'on' or 'off'" style="width: 170px; height:130px; float:left; display:inline; margin-right: 10px; overflow:hidden;">
<div class="mt16 text-center" style="height: 100%;">
<t t-set="first_combination" t-value="alt_product._get_first_possible_combination(False)"/>
<t t-set="alternative_variant" t-value="alt_product._get_variant_for_combination(combination)"/>
<div t-if="alternative_variant"
t-field="alternative_variant.image_128"
t-options="{'widget': 'image', 'qweb_img_responsive': False, 'class': 'rounded shadow o_alternative_product o_image_64_max' }"/>
<div t-else=""
t-field="alt_product.image_128"
t-options="{'widget': 'image', 'qweb_img_responsive': False, 'class': 'rounded shadow o_alternative_product o_image_64_max' }"/>
<h6>
<a t-att-href="alt_product.website_url" style="display: block">
<span t-att-title="alt_product.name" t-field="alt_product.name" class="o_text_overflow" style="display: block;" />
</a>
</h6>
</div>
</div>
</t>
</div>
</div>
</xpath>
</template>
<!-- Product options: OpenChatter -->
<template id="product_comment" inherit_id="website_sale.product" active="False" name="Discussion and Rating" priority="15">
<xpath expr="//div[@t-field='product.website_description']" position="after">
@@ -2,7 +2,6 @@ odoo.define('website_sale_comparison.tour_comparison', function (require) {
'use strict';
var tour = require('web_tour.tour');
var rpc = require('web.rpc');
const tourUtils = require('website_sale.tour_utils');
tour.register('product_comparison', {
@@ -166,79 +165,5 @@ odoo.define('website_sale_comparison.tour_comparison', function (require) {
trigger: '#cart_products:contains("Conference Chair") .js_quantity[value="1"]',
run: function () {},
},
// test advanced configuration and alternative product
{
content: "create product with newly created attribute and its values and set alternative product",
trigger: 'body',
run: function () {
rpc.query({
model: 'product.attribute',
method: 'create',
args: [{
'name': 'color',
'display_type': 'color',
'create_variant': 'dynamic',
}],
}).then(function (attributeId) {
rpc.query({
model: 'product.template',
method: 'create',
args: [{
'name': 'Basic Chair',
'is_published': true,
'attribute_line_ids': [[0, 0, {
'attribute_id': attributeId,
'value_ids': [
[0, 0, {'name': 'red', 'attribute_id': attributeId, 'sequence': 1}],
[0, 0, {'name': 'blue', 'attribute_id': attributeId, 'sequence': 2}],
],
}]],
}],
}).then(function (productId) {
rpc.query({
model: 'product.template',
method: 'create',
args: [{
'name': 'Classic Chair',
'is_published': true,
'attribute_line_ids': [[0, 0, {
'attribute_id': attributeId,
'value_ids': [
[0, 0, {'name': 'green', 'attribute_id': attributeId, 'sequence': 3}],
[0, 0, {'name': 'yellow', 'attribute_id': attributeId, 'sequence': 4}],
],
}]],
'alternative_product_ids': [[6, 0, [productId]]],
}],
}).then(function () {
window.location.href = '/shop?search=Classic Chair';
});
});
});
},
},
{
content: "click on product",
trigger: '.oe_product_cart a:contains("Classic Chair")',
},
{
content: "click on compare button",
trigger: '.btn.btn-primary:not(.btn-block):contains("Compare")',
},
{
content: "check product 'Classic Chair' with first variant (green) is on compare page",
trigger: '.o_product_comparison_table:contains("Classic Chair (green)")',
run: function () {},
},
{
content: "check alternative product 'Basic Chair' with first variant (red) is on compare page",
trigger: '.o_product_comparison_table:contains("Basic Chair (red)")',
run: function () {},
},
{
content: "check there is the correct attribute",
trigger: '.o_ws_category_0:contains("color"):contains("red")',
run: function () {},
},
]);
});
@@ -64,24 +64,6 @@
</xpath>
</template>
<template id='recommended_product' inherit_id="website_sale.recommended_products" name="Alternative Products">
<xpath expr="//h3" position="replace">
<!--
Create the first variant if it doesn't exist for the current
product and every alternative product because the product
comparator need the variants and not the templates.
-->
<t t-set="product_variant" t-value="product_variant or product._create_first_product_variant()"/>
<t t-set="id_list" t-value="[product_variant.id] if product_variant else []"/>
<t t-foreach="product._get_website_alternative_product()" t-as="alt_product">
<t t-set="alt_product_variant_id" t-value="alt_product._create_first_product_variant().id"/>
<t t-if="alt_product_variant_id" t-set="id_list" t-value="id_list + [alt_product_variant_id]"/>
</t>
<t t-set="ids" t-value="','.join([str(id) for id in id_list])"/>
<div><span class='h3'>Suggested alternatives: </span><a t-if="len(id_list) > 1 and is_view_active('website_sale_comparison.add_to_compare')" role="button" class="btn btn-primary" t-attf-href="/shop/compare?products=#{ids}"><i class="fa fa-exchange"></i> Compare</a></div>
</xpath>
</template>
<template id="product_compare" name="Comparator Page">
<t t-call="website.layout">
<t t-set="additional_title">Shop Comparator</t>