@ -0,0 +1,53 @@ |
|||
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg |
|||
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
|
|||
Ecommerce Sequential Variant Selector |
|||
================================= |
|||
This module enables customers to select product attribute values from the |
|||
website in an ordered manner. It allows customers to choose attribute values one |
|||
by one. |
|||
|
|||
Configuration |
|||
============= |
|||
* No additional configurations needed |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
License |
|||
------- |
|||
GNU AFFERO GENERAL PUBLIC LICENSE, Version 3 (AGPLv3) |
|||
(https://www.gnu.org/licenses/agpl.html) |
|||
|
|||
Credits |
|||
------- |
|||
* Developers: (V16) Abhijith PG, |
|||
(V17) Farook Al Ameen, |
|||
(V18) Henna Mehjabin |
|||
Contact: odoo@cybrosys.com |
|||
|
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if |
|||
your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. image:: https://cybrosys.com/images/logo.png |
|||
:target: https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
|
|||
For support and more information, please visit `Our Website <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
|||
@ -0,0 +1,21 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Henna Mehjabin (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
@ -0,0 +1,49 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Henna Mehjabin (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
{ |
|||
'name': "Ecommerce Sequential Variant Selector", |
|||
'version': '18.0.1.0.0', |
|||
'category': 'Website', |
|||
'summary': """Sequential attribute selection in odoo eCommerce""", |
|||
'description': "This module enables customers to select product attribute " |
|||
"values from the website in an ordered manner. It allows " |
|||
"customers to choose attribute values one by one.", |
|||
'author': "Cybrosys Techno Solutions", |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': "https://www.cybrosys.com", |
|||
'depends': ['base', 'website_sale'], |
|||
'data': [ |
|||
'views/variant_templates.xml', |
|||
], |
|||
'assets': { |
|||
'web.assets_frontend': [ |
|||
'website_sale_variant_selection/static/src/js/variant_mixin.js', |
|||
'website_sale_variant_selection/static/src/scss/website_sale_attribute_selection.scss', |
|||
], |
|||
}, |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'application': True, |
|||
'auto_install': False, |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
## Module <website_sale_variant_selection> |
|||
|
|||
#### 28.07.2025 |
|||
#### Version 18.0.1.0.0 |
|||
#### ADD |
|||
- Initial Commit for Ecommerce Sequential Variant Selector |
|||
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 600 B |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 565 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 912 KiB |
|
After Width: | Height: | Size: 767 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 697 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,135 @@ |
|||
/** @odoo-module */ |
|||
import publicWidget from '@web/legacy/js/public/public_widget'; |
|||
|
|||
publicWidget.registry.AttributeSelection = publicWidget.Widget.extend({ |
|||
selector: '.attr_container', |
|||
events: { |
|||
'change .js_variant_change': '_onVariantChange', |
|||
'click .js_variant_change': '_onVariantChange' |
|||
}, |
|||
|
|||
start() { |
|||
this._setupInitialState(); |
|||
return this._super(...arguments); |
|||
}, |
|||
|
|||
/** |
|||
* Sets up initial state and handles hidden attributes |
|||
* @private |
|||
*/ |
|||
_setupInitialState() { |
|||
const variants = Array.from(this.el.querySelectorAll('li.variant_attribute')); |
|||
|
|||
// Handle hidden attributes first
|
|||
variants.filter(variant => variant.classList.contains('d-none')) |
|||
.forEach(hiddenVariant => { |
|||
// Auto-select values for hidden attributes
|
|||
hiddenVariant.querySelectorAll('input[type="radio"], select option').forEach(input => { |
|||
if (input.tagName === 'OPTION') { |
|||
input.selected = true; |
|||
} else { |
|||
input.checked = true; |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
// Handle visible variants
|
|||
const visibleVariants = variants.filter(v => !v.classList.contains('d-none')); |
|||
|
|||
// Disable all visible variants except the first one
|
|||
visibleVariants.forEach((variant, index) => { |
|||
index === 0 ? this._enableVariant(variant) : this._disableVariant(variant); |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* Handles variant selection changes |
|||
* @private |
|||
* @param {Event} ev |
|||
*/ |
|||
_onVariantChange(ev) { |
|||
const input = ev.target; |
|||
if (!input.classList.contains('js_variant_change')) { |
|||
return; |
|||
} |
|||
|
|||
const currentVariant = input.closest('li.variant_attribute'); |
|||
if (!currentVariant) { |
|||
return; |
|||
} |
|||
|
|||
this._updateHighlight(input); |
|||
|
|||
// Find and enable next visible variant
|
|||
const nextVariant = this._getNextVisibleVariant(currentVariant); |
|||
if (nextVariant) { |
|||
this._enableVariant(nextVariant); |
|||
} else { |
|||
// If last variant, enable all for editing
|
|||
this.el.querySelectorAll('li.variant_attribute:not(.d-none)') |
|||
.forEach(variant => this._enableVariant(variant)); |
|||
} |
|||
|
|||
ev.stopPropagation(); |
|||
}, |
|||
|
|||
/** |
|||
* Finds the next visible variant |
|||
* @private |
|||
* @param {Element} currentVariant |
|||
* @returns {Element|null} |
|||
*/ |
|||
_getNextVisibleVariant(currentVariant) { |
|||
let next = currentVariant.nextElementSibling; |
|||
while (next && next.classList.contains('d-none')) { |
|||
next = next.nextElementSibling; |
|||
} |
|||
return next; |
|||
}, |
|||
|
|||
/** |
|||
* Updates highlight state for selected variant |
|||
* @private |
|||
* @param {Element} input |
|||
*/ |
|||
_updateHighlight(input) { |
|||
const container = input.closest('li.variant_attribute'); |
|||
|
|||
if (input.closest('.css_attribute_color')) { |
|||
container.querySelectorAll('.css_attribute_color').forEach(el => { |
|||
el.classList.toggle('active', el === input.closest('.css_attribute_color')); |
|||
}); |
|||
} else if (input.type === 'radio') { |
|||
container.querySelectorAll('label').forEach(label => { |
|||
label.classList.toggle('active', label === input.closest('label')); |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Enables a variant |
|||
* @private |
|||
* @param {Element} variant |
|||
*/ |
|||
_enableVariant(variant) { |
|||
variant.classList.remove('disabled'); |
|||
variant.querySelectorAll('input, select').forEach(input => { |
|||
input.disabled = false; |
|||
if (input.checked || input.selected) { |
|||
this._updateHighlight(input); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* Disables a variant |
|||
* @private |
|||
* @param {Element} variant |
|||
*/ |
|||
_disableVariant(variant) { |
|||
variant.classList.add('disabled'); |
|||
variant.querySelectorAll('input, select').forEach(input => { |
|||
input.disabled = true; |
|||
}); |
|||
} |
|||
}); |
|||
@ -0,0 +1,5 @@ |
|||
.disabled { |
|||
opacity: 0.36; |
|||
z-index: 100; |
|||
pointer-events: none; |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Custom Template to Extend the Base Variants Template --> |
|||
<template id="website_sale.variants"> |
|||
<div class="attr_container"> |
|||
<t t-set="attribute_exclusions" |
|||
t-value="product._get_attribute_exclusions(parent_combination, parent_name)"/> |
|||
<ul t-attf-class="list-unstyled js_add_cart_variants mb-0 #{ul_class}" |
|||
t-att-data-attribute_exclusions="json.dumps(attribute_exclusions)"> |
|||
<t t-foreach="product.valid_product_template_attribute_line_ids" t-as="ptal"> |
|||
<!-- Attributes selection is hidden if there is only one value available and it's not a custom value --> |
|||
<li t-att-data-attribute_id="ptal.attribute_id.id" |
|||
t-att-data-ptal_index="ptal_index" |
|||
t-att-data-attribute_name="ptal.attribute_id.name" |
|||
t-att-data-attribute_display_type="ptal.attribute_id.display_type" |
|||
t-attf-class="variant_attribute disabled #{'d-none' if len(ptal.product_template_value_ids._only_active()) == 1 and not ptal.product_template_value_ids._only_active()[0].is_custom else ''}"> |
|||
<!-- Used to customize layout if the only available attribute value is custom --> |
|||
<t t-set="single" t-value="len(ptal.product_template_value_ids._only_active()) == 1"/> |
|||
<t t-set="single_and_custom" |
|||
t-value="single and ptal.product_template_value_ids._only_active()[0].is_custom"/> |
|||
<strong t-field="ptal.attribute_id.name" class="attribute_name"/> |
|||
<t t-if="ptal.attribute_id.display_type == 'select'"> |
|||
<select |
|||
t-att-data-attribute_id="ptal.attribute_id.id" |
|||
t-attf-class="form-select css_attribute_select o_wsale_product_attribute js_variant_change #{ptal.attribute_id.create_variant} #{'d-none' if single_and_custom else ''}" |
|||
t-att-name="'ptal-%s' % ptal.id"> |
|||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav"> |
|||
<option t-att-value="ptav.id" |
|||
t-att-data-value_id="ptav.id" |
|||
t-att-data-value_name="ptav.name" |
|||
t-att-data-attribute_name="ptav.attribute_id.name" |
|||
t-att-data-is_custom="ptav.is_custom" |
|||
t-att-data-is_single="single" |
|||
t-att-data-is_single_and_custom="single_and_custom"> |
|||
<span t-field="ptav.name"/> |
|||
<t t-call="website_sale.badge_extra_price"/> |
|||
</option> |
|||
</t> |
|||
</select> |
|||
</t> |
|||
<t t-if="ptal.attribute_id.display_type == 'radio'"> |
|||
<ul t-att-data-attribute_id="ptal.attribute_id.id" |
|||
t-attf-class="list-inline list-unstyled o_wsale_product_attribute #{'d-none' if single_and_custom else ''}"> |
|||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav"> |
|||
<li class="list-inline-item mb-3 js_attribute_value" style="margin: 0;"> |
|||
<label class="col-form-label"> |
|||
<div class="form-check"> |
|||
<input type="radio" |
|||
t-attf-class="form-check-input js_variant_change #{ptal.attribute_id.create_variant}" |
|||
t-att-name="'ptal-%s' % ptal.id" |
|||
t-att-value="ptav.id" |
|||
t-att-data-value_id="ptav.id" |
|||
t-att-data-value_name="ptav.name" |
|||
t-att-data-attribute_name="ptav.attribute_id.name" |
|||
t-att-data-is_custom="ptav.is_custom" |
|||
t-att-data-is_single="single" |
|||
t-att-data-is_single_and_custom="single_and_custom" |
|||
t-att-data-checked="ptav in combination"/> |
|||
<div class="radio_input_value form-check-label"> |
|||
<span t-field="ptav.name"/> |
|||
<t t-call="website_sale.badge_extra_price"/> |
|||
</div> |
|||
</div> |
|||
</label> |
|||
</li> |
|||
</t> |
|||
</ul> |
|||
</t> |
|||
<t t-if="ptal.attribute_id.display_type == 'pills'"> |
|||
<ul t-att-data-attribute_id="ptal.attribute_id.id" |
|||
t-attf-class="btn-group-toggle list-inline list-unstyled o_wsale_product_attribute #{'d-none' if single_and_custom else ''}" |
|||
data-bs-toggle="buttons"> |
|||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav"> |
|||
<li t-attf-class="o_variant_pills btn btn-primary mb-1 list-inline-item js_attribute_value"> |
|||
<input type="radio" |
|||
t-attf-class="js_variant_change #{ptal.attribute_id.create_variant}" |
|||
t-att-name="'ptal-%s' % ptal.id" |
|||
t-att-value="ptav.id" |
|||
t-att-data-value_id="ptav.id" |
|||
t-att-id="ptav.id" |
|||
t-att-data-value_name="ptav.name" |
|||
t-att-data-attribute_name="ptav.attribute_id.name" |
|||
t-att-data-is_custom="ptav.is_custom" |
|||
t-att-data-is_single_and_custom="single_and_custom" |
|||
t-att-autocomplete="off"/> |
|||
<div class="radio_input_value o_variant_pills_input_value"> |
|||
<span t-field="ptav.name"/> |
|||
<t t-call="website_sale.badge_extra_price"/> |
|||
</div> |
|||
</li> |
|||
</t> |
|||
</ul> |
|||
</t> |
|||
<t t-if="ptal.attribute_id.display_type == 'color'"> |
|||
<ul t-att-data-attribute_id="ptal.attribute_id.id" |
|||
t-attf-class="list-inline o_wsale_product_attribute #{'d-none' if single_and_custom else ''}"> |
|||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav"> |
|||
<li class="list-inline-item me-1"> |
|||
<label t-attf-style="background-color:#{ptav.html_color or ptav.product_attribute_value_id.name if not ptav.is_custom else ''}" |
|||
t-attf-class="css_attribute_color #{'custom_value' if ptav.is_custom else ''} #{'transparent' if (not ptav.is_custom and not ptav.html_color) else ''}"> |
|||
<input type="radio" |
|||
t-attf-class="js_variant_change #{ptal.attribute_id.create_variant}" |
|||
t-att-name="'ptal-%s' % ptal.id" |
|||
t-att-value="ptav.id" |
|||
t-att-title="ptav.name" |
|||
t-att-data-value_id="ptav.id" |
|||
t-att-data-value_name="ptav.name" |
|||
t-att-data-attribute_name="ptav.attribute_id.name" |
|||
t-att-data-is_custom="ptav.is_custom" |
|||
t-att-data-is_single="single" |
|||
t-att-data-is_single_and_custom="single_and_custom"/> |
|||
</label> |
|||
</li> |
|||
</t> |
|||
</ul> |
|||
</t> |
|||
</li> |
|||
</t> |
|||
</ul> |
|||
</div> |
|||
</template> |
|||
<template id="badge_extra_price" name="Badge Extra Price"> |
|||
<t t-set="combination_info_variant" t-value="product._get_combination_info(ptav, pricelist=pricelist)"/> |
|||
<span class="badge rounded-pill text-bg-light border" t-if="combination_info_variant['price_extra']"> |
|||
<span class="sign_badge_price_extra" t-out="combination_info_variant['price_extra'] > 0 and '+' or '-'"/> |
|||
<span t-out="abs(combination_info_variant['price_extra'])" class="variant_price_extra text-muted fst-italic" |
|||
style="white-space: nowrap;" |
|||
t-options='{"widget": "monetary", "display_currency": (pricelist or product).currency_id}'/> |
|||
</span> |
|||
</template> |
|||
</odoo> |
|||