diff --git a/attribute_set/tests/test_build_view.py b/attribute_set/tests/test_build_view.py index 5be40f031..5438d79f9 100644 --- a/attribute_set/tests/test_build_view.py +++ b/attribute_set/tests/test_build_view.py @@ -45,8 +45,9 @@ def setUpClass(cls): cls.loader = FakeModelLoader(cls.env, cls.__module__) cls.loader.backup_registry() from .models import ResCountry, ResPartner + from ..models.attribute_set_owner import AttributeSetOwnerMixin - cls.loader.update_registry((ResPartner, ResCountry)) + cls.loader.update_registry((AttributeSetOwnerMixin, ResPartner, ResCountry)) # Create a new inherited view with the 'attributes' placeholder. cls.view = cls.env["ir.ui.view"].create( diff --git a/product_catalog_attribute_set/README.rst b/product_catalog_attribute_set/README.rst new file mode 100644 index 000000000..a32d91c8c --- /dev/null +++ b/product_catalog_attribute_set/README.rst @@ -0,0 +1,76 @@ +============================= +Product Catalog Attribute Set +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:90ca4229a4bc6ac1a4599e4c6215399a0a3e4721ae029bbc6497fcda6f43288c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fodoo--pim-lightgray.png?logo=github + :target: https://github.com/OCA/odoo-pim/tree/18.0/product_catalog_attribute_set + :alt: OCA/odoo-pim +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/odoo-pim-18-0/odoo-pim-18-0-product_catalog_attribute_set + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/odoo-pim&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +A module to show OCA attribute sets in product's catalog kanban view. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Kencove + +Contributors +------------ + +- Mohamed Alkobrosli + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/odoo-pim `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_catalog_attribute_set/__init__.py b/product_catalog_attribute_set/__init__.py new file mode 100644 index 000000000..74d287b3f --- /dev/null +++ b/product_catalog_attribute_set/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2025 Kencove (http://www.kencove.com). +# @author Mohamed Alkobrosli +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/product_catalog_attribute_set/__manifest__.py b/product_catalog_attribute_set/__manifest__.py new file mode 100644 index 000000000..1d01e536b --- /dev/null +++ b/product_catalog_attribute_set/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright 2025 Kencove (http://www.kencove.com). +# @author Mohamed Alkobrosli +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product Catalog Attribute Set", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Kencove, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/odoo-pim", + "depends": [ + "web", + "product", + "pim", + "website_attribute_set", + ], + "data": [ + "views/product_views.xml", + ], + "assets": { + "web.assets_backend": [ + "product_catalog_attribute_set/static/src/search_model.esm.js", + "product_catalog_attribute_set/static/src/kanban_model.esm.js", + "product_catalog_attribute_set/static/src/search_panel.xml", + "product_catalog_attribute_set/static/src/search_panel.esm.js", + ], + }, + "installable": True, + "application": True, +} diff --git a/product_catalog_attribute_set/models/__init__.py b/product_catalog_attribute_set/models/__init__.py new file mode 100644 index 000000000..711e69fd4 --- /dev/null +++ b/product_catalog_attribute_set/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2025 Kencove (http://www.kencove.com). +# @author Mohamed Alkobrosli +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import product_product diff --git a/product_catalog_attribute_set/models/product_product.py b/product_catalog_attribute_set/models/product_product.py new file mode 100644 index 000000000..3a2e567a3 --- /dev/null +++ b/product_catalog_attribute_set/models/product_product.py @@ -0,0 +1,147 @@ +# Copyright 2025 Kencove (http://www.kencove.com). +# @author Mohamed Alkobrosli +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from collections import Counter + +from lxml import etree + +from odoo import _, api, models +from odoo.exceptions import ValidationError + + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.model + def _get_extra_attributes(self): + """Override Attribute's method _build_attribute_eview() to build an + attribute eview with the mixin model's attributes""" + domain = [ + ("attribute_set_ids", "!=", False), + ("model", "=", "product.template"), + ("nature", "=", "custom"), + ] + attributes = self.env["attribute.attribute"].sudo().search(domain) + return attributes + + def _create_filter_attributes(self, attributes, parent, index): + for attribute in attributes: + if attribute.ttype == "many2many": + field_node = etree.Element( + "field", + name=attribute.name, + icon="fa-th-list", + enable_counters="1", + select="multi", + ) + parent.insert(index + 1, field_node) + index += 1 + elif attribute.ttype in ("many2one"): + field_node = etree.Element( + "field", + name=attribute.name, + icon="fa-th-list", + ) + parent.insert(index + 1, field_node) + index += 1 + return parent + + def _create_search_attributes(self, attributes, parent, index): + for attribute in attributes: + field_node = etree.Element( + "field", + name=attribute.name, + ) + parent.insert(index + 1, field_node) + index += 1 + return parent + + def _insert_extra_search_attribute(self, arch, separator, is_filter=False): + """Replace attributes' placeholders with real fields in form view arch.""" + eview = etree.fromstring(arch) + form_name = eview.get("string") + placeholder = eview.xpath(f"//separator[@name='{separator}']") + if len(placeholder) != 1: + raise ValidationError( + _( + """It is impossible to add Attributes on "%(name)s" xml + view as there is + not one "" in it. + """, + name=form_name, + separator=separator, + ) + ) + attributes = self._get_extra_attributes() + parent = placeholder[0].getparent() + index = parent.index(placeholder[0]) + if is_filter: + parent = self._create_filter_attributes(attributes, parent, index) + else: + parent = self._create_search_attributes(attributes, parent, index) + # Remove the placeholder + parent.remove(placeholder[0]) + return etree.tostring(eview, pretty_print=True) + + def get_view(self, view_id=None, view_type="search", **options): + result = super().get_view(view_id=view_id, view_type=view_type, **options) + if view_type == "search": + form_arch = result.get("arch") + if form_arch: + # Add attributes in filter sidebar + result["arch"] = self._insert_extra_search_attribute( + result["arch"], + separator="attributes_filter_placeholder", + is_filter=True, + ) + # Add attributes in search panel + result["arch"] = self._insert_extra_search_attribute( + result["arch"], separator="attributes_search_placeholder" + ) + return result + + def extra_attr_vals(self, all_products, attr): + """ + Return a list of each attribute value or a list of + lists having id, value if value of type attribute.option + """ + count = 0 + vals_list = [] + for product in all_products: + if product[attr.name]: + count += 1 + product_tmpl_id = product.product_tmpl_id + val = product_tmpl_id.get_extra_attribute_values(attr) + if val: + if isinstance(val, models.BaseModel) and len(val) >= 1: + for v in val: + vals_list.append(v.name) + else: + vals_list.append(val) + counter = Counter(vals_list) + # Convert to list of [value, count] + result = [[val, count] for val, count in counter.items()] + return result, count + + def catalog_extra_attrs(self): + product = self.env["product.product"].sudo() + all_attrs = self._get_extra_attributes() + filtered_attrs = all_attrs.filtered( + lambda r: r.ttype not in ["many2many", "many2one"] + ) + all_products = product.search([]) + attrs_data = [] + for attr in filtered_attrs: + vals, count = self.extra_attr_vals(all_products, attr) + attr_data = { + "id": attr.id, + "name": attr.name, + "display_name": attr.field_description, + "count": count, + } + if vals: + attr_data["vals"] = vals + attrs_data.append(attr_data) + return attrs_data diff --git a/product_catalog_attribute_set/pyproject.toml b/product_catalog_attribute_set/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/product_catalog_attribute_set/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_catalog_attribute_set/readme/CONTRIBUTORS.md b/product_catalog_attribute_set/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..60a4078f3 --- /dev/null +++ b/product_catalog_attribute_set/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Mohamed Alkobrosli \<\> diff --git a/product_catalog_attribute_set/readme/DESCRIPTION.md b/product_catalog_attribute_set/readme/DESCRIPTION.md new file mode 100644 index 000000000..694a1578c --- /dev/null +++ b/product_catalog_attribute_set/readme/DESCRIPTION.md @@ -0,0 +1 @@ +A module to show OCA attribute sets in product's catalog kanban view. diff --git a/product_catalog_attribute_set/static/description/icon.png b/product_catalog_attribute_set/static/description/icon.png new file mode 100644 index 000000000..ffded50af Binary files /dev/null and b/product_catalog_attribute_set/static/description/icon.png differ diff --git a/product_catalog_attribute_set/static/description/index.html b/product_catalog_attribute_set/static/description/index.html new file mode 100644 index 000000000..cdf492d11 --- /dev/null +++ b/product_catalog_attribute_set/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +Product Catalog Attribute Set + + + +
+

Product Catalog Attribute Set

+ + +

Beta License: AGPL-3 OCA/odoo-pim Translate me on Weblate Try me on Runboat

+

A module to show OCA attribute sets in product’s catalog kanban view.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Kencove
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/odoo-pim project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_catalog_attribute_set/static/src/kanban_model.esm.js b/product_catalog_attribute_set/static/src/kanban_model.esm.js new file mode 100644 index 000000000..a5b509ac8 --- /dev/null +++ b/product_catalog_attribute_set/static/src/kanban_model.esm.js @@ -0,0 +1,31 @@ +/** @odoo-module **/ + +/** Copyright 2025 Kencove (http://www.kencove.com). + @author Mohamed Alkobrosli + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). **/ + +import {ProductCatalogKanbanModel} from "@product/product_catalog/kanban_model"; +import {patch} from "@web/core/utils/patch"; +import {reactive} from "@odoo/owl"; + +export const productCatalogStore = reactive({ + currentProducts: [], + visibleIds: [], + + updateIds(data) { + this.currentProducts = data.records; + this.visibleIds = this.currentProducts.map((r) => r.id); + return this.visibleIds; + }, +}); + +patch(ProductCatalogKanbanModel.prototype, { + // eslint-disable-next-line no-unused-vars + async _loadData(params) { + const result = await super._loadData(...arguments); + if (result?.records?.length) { + productCatalogStore.updateIds(result); + } + return result; + }, +}); diff --git a/product_catalog_attribute_set/static/src/search_model.esm.js b/product_catalog_attribute_set/static/src/search_model.esm.js new file mode 100644 index 000000000..92f60dded --- /dev/null +++ b/product_catalog_attribute_set/static/src/search_model.esm.js @@ -0,0 +1,45 @@ +/** @odoo-module **/ + +/** Copyright 2025 Kencove (http://www.kencove.com). + @author Mohamed Alkobrosli + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). **/ + +import {Domain} from "@web/core/domain"; +import {SearchModel} from "@web/search/search_model"; +import {patch} from "@web/core/utils/patch"; + +/** + * The patch is to override sending a value to limit parameter. + * The default value for limit parameter is None and we prefer it. + **/ +patch(SearchModel.prototype, { + async _fetchFilters(filters) { + const evalContext = {}; + for (const category of this.categories) { + evalContext[category.fieldName] = category.activeValueId; + } + const categoryDomain = this._getCategoryDomain(); + const searchDomain = this.searchDomain; + await Promise.all( + filters.map(async (filter) => { + const result = await this.orm.call( + this.resModel, + "search_panel_select_multi_range", + [filter.fieldName], + { + category_domain: categoryDomain, + comodel_domain: new Domain(filter.domain).toList(evalContext), + context: this.globalContext, + enable_counters: filter.enableCounters, + filter_domain: this._getFilterDomain(filter.id), + expand: filter.expand, + group_by: filter.groupBy || false, + group_domain: this._getGroupDomain(filter), + search_domain: searchDomain, + } + ); + this._createFilterTree(filter.id, result); + }) + ); + }, +}); diff --git a/product_catalog_attribute_set/static/src/search_panel.esm.js b/product_catalog_attribute_set/static/src/search_panel.esm.js new file mode 100644 index 000000000..e2afe3f6e --- /dev/null +++ b/product_catalog_attribute_set/static/src/search_panel.esm.js @@ -0,0 +1,87 @@ +/** @odoo-module **/ + +/** Copyright 2025 Kencove (http://www.kencove.com). + @author Mohamed Alkobrosli + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). **/ + +import {registry} from "@web/core/registry"; +import {ProductCatalogSearchPanel} from "@product/product_catalog/search/search_panel"; +import {productCatalogKanbanView} from "@product/product_catalog/kanban_view"; +import {useService} from "@web/core/utils/hooks"; +import {onWillStart, useState} from "@odoo/owl"; +import {productCatalogStore} from "./kanban_model.esm"; + +export class ProductCatalogSearchPanel2 extends ProductCatalogSearchPanel { + static template = "web.SearchPanel.Custom"; + static subTemplates = { + ...ProductCatalogSearchPanel.subTemplates, + }; + setup() { + super.setup(); + this.orm = useService("orm"); + this.state = useState({ + ...this.state, + extra_attrs: [], + productCatalogStore, + isDomainInSearch: (x, y, z) => this.checkIfDomainInSearch(x, y, z), + }); + onWillStart(async () => { + await this.loadExtraAttrs(); + }); + } + + checkIfDomainInSearch(facets, attr, value) { + const domain = this.getDomain(attr, value); + const facet = facets.find((q) => q.domain === domain); + if (facet) return true; + return false; + } + + async loadExtraAttrs() { + const ids = this.state.productCatalogStore.visibleIds; + if (this.state.productCatalogStore?.visibleIds?.length) { + this.state.extra_attrs = await this.orm.call( + "product.product", + "catalog_extra_attrs", + [ids], + {} + ); + this.updateActiveValues(); + } + } + + getDomain(attr, value = null) { + let domain = ""; + if (value !== null) { + domain = `[("${attr.name}", "ilike", "${value}")]`; + } else { + domain = `[("${attr.name}", "!=", None)]`; + } + return domain; + } + + async toggleSectionFilterValue2(attr, value = null, ev = {}) { + const domain = this.getDomain(attr, value); + if (ev.currentTarget.checked) { + const preFilter = { + description: domain, + domain: domain, + invisible: "True", + type: "filter", + }; + this.env.searchModel.createNewFilters([preFilter]); + } else { + const facets = this.env.searchModel.facets; + const facet = facets.find((q) => q.domain === domain); + if (facet) { + this.env.searchModel.deactivateGroup(facet.groupId); + } + } + } +} + +registry.category("views").remove("product_kanban_catalog"); +registry.category("views").add("product_kanban_catalog", { + ...productCatalogKanbanView, + SearchPanel: ProductCatalogSearchPanel2, +}); diff --git a/product_catalog_attribute_set/static/src/search_panel.xml b/product_catalog_attribute_set/static/src/search_panel.xml new file mode 100644 index 000000000..5fa08c8aa --- /dev/null +++ b/product_catalog_attribute_set/static/src/search_panel.xml @@ -0,0 +1,235 @@ + + + + + false + + +
  • + +
    + + +
    +
  • +
    +
    + + +
  • + + + + +
    + + + + + + + + + +
    +
  • +
    + + + +
    + + +
    +
  • +
    + + + + + + + + + + + +
    +
  • +
    +
    + + +
      +
    • + +
      +
      + + + + + + + + + +
      +
      +
    • +
    +
    + + + + + + + + + +
    + +
    +
    +
    +
    +
    + + + + + + + + + + +
    diff --git a/product_catalog_attribute_set/views/product_views.xml b/product_catalog_attribute_set/views/product_views.xml new file mode 100644 index 000000000..afc545c42 --- /dev/null +++ b/product_catalog_attribute_set/views/product_views.xml @@ -0,0 +1,17 @@ + + + + custom.product.view.search.catalog + product.product + + + + + + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt index 66bc2cbae..b69005c90 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ odoo_test_helper +odoo-addon-website_attribute_set @ git+https://github.com/OCA/odoo-pim.git@refs/pull/202/head#subdirectory=website_attribute_set