diff --git a/product_multi_image/README.rst b/product_multi_image/README.rst new file mode 100644 index 00000000000..96fcc017990 --- /dev/null +++ b/product_multi_image/README.rst @@ -0,0 +1,161 @@ +=========================== +Multiple Images in Products +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6dc98dd3fc0494ff6c8f8250d65117a27e8f2cc176db276ff0573ad890116609 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/18.0/product_multi_image + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_multi_image + :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/product-attribute&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module implements the possibility to have multiple images for a +product template, a.k.a. an image gallery. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +To install this module, you need to: + +- Install ``base_multi_image`` from + `OCA/server-tools `__. + +Configuration +============= + +You can manage your images at Product template level: + +1. Go to *Sales > Products > Products* and choose a product template. +2. Go to the *Images* tab. +3. Add a new image or edit the existing ones. +4. You can select for which variants you want to make available the + image. Keep it empty for making visible in all. +5. Refresh the page. +6. The first image in the collection is the main image for the product + template. + +Going to product variants form, you can manage also your images, but +take into account this behaviour: + +1. Go to *Sales > Products > Product Variants* and choose a product + variant. +2. If you add an image here, the image is actually added to the product + template, and restricted to this variant. +3. When editing an existing image, the image is changed generally for + all the variants where is enabled, not only for this variant. +4. When removing an image from this form, if the image is only in this + variant, the image is removed. Otherwise, the image gets restricted + to the rest of the variants where is available. + +|Try me on Runbot| + +.. |Try me on Runbot| image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :target: https://runbot.odoo-community.org/runbot/135/9.0 + +Known issues / Roadmap +====================== + +- When you change the image on the product variant, the preview image of + the *Images* tab doesn't get refreshed until you refresh the browser, + or if you go to its template, but the image has been actually saved! +- The field "Available in these variants" appears when opening the image + from the product variant. +- Add logic for handling to add images with the same name that another + variant of the same template, renaming the new image to a unique name. +- Add logic for handling to add the same image in several variants to a + already in another variant for not duplicating bytes. +- Provide proper migration scripts from module product_images from 7.0. +- Migrate to v8 api when https://github.com/odoo/odoo/issues/10799 gets + fixed. +- If you try to sort images before saving the product variant or + template, you will get an error similar to + ``DataError: invalid input syntax for integer: "one2many_v_id_62"``. + This bug has not been fixed yet, but a workaround is to save and edit + again to sort images. + +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 +------- + +* Antiun Ingeniería +* Tecnativa +* LasLabs + +Contributors +------------ + +- Pedro M. Baeza +- Rafael Blasco +- Jairo Llopis +- Dave Lasley +- Shepilov Vladislav +- Marc Poch Mallandrich +- Hai Lang +- `Greenice `__: + + - Fernando La Chica + +Other credits +------------- + +- The migration of this module from 12.0 to 14.0 was financially + supported by Camptocamp. + +Original implementation +~~~~~~~~~~~~~~~~~~~~~~~ + +- This module is inspired in previous module *product_images* from + OpenLabs and Akretion. + +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/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_multi_image/__init__.py b/product_multi_image/__init__.py new file mode 100644 index 00000000000..4ca20d3d936 --- /dev/null +++ b/product_multi_image/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3). + +from . import models +from .hooks import post_init_hook, uninstall_hook diff --git a/product_multi_image/__manifest__.py b/product_multi_image/__manifest__.py new file mode 100644 index 00000000000..92e010df7c8 --- /dev/null +++ b/product_multi_image/__manifest__.py @@ -0,0 +1,25 @@ +# © 2014-2016 Pedro M. Baeza +# © 2015 Antiun Ingeniería S.L. - Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Multiple Images in Products", + "version": "18.0.1.0.0", + "author": "Antiun Ingeniería, Tecnativa, LasLabs, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/product-attribute", + "category": "Product", + "development_status": "Beta", + "summary": "Add multiple images for a product, a.k.a. an image gallery.", + "post_init_hook": "post_init_hook", + "uninstall_hook": "uninstall_hook", + "depends": [ + "base_multi_image", + "product", + ], + "data": [ + "views/image_view.xml", + "views/product_template_view.xml", + ], + "installable": True, +} diff --git a/product_multi_image/hooks.py b/product_multi_image/hooks.py new file mode 100644 index 00000000000..477502e9be4 --- /dev/null +++ b/product_multi_image/hooks.py @@ -0,0 +1,25 @@ +# © 2016 Antiun Ingeniería S.L. - Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging + +_logger = logging.getLogger(__name__) + + +try: + from odoo.addons.base_multi_image.hooks import ( + pre_init_hook_for_submodules, + uninstall_hook_for_submodules, + ) +except ImportError: + _logger.info("Cannot import base_multi_image hooks") + + +def post_init_hook(env): + pre_init_hook_for_submodules(env, "product.template", "image_1920") + pre_init_hook_for_submodules(env, "product.product", "image_variant_1920") + + +def uninstall_hook(env): + """Remove multi images for models that no longer use them.""" + uninstall_hook_for_submodules(env, "product.template", "image_1920") + uninstall_hook_for_submodules(env, "product.product", "image_variant_1920") diff --git a/product_multi_image/i18n/es.po b/product_multi_image/i18n/es.po new file mode 100644 index 00000000000..71e58a4d1d0 --- /dev/null +++ b/product_multi_image/i18n/es.po @@ -0,0 +1,100 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_multi_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-05-21 10:58+0000\n" +"PO-Revision-Date: 2023-10-28 20:20+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: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_base_multi_image_image +msgid " image model for multiple image functionality " +msgstr " modelo de imagen para la funcionalidad de imágenes múltiples " + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_form_view +msgid "(keep empty for being visible for all variants)" +msgstr "(dejar vacío para hacerlo visible para todas las variantes)" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "Visible in all variants" +msgstr "Visible en todas las variantes" + +#. module: product_multi_image +#: model:ir.model.fields,help:product_multi_image.field_base_multi_image_image__product_variant_ids +msgid "" +"If you leave it empty, all variants will show this image. Selecting one or " +"several of the available variants, you restrict the availability of the " +"image to those variants." +msgstr "" +"Si lo deja vacío, todas las variantes mostrarán esta imagen. Seleccionando " +"una o más de las variantes disponibles, restringirá la disponibilidad de la " +"imagen a esas variantes." + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:product_multi_image.product_variant_easy_edit_view +#: model_terms:ir.ui.view,arch_db:product_multi_image.view_product_template_form_img_inh +msgid "Images" +msgstr "Imágenes" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main +msgid "Main image" +msgstr "Imagen principal" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main_medium +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main_medium +msgid "Medium image" +msgstr "Imagen media" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_product_product +msgid "Product" +msgstr "Producto" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_product_template +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_base_multi_image_image__product_variant_count +msgid "Product Variant Count" +msgstr "Nº de variantes" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main_small +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main_small +msgid "Small image" +msgstr "Imagen pequeña" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "Visible in" +msgstr "Visible en" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_base_multi_image_image__product_variant_ids +msgid "Visible in these variants" +msgstr "Visible en estas variantes" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "variant(s)" +msgstr "variante(s)" diff --git a/product_multi_image/i18n/it.po b/product_multi_image/i18n/it.po new file mode 100644 index 00000000000..db54b2261fe --- /dev/null +++ b/product_multi_image/i18n/it.po @@ -0,0 +1,99 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_multi_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-01 00:50+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: product_multi_image +#: model:ir.model,name:product_multi_image.model_base_multi_image_image +msgid " image model for multiple image functionality " +msgstr " modello immagine per funzionalità immagine multipla " + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_form_view +msgid "(keep empty for being visible for all variants)" +msgstr "(lasciare vuoto per essere visibile per tutte le varianti)" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "Visible in all variants" +msgstr "Visibile in tutte le varianti" + +#. module: product_multi_image +#: model:ir.model.fields,help:product_multi_image.field_base_multi_image_image__product_variant_ids +msgid "" +"If you leave it empty, all variants will show this image. Selecting one or " +"several of the available variants, you restrict the availability of the " +"image to those variants." +msgstr "" +"Se lo si lascia vuoto, tutte le varianti visualizzeranno questa immagine. " +"Velezionandone una o alcune delle varianti disponibili, si restringe la " +"disponibilità dell'immagine a queste varianti." + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:product_multi_image.product_variant_easy_edit_view +#: model_terms:ir.ui.view,arch_db:product_multi_image.view_product_template_form_img_inh +msgid "Images" +msgstr "Immagini" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main +msgid "Main image" +msgstr "Immagine principale" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main_medium +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main_medium +msgid "Medium image" +msgstr "Immagine media" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_product_product +msgid "Product" +msgstr "Prodotto" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_product_template +msgid "Product Template" +msgstr "Modello prodotto" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_base_multi_image_image__product_variant_count +msgid "Product Variant Count" +msgstr "Coneggio varianti prodotto" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main_small +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main_small +msgid "Small image" +msgstr "Immagine piccola" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "Visible in" +msgstr "Visibile in" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_base_multi_image_image__product_variant_ids +msgid "Visible in these variants" +msgstr "Visibile in queste varianti" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "variant(s)" +msgstr "variante(i)" diff --git a/product_multi_image/i18n/product_multi_image.pot b/product_multi_image/i18n/product_multi_image.pot new file mode 100644 index 00000000000..43de9195e1d --- /dev/null +++ b/product_multi_image/i18n/product_multi_image.pot @@ -0,0 +1,93 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_multi_image +# +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: product_multi_image +#: model:ir.model,name:product_multi_image.model_base_multi_image_image +msgid " image model for multiple image functionality " +msgstr "" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_form_view +msgid "(keep empty for being visible for all variants)" +msgstr "" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "Visible in all variants" +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,help:product_multi_image.field_base_multi_image_image__product_variant_ids +msgid "" +"If you leave it empty, all variants will show this image. Selecting one or " +"several of the available variants, you restrict the availability of the " +"image to those variants." +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:product_multi_image.product_variant_easy_edit_view +#: model_terms:ir.ui.view,arch_db:product_multi_image.view_product_template_form_img_inh +msgid "Images" +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main +msgid "Main image" +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main_medium +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main_medium +msgid "Medium image" +msgstr "" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_product_product +msgid "Product" +msgstr "" + +#. module: product_multi_image +#: model:ir.model,name:product_multi_image.model_product_template +msgid "Product Template" +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_base_multi_image_image__product_variant_count +msgid "Product Variant Count" +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_product_product__image_main_small +#: model:ir.model.fields,field_description:product_multi_image.field_product_template__image_main_small +msgid "Small image" +msgstr "" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "Visible in" +msgstr "" + +#. module: product_multi_image +#: model:ir.model.fields,field_description:product_multi_image.field_base_multi_image_image__product_variant_ids +msgid "Visible in these variants" +msgstr "" + +#. module: product_multi_image +#: model_terms:ir.ui.view,arch_db:product_multi_image.image_kanban_view +msgid "variant(s)" +msgstr "" diff --git a/product_multi_image/images/db.png b/product_multi_image/images/db.png new file mode 100644 index 00000000000..2bfaab57ef4 Binary files /dev/null and b/product_multi_image/images/db.png differ diff --git a/product_multi_image/images/file.png b/product_multi_image/images/file.png new file mode 100644 index 00000000000..6efdafd82d6 Binary files /dev/null and b/product_multi_image/images/file.png differ diff --git a/product_multi_image/images/product.png b/product_multi_image/images/product.png new file mode 100644 index 00000000000..2c43bedc281 Binary files /dev/null and b/product_multi_image/images/product.png differ diff --git a/product_multi_image/images/url.png b/product_multi_image/images/url.png new file mode 100644 index 00000000000..00f0c3819ae Binary files /dev/null and b/product_multi_image/images/url.png differ diff --git a/product_multi_image/models/__init__.py b/product_multi_image/models/__init__.py new file mode 100644 index 00000000000..9d32c1f078a --- /dev/null +++ b/product_multi_image/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3). + +from . import image +from . import product_template +from . import product_product diff --git a/product_multi_image/models/image.py b/product_multi_image/models/image.py new file mode 100644 index 00000000000..ae95ea8b233 --- /dev/null +++ b/product_multi_image/models/image.py @@ -0,0 +1,22 @@ +# © 2016 Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3). + +from odoo import api, fields, models + + +class Image(models.Model): + _inherit = "base_multi_image.image" + + product_variant_ids = fields.Many2many( + comodel_name="product.product", + string="Visible in these variants", + help="If you leave it empty, all variants will show this image. " + "Selecting one or several of the available variants, you " + "restrict the availability of the image to those variants.", + ) + product_variant_count = fields.Integer(compute="_compute_product_variant_count") + + @api.depends("product_variant_ids") + def _compute_product_variant_count(self): + for image in self: + image.product_variant_count = len(image.product_variant_ids) diff --git a/product_multi_image/models/product_product.py b/product_multi_image/models/product_product.py new file mode 100644 index 00000000000..465e590a3da --- /dev/null +++ b/product_multi_image/models/product_product.py @@ -0,0 +1,113 @@ +# © 2016 Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3). + +from odoo import api, fields, models + + +class ProductProduct(models.Model): + _name = "product.product" + _inherit = [_name, "base_multi_image.owner"] + + # Make this field computed for getting only the available images + image_ids = fields.One2many( + comodel_name="base_multi_image.image", + compute="_compute_image_ids", + inverse="_inverse_image_ids", + ) + + @api.depends( + "product_tmpl_id", + "product_tmpl_id.image_ids", + "product_tmpl_id.image_ids.product_variant_ids", + ) + def _compute_image_ids(self): + for product in self: + images = product.product_tmpl_id.image_ids.filtered( + lambda x, p=product: ( + not x.product_variant_ids or p.id in x.product_variant_ids.ids + ) + ) + product.image_ids = [(6, 0, images.ids)] + + @api.depends( + "product_tmpl_id", + "product_tmpl_id.image_ids", + "product_tmpl_id.image_ids.product_variant_ids", + "product_tmpl_id.image_ids.image_1920", + ) + def _compute_image_1920(self): + for product in self: + # Priority: variant-specific images first, then generic template images + images = product.product_tmpl_id.image_ids.filtered( + lambda x, p=product: p.id in x.product_variant_ids.ids + ) + + # If no variant-specific images, use generic template images + if not images: + images = product.product_tmpl_id.image_ids.filtered( + lambda x, p=product: not x.product_variant_ids + ) + + if images: + gallery_image = images[0].with_context(bin_size=False).image_1920 + product.image_1920 = gallery_image + + # Update the stored field that POS uses + if product.image_variant_1920 != gallery_image: + product.image_variant_1920 = gallery_image + else: + # No gallery images - Odoo Core will fallback to template.image_1920 + product.image_1920 = False + # Clear stored field when no gallery images + if product.image_variant_1920: + product.image_variant_1920 = False + + def _inverse_image_ids(self): + for product in self: + # Remember the list of images that were before changes + previous_images = product.product_tmpl_id.image_ids.filtered( + lambda x, p=product: ( + not x.product_variant_ids or p.id in x.product_variant_ids.ids + ) + ) + for image in product.image_ids: + if isinstance(image.id, models.NewId): + # Image added + image.owner_id = product.product_tmpl_id.id + image.owner_model = "product.template" + image.product_variant_ids = [(6, 0, product.ids)] + image.create(image._convert_to_write(image._cache)) + else: + previous_images -= image + # Update existing records only if there are actual changes + # to avoid unnecessary write_date updates on parent product + vals = image._convert_to_write(image._cache) + if vals: + image.write(vals) + for image in previous_images: + # Images removed + if not image.product_variant_ids: + variants = product.product_tmpl_id.product_variant_ids + else: + variants = image.product_variant_ids + variants -= product + if not variants: + # Remove the image, as there's no variant that contains it + image.unlink() + else: + # Leave the images for the rest of the variants + image.product_variant_ids = [(6, 0, variants.ids)] + + def unlink(self): + obj = self.with_context(bypass_image_removal=True) + # Remove images that are linked only to the product variant + for product in self: + images2remove = product.image_ids.filtered( + lambda image, p=product: ( + p in image.product_variant_ids + and len(image.product_variant_ids) == 1 + ) + ) + images2remove.unlink() + # We need to pass context to super so this syntax is valid + return super(ProductProduct, obj).unlink() diff --git a/product_multi_image/models/product_template.py b/product_multi_image/models/product_template.py new file mode 100644 index 00000000000..dc3f45fbd70 --- /dev/null +++ b/product_multi_image/models/product_template.py @@ -0,0 +1,67 @@ +# © 2014-2016 Pedro M. Baeza +# © 2015 Antiun Ingeniería S.L. - Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _name = "product.template" + _inherit = [_name, "base_multi_image.owner"] + + # image, image_medium, image_small fields are not available since 13.0 + + image_1920 = fields.Binary( + compute="_compute_image_1920", + inverse="_inverse_image_1920", + store=True, + ) + + @api.depends( + "image_ids", + ) + def _compute_image_1920(self): + for product in self: + if not product.image_ids: + product.image_1920 = False + continue + else: + images = product.image_ids.filtered( + lambda x, p=product: ( + not x.product_variant_ids or p.product_variant_count == 1 + ) + ) + if images: + product.image_1920 = ( + images[0].with_context(bin_size=False).image_1920 + ) + else: + product.image_1920 = ( + product.image_ids[0].with_context(bin_size=False).image_1920 + ) + + def _inverse_image_1920(self): + for product in self: + images = product.image_ids.filtered( + lambda x, p=product: ( + not x.product_variant_ids or p.product_variant_count == 1 + ) + ) + img_new = product.with_context(bin_size=False).image_1920 + if images: + if images[0].attachment_image != img_new: + images[0].attachment_image = img_new + else: + if img_new: + product.image_ids = [ + ( + 0, + False, + { + "name": product.name, + "attachment_image": img_new, + "owner_id": product.id, + "owner_model": "product.template", + }, + ) + ] diff --git a/product_multi_image/pyproject.toml b/product_multi_image/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/product_multi_image/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_multi_image/readme/CONFIGURE.md b/product_multi_image/readme/CONFIGURE.md new file mode 100644 index 00000000000..14834f090fc --- /dev/null +++ b/product_multi_image/readme/CONFIGURE.md @@ -0,0 +1,25 @@ +You can manage your images at Product template level: + +1. Go to *Sales \> Products \> Products* and choose a product template. +2. Go to the *Images* tab. +3. Add a new image or edit the existing ones. +4. You can select for which variants you want to make available the + image. Keep it empty for making visible in all. +5. Refresh the page. +6. The first image in the collection is the main image for the product + template. + +Going to product variants form, you can manage also your images, but +take into account this behaviour: + +1. Go to *Sales \> Products \> Product Variants* and choose a product + variant. +2. If you add an image here, the image is actually added to the product + template, and restricted to this variant. +3. When editing an existing image, the image is changed generally for + all the variants where is enabled, not only for this variant. +4. When removing an image from this form, if the image is only in this + variant, the image is removed. Otherwise, the image gets restricted + to the rest of the variants where is available. + +[![Try me on Runbot](https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas)](https://runbot.odoo-community.org/runbot/135/9.0) diff --git a/product_multi_image/readme/CONTRIBUTORS.md b/product_multi_image/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..6660be99e5a --- /dev/null +++ b/product_multi_image/readme/CONTRIBUTORS.md @@ -0,0 +1,9 @@ +- Pedro M. Baeza \<\> +- Rafael Blasco \<\> +- Jairo Llopis \<\> +- Dave Lasley \<\> +- Shepilov Vladislav \<\> +- Marc Poch Mallandrich \<\> +- Hai Lang \<\> +- [Greenice](https://www.greenice.com): + - Fernando La Chica \<\> diff --git a/product_multi_image/readme/CREDITS.md b/product_multi_image/readme/CREDITS.md new file mode 100644 index 00000000000..a89baf94e1e --- /dev/null +++ b/product_multi_image/readme/CREDITS.md @@ -0,0 +1,7 @@ +- The migration of this module from 12.0 to 14.0 was financially + supported by Camptocamp. + +## Original implementation + +- This module is inspired in previous module *product_images* from + OpenLabs and Akretion. diff --git a/product_multi_image/readme/DESCRIPTION.md b/product_multi_image/readme/DESCRIPTION.md new file mode 100644 index 00000000000..a48829875e5 --- /dev/null +++ b/product_multi_image/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module implements the possibility to have multiple images for a +product template, a.k.a. an image gallery. diff --git a/product_multi_image/readme/INSTALL.md b/product_multi_image/readme/INSTALL.md new file mode 100644 index 00000000000..57f6f95e79d --- /dev/null +++ b/product_multi_image/readme/INSTALL.md @@ -0,0 +1,4 @@ +To install this module, you need to: + +- Install `base_multi_image` from + [OCA/server-tools](https://github.com/OCA/server-tools). diff --git a/product_multi_image/readme/ROADMAP.md b/product_multi_image/readme/ROADMAP.md new file mode 100644 index 00000000000..2a8eaf57aa0 --- /dev/null +++ b/product_multi_image/readme/ROADMAP.md @@ -0,0 +1,17 @@ +- When you change the image on the product variant, the preview image of + the *Images* tab doesn't get refreshed until you refresh the browser, + or if you go to its template, but the image has been actually saved! +- The field "Available in these variants" appears when opening the image + from the product variant. +- Add logic for handling to add images with the same name that another + variant of the same template, renaming the new image to a unique name. +- Add logic for handling to add the same image in several variants to a + already in another variant for not duplicating bytes. +- Provide proper migration scripts from module product_images from 7.0. +- Migrate to v8 api when + gets fixed. +- If you try to sort images before saving the product variant or + template, you will get an error similar to + `DataError: invalid input syntax for integer: "one2many_v_id_62"`. + This bug has not been fixed yet, but a workaround is to save and edit + again to sort images. diff --git a/product_multi_image/static/description/icon.png b/product_multi_image/static/description/icon.png new file mode 100644 index 00000000000..b37cfe46958 Binary files /dev/null and b/product_multi_image/static/description/icon.png differ diff --git a/product_multi_image/static/description/icon.svg b/product_multi_image/static/description/icon.svg new file mode 100644 index 00000000000..7d0b12cd867 --- /dev/null +++ b/product_multi_image/static/description/icon.svg @@ -0,0 +1,464 @@ + + + Drawings Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + STUFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Drawings Icon + 2012-01-29T15:13:42 + Icon for Drawings/Pictures folder. + https://openclipart.org/detail/167547/drawings-icon-by-andreibranescu + + + andreibranescu + + + + + Inkscape + drawings + icon + pictures + + + + + + + + + + + diff --git a/product_multi_image/static/description/index.html b/product_multi_image/static/description/index.html new file mode 100644 index 00000000000..e65f1201b36 --- /dev/null +++ b/product_multi_image/static/description/index.html @@ -0,0 +1,512 @@ + + + + + +Multiple Images in Products + + + +
+

Multiple Images in Products

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

This module implements the possibility to have multiple images for a +product template, a.k.a. an image gallery.

+

Table of contents

+ +
+

Installation

+

To install this module, you need to:

+ +
+
+

Configuration

+

You can manage your images at Product template level:

+
    +
  1. Go to Sales > Products > Products and choose a product template.
  2. +
  3. Go to the Images tab.
  4. +
  5. Add a new image or edit the existing ones.
  6. +
  7. You can select for which variants you want to make available the +image. Keep it empty for making visible in all.
  8. +
  9. Refresh the page.
  10. +
  11. The first image in the collection is the main image for the product +template.
  12. +
+

Going to product variants form, you can manage also your images, but +take into account this behaviour:

+
    +
  1. Go to Sales > Products > Product Variants and choose a product +variant.
  2. +
  3. If you add an image here, the image is actually added to the product +template, and restricted to this variant.
  4. +
  5. When editing an existing image, the image is changed generally for +all the variants where is enabled, not only for this variant.
  6. +
  7. When removing an image from this form, if the image is only in this +variant, the image is removed. Otherwise, the image gets restricted +to the rest of the variants where is available.
  8. +
+

Try me on Runbot

+
+
+

Known issues / Roadmap

+
    +
  • When you change the image on the product variant, the preview image of +the Images tab doesn’t get refreshed until you refresh the browser, +or if you go to its template, but the image has been actually saved!
  • +
  • The field “Available in these variants” appears when opening the image +from the product variant.
  • +
  • Add logic for handling to add images with the same name that another +variant of the same template, renaming the new image to a unique name.
  • +
  • Add logic for handling to add the same image in several variants to a +already in another variant for not duplicating bytes.
  • +
  • Provide proper migration scripts from module product_images from 7.0.
  • +
  • Migrate to v8 api when https://github.com/odoo/odoo/issues/10799 gets +fixed.
  • +
  • If you try to sort images before saving the product variant or +template, you will get an error similar to +DataError: invalid input syntax for integer: "one2many_v_id_62". +This bug has not been fixed yet, but a workaround is to save and edit +again to sort images.
  • +
+
+
+

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

+
    +
  • Antiun Ingeniería
  • +
  • Tecnativa
  • +
  • LasLabs
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+
    +
  • The migration of this module from 12.0 to 14.0 was financially +supported by Camptocamp.
  • +
+
+

Original implementation

+
    +
  • This module is inspired in previous module product_images from +OpenLabs and Akretion.
  • +
+
+
+
+

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/product-attribute project on GitHub.

+

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

+
+
+
+ + diff --git a/product_multi_image/tests/__init__.py b/product_multi_image/tests/__init__.py new file mode 100644 index 00000000000..d4608f19bed --- /dev/null +++ b/product_multi_image/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3). + +from . import test_product_multi_image diff --git a/product_multi_image/tests/test_product_multi_image.py b/product_multi_image/tests/test_product_multi_image.py new file mode 100644 index 00000000000..bf519048e3b --- /dev/null +++ b/product_multi_image/tests/test_product_multi_image.py @@ -0,0 +1,223 @@ +# © 2016 Pedro M. Baeza +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl-3). + +from odoo.tests import common + +from .. import hooks + + +class TestProductMultiImage(common.TransactionCase): + def setUp(self): + super().setUp() + self.transparent_image = ( # 1x1 Transparent GIF + b"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" + ) + self.grey_image = ( # 1x1 Grey GIF + b"R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw ==" + ) + self.black_image = ( # 1x1 Black GIF + b"R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=" + ) + self.attribute = self.env["product.attribute"].create( + { + "name": "Test attribute", + } + ) + self.value_1 = self.env["product.attribute.value"].create( + { + "name": "Test value 1", + "attribute_id": self.attribute.id, + } + ) + self.value_2 = self.env["product.attribute.value"].create( + { + "name": "Test value 2", + "attribute_id": self.attribute.id, + } + ) + self.product_template = self.env["product.template"].create( + { + "name": "Test product", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.attribute.id, + "value_ids": [(6, 0, (self.value_1 + self.value_2).ids)], + }, + ) + ], + "image_ids": [ + ( + 0, + 0, + { + "name": "Image 1", + "attachment_image": self.transparent_image, + "owner_model": "product.template", + }, + ), + ( + 0, + 0, + { + "name": "Image 2", + "attachment_image": self.black_image, + "owner_model": "product.template", + }, + ), + ], + } + ) + self.product_1 = self.product_template.product_variant_ids[0] + self.product_2 = self.product_template.product_variant_ids[1] + + def test_all_images(self): + self.assertEqual(len(self.product_template.image_ids), 2) + self.assertEqual(len(self.product_1.image_ids), 2) + self.assertEqual(len(self.product_2.image_ids), 2) + + def test_restrict_one_image(self): + self.product_template.image_ids[0].product_variant_ids = [ + (6, 0, self.product_1.ids) + ] + self.assertEqual(len(self.product_1.image_ids), 2) + self.assertEqual(len(self.product_2.image_ids), 1) + self.assertEqual(self.product_1.image_1920, self.transparent_image) + self.assertEqual(self.product_2.image_1920, self.black_image) + + def test_add_image_variant(self): + self.product_1.image_ids = [ + ( + 0, + 0, + {"attachment_image": self.grey_image}, + ) + ] + self.product_template.invalidate_recordset() + self.assertEqual(len(self.product_template.image_ids), 3) + self.assertEqual( + self.product_template.image_ids[-1].product_variant_ids, self.product_1 + ) + + def test_remove_image_variant(self): + self.product_1.image_ids = [(3, self.product_1.image_ids[0].id)] + self.product_template.invalidate_recordset() + self.assertEqual(len(self.product_template.image_ids), 2) + self.assertEqual( + self.product_template.image_ids[0].product_variant_ids, self.product_2 + ) + + def test_remove_image_all_variants(self): + self.product_1.image_ids = [(3, self.product_1.image_ids[0].id)] + self.product_2.image_ids = [(3, self.product_2.image_ids[0].id)] + self.product_template.invalidate_recordset() + self.assertEqual(len(self.product_template.image_ids), 1) + + def test_edit_image_variant(self): + text = "Test name changed" + self.product_1.image_ids[0].name = text + self.product_template.invalidate_recordset() + self.assertEqual(self.product_template.image_ids[0].name, text) + + def test_create_variant_afterwards(self): + """Create a template, assign an image, and then create the variant. + Check that the images are not lost. + """ + template = self.env["product.template"].create( + { + "name": "Test 2", + "image_ids": [ + ( + 0, + 0, + { + "name": "Image 1", + "attachment_image": self.transparent_image, + "owner_model": "product.template", + }, + ) + ], + } + ) + self.assertEqual( + len(template.image_ids), + 1, + "Product template did not start with singleton image_ids.", + ) + template.write( + { + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.attribute.id, + "value_ids": [(6, 0, (self.value_1 + self.value_2).ids)], + }, + ) + ], + } + ) + self.assertEqual( + len(template.image_ids), + 1, + "Product template did not retain the singleton image_ids.", + ) + for variant in template.product_variant_ids: + self.assertEqual( + len(variant.image_ids), + 1, + "Product variant did not receive the image_ids." + f" Got {variant.image_ids}", + ) + + def test_remove_variant_with_image(self): + self.product_template.image_ids[0].product_variant_ids = [ + (6, 0, self.product_1.ids) + ] + self.product_1.unlink() + self.assertEqual(len(self.product_template.image_ids), 1) + + def test_image_product_variant_count(self): + """It should provide a total of variants related to image""" + image = self.product_1.image_ids[0] + image.product_variant_ids = [(6, 0, self.product_1.ids)] + self.assertEqual( + image.product_variant_count, + 1, + ) + + def test_pre_init_hook_product(self): + """It should populate the ``image_ids`` on existing product""" + product = self.env.ref("product.product_product_3") + self.assertEqual( + len(product.image_ids), + 1, + ) + + def test_pre_init_hook_template(self): + """It should populate the ``image_ids`` on existing template""" + product = self.env.ref("product.product_product_3_product_template") + self.assertEqual( + len(product.image_ids), + 1, + ) + + def test_uninstall_hook_product(self): + """It should remove ``image_ids`` associated with products""" + hooks.uninstall_hook(self.env) + images = self.env["base_multi_image.image"].search( + [("owner_model", "=", "product.product")], + ) + self.assertFalse(len(images)) + + def test_uninstall_hook_teplate(self): + """It should remove ``image_ids`` associated with templates""" + hooks.uninstall_hook(self.env) + images = self.env["base_multi_image.image"].search( + [("owner_model", "=", "product.template")], + ) + self.assertFalse(len(images)) diff --git a/product_multi_image/views/image_view.xml b/product_multi_image/views/image_view.xml new file mode 100644 index 00000000000..7738f457973 --- /dev/null +++ b/product_multi_image/views/image_view.xml @@ -0,0 +1,48 @@ + + + + + Multi image form + base_multi_image.image + + primary + + + + + + + + + Product multi image kanban + base_multi_image.image + + primary + + + + + + + + + + Visible in all variants + + + Visible in variant(s) + + + + + + + diff --git a/product_multi_image/views/product_template_view.xml b/product_multi_image/views/product_template_view.xml new file mode 100644 index 00000000000..851d4748c39 --- /dev/null +++ b/product_multi_image/views/product_template_view.xml @@ -0,0 +1,51 @@ + + + + + Add multi images + product.template + + + + + + + + + + + + product_multi_image.product_variant_easy_edit_view + product.product + + + + + + + + + +