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..de81a7ab5b --- /dev/null +++ b/website_sale_product_assortment/README.rst @@ -0,0 +1,109 @@ +============================ +eCommerce product assortment +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:022a205f459622d9a048aa1958adcec5c5025bb54a3b6a8397e02e90474956e7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/15.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-15-0/e-commerce-15-0-website_sale_product_assortment + :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/e-commerce&target_branch=15.0 + :alt: Try me on Runboat + +|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 to smash 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 + * Stefan Ungureanu + +* `Ooops `_: + * Ashish Hirpara (https://ashish-hirpara.com) + +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..0fe6536d2a --- /dev/null +++ b/website_sale_product_assortment/__manifest__.py @@ -0,0 +1,30 @@ +# 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": "16.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": [ + "views/ir_filters_views.xml", + ], + "assets": { + "web.assets_frontend": [ + "website_sale_product_assortment/static/src/js/variant_mixin.js", + "website_sale_product_assortment/static/src/js/assortment_list_preview.js", + "website_sale_product_assortment/static/src/xml/product_availability.xml", + ], + "web.assets_tests": [ + "website_sale_product_assortment/static/src/tests/tours/no_purchase_tour.js", + "website_sale_product_assortment/static/src/tests/tours/no_restriction_tour.js", + "website_sale_product_assortment/static/src/tests/tours/no_show_tour.js", + ], + }, +} 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..20252bafb8 --- /dev/null +++ b/website_sale_product_assortment/controllers/website_sale.py @@ -0,0 +1,79 @@ +# 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.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_options( + self, + category=None, + attrib_values=None, + pricelist=None, + min_price=0.0, + max_price=0.0, + conversion_rate=1, + **post + ): + """Overriding _get_search_options method to avoid show product templates that + has all their variants not allowed to be shown.""" + res = super()._get_search_options( + category=category, + attrib_values=attrib_values, + pricelist=pricelist, + min_price=min_price, + max_price=max_price, + conversion_rate=conversion_rate, + **post + ) + allowed_product_ids, assortment_restriction = self._get_products_allowed() + if assortment_restriction: + res["allowed_product_domain"] = [ + ("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..3b84703cdb --- /dev/null +++ b/website_sale_product_assortment/i18n/es.po @@ -0,0 +1,140 @@ +# 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: 2023-07-05 14:08+0000\n" +"Last-Translator: Ivorra78 \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: Weblate 4.17\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__apply_on_public_user +msgid "Apply 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" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__website_ids +msgid "Websites" +msgstr "Páginas web" + +#~ msgid "Aplly On Public User" +#~ msgstr "Aplicar en usuario público" diff --git a/website_sale_product_assortment/i18n/it.po b/website_sale_product_assortment/i18n/it.po new file mode 100644 index 0000000000..7ddd7a1f02 --- /dev/null +++ b/website_sale_product_assortment/i18n/it.po @@ -0,0 +1,145 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_product_assortment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-06-13 16:10+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\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" +" Le opzioni disponibili sono:\n" +"\n" +" \t- Non applicare restrizioni: Mostra tutti i prodotti disponibili " +"alla vendita sul sito web.\n" +"\n" +" \t- Non mostrare prodotti non disponibili: Mostra solo i prodotti " +"disponibili alla vendita\n" +" sul sito web.\n" +"\n" +" \t- Non vendere prodotti non disponibili: Mostra tutti i prodotti, " +"ma non permettere l'acquisto\n" +" sul sito 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 "Tutti i prodotti" + +#. 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 "Applica ad Utente Pubblico (Public User)" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__assortment_information +msgid "Assortment Information" +msgstr "Informazione assortimento" + +#. 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 "Disponibilità sul sito 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 "Non vendere prodotti non disponibili" + +#. 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 "Non mostrare prodotti non disponibili" + +#. 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 "Non applicare restrizioni" + +#. module: website_sale_product_assortment +#: model:ir.model,name:website_sale_product_assortment.model_ir_filters +msgid "Filters" +msgstr "Filtri" + +#. 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 "" +"Messaggio visualizzato quando un prodotto non è disponibile e l'opzione\n" +" 'Non vendere prodotti non disponibili' è selezionata.\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 "Messaggio se non disponibile" + +#. module: website_sale_product_assortment +#: code:addons/website_sale_product_assortment/controllers/variant.py:0 +#, python-format +msgid "Not available" +msgstr "Non disponibile" + +#. module: website_sale_product_assortment +#: model:ir.model,name:website_sale_product_assortment.model_product_template +msgid "Product Template" +msgstr "Modello prodotto" + +#. 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 "Attenzione" + +#. module: website_sale_product_assortment +#: model_terms:ir.ui.view,arch_db:website_sale_product_assortment.product_assortment_view_form +msgid "Website Availability" +msgstr "Disponibilità sito web" + +#. module: website_sale_product_assortment +#: model:ir.model.fields,field_description:website_sale_product_assortment.field_ir_filters__website_ids +msgid "Websites" +msgstr "Siti web" + +#~ msgid "Display Name" +#~ msgstr "Nome da visualizzare" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" 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..6ec9de34fa --- /dev/null +++ b/website_sale_product_assortment/i18n/website_sale_product_assortment.pot @@ -0,0 +1,113 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_product_assortment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.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 +#: 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..bc57f83004 --- /dev/null +++ b/website_sale_product_assortment/models/ir_filters.py @@ -0,0 +1,59 @@ +# 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): + res = 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 + return res 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..3f96895afd --- /dev/null +++ b/website_sale_product_assortment/models/product_template.py @@ -0,0 +1,82 @@ +# 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 + + @api.model + def _search_get_detail(self, website, order, options): + res = super()._search_get_detail(website, order, options) + domain = res["base_domain"] + allowed_product_domain = options.get("allowed_product_domain") + if allowed_product_domain: + domain.append(allowed_product_domain) + res["base_domain"] = domain + 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..41a550b906 --- /dev/null +++ b/website_sale_product_assortment/readme/CONTRIBUTORS.rst @@ -0,0 +1,8 @@ +* `Tecnativa `_: + + * Carlos Roca + * Pedro M. Baeza + * Stefan Ungureanu + +* `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..41b395ac9a --- /dev/null +++ b/website_sale_product_assortment/static/description/index.html @@ -0,0 +1,456 @@ + + + + + + +eCommerce product assortment + + + +
+

eCommerce product assortment

+ + +

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

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

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.

+

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..e8d9a3b9f2 --- /dev/null +++ b/website_sale_product_assortment/static/src/js/assortment_list_preview.js @@ -0,0 +1,51 @@ +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", + + 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 span").getAttribute("data-oe-id")] = + 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/variant_mixin.js b/website_sale_product_assortment/static/src/js/variant_mixin.js new file mode 100644 index 0000000000..5a4d24b0eb --- /dev/null +++ b/website_sale_product_assortment/static/src/js/variant_mixin.js @@ -0,0 +1,65 @@ +// 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 core = require("web.core"); + var QWeb = core.qweb; + + require("website_sale.website_sale"); + + 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, 10); + 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"); + $(".oe_website_sale") + .find("#add_to_cart_wrap") + .after( + 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/tests/tours/no_purchase_tour.js b/website_sale_product_assortment/static/src/tests/tours/no_purchase_tour.js new file mode 100644 index 0000000000..9365149e65 --- /dev/null +++ b/website_sale_product_assortment/static/src/tests/tours/no_purchase_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_purchase", function (require) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: + ".o_wsale_product_information: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, + }, + steps + ); + return { + steps: steps, + }; +}); diff --git a/website_sale_product_assortment/static/src/tests/tours/no_restriction_tour.js b/website_sale_product_assortment/static/src/tests/tours/no_restriction_tour.js new file mode 100644 index 0000000000..2ae639bfba --- /dev/null +++ b/website_sale_product_assortment/static/src/tests/tours/no_restriction_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_restriction", function (require) { + "use strict"; + + var tour = require("web_tour.tour"); + + var steps = [ + { + trigger: "a:contains('Test Product 1')", + }, + { + trigger: "a#add_to_cart", + }, + { + trigger: "a[href='/shop/cart']", + extra_trigger: "sup.my_cart_quantity:contains('1')", + }, + { + trigger: "a:contains('Test Product 1')", + extra_trigger: "input.js_quantity[value='1']", + }, + ]; + + tour.register( + "test_assortment_with_no_restriction", + { + url: "/shop", + test: true, + }, + steps + ); + return { + steps: steps, + }; +}); diff --git a/website_sale_product_assortment/static/src/tests/tours/no_show_tour.js b/website_sale_product_assortment/static/src/tests/tours/no_show_tour.js new file mode 100644 index 0000000000..3cba54c699 --- /dev/null +++ b/website_sale_product_assortment/static/src/tests/tours/no_show_tour.js @@ -0,0 +1,27 @@ +/* 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 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, + }, + steps + ); + return { + steps: steps, + }; +}); diff --git a/website_sale_product_assortment/static/src/xml/product_availability.xml b/website_sale_product_assortment/static/src/xml/product_availability.xml new file mode 100644 index 0000000000..c48694cd00 --- /dev/null +++ b/website_sale_product_assortment/static/src/xml/product_availability.xml @@ -0,0 +1,20 @@ + + + + +
+ + +
+
+
diff --git a/website_sale_product_assortment/templates/website_sale_views.xml b/website_sale_product_assortment/templates/website_sale_views.xml new file mode 100644 index 0000000000..0e1dfb8ad8 --- /dev/null +++ b/website_sale_product_assortment/templates/website_sale_views.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/website_sale_product_assortment/tests/__init__.py b/website_sale_product_assortment/tests/__init__.py new file mode 100644 index 0000000000..6dab214ac8 --- /dev/null +++ b/website_sale_product_assortment/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ui diff --git a/website_sale_product_assortment/tests/test_ui.py b/website_sale_product_assortment/tests/test_ui.py new file mode 100644 index 0000000000..9e38ae782d --- /dev/null +++ b/website_sale_product_assortment/tests/test_ui.py @@ -0,0 +1,63 @@ +# Copyright 2021 Tecnativa - Carlos Roca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests.common import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestUI(HttpCase): + def setUp(self): + super().setUp() + self.product = self.env["product.template"].create( + { + "name": "Test Product 1", + "is_published": True, + "website_sequence": 1, + "type": "consu", + } + ) + + def test_01_ui_no_restriction(self): + self.env["ir.filters"].create( + { + "name": "Test Assortment", + "model_id": "product.product", + "is_assortment": True, + "domain": [("id", "!=", self.product.product_variant_id.id)], + "partner_domain": "[('id', '=', %s)]" + % self.env.ref("base.partner_admin").id, + } + ) + self.start_tour("/shop", "test_assortment_with_no_restriction", login="admin") + + def test_02_ui_no_show(self): + self.env["ir.filters"].create( + { + "name": "Test Assortment", + "model_id": "product.product", + "is_assortment": True, + "domain": [("id", "!=", self.product.product_variant_id.id)], + "partner_domain": "[('id', '=', %s)]" + % self.env.ref("base.partner_admin").id, + "website_availability": "no_show", + } + ) + self.start_tour("/shop", "test_assortment_with_no_show", login="admin") + + def test_03_ui_no_purchase(self): + self.env["ir.filters"].create( + { + "name": "Test Assortment", + "model_id": "product.product", + "is_assortment": True, + "domain": [("id", "!=", self.product.product_variant_id.id)], + "partner_domain": "[('id', '=', %s)]" + % self.env.ref("base.partner_admin").id, + "website_availability": "no_purchase", + "message_unavailable": "Can't purchase", + "assortment_information": """ + This product is not available for purchase + + """, + } + ) + self.start_tour("/shop", "test_assortment_with_no_purchase", login="admin") diff --git a/website_sale_product_assortment/views/ir_filters_views.xml b/website_sale_product_assortment/views/ir_filters_views.xml new file mode 100644 index 0000000000..51142aa750 --- /dev/null +++ b/website_sale_product_assortment/views/ir_filters_views.xml @@ -0,0 +1,31 @@ + + + + + ir.filters + + + + + + + + + + + + + + + +