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 @@
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Multi images
+ base_multi_image.image
+ kanban,tree,form
+
+
+
+
+
+