diff --git a/base_multi_image/README.rst b/base_multi_image/README.rst new file mode 100644 index 000000000..35934e6a1 --- /dev/null +++ b/base_multi_image/README.rst @@ -0,0 +1,109 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==================== +Multiple Images Base +==================== + +This module extends the functionality of any model to support multiple images +(a gallery) attached to it and allow you to manage them. + +Installation +============ + +This module adds abstract models to work on. Its sole purpose is to serve as +base for other modules that implement galleries, so if you install this one +manually you will notice no change. You should install any other module based +on this one and this will get installed automatically. + +Usage +===== + +To manage all stored images, you need to: + +* Go to *Settings > Configuration > Multi images*. + +... but you probably prefer to manage them from the forms supplied by +submodules that inherit this behavior. + +Development +=========== + +To develop a module based on this one: + +* See module ``product_multi_image`` as an example. + +* You have to inherit model ``base_multi_image.owner`` to the model that needs + the gallery:: + + class MyOwner(models.Model): + _name = "my.model.name" + _inherit = ["my.model.name", "base_multi_image.owner"] + + # If you need this, you will need ``post_init_hook_for_submodules`` + old_image_field = fields.Binary(related="image_main", store=False) + +* Somewhere in the owner view, add:: + + + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/149/8.0 + +Known issues / Roadmap +====================== + +* *OS file* storage mode for images is meant to provide a path where Odoo has + read access and the image is already found, **not for making the module store + images there**. It would be nice to add that feature though. + +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 +`_. + +Credits +======= + +Original implementation +----------------------- +This module is inspired in previous module *product_images* from OpenLabs +and Akretion. + +Contributors +------------ + +* Pedro M. Baeza +* Rafael Blasco +* Jairo Llopis + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/base_multi_image/__init__.py b/base_multi_image/__init__.py new file mode 100644 index 000000000..ef1af5949 --- /dev/null +++ b/base_multi_image/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2014 Serv. Tecnol. Avanzados (http://www.serviciosbaeza.com) +# 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 . import models diff --git a/base_multi_image/__openerp__.py b/base_multi_image/__openerp__.py new file mode 100644 index 000000000..e7fefd8e8 --- /dev/null +++ b/base_multi_image/__openerp__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# © 2014 Serv. Tecnol. Avanzados (http://www.serviciosbaeza.com) +# 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 base", + "summary": "Allow multiple images for database objects", + "version": "8.0.1.0.0", + "author": "Serv. Tecnol. Avanzados - Pedro M. Baeza, " + "Antiun Ingeniería, S.L., " + "Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "http://www.antiun.com", + "category": "Tools", + "depends": ['base'], + 'installable': True, + "data": [ + "security/ir.model.access.csv", + "views/image_view.xml", + ], + "images": [ + "images/form.png", + "images/kanban.png", + ], +} diff --git a/base_multi_image/hooks.py b/base_multi_image/hooks.py new file mode 100644 index 000000000..8d11fefe6 --- /dev/null +++ b/base_multi_image/hooks.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# © 2016 Antiun Ingeniería S.L. - Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import SUPERUSER_ID +import logging + +_logger = logging.getLogger(__name__) + + +def post_init_hook_for_submodules(cr, registry, model, field): + """Moves images from single to multi mode. + + Feel free to use this as a ``post_init_hook`` for submodules. + + :param str model: + Model name, like ``product.template``. + + :param str field: + Binary field that had the images in that :param:`model`, like + ``image``. + """ + with cr.savepoint(): + records = registry[model].search( + cr, + SUPERUSER_ID, + [(field, "!=", False)], + context=dict()) + + _logger.info("Moving images from %s to multi image mode.", model) + for r in registry[model].browse(cr, SUPERUSER_ID, records): + _logger.debug("Setting up multi image for record %d.", r.id) + r.image_main = r[field] diff --git a/base_multi_image/i18n/sv.po b/base_multi_image/i18n/sv.po new file mode 100644 index 000000000..ba538d6ff --- /dev/null +++ b/base_multi_image/i18n/sv.po @@ -0,0 +1,22 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * product_images_olbs +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 5.0.14\n" +"Report-Msgid-Bugs-To: support@openerp.com\n" +"POT-Creation-Date: 2010-11-22 10:19:32+0000\n" +"PO-Revision-Date: 2010-11-22 10:19:32+0000\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_images_olbs +#: model:ir.module.module,shortdesc:product_images_olbs.module_meta_information +msgid "Product Image Gallery" +msgstr "Product Image Gallery" + diff --git a/base_multi_image/images/form.png b/base_multi_image/images/form.png new file mode 100644 index 000000000..62f619b6a Binary files /dev/null and b/base_multi_image/images/form.png differ diff --git a/base_multi_image/images/kanban.png b/base_multi_image/images/kanban.png new file mode 100644 index 000000000..067cd4088 Binary files /dev/null and b/base_multi_image/images/kanban.png differ diff --git a/base_multi_image/models/__init__.py b/base_multi_image/models/__init__.py new file mode 100644 index 000000000..aa8f852f6 --- /dev/null +++ b/base_multi_image/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingeniería S.L. - Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import image, owner diff --git a/base_multi_image/models/image.py b/base_multi_image/models/image.py new file mode 100644 index 000000000..187cbbf90 --- /dev/null +++ b/base_multi_image/models/image.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# © 2014 Serv. Tecnol. Avanzados (http://www.serviciosbaeza.com) +# Pedro M. Baeza +# © 2015 Antiun Ingeniería S.L. - Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import base64 +import urllib +import os +import logging +from openerp import models, fields, api, exceptions, _ +from openerp import tools + +_logger = logging.getLogger(__name__) + + +class Image(models.Model): + _name = "base_multi_image.image" + _order = "sequence, owner_model, owner_id, id" + _sql_constraints = [ + ('uniq_name_owner', 'UNIQUE(owner_id, owner_model, name)', + _('A document can have only one image with the same name.')), + ] + + owner_id = fields.Integer( + "Owner", + required=True) + owner_model = fields.Char( + required=True) + storage = fields.Selection( + [('url', 'URL'), ('file', 'OS file'), ('db', 'Database')], + required=True) + name = fields.Char( + 'Image title', + translate=True) + filename = fields.Char() + extension = fields.Char( + 'File extension', + readonly=True) + file_db_store = fields.Binary( + 'Image stored in database', + filters='*.png,*.jpg,*.gif') + path = fields.Char( + "Image path", + help="Image path") + url = fields.Char( + 'Image remote URL') + image_main = fields.Binary( + "Full-sized image", + compute="_get_image") + image_medium = fields.Binary( + "Medium-sized image", + compute="_get_image_sizes", + help="Medium-sized image. It is automatically resized as a " + "128 x 128 px image, with aspect ratio preserved, only when the " + "image exceeds one of those sizes. Use this field in form views " + "or kanban views.") + image_small = fields.Binary( + "Small-sized image", + compute="_get_image_sizes", + help="Small-sized image. It is automatically resized as a 64 x 64 px " + "image, with aspect ratio preserved. Use this field anywhere a " + "small image is required.") + comments = fields.Text( + 'Comments', + translate=True) + sequence = fields.Integer( + default=10) + show_technical = fields.Boolean( + compute="_show_technical") + + @api.multi + @api.depends('storage', 'path', 'file_db_store', 'url') + def _get_image(self): + """Get image data from the right storage type.""" + for s in self: + s.image_main = getattr(s, "_get_image_from_%s" % s.storage)() + + @api.multi + @api.depends("owner_id", "owner_model") + def _show_technical(self): + """Know if you need to show the technical fields.""" + self.show_technical = all( + "default_owner_%s" % f not in self.env.context + for f in ("id", "model")) + + @api.multi + def _get_image_from_db(self): + return self.file_db_store + + @api.multi + def _get_image_from_file(self): + if self.path and os.path.exists(self.path): + try: + with open(self.path, 'rb') as f: + return base64.b64encode(f.read()) + except Exception as e: + _logger.error("Can not open the image %s, error : %s", + self.path, e, exc_info=True) + else: + _logger.error("The image %s doesn't exist ", self.path) + + return False + + @api.multi + def _get_image_from_url(self): + return self._get_image_from_url_cached(self.url) + + @api.model + @tools.ormcache(skiparg=1) + def _get_image_from_url_cached(self, url): + """Allow to download an image and cache it by its URL.""" + if url: + try: + (filename, header) = urllib.urlretrieve(url) + with open(filename, 'rb') as f: + return base64.b64encode(f.read()) + except: + _logger.error("URL %s cannot be fetched", url, + exc_info=True) + + return False + + @api.multi + @api.depends('image_main') + def _get_image_sizes(self): + for s in self: + try: + vals = tools.image_get_resized_images( + s.with_context(bin_size=False).image_main) + except: + vals = {"image_medium": False, + "image_small": False} + s.update(vals) + + @api.model + def _make_name_pretty(self, name): + return name.replace('_', ' ').capitalize() + + @api.onchange('url') + def _onchange_url(self): + if self.url: + filename = self.url.split('/')[-1] + self.name, self.extension = os.path.splitext(filename) + self.name = self._make_name_pretty(self.name) + + @api.onchange('path') + def _onchange_path(self): + if self.path: + self.name, self.extension = os.path.splitext(os.path.basename( + self.path)) + self.name = self._make_name_pretty(self.name) + + @api.onchange('filename') + def _onchange_filename(self): + if self.filename: + self.name, self.extension = os.path.splitext(self.filename) + self.name = self._make_name_pretty(self.name) + + @api.constrains('storage', 'url') + def _check_url(self): + if self.storage == 'url' and not self.url: + raise exceptions.ValidationError( + 'You must provide an URL for the image.') + + @api.constrains('storage', 'path') + def _check_path(self): + if self.storage == 'file' and not self.path: + raise exceptions.ValidationError( + 'You must provide a file path for the image.') + + @api.constrains('storage', 'file_db_store') + def _check_store(self): + if self.storage == 'db' and not self.file_db_store: + raise exceptions.ValidationError( + 'You must provide an attached file for the image.') diff --git a/base_multi_image/models/owner.py b/base_multi_image/models/owner.py new file mode 100644 index 000000000..1f85bc03c --- /dev/null +++ b/base_multi_image/models/owner.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# © 2014 Serv. Tecnol. Avanzados (http://www.serviciosbaeza.com) +# 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 openerp import _, api, fields, models, tools + + +class Owner(models.AbstractModel): + _name = "base_multi_image.owner" + + image_ids = fields.One2many( + comodel_name='base_multi_image.image', + inverse_name='owner_id', + string='Images', + domain=lambda self: [("owner_model", "=", self._name)], + copy=True) + image_main = fields.Binary( + string="Main image", + store=False, + compute="_get_multi_image", + inverse="_set_multi_image_main") + image_main_medium = fields.Binary( + string="Medium image", + compute="_get_multi_image", + inverse="_set_multi_image_main_medium", + store=False) + image_main_small = fields.Binary( + string="Small image", + compute="_get_multi_image", + inverse="_set_multi_image_main_small", + store=False) + + @api.multi + @api.depends('image_ids') + def _get_multi_image(self): + """Get the main image for this object. + + This is provided as a compatibility layer for submodels that already + had one image per record. + """ + for s in self: + first = s.image_ids[:1] + s.image_main = first.image_main + s.image_main_medium = first.image_medium + s.image_main_small = first.image_small + + @api.multi + def _set_multi_image(self, image=False, name=False): + """Save or delete the main image for this record. + + This is provided as a compatibility layer for submodels that already + had one image per record. + """ + # Values to save + values = { + "storage": "db", + "file_db_store": tools.image_resize_image_big(image), + "owner_model": self._name, + } + if name: + values["name"] = name + + for s in self: + if image: + values["owner_id"] = s.id + # Editing + if s.image_ids: + s.image_ids[0].write(values) + # Adding + else: + values.setdefault("name", name or _("Main image")) + s.image_ids = [(0, 0, values)] + # Deleting + elif s.image_ids: + s.image_ids[0].unlink() + + @api.multi + def _set_multi_image_main(self): + self._set_multi_image(self.image_main) + + @api.multi + def _set_multi_image_main_medium(self): + self._set_multi_image(self.image_main_medium) + + @api.multi + def _set_multi_image_main_small(self): + self._set_multi_image(self.image_main_small) + + @api.multi + def unlink(self): + """Mimic `ondelete="cascade"` for multi images.""" + images = self.mapped("image_ids") + result = super(Owner, self).unlink() + if result: + images.unlink() + return result diff --git a/base_multi_image/security/ir.model.access.csv b/base_multi_image/security/ir.model.access.csv new file mode 100644 index 000000000..0b4130359 --- /dev/null +++ b/base_multi_image/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_images","Manage multi images","model_base_multi_image_image","base.group_user",1,1,1,1 diff --git a/base_multi_image/static/description/icon.png b/base_multi_image/static/description/icon.png new file mode 100644 index 000000000..c11adb164 Binary files /dev/null and b/base_multi_image/static/description/icon.png differ diff --git a/base_multi_image/static/description/icon.svg b/base_multi_image/static/description/icon.svg new file mode 100644 index 000000000..8d552fcb9 --- /dev/null +++ b/base_multi_image/static/description/icon.svg @@ -0,0 +1,320 @@ + + + + + Drawings Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/base_multi_image/views/image_view.xml b/base_multi_image/views/image_view.xml new file mode 100644 index 000000000..d6907a511 --- /dev/null +++ b/base_multi_image/views/image_view.xml @@ -0,0 +1,142 @@ + + + + + + Multi image form + base_multi_image.image + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Multi image tree + base_multi_image.image + + + + + + + + + + + + + Product multi image kanban + base_multi_image.image + + + + + + + +
+ X +
+ + + +
+
+ + + + +
+

+ + + +

+ +
+
+
+
+
+
+
+
+
+
+ + + Multi images + base_multi_image.image + kanban,tree,form + + + + +
+