From c53724ebc35150d9111e165ef5089beb26bedc29 Mon Sep 17 00:00:00 2001 From: Leonardo Pavan Rocha Date: Mon, 26 Apr 2021 13:12:03 +0000 Subject: [PATCH] [IMP] *: adds generic user avatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description of the issue/feature this PR addresses: It is currently quite difficult to differentiate users. Most of the time, people don't take the time to upload an actual avatar so everybody looks the same. This PR generates a custom avatar with the users initials and random color to differentiate them. For res.users, res.partner and hr.employee, image fields now hold the binary image and avatar are used to show the image or svg. Current behavior before PR: Avatar had only random colors and was being saved in database, being inefficient Desired behavior after PR is merged: A new mixin defines image fields and in case no image is set, it generates an SVG image with the user's initials and random color. closes odoo/odoo#69819 Task: 2404630 Related: odoo/enterprise#18199 Signed-off-by: Sébastien Theys (seb) --- addons/account/views/account_move_views.xml | 2 +- .../views/account_portal_templates.xml | 3 +- .../calendar/static/tests/calendar_tests.js | 2 +- addons/calendar/views/calendar_views.xml | 2 +- addons/crm/views/crm_lead_views.xml | 4 +- addons/event/views/event_views.xml | 2 +- .../fleet/views/fleet_vehicle_cost_views.xml | 2 +- addons/gamification/views/badge.xml | 2 +- addons/gamification/views/challenge.xml | 2 +- addons/gamification/views/goal.xml | 2 +- addons/hr/data/hr_data.xml | 1 - addons/hr/models/hr_employee.py | 40 ++++++++--- addons/hr/models/hr_employee_public.py | 29 ++++---- addons/hr/report/hr_employee_badge.xml | 2 +- .../static/tests/m2x_avatar_employee_tests.js | 12 ++-- addons/hr/views/hr_employee_public_views.xml | 4 +- addons/hr/views/hr_employee_views.xml | 7 +- addons/hr/views/res_users.xml | 3 +- .../static/src/xml/attendance.xml | 6 +- .../views/hr_attendance_view.xml | 2 +- .../hr_attendance/views/hr_employee_view.xml | 2 +- .../hr_contract/views/hr_contract_views.xml | 2 +- addons/hr_expense/views/hr_expense_views.xml | 4 +- .../hr_holidays/report/hr_leave_reports.xml | 2 +- .../views/hr_leave_allocation_views.xml | 4 +- addons/hr_holidays/views/hr_leave_views.xml | 4 +- .../static/src/xml/hr_org_chart.xml | 6 +- .../data/mail_template_data.xml | 4 +- .../hr_timesheet/views/hr_timesheet_views.xml | 2 +- .../views/hr_work_entry_views.xml | 2 +- .../static/src/legacy/public_livechat.js | 4 +- .../views/im_livechat_channel_views.xml | 2 +- addons/lunch/controllers/main.py | 2 +- addons/mail/controllers/main.py | 2 +- addons/mail/models/mail_message.py | 2 +- .../src/components/activity/activity.xml | 6 +- .../src/components/composer/composer.js | 2 +- .../components/discuss/tests/discuss_tests.js | 2 +- .../src/components/message/message_tests.js | 2 +- .../mail/static/src/models/partner/partner.js | 2 +- addons/mail/static/src/xml/activity.xml | 6 +- .../mail/static/tests/helpers/mock_models.js | 2 +- .../static/tests/m2x_avatar_user_tests.js | 10 +-- addons/mail/views/mail_activity_views.xml | 2 +- addons/mail/views/res_partner_views.xml | 2 +- addons/mrp/views/mrp_production_views.xml | 2 +- addons/mrp/views/mrp_workorder_views.xml | 2 +- addons/note/views/note_views.xml | 2 +- .../ClientListScreen/ClientDetailsEdit.js | 2 +- .../portal/static/src/xml/portal_chatter.xml | 2 +- addons/portal/views/portal_templates.xml | 3 +- .../static/src/js/portal_chatter.js | 4 +- .../views/project_portal_templates.xml | 12 ++-- addons/project/views/project_views.xml | 4 +- addons/purchase/views/portal_templates.xml | 3 +- addons/sale/views/sale_portal_templates.xml | 3 +- addons/sale/views/sale_views.xml | 2 +- .../views/hr_timesheet_templates.xml | 2 +- .../views/crm_team_member_views.xml | 2 +- addons/sales_team/views/crm_team_views.xml | 4 +- addons/web/controllers/main.py | 25 ++----- addons/web/static/src/js/chrome/user_menu.js | 2 +- .../web/static/src/js/fields/basic_fields.js | 3 +- .../static/src/js/fields/relational_fields.js | 4 +- .../src/js/views/calendar/calendar_view.js | 2 +- .../relational_fields/field_many2one_tests.js | 22 +++--- .../tests/fields/relational_fields_tests.js | 2 +- addons/website/tests/template_qweb_test.xml | 2 +- addons/website/tests/test_qweb.py | 2 +- .../website/views/website_visitor_views.xml | 2 +- addons/website_blog/static/src/js/options.js | 2 +- .../website_blog/views/website_blog_views.xml | 2 +- .../website_crm_partner_assign_templates.xml | 4 +- .../views/website_customer_templates.xml | 6 +- .../views/event_track_views.xml | 6 +- addons/website_forum/views/website_forum.xml | 14 ++-- .../website_hr_recruitment_templates.xml | 2 +- .../views/website_livechat.xml | 2 +- .../views/website_visitor_views.xml | 4 +- .../views/website_membership_templates.xml | 2 +- .../views/website_partner_templates.xml | 2 +- addons/website_profile/controllers/main.py | 4 +- .../website_profile/views/website_profile.xml | 8 +-- addons/website_slides/controllers/main.py | 3 +- .../website_slides_templates_homepage.xml | 2 +- odoo/addons/base/data/res_partner_data.xml | 1 - odoo/addons/base/data/res_users_data.xml | 1 - odoo/addons/base/models/__init__.py | 1 + odoo/addons/base/models/avatar_mixin.py | 70 +++++++++++++++++++ odoo/addons/base/models/res_partner.py | 39 +++++++++-- odoo/addons/base/models/res_users.py | 18 +---- odoo/addons/base/views/res_company_views.xml | 2 +- odoo/addons/base/views/res_partner_views.xml | 24 ++++--- odoo/addons/base/views/res_users_views.xml | 11 +-- odoo/models.py | 7 -- 95 files changed, 320 insertions(+), 241 deletions(-) create mode 100644 odoo/addons/base/models/avatar_mixin.py diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 855ced734d6..3ec79858fc6 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -111,7 +111,7 @@
- +
diff --git a/addons/account/views/account_portal_templates.xml b/addons/account/views/account_portal_templates.xml index 89f42cb6f7f..66e338a39f6 100644 --- a/addons/account/views/account_portal_templates.xml +++ b/addons/account/views/account_portal_templates.xml @@ -118,8 +118,7 @@
- Contact - Contact + Contact
diff --git a/addons/calendar/static/tests/calendar_tests.js b/addons/calendar/static/tests/calendar_tests.js index 004e9b7ad65..1e6f5ddd8f1 100644 --- a/addons/calendar/static/tests/calendar_tests.js +++ b/addons/calendar/static/tests/calendar_tests.js @@ -79,7 +79,7 @@ QUnit.module('calendar', { "should have img tag"); assert.hasAttrValue(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img'), 'data-src', - '/web/image/partner/1/image_128', + '/web/image/partner/1/avatar_128', "should have correct avatar image"); form.destroy(); diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml index e0cc7cd0544..4c63dd10ab6 100644 --- a/addons/calendar/views/calendar_views.xml +++ b/addons/calendar/views/calendar_views.xml @@ -277,7 +277,7 @@ - + diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 4a7c45ad514..fd1dd63ad54 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -428,7 +428,7 @@ - + @@ -498,7 +498,7 @@
- +
diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index e044df6f0ff..0becfb2abc4 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -408,7 +408,7 @@ - + diff --git a/addons/fleet/views/fleet_vehicle_cost_views.xml b/addons/fleet/views/fleet_vehicle_cost_views.xml index 2bcacb48796..f9de45c7464 100644 --- a/addons/fleet/views/fleet_vehicle_cost_views.xml +++ b/addons/fleet/views/fleet_vehicle_cost_views.xml @@ -167,7 +167,7 @@
- +
diff --git a/addons/gamification/views/badge.xml b/addons/gamification/views/badge.xml index 72418eb0cd9..6193548c091 100644 --- a/addons/gamification/views/badge.xml +++ b/addons/gamification/views/badge.xml @@ -148,7 +148,7 @@
- Owner + Owner
diff --git a/addons/gamification/views/challenge.xml b/addons/gamification/views/challenge.xml index a0876987ee1..b028ccb3391 100644 --- a/addons/gamification/views/challenge.xml +++ b/addons/gamification/views/challenge.xml @@ -156,7 +156,7 @@
- Member + Member
diff --git a/addons/gamification/views/goal.xml b/addons/gamification/views/goal.xml index 13e64236f15..fef7cbfead3 100644 --- a/addons/gamification/views/goal.xml +++ b/addons/gamification/views/goal.xml @@ -153,7 +153,7 @@

- +
diff --git a/addons/hr/data/hr_data.xml b/addons/hr/data/hr_data.xml index e3a3da5370a..94d72d10471 100644 --- a/addons/hr/data/hr_data.xml +++ b/addons/hr/data/hr_data.xml @@ -14,7 +14,6 @@ Administrator admin@example.com - private diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index e55763eae27..f524a42aaaf 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -10,9 +10,8 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.osv.query import Query from odoo.exceptions import ValidationError, AccessError -from odoo.modules.module import get_module_resource -from odoo.tools.misc import format_date from odoo.osv import expression +from odoo.tools.misc import format_date class HrEmployeePrivate(models.Model): @@ -26,7 +25,7 @@ class HrEmployeePrivate(models.Model): _name = "hr.employee" _description = "Employee" _order = 'name' - _inherit = ['hr.employee.base', 'mail.thread', 'mail.activity.mixin', 'resource.mixin', 'image.mixin'] + _inherit = ['hr.employee.base', 'mail.thread', 'mail.activity.mixin', 'resource.mixin', 'avatar.mixin'] _mail_post_access = 'read' # resource and user @@ -100,7 +99,6 @@ class HrEmployeePrivate(models.Model): km_home_work = fields.Integer(string="Home-Work Distance", groups="hr.group_hr_user", tracking=True) job_id = fields.Many2one(tracking=True) - image_1920 = fields.Image() phone = fields.Char(related='address_home_id.phone', related_sudo=False, readonly=False, string="Private Phone", groups="hr.group_hr_user") # employee in company child_ids = fields.One2many('hr.employee', 'parent_id', string='Direct subordinates') @@ -125,11 +123,35 @@ class HrEmployeePrivate(models.Model): ('user_uniq', 'unique (user_id, company_id)', "A user cannot be linked to multiple employees in the same company.") ] - 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: - return 'hr/static/src/img/default_image.png' - return super()._get_placeholder_filename(field=field) + @api.depends('name', 'user_id.avatar_1920', 'image_1920') + def _compute_avatar_1920(self): + super()._compute_avatar_1920() + + @api.depends('name', 'user_id.avatar_1024', 'image_1024') + def _compute_avatar_1024(self): + super()._compute_avatar_1024() + + @api.depends('name', 'user_id.avatar_512', 'image_512') + def _compute_avatar_512(self): + super()._compute_avatar_512() + + @api.depends('name', 'user_id.avatar_256', 'image_256') + def _compute_avatar_256(self): + super()._compute_avatar_256() + + @api.depends('name', 'user_id.avatar_128', 'image_128') + def _compute_avatar_128(self): + super()._compute_avatar_128() + + def _compute_avatar(self, avatar_field, image_field): + for employee in self: + avatar = employee[image_field] + if not avatar: + if employee.user_id: + avatar = employee.user_id[avatar_field] + else: + avatar = employee._avatar_get_placeholder() + employee[avatar_field] = avatar def name_get(self): if self.check_access_rights('read', raise_exception=False): diff --git a/addons/hr/models/hr_employee_public.py b/addons/hr/models/hr_employee_public.py index 9e9ad926032..505711b06ef 100644 --- a/addons/hr/models/hr_employee_public.py +++ b/addons/hr/models/hr_employee_public.py @@ -32,26 +32,29 @@ class HrEmployeePublic(models.Model): color = fields.Integer(readonly=True) employee_type = fields.Selection(readonly=True) + employee_id = fields.Many2one('hr.employee', 'Employee', compute="_compute_employee_id", search="_search_employee_id", compute_sudo=True) # hr.employee.public specific fields child_ids = fields.One2many('hr.employee.public', 'parent_id', string='Direct subordinates', readonly=True) - image_1920 = fields.Image("Original Image", compute='_compute_image', compute_sudo=True) - image_1024 = fields.Image("Image 1024", compute='_compute_image', compute_sudo=True) - image_512 = fields.Image("Image 512", compute='_compute_image', compute_sudo=True) - image_256 = fields.Image("Image 256", compute='_compute_image', compute_sudo=True) - image_128 = fields.Image("Image 128", compute='_compute_image', compute_sudo=True) + image_1920 = fields.Image("Image", related='employee_id.image_1920', compute_sudo=True) + image_1024 = fields.Image("Image 1024", related='employee_id.image_1024', compute_sudo=True) + image_512 = fields.Image("Image 512", related='employee_id.image_512', compute_sudo=True) + image_256 = fields.Image("Image 256", related='employee_id.image_256', compute_sudo=True) + image_128 = fields.Image("Image 128", related='employee_id.image_128', compute_sudo=True) + avatar_1920 = fields.Image("Avatar", related='employee_id.avatar_1920', compute_sudo=True) + avatar_1024 = fields.Image("Avatar 1024", related='employee_id.avatar_1024', compute_sudo=True) + avatar_512 = fields.Image("Avatar 512", related='employee_id.avatar_512', compute_sudo=True) + avatar_256 = fields.Image("Avatar 256", related='employee_id.avatar_256', compute_sudo=True) + avatar_128 = fields.Image("Avatar 128", related='employee_id.avatar_128', compute_sudo=True) parent_id = fields.Many2one('hr.employee.public', 'Manager', readonly=True) coach_id = fields.Many2one('hr.employee.public', 'Coach', readonly=True) user_partner_id = fields.Many2one(related='user_id.partner_id', related_sudo=False, string="User's partner") - def _compute_image(self): + def _search_employee_id(self, operator, value): + return [('id', operator, value)] + + def _compute_employee_id(self): for employee in self: - # We have to be in sudo to have access to the images - employee_id = self.sudo().env['hr.employee'].browse(employee.id) - employee.image_1920 = employee_id.image_1920 - employee.image_1024 = employee_id.image_1024 - employee.image_512 = employee_id.image_512 - employee.image_256 = employee_id.image_256 - employee.image_128 = employee_id.image_128 + employee.employee_id = self.env['hr.employee'].browse(employee.id) @api.model def _get_fields(self): diff --git a/addons/hr/report/hr_employee_badge.xml b/addons/hr/report/hr_employee_badge.xml index 0668f54608d..98f01955757 100644 --- a/addons/hr/report/hr_employee_badge.xml +++ b/addons/hr/report/hr_employee_badge.xml @@ -26,7 +26,7 @@ - Employee Image + Employee Image diff --git a/addons/hr/static/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index 99bd0097d59..f1787312b87 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -156,10 +156,10 @@ QUnit.module('hr', {}, function () { assert.strictEqual(kanban.$('.o_kanban_record').text().trim(), ''); assert.containsN(kanban, '.o_m2o_avatar', 4); - assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0) > img').data('src'), '/web/image/hr.employee.public/11/image_128'); - assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1) > img').data('src'), '/web/image/hr.employee.public/7/image_128'); - assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2) > img').data('src'), '/web/image/hr.employee.public/11/image_128'); - assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3) > img').data('src'), '/web/image/hr.employee.public/23/image_128'); + assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0) > img').data('src'), '/web/image/hr.employee.public/11/avatar_128'); + assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1) > img').data('src'), '/web/image/hr.employee.public/7/avatar_128'); + assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2) > img').data('src'), '/web/image/hr.employee.public/11/avatar_128'); + assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3) > img').data('src'), '/web/image/hr.employee.public/23/avatar_128'); kanban.destroy(); }); @@ -235,7 +235,7 @@ QUnit.module('hr', {}, function () { assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, "should have 2 records"); assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), - '/web/image/hr.employee.public/11/image_128', + '/web/image/hr.employee.public/11/avatar_128', "should have correct avatar image"); await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar')); @@ -350,7 +350,7 @@ QUnit.module('hr', {}, function () { assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, "should have 2 records"); assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), - '/web/image/hr.employee.public/11/image_128', + '/web/image/hr.employee.public/11/avatar_128', "should have correct avatar image"); await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar')); diff --git a/addons/hr/views/hr_employee_public_views.xml b/addons/hr/views/hr_employee_public_views.xml index a64dae4926b..77a4399f4a1 100644 --- a/addons/hr/views/hr_employee_public_views.xml +++ b/addons/hr/views/hr_employee_public_views.xml @@ -40,7 +40,7 @@
- +