@ -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> |
||||