diff --git a/addons/account_analytic_default/views/account_analytic_default_view.xml b/addons/account_analytic_default/views/account_analytic_default_view.xml
index 805ec5250ca..bddeeaf80eb 100644
--- a/addons/account_analytic_default/views/account_analytic_default_view.xml
+++ b/addons/account_analytic_default/views/account_analytic_default_view.xml
@@ -59,7 +59,8 @@
-
![]()
+
+
From to
Product
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index aad23b2abb9..469661290ee 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -377,7 +377,8 @@
-
![]()
+
+
@@ -508,7 +509,8 @@
-
![]()
+
+
diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml
index 15f9c723a90..ea11d7f5e0e 100644
--- a/addons/hr_recruitment/views/hr_recruitment_views.xml
+++ b/addons/hr_recruitment/views/hr_recruitment_views.xml
@@ -329,7 +329,8 @@
-
+
+
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml
index 17976dea62f..86209c2d090 100644
--- a/addons/maintenance/views/maintenance_views.xml
+++ b/addons/maintenance/views/maintenance_views.xml
@@ -164,7 +164,8 @@
-
![]()
+
+
diff --git a/addons/mass_mailing/views/mailing_mailing_views.xml b/addons/mass_mailing/views/mailing_mailing_views.xml
index 5e8cfdb390e..c6fc0a3dc29 100644
--- a/addons/mass_mailing/views/mailing_mailing_views.xml
+++ b/addons/mass_mailing/views/mailing_mailing_views.xml
@@ -351,7 +351,8 @@
Mailing Contact
-
+
+
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index a42c64e6c05..47573ae7024 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -256,7 +256,8 @@
-
![]()
+
+
@@ -660,7 +661,8 @@
-
![]()
+
+
diff --git a/addons/purchase_requisition/views/purchase_requisition_views.xml b/addons/purchase_requisition/views/purchase_requisition_views.xml
index 265a747b23f..3a18208b13e 100644
--- a/addons/purchase_requisition/views/purchase_requisition_views.xml
+++ b/addons/purchase_requisition/views/purchase_requisition_views.xml
@@ -244,7 +244,8 @@
-
![]()
+
+
diff --git a/addons/stock_picking_batch/views/stock_picking_batch_views.xml b/addons/stock_picking_batch/views/stock_picking_batch_views.xml
index a729315f9bd..24474a6e0b3 100644
--- a/addons/stock_picking_batch/views/stock_picking_batch_views.xml
+++ b/addons/stock_picking_batch/views/stock_picking_batch_views.xml
@@ -78,7 +78,8 @@
-
![]()
+
+
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index cdc2fa3b950..3041058826b 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1367,7 +1367,8 @@ class Binary(http.Controller):
@staticmethod
def placeholder(image='placeholder.png'):
- with tools.file_open(get_resource_path('web', 'static/src/img', image), 'rb') as fd:
+ image_path = image.lstrip('/').split('/') if '/' in image else ['web', 'static', 'src', 'img', image]
+ with tools.file_open(get_resource_path(*image_path), 'rb') as fd:
return fd.read()
@http.route(['/web/content',
@@ -1437,23 +1438,35 @@ class Binary(http.Controller):
def _content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas',
filename_field='name', unique=None, filename=None, mimetype=None,
download=None, width=0, height=0, crop=False, quality=0, access_token=None,
- placeholder='placeholder.png', **kwargs):
+ placeholder=None, **kwargs):
status, headers, image_base64 = request.env['ir.http'].binary_content(
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype='image/png', access_token=access_token)
- return Binary._content_image_get_response(status, headers, image_base64, field=field, download=download,
- width=width, height=height, crop=crop, quality=quality,
- placeholder=placeholder)
+ return Binary._content_image_get_response(
+ status, headers, image_base64, model=model, id=id, field=field, download=download,
+ width=width, height=height, crop=crop, quality=quality,
+ placeholder=placeholder)
@staticmethod
- def _content_image_get_response(status, headers, image_base64, field='datas', download=None,
- width=0, height=0, crop=False, quality=0, placeholder='placeholder.png'):
+ def _content_image_get_response(
+ status, headers, image_base64, model='ir.attachment', id=None,
+ field='datas', download=None, width=0, height=0, crop=False,
+ quality=0, placeholder='placeholder.png'):
if status in [301, 304] or (status != 200 and download):
return request.env['ir.http']._response_by_status(status, headers, image_base64)
if not image_base64:
- image_base64 = base64.b64encode(Binary.placeholder(image=placeholder))
+ if placeholder is None and model in request.env:
+ # Try to browse the record in case a specific placeholder
+ # is supposed to be used. (eg: Unassigned users on a task)
+ record = request.env[model].browse(int(id)) if id else request.env[model]
+ placeholder_filename = record._get_placeholder_filename(field=field)
+ placeholder_content = Binary.placeholder(image=placeholder_filename)
+ else:
+ placeholder_content = Binary.placeholder()
+ image_base64 = base64.b64encode(placeholder_content)
+
if not (width or height):
width, height = odoo.tools.image_guess_size_from_field_name(field)
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index ab19e73d2eb..f199a286db6 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -184,14 +184,13 @@ var KanbanRecord = Widget.extend({
* @returns {string} the url of the image
*/
_getImageURL: function (model, field, id, placeholder) {
- id = (_.isArray(id) ? id[0] : id) || false;
- placeholder = placeholder || "/web/static/src/img/placeholder.png";
+ id = (_.isArray(id) ? id[0] : id) || null;
var isCurrentRecord = this.modelName === model && this.recordData.id === id;
var url;
if (isCurrentRecord && this.record[field] && this.record[field].raw_value && !utils.is_bin_size(this.record[field].raw_value)) {
// Use magic-word technique for detecting image type
url = 'data:image/' + this.file_type_magic_word[this.record[field].raw_value[0]] + ';base64,' + this.record[field].raw_value;
- } else if (!model || !field || !id || (isCurrentRecord && this.record[field] && !this.record[field].raw_value)) {
+ } else if (placeholder && (!model || !field || !id || (isCurrentRecord && this.record[field] && !this.record[field].raw_value))) {
url = placeholder;
} else {
var session = this.getSession();
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 0b8334472ad..c0a07c699fa 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -5264,8 +5264,15 @@ QUnit.module('Views', {
return this._super.apply(this, arguments);
},
});
+ var images = kanban.el.querySelectorAll('img');
+ var placeholders = [];
+ for (var [index, img] of images.entries()) {
+ if (img.dataset.src.indexOf(this.data.partner.records[index].image) === -1) {
+ // Then we display a placeholder
+ placeholders.push(img);
+ }
+ }
- var placeholders = kanban.$('img[data-src$="/web/static/src/img/placeholder.png"]');
assert.strictEqual(placeholders.length, this.data.partner.records.length - 1,
"partner with no image should display the placeholder");
diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml
index 0301710e345..1f98a26f89d 100644
--- a/addons/website_slides/views/slide_slide_views.xml
+++ b/addons/website_slides/views/slide_slide_views.xml
@@ -211,7 +211,8 @@
-
+
+
diff --git a/odoo/addons/base/data/res_users_data.xml b/odoo/addons/base/data/res_users_data.xml
index 6439180c2a7..dd1b504ff32 100644
--- a/odoo/addons/base/data/res_users_data.xml
+++ b/odoo/addons/base/data/res_users_data.xml
@@ -19,6 +19,7 @@ System]]>
+
--
Administrator]]>
@@ -29,7 +30,7 @@ Administrator]]>
default
-
+
Public user
public
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 718a62c12aa..e428fb68b9a 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import contextlib
+import base64
import pytz
import datetime
import ipaddress
@@ -21,7 +22,8 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
from odoo.http import request
from odoo.osv import expression
from odoo.service.db import check_super
-from odoo.tools import partition, collections, frozendict, lazy_property
+from odoo.tools import partition, collections, frozendict, lazy_property, image_process
+from odoo.modules.module import get_module_resource
_logger = logging.getLogger(__name__)
@@ -214,6 +216,17 @@ class Users(models.Model):
def _companies_count(self):
return self.env['res.company'].sudo().search_count([])
+ @api.model
+ def _get_default_image(self):
+ """ Get a default image when the user is created without image
+
+ Inspired to _get_default_image method in
+ https://github.com/odoo/odoo/blob/11.0/odoo/addons/base/res/res_partner.py
+ """
+ image_path = get_module_resource('base', 'static/img', 'avatar.png')
+ image = base64.b64encode(open(image_path, 'rb').read())
+ return image_process(image, colorize=True)
+
partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True,
string='Related Partner', help='Partner-related data of the user')
login = fields.Char(required=True, help="Used to log into the system")
@@ -258,6 +271,7 @@ class Users(models.Model):
compute='_compute_accesses_count')
groups_count = fields.Integer('# Groups', help='Number of groups that apply to the current user',
compute='_compute_accesses_count')
+ image_1920 = fields.Image(related='partner_id.image_1920', inherited=True, readonly=False, default=_get_default_image)
_sql_constraints = [
('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')
@@ -913,6 +927,13 @@ class Users(models.Model):
def _register_hook(self):
if hasattr(self, 'check_credentials'):
_logger.warning("The check_credentials method of res.users has been renamed _check_credentials. One of your installed modules defines one, but it will not be called anymore.")
+
+ def _get_placeholder_filename(self, field=None):
+ image_fields = ['image_%s' % size for size in [1920, 1024, 512, 256, 128]]
+ if field in image_fields and not self:
+ return 'base/static/img/user-slash.png'
+ return super()._get_placeholder_filename(field=field)
+
#
# Implied groups
#
diff --git a/odoo/addons/base/static/img/user-slash.png b/odoo/addons/base/static/img/user-slash.png
new file mode 100644
index 00000000000..626ddfcb8d2
Binary files /dev/null and b/odoo/addons/base/static/img/user-slash.png differ
diff --git a/odoo/models.py b/odoo/models.py
index bf8d8169666..37cd8386229 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -6152,6 +6152,13 @@ Record ids: %(records)s
return result
+ def _get_placeholder_filename(self, field=None):
+ """ Returns the filename of the placeholder to use,
+ set on web/static/src/img by default, or the
+ complete path to access it (eg: module/path/to/image.png).
+ """
+ return 'placeholder.png'
+
collections.Set.register(BaseModel)
# not exactly true as BaseModel doesn't have __reversed__, index or count
collections.Sequence.register(BaseModel)