diff --git a/setup/website_sale_product_assortment/odoo/addons/website_sale_product_assortment b/setup/website_sale_product_assortment/odoo/addons/website_sale_product_assortment new file mode 120000 index 0000000000..2f78bc2d73 --- /dev/null +++ b/setup/website_sale_product_assortment/odoo/addons/website_sale_product_assortment @@ -0,0 +1 @@ +../../../../website_sale_product_assortment \ No newline at end of file diff --git a/setup/website_sale_product_assortment/setup.py b/setup/website_sale_product_assortment/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/website_sale_product_assortment/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/website_sale_product_assortment/README.rst b/website_sale_product_assortment/README.rst new file mode 100644 index 0000000000..ac7f8674d0 --- /dev/null +++ b/website_sale_product_assortment/README.rst @@ -0,0 +1,102 @@ +============================ +eCommerce product assortment +============================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fe--commerce-lightgray.png?logo=github + :target: https://github.com/OCA/e-commerce/tree/13.0/website_sale_product_assortment + :alt: OCA/e-commerce +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/e-commerce-13-0/e-commerce-13-0-website_sale_product_assortment + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/113/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to set e-commerce restrictions on product assortments. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To see this module working, you have to define a product assortment and select +an option on the website availability field. + +#. **Don't apply restriction**: This option will not set any kind of restriction on +product items. +#. **Avoid to show non available products**: This option will hide on the e-commerce, the +products that are not added to the products domain. If a product template has at least +one allowed variant to show, the product will appear on the product items view but only +that variants will be able to be bought. +#. **Avoid selling not available products**: This option will restrict to buy the +products that are added to the assortment on the e-commerce. To inform the clients, +two more fields were added: "Message when unavailable" and "Assortment information". +The first one will add a short description to the product item and the other one will set a +detailed description on the product sheet. This second one is editable from the website editor. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Carlos Roca + * Pedro M. Baeza + +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. + +.. |maintainer-CarlosRoca13| image:: https://github.com/CarlosRoca13.png?size=40px + :target: https://github.com/CarlosRoca13 + :alt: CarlosRoca13 + +Current `maintainer `__: + +|maintainer-CarlosRoca13| + +This module is part of the `OCA/e-commerce `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_sale_product_assortment/__init__.py b/website_sale_product_assortment/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/website_sale_product_assortment/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/website_sale_product_assortment/__manifest__.py b/website_sale_product_assortment/__manifest__.py new file mode 100644 index 0000000000..df374ee400 --- /dev/null +++ b/website_sale_product_assortment/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2021 Tecnativa - Carlos Roca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "eCommerce product assortment", + "summary": "Use product assortments to display products available on e-commerce.", + "version": "14.0.1.0.0", + "development_status": "Beta", + "license": "AGPL-3", + "category": "Website", + "website": "https://github.com/OCA/e-commerce", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["CarlosRoca13"], + "installable": True, + "depends": ["product_assortment", "website_sale"], + "data": ["templates/assets.xml", "views/ir_filters_views.xml"], +} diff --git a/website_sale_product_assortment/controllers/__init__.py b/website_sale_product_assortment/controllers/__init__.py new file mode 100644 index 0000000000..97b95b7d8c --- /dev/null +++ b/website_sale_product_assortment/controllers/__init__.py @@ -0,0 +1,2 @@ +from . import variant +from . import website_sale diff --git a/website_sale_product_assortment/controllers/variant.py b/website_sale_product_assortment/controllers/variant.py new file mode 100644 index 0000000000..f80dcf5044 --- /dev/null +++ b/website_sale_product_assortment/controllers/variant.py @@ -0,0 +1,42 @@ +# Copyright 2020 Tecnativa - Carlos Roca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, http +from odoo.http import request + +from odoo.addons.sale.controllers.variant import VariantController + + +class WebsiteSaleVariantController(VariantController): + @http.route( + ["/sale/get_info_assortment_preview"], + type="json", + auth="public", + methods=["POST"], + website=True, + ) + def get_info_assortment_preview(self, product_template_ids, **kw): + """Special route to use website logic in get_combination_info override. + This route is called in JS by appending _website to the base route. + """ + res = [] + templates = request.env["product.template"].sudo().browse(product_template_ids) + not_allowed_product_dict = templates.get_product_assortment_restriction_info( + templates.mapped("product_variant_ids.id") + ) + for template in templates: + variant_ids = set(template.product_variant_ids.ids) + if ( + variant_ids + and variant_ids & set(not_allowed_product_dict.keys()) == variant_ids + ): + res.append( + { + "id": template.id, + "message_unavailable": not_allowed_product_dict[ + variant_ids.pop() + ][0].message_unavailable + or _("Not available"), + } + ) + return res diff --git a/website_sale_product_assortment/controllers/website_sale.py b/website_sale_product_assortment/controllers/website_sale.py new file mode 100644 index 0000000000..ed7e92cee2 --- /dev/null +++ b/website_sale_product_assortment/controllers/website_sale.py @@ -0,0 +1,68 @@ +# Copyright 2021 Tecnativa - Carlos Roca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from werkzeug.exceptions import NotFound + +from odoo.http import request, route +from odoo.osv import expression + +from odoo.addons.website_sale.controllers.main import WebsiteSale + + +class WebsiteSale(WebsiteSale): + def _get_products_allowed(self): + partner = request.env.user.partner_id + website_id = request.website.id + assortments = ( + request.env["ir.filters"] + .sudo() + .search( + [ + ("is_assortment", "=", True), + ("website_availability", "=", "no_show"), + "|", + ("website_ids", "=", False), + ("website_ids", "=", website_id), + ] + ) + ) + assortment_restriction = False + allowed_product_ids = set() + for assortment in assortments: + if ( + # Set active_test to False to allow filtering by partners + # that are not active, (for example Public User) + partner + & assortment.with_context(active_test=False).all_partner_ids + ): + assortment_restriction = True + allowed_product_ids = allowed_product_ids.union( + set(assortment.all_product_ids.ids) + ) + return allowed_product_ids, assortment_restriction + + @route() + def product(self, product, category="", search="", **kwargs): + """Overriding product method to avoid accessing to product sheet when the + product assortments prevent to show them. + """ + allowed_product_ids, assortment_restriction = self._get_products_allowed() + if assortment_restriction: + if len(set(product.product_variant_ids.ids) & allowed_product_ids) == 0: + raise NotFound() + return super().product(product, category=category, search=search, **kwargs) + + def _get_search_domain( + self, search, category, attrib_values, search_in_description=True + ): + """Overriding _get_search_domain method to avoid show product templates that + has all their variants not allowed to be shown. + """ + res = super()._get_search_domain( + search, category, attrib_values, search_in_description=search_in_description + ) + allowed_product_ids, assortment_restriction = self._get_products_allowed() + if assortment_restriction: + return expression.AND( + [res, [("product_variant_ids", "in", list(allowed_product_ids))]] + ) + return res diff --git a/website_sale_product_assortment/i18n/es.po b/website_sale_product_assortment/i18n/es.po new file mode 100644 index 0000000000..d9d328032e --- /dev/null +++ b/website_sale_product_assortment/i18n/es.po @@ -0,0 +1,132 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_product_assortment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-25 06:21+0000\n" +"PO-Revision-Date: 2022-05-25 08:22+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.3\n" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,help:website_sale_product_assortment.field_ir_filters__website_availability +msgid "" +"\n" +" Each point is used to:\n" +"\n" +" \t- Don't apply restriction: Show all products available for sale on " +"website.\n" +"\n" +" \t- Avoid to show non available products: Show only products " +"available for sale\n" +" on website.\n" +"\n" +" \t- Avoid selling not available products: Show all products, but " +"avoid\n" +" purchase on website.\n" +"\n" +" " +msgstr "" +"\n" +" Cada punto se utiliza para:\n" +"\n" +" \t- No aplicar restricción: Mostrar todos los productos disponibles " +"para la venta en el sitio web.\n" +"\n" +" \t- Evitar mostrar los productos no disponibles: Mostrar sólo los " +"productos disponibles para la venta\n" +" en el sitio web.\n" +"\n" +" \t- Evitar la venta de productos no disponibles: Mostrar todos los " +"productos, pero evitar\n" +" la venta en el sitio web.\n" +"\n" +" " + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__all_product_ids +msgid "All Product" +msgstr "Todos los productos" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__aplly_on_public_user +msgid "Aplly On Public User" +msgstr "Aplicar en usuario público" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__assortment_information +msgid "Assortment Information" +msgstr "Información de surtido" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__website_availability +msgid "Availability on Website" +msgstr "Disponibilidad en sitio web" + +#. module: website_sale_product_assortment +#: model:ir.model.fields.selection,name:website_sale_product_assortment.selection__ir_filters__website_availability__no_purchase +msgid "Avoid selling not available products" +msgstr "No permitir vender productos no disponibles" + +#. module: website_sale_product_assortment +#: model:ir.model.fields.selection,name:website_sale_product_assortment.selection__ir_filters__website_availability__no_show +msgid "Avoid to show non available products" +msgstr "No permitir mostrar productos no disponibles" + +#. module: website_sale_product_assortment +#: model:ir.model.fields.selection,name:website_sale_product_assortment.selection__ir_filters__website_availability__no_restriction +msgid "Don't apply restriction" +msgstr "No aplicar restricción" + +#. module: website_sale_product_assortment +#: model:ir.model,name:website_sale_product_assortment.model_ir_filters +msgid "Filters" +msgstr "Filtros" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,help:website_sale_product_assortment.field_ir_filters__message_unavailable +msgid "" +"Message showed when some product is not available and the option\n" +" 'Avoid selling not available products' is selected.\n" +" " +msgstr "" +"Mensaje mostrado cuando algún producto no está disponible y la opción\n" +" 'Evitar la venta de productos no disponibles' está seleccionada.\n" +" " + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__message_unavailable +msgid "Message when unavailable" +msgstr "Mensaje cuando no está disponible" + +#. module: website_sale_product_assortment +#: code:addons/website_sale_product_assortment/controllers/variant.py:0 +#, python-format +msgid "Not available" +msgstr "No disponible" + +#. module: website_sale_product_assortment +#: model:ir.model,name:website_sale_product_assortment.model_product_template +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: website_sale_product_assortment +#. openerp-web +#: code:addons/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml:0 +#, python-format +msgid "Warning" +msgstr "Advertencia" + +#. module: website_sale_product_assortment +#: model_terms:ir.ui.view,arch_db:website_sale_product_assortment.product_assortment_view_form +msgid "Website Availability" +msgstr "Disponibilidad en sitio web" diff --git a/website_sale_product_assortment/i18n/website_sale_product_assortment.pot b/website_sale_product_assortment/i18n/website_sale_product_assortment.pot new file mode 100644 index 0000000000..6256c79afe --- /dev/null +++ b/website_sale_product_assortment/i18n/website_sale_product_assortment.pot @@ -0,0 +1,112 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_product_assortment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,help:website_sale_product_assortment.field_ir_filters__website_availability +msgid "" +"\n" +" Each point is used to:\n" +"\n" +" \t- Don't apply restriction: Show all products available for sale on website.\n" +"\n" +" \t- Avoid to show non available products: Show only products available for sale\n" +" on website.\n" +"\n" +" \t- Avoid selling not available products: Show all products, but avoid\n" +" purchase on website.\n" +"\n" +" " +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__all_product_ids +msgid "All Product" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__apply_on_public_user +msgid "Apply On Public User" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__assortment_information +msgid "Assortment Information" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__website_availability +msgid "Availability on Website" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields.selection,name:website_sale_product_assortment.selection__ir_filters__website_availability__no_purchase +msgid "Avoid selling not available products" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields.selection,name:website_sale_product_assortment.selection__ir_filters__website_availability__no_show +msgid "Avoid to show non available products" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields.selection,name:website_sale_product_assortment.selection__ir_filters__website_availability__no_restriction +msgid "Don't apply restriction" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model,name:website_sale_product_assortment.model_ir_filters +msgid "Filters" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,help:website_sale_product_assortment.field_ir_filters__message_unavailable +msgid "" +"Message showed when some product is not available and the option\n" +" 'Avoid selling not available products' is selected.\n" +" " +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__message_unavailable +msgid "Message when unavailable" +msgstr "" + +#. module: website_sale_product_assortment +#: code:addons/website_sale_product_assortment/controllers/variant.py:0 +#, python-format +msgid "Not available" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model,name:website_sale_product_assortment.model_product_template +msgid "Product Template" +msgstr "" + +#. module: website_sale_product_assortment +#. openerp-web +#: code:addons/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml:0 +#, python-format +msgid "Warning" +msgstr "" + +#. module: website_sale_product_assortment +#: model_terms:ir.ui.view,arch_db:website_sale_product_assortment.product_assortment_view_form +msgid "Website Availability" +msgstr "" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__website_ids +msgid "Websites" +msgstr "" diff --git a/website_sale_product_assortment/models/__init__.py b/website_sale_product_assortment/models/__init__.py new file mode 100644 index 0000000000..26a3fb7e1d --- /dev/null +++ b/website_sale_product_assortment/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_filters +from . import product_template diff --git a/website_sale_product_assortment/models/ir_filters.py b/website_sale_product_assortment/models/ir_filters.py new file mode 100644 index 0000000000..70dd726d6c --- /dev/null +++ b/website_sale_product_assortment/models/ir_filters.py @@ -0,0 +1,58 @@ +# Copyright 2021 Tecnativa - Carlos Roca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class IrFilters(models.Model): + _inherit = "ir.filters" + + website_availability = fields.Selection( + selection=[ + ("no_restriction", "Don't apply restriction"), + ("no_show", "Avoid to show non available products"), + ("no_purchase", "Avoid selling not available products"), + ], + string="Availability on Website", + default="no_restriction", + required=True, + help=""" + Each point is used to:\n + \t- Don't apply restriction: Show all products available for sale on website.\n + \t- Avoid to show non available products: Show only products available for sale + on website.\n + \t- Avoid selling not available products: Show all products, but avoid + purchase on website.\n + """, + ) + message_unavailable = fields.Char( + string="Message when unavailable", + help="""Message showed when some product is not available and the option + 'Avoid selling not available products' is selected. + """, + ) + website_ids = fields.Many2many( + comodel_name="website", ondelete="cascade", string="Websites" + ) + apply_on_public_user = fields.Boolean() + assortment_information = fields.Html() + all_product_ids = fields.Many2many( + comodel_name="product.product", + relation="assortment_all_products", + compute="_compute_all_product_ids", + ) + + @api.depends("domain", "blacklist_product_ids", "whitelist_product_ids") + def _compute_all_product_ids(self): + for record in self: + record.all_product_ids = record.env["product.product"] + if record.is_assortment: + record.all_product_ids = record.env["product.product"].search( + record._get_eval_domain() + ) + + @api.depends("apply_on_public_user") + def _compute_all_partner_ids(self): + super()._compute_all_partner_ids() + for ir_filter in self: + if ir_filter.apply_on_public_user: + ir_filter.all_partner_ids += self.env.ref("base.public_user").partner_id diff --git a/website_sale_product_assortment/models/product_template.py b/website_sale_product_assortment/models/product_template.py new file mode 100644 index 0000000000..0be4e10b60 --- /dev/null +++ b/website_sale_product_assortment/models/product_template.py @@ -0,0 +1,72 @@ +# Copyright 2021 Tecnativa - Carlos Roca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + @api.model + def get_product_assortment_restriction_info(self, product_ids): + partner = self.env.user.partner_id + website = self.env["website"].get_current_website() + assortments = ( + self.env["ir.filters"] + .sudo() + .search( + [ + ("is_assortment", "=", True), + ("website_availability", "in", ["no_purchase", "no_show"]), + "|", + ("website_ids", "=", website.id), + ("website_ids", "=", False), + ] + ) + ) + assortment_dict = {} + for assortment in assortments: + if partner & assortment.with_context(active_test=False).all_partner_ids: + allowed_product_ids = assortment.all_product_ids.ids + for product in product_ids: + if product not in allowed_product_ids: + assortment_dict.setdefault(product, self.env["ir.filters"]) + assortment_dict[product] |= assortment + return assortment_dict + + def _get_combination_info( + self, + combination=False, + product_id=False, + add_qty=1, + pricelist=False, + parent_combination=False, + only_template=False, + ): + res = super()._get_combination_info( + combination=combination, + product_id=product_id, + add_qty=add_qty, + pricelist=pricelist, + parent_combination=parent_combination, + only_template=only_template, + ) + product_res_id = res["product_id"] + if self.env.context.get("website_id") and not only_template and product_res_id: + not_allowed_product_dict = self.get_product_assortment_restriction_info( + [product_res_id] + ) + if not_allowed_product_dict and product_res_id in not_allowed_product_dict: + res["product_avoid_purchase"] = True + res["product_assortment_type"] = "no_purchase" + assortments = not_allowed_product_dict[product_res_id] + for assortment in assortments: + if assortment.website_availability == "no_show": + res["product_assortment_type"] = "no_show" + break + if res["product_assortment_type"] != "no_show": + assortment = assortments[0] + res["message_unavailable"] = assortment.message_unavailable + res["assortment_information"] = assortment.assortment_information + else: + res["product_avoid_purchase"] = False + return res diff --git a/website_sale_product_assortment/readme/CONFIGURE.rst b/website_sale_product_assortment/readme/CONFIGURE.rst new file mode 100644 index 0000000000..8387e8b3a4 --- /dev/null +++ b/website_sale_product_assortment/readme/CONFIGURE.rst @@ -0,0 +1,14 @@ +To see this module working, you have to define a product assortment and select +an option on the website availability field. + +#. **Don't apply restriction**: This option will not set any kind of restriction on +product items. +#. **Avoid to show non available products**: This option will hide on the e-commerce, the +products that are not added to the products domain. If a product template has at least +one allowed variant to show, the product will appear on the product items view but only +that variants will be able to be bought. +#. **Avoid selling not available products**: This option will restrict to buy the +products that are added to the assortment on the e-commerce. To inform the clients, +two more fields were added: "Message when unavailable" and "Assortment information". +The first one will add a short description to the product item and the other one will set a +detailed description on the product sheet. This second one is editable from the website editor. diff --git a/website_sale_product_assortment/readme/CONTRIBUTORS.rst b/website_sale_product_assortment/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..373f8ecf25 --- /dev/null +++ b/website_sale_product_assortment/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* `Tecnativa `_: + + * Carlos Roca + * Pedro M. Baeza + +* `Ooops `_: + * Ashish Hirpara (https://ashish-hirpara.com) diff --git a/website_sale_product_assortment/readme/DESCRIPTION.rst b/website_sale_product_assortment/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..ffd0a25f89 --- /dev/null +++ b/website_sale_product_assortment/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to set e-commerce restrictions on product assortments. diff --git a/website_sale_product_assortment/static/description/icon.png b/website_sale_product_assortment/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/website_sale_product_assortment/static/description/icon.png differ diff --git a/website_sale_product_assortment/static/description/index.html b/website_sale_product_assortment/static/description/index.html new file mode 100644 index 0000000000..7bafc8d496 --- /dev/null +++ b/website_sale_product_assortment/static/description/index.html @@ -0,0 +1,445 @@ + + + + + + +eCommerce product assortment + + + +
+

eCommerce product assortment

+ + +

Beta License: AGPL-3 OCA/e-commerce Translate me on Weblate Try me on Runbot

+

This module allows to set e-commerce restrictions on product assortments.

+

Table of contents

+ +
+

Configuration

+

To see this module working, you have to define a product assortment and select +an option on the website availability field.

+

#. Don’t apply restriction: This option will not set any kind of restriction on +product items. +#. Avoid to show non available products: This option will hide on the e-commerce, the +products that are not added to the products domain. If a product template has at least +one allowed variant to show, the product will appear on the product items view but only +that variants will be able to be bought. +#. Avoid selling not available products: This option will restrict to buy the +products that are added to the assortment on the e-commerce. To inform the clients, +two more fields were added: “Message when unavailable” and “Assortment information”. +The first one will add a short description to the product item and the other one will set a +detailed description on the product sheet. This second one is editable from the website editor.

+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:

    +
    +
      +
    • Carlos Roca
    • +
    • Pedro M. Baeza
    • +
    +
    +
  • +
+
+
+

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.

+

Current maintainer:

+

CarlosRoca13

+

This module is part of the OCA/e-commerce project on GitHub.

+

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

+
+
+
+ + diff --git a/website_sale_product_assortment/static/src/js/assortment_list_preview.js b/website_sale_product_assortment/static/src/js/assortment_list_preview.js new file mode 100644 index 0000000000..a61d58839b --- /dev/null +++ b/website_sale_product_assortment/static/src/js/assortment_list_preview.js @@ -0,0 +1,53 @@ +odoo.define("website_sale_product_assortment.assortment_preview", function (require) { + "use strict"; + + const publicWidget = require("web.public.widget"); + const core = require("web.core"); + + publicWidget.registry.WebsiteSaleProductAssortment = publicWidget.Widget.extend({ + selector: "#products_grid", + xmlDependencies: [ + "/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml", + ], + + start: function () { + return $.when.apply($, [ + this._super.apply(this, arguments), + this.render_assortments(), + ]); + }, + render_assortments: function () { + const $products = $(".o_wsale_product_grid_wrapper"); + const product_dic = {}; + $products.each(function () { + product_dic[this.querySelector("a img").src.split("/")[6]] = this; + }); + const product_ids = Object.keys(product_dic).map(Number); + return this._rpc({ + route: "/sale/get_info_assortment_preview", + params: {product_template_ids: product_ids}, + }).then((product_values) => { + for (const product of product_values) { + this.render_product_assortment(product_dic[product.id], product); + } + }); + }, + render_product_assortment: function (product_info, product) { + $(product_info) + .find(".product_price") + .append( + $( + core.qweb.render( + "website_sale_product_assortment.product_availability", + { + message_unavailable: product.message_unavailable, + product_template_id: product.id, + } + ) + ).get(0) + ); + + $(product_info).find(".fa-shopping-cart").parent().addClass("disabled"); + }, + }); +}); diff --git a/website_sale_product_assortment/static/src/js/no_purchase_tour.js b/website_sale_product_assortment/static/src/js/no_purchase_tour.js new file mode 100644 index 0000000000..70ed6a31a0 --- /dev/null +++ b/website_sale_product_assortment/static/src/js/no_purchase_tour.js @@ -0,0 +1,36 @@ +/* Copyright 2021 Tecnativa - Carlos Roca + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("website_sale_product_assortment.tour_no_purchase", function (require) { + "use strict"; + + var tour = require("web_tour.tour"); + var base = require("web_editor.base"); + + var steps = [ + { + trigger: + ".o_wsale_product_information_text:has(.text-danger:has(.fa-exclamation-triangle)) a:contains('Test Product 1')", + }, + { + trigger: "a#add_to_cart.disabled", + extra_trigger: ".text-danger:has(.fa-exclamation-triangle)", + }, + { + trigger: "a[href='/shop']", + extra_trigger: "span[name='testing']", + }, + ]; + + tour.register( + "test_assortment_with_no_purchase", + { + url: "/shop", + test: true, + wait_for: base.ready(), + }, + steps + ); + return { + steps: steps, + }; +}); diff --git a/website_sale_product_assortment/static/src/js/no_restriction_tour.js b/website_sale_product_assortment/static/src/js/no_restriction_tour.js new file mode 100644 index 0000000000..cc1e384b64 --- /dev/null +++ b/website_sale_product_assortment/static/src/js/no_restriction_tour.js @@ -0,0 +1,34 @@ +/* Copyright 2021 Tecnativa - Carlos Roca + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("website_sale_product_assortment.tour_no_restriction", function (require) { + "use strict"; + + var tour = require("web_tour.tour"); + var base = require("web_editor.base"); + + var steps = [ + { + trigger: "a:contains('Test Product 1')", + }, + { + trigger: "a#add_to_cart", + }, + { + trigger: "a:contains('Test Product 1')", + extra_trigger: "input.js_quantity[value='1']", + }, + ]; + + tour.register( + "test_assortment_with_no_restriction", + { + url: "/shop", + test: true, + wait_for: base.ready(), + }, + steps + ); + return { + steps: steps, + }; +}); diff --git a/website_sale_product_assortment/static/src/js/no_show_tour.js b/website_sale_product_assortment/static/src/js/no_show_tour.js new file mode 100644 index 0000000000..6dfe8295ae --- /dev/null +++ b/website_sale_product_assortment/static/src/js/no_show_tour.js @@ -0,0 +1,29 @@ +/* Copyright 2021 Tecnativa - Carlos Roca + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("website_sale_product_assortment.tour_no_show", function (require) { + "use strict"; + + var tour = require("web_tour.tour"); + var base = require("web_editor.base"); + + var steps = [ + { + trigger: "a[href='/shop']", + extra_trigger: + ".o_wsale_product_grid_wrapper:not(:has(a:contains('Test Product 1')))", + }, + ]; + + tour.register( + "test_assortment_with_no_show", + { + url: "/shop", + test: true, + wait_for: base.ready(), + }, + steps + ); + return { + steps: steps, + }; +}); diff --git a/website_sale_product_assortment/static/src/js/variant_mixin.js b/website_sale_product_assortment/static/src/js/variant_mixin.js new file mode 100644 index 0000000000..c9e4c91976 --- /dev/null +++ b/website_sale_product_assortment/static/src/js/variant_mixin.js @@ -0,0 +1,70 @@ +// Copyright 2021 Tecnativa - Carlos Roca +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +odoo.define("website_sale_product_assortment.VariantMixin", function (require) { + "use strict"; + + var VariantMixin = require("sale.VariantMixin"); + var publicWidget = require("web.public.widget"); + var ajax = require("web.ajax"); + var core = require("web.core"); + var QWeb = core.qweb; + var xml_load = ajax.loadXML( + "/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml", + QWeb + ); + + VariantMixin._onChangeCombinationAssortment = function (ev, $parent, combination) { + let product_id = 0; + if ($parent.find("input.product_id:checked").length) { + product_id = $parent.find("input.product_id:checked").val(); + } else { + product_id = $parent.find(".product_id").val(); + } + const isMainProduct = + combination.product_id && + ($parent.is(".js_main_product") || $parent.is(".main_product")) && + combination.product_id === parseInt(product_id); + if (!this.isWebsite || !isMainProduct) { + return; + } + $(".oe_website_sale") + .find("#message_unavailable_" + combination.product_template_id) + .remove(); + $("#product_full_assortment_description").remove(); + if (!combination.product_avoid_purchase) { + return; + } + $parent.find("#add_to_cart").addClass("disabled"); + $parent.find("#buy_now").addClass("disabled"); + xml_load.then(function () { + $(".oe_website_sale") + .find("#product_option_block") + .prepend( + QWeb.render( + "website_sale_product_assortment.product_availability", + combination + ) + ); + if (combination.assortment_information) { + $("#product_detail").after( + "
" + + combination.assortment_information + + "
" + ); + } + }); + }; + + publicWidget.registry.WebsiteSale.include({ + /** + * Adds the stock checking to the regular _onChangeCombination method + * @override + */ + _onChangeCombination: function () { + this._super.apply(this, arguments); + VariantMixin._onChangeCombinationAssortment.apply(this, arguments); + }, + }); + + return VariantMixin; +}); diff --git a/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml b/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml new file mode 100644 index 0000000000..53e950d6fe --- /dev/null +++ b/website_sale_product_assortment/static/src/xml/website_sale_product_assortment.xml @@ -0,0 +1,20 @@ + + + + +
+ + +
+
+
diff --git a/website_sale_product_assortment/templates/assets.xml b/website_sale_product_assortment/templates/assets.xml new file mode 100644 index 0000000000..5419405530 --- /dev/null +++ b/website_sale_product_assortment/templates/assets.xml @@ -0,0 +1,33 @@ + + + +