+
+
+
diff --git a/addons/website_blog/data/website_blog_data.xml b/addons/website_blog/data/website_blog_data.xml
index ae2bd0a14b1..bcf5f54d130 100644
--- a/addons/website_blog/data/website_blog_data.xml
+++ b/addons/website_blog/data/website_blog_data.xml
@@ -21,6 +21,7 @@
Published Post
+
diff --git a/addons/website_blog/data/website_blog_demo.xml b/addons/website_blog/data/website_blog_demo.xml
index f140b243c7d..2a17445d21c 100644
--- a/addons/website_blog/data/website_blog_demo.xml
+++ b/addons/website_blog/data/website_blog_demo.xml
@@ -74,6 +74,7 @@
+ 246
{"background-image": "url('/website_blog/static/src/img/cover_2.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}
@@ -120,6 +121,7 @@ People who live on the island have never even seen this remarkable scenery unles
+ 467
{"background-image": "url('/website_blog/static/src/img/cover_3.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}
@@ -172,6 +174,7 @@ People who live on the island have never even seen this remarkable scenery unles
+ 453
{"background-image": "url('/website_blog/static/src/img/cover_4.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "text_align_class": "text-center", "opacity": "0.2"}
diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py
index 2e8f8c0a690..c67d4036e7b 100644
--- a/addons/website_sale/__manifest__.py
+++ b/addons/website_sale/__manifest__.py
@@ -20,7 +20,8 @@
'views/sale_order_views.xml',
'views/crm_team_views.xml',
'views/templates.xml',
- 'views/snippets.xml',
+ 'views/snippets/snippets.xml',
+ 'views/snippets/s_dynamic_snippet_products.xml',
'views/snippets/s_products_searchbar.xml',
'views/res_config_settings_views.xml',
'views/digest_views.xml',
diff --git a/addons/website_sale/data/data.xml b/addons/website_sale/data/data.xml
index c2fddedad54..88291c2a23f 100644
--- a/addons/website_sale/data/data.xml
+++ b/addons/website_sale/data/data.xml
@@ -45,6 +45,55 @@
+
+
+
+ Products Dynamic Snippet
+
+ code
+
+ProductProduct = model.env['product.product']
+FieldMonetary = model.env['ir.qweb.field.monetary']
+
+website = request.website.get_current_website()
+dynamic_filter = model.env.context.get('dynamic_filter')
+limit = model.env.context.get('limit')
+search_domain = model.env.context.get('search_domain')
+
+domain = [('website_published', '=', True)] + website.website_domain() + (search_domain or [])
+products = ProductProduct.search(domain, limit=limit)
+_ = products.mapped('name')
+
+monetary_options = {
+ 'display_currency': request.website.get_current_pricelist().currency_id,
+}
+
+max_nb_chars = 100
+res_products = []
+for product in products:
+ res_product = product._read_format(['id', 'name', 'website_url', 'description_sale'])[0]
+ res_product.update(product._get_combination_info_variant())
+
+ if res_product['description_sale'] and len(res_product['description_sale']) > max_nb_chars:
+ res_product['description_sale'] = "%s ..." % res_product['description_sale'][:max_nb_chars]
+ res_product['list_price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options)
+ for field_name in dynamic_filter.field_names.split(","):
+ field = ProductProduct._fields.get(field_name)
+ if field and field.type == 'binary':
+ res_product[field_name] = website.image_url(product, field_name)
+ res_products.append(res_product)
+
+response = res_products
+
+
+
+
+
+ display_name,description_sale,image_512,list_price
+
+ Products
+
+
diff --git a/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js b/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js
new file mode 100644
index 00000000000..73b7d3abb02
--- /dev/null
+++ b/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js
@@ -0,0 +1,41 @@
+odoo.define('website_sale.s_dynamic_snippet_products', function (require) {
+'use strict';
+
+const config = require('web.config');
+const core = require('web.core');
+const publicWidget = require('web.public.widget');
+const DynamicSnippetCarousel = require('website.s_dynamic_snippet_carousel');
+
+const DynamicSnippetProducts = DynamicSnippetCarousel.extend({
+ selector: '.s_dynamic_snippet_products',
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Method to be overridden in child components if additional configuration elements
+ * are required in order to fetch data.
+ * @override
+ * @private
+ */
+ _isConfigComplete: function() {
+ return this._super.apply(this, arguments) && this.$el.get(0).dataset.productCategoryId !== undefined;
+ },
+ /**
+ * Method to be overridden in child components in order to provide a search
+ * domain if needed.
+ * @override
+ * @private
+ */
+ _getSearchDomain: function () {
+ const searchDomain = this._super.apply(this, arguments);
+ searchDomain.push(['public_categ_ids', 'child_of', parseInt(this.$el.get(0).dataset.productCategoryId)]);
+ return searchDomain;
+ },
+
+});
+publicWidget.registry.dynamic_snippet_products = DynamicSnippetProducts;
+
+return DynamicSnippetProducts;
+});
diff --git a/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.xml b/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.xml
new file mode 100644
index 00000000000..f45bf3c51bb
--- /dev/null
+++ b/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/options.js b/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/options.js
new file mode 100644
index 00000000000..7acf98a866c
--- /dev/null
+++ b/addons/website_sale/static/src/snippets/s_dynamic_snippet_products/options.js
@@ -0,0 +1,93 @@
+odoo.define('website_sale.s_dynamic_snippet_products_options', function (require) {
+'use strict';
+
+const options = require('web_editor.snippets.options');
+const s_dynamic_snippet_carousel_options = require('website.s_dynamic_snippet_carousel_options');
+
+const dynamicSnippetProductsOptions = s_dynamic_snippet_carousel_options.extend({
+
+ /**
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.productCategories = {};
+ },
+ /**
+ *
+ * @override
+ */
+ onBuilt: function () {
+ this._super.apply(this, arguments);
+ this._rpc({
+ model: 'ir.model.data',
+ method: 'search_read',
+ kwargs: {
+ domain: [['module', '=', 'website_sale'], ['model', '=', 'website.snippet.filter']],
+ fields: ['id', 'res_id'],
+ }
+ }).then((data) => {
+ this.$target.get(0).dataset.filterId = data[0].res_id;
+ this.$target.get(0).dataset.numberOfRecords = this.dynamicFilters[data[0].res_id].limit;
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ *
+ * @override
+ * @private
+ */
+ _computeWidgetVisibility: function (widgetName, params) {
+ if (widgetName === 'filter_opt') {
+ return false;
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Fetches product categories.
+ * @private
+ * @returns {Promise}
+ */
+ _fetchProductCategories: function () {
+ return this._rpc({
+ model: 'product.public.category',
+ method: 'search_read',
+ kwargs: {
+ domain: [],
+ fields: ['id', 'name'],
+ }
+ });
+ },
+ /**
+ *
+ * @override
+ * @private
+ */
+ _renderCustomXML: function (uiFragment) {
+ return Promise.all([this._super.apply(this, arguments), this._renderProductCategorySelector(uiFragment)]);
+ },
+ /**
+ * Renders the product categories option selector content into the provided uiFragment.
+ * @private
+ * @param {HTMLElement} uiFragment
+ */
+ _renderProductCategorySelector: async function (uiFragment) {
+ const productCategories = await this._fetchProductCategories();
+ for (let index in productCategories) {
+ this.productCategories[productCategories[index].id] = productCategories[index];
+ }
+ const productCategoriesSelectorEl = uiFragment.querySelector('[data-name="product_category_opt"]');
+ return this._renderSelectUserValueWidgetButtons(productCategoriesSelectorEl, this.productCategories);
+ },
+
+});
+
+options.registry.dynamic_snippet_products = dynamicSnippetProductsOptions;
+
+return dynamicSnippetProductsOptions;
+});
diff --git a/addons/website_sale/views/snippets/s_dynamic_snippet_products.xml b/addons/website_sale/views/snippets/s_dynamic_snippet_products.xml
new file mode 100644
index 00000000000..9b2c18a9a69
--- /dev/null
+++ b/addons/website_sale/views/snippets/s_dynamic_snippet_products.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/website_sale/views/snippets.xml b/addons/website_sale/views/snippets/snippets.xml
similarity index 95%
rename from addons/website_sale/views/snippets.xml
rename to addons/website_sale/views/snippets/snippets.xml
index 9b454b92371..f370856490f 100644
--- a/addons/website_sale/views/snippets.xml
+++ b/addons/website_sale/views/snippets/snippets.xml
@@ -1,7 +1,7 @@
-
+
@@ -17,6 +17,9 @@
+
+
+
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 0749dceab81..546a304f160 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -39,6 +39,7 @@
+