From b3be3af61cc08d0dea84969425d24957f215b26f Mon Sep 17 00:00:00 2001 From: "Melanie Peyrat (MEPE)" Date: Wed, 19 Jul 2023 12:34:45 +0000 Subject: [PATCH] [ADD] hr_homeworking: add homeworking Users and employees can now define their usual worklocation for a day of the week aswell as define unusual ones from the calendar app. The worklocation is then displayed in the calendar in two different ways: when only one person is selected, there is a circle with an icon in it, and a ray that extends to the right if the person is at the same location for multiple days. If there are multiple persons selected there is no ray, instead the circles are grouped by location type and colored accordingly to the filters in the left panel. closes odoo/odoo#129585 Task: 3060685 X-original-commit: c74ac3f698d4ea4c9b7a5e060fba4cf77cc9a6d7 Signed-off-by: Kevin Baptiste Co-authored-by: dasz --- addons/hr/data/hr_data.xml | 18 ++ addons/hr/data/hr_demo.xml | 9 +- addons/hr/models/hr_employee_base.py | 9 +- addons/hr/models/hr_work_location.py | 4 + addons/hr/populate/hr.py | 1 - .../radio_image_field/radio_image_field.js | 17 + .../radio_image_field/radio_image_field.xml | 36 +++ addons/hr/static/src/scss/hr.scss | 10 + addons/hr/views/hr_employee_public_views.xml | 70 ++-- addons/hr/views/hr_employee_views.xml | 77 ++--- addons/hr/views/hr_work_location_views.xml | 11 +- .../hr_attendance/models/hr_employee_base.py | 2 +- .../hr_contract/views/hr_contract_views.xml | 3 +- addons/hr_holidays/views/hr_views.xml | 73 ++--- addons/hr_homeworking/__init__.py | 5 + addons/hr_homeworking/__manifest__.py | 30 ++ .../data/hr_homeworking_demo.xml | 10 + addons/hr_homeworking/models/__init__.py | 6 + addons/hr_homeworking/models/hr_employee.py | 184 +++++++++++ .../hr_homeworking/models/hr_homeworking.py | 305 ++++++++++++++++++ addons/hr_homeworking/models/res_partner.py | 25 ++ addons/hr_homeworking/models/res_users.py | 26 ++ .../security/ir.model.access.csv | 5 + addons/hr_homeworking/security/security.xml | 40 +++ .../common/calendar_common_renderer.js | 204 ++++++++++++ .../common/calendar_common_renderer.scss | 89 +++++ .../src/calendar/common/calendar_model.js | 99 ++++++ .../common/calender_common_renderer.xml | 43 +++ .../common/popover/calendar_common_popover.js | 50 +++ .../popover/calendar_common_popover.xml | 75 +++++ .../hr_homeworking_calendar_controller.js | 82 +++++ .../static/src/im_status_patch.xml | 44 +++ .../static/tests/hr_org_chart_tests.js | 13 + addons/hr_homeworking/tests/__init__.py | 1 + addons/hr_homeworking/tests/common.py | 77 +++++ .../tests/test_hr_employee_location.py | 191 +++++++++++ .../views/hr_employee_views.xml | 98 ++++++ .../views/hr_homeworking_views.xml | 40 +++ addons/hr_homeworking/views/res_users.xml | 24 ++ addons/hr_homeworking/wizard/__init__.py | 4 + ...calendar_popover_delete_wizard_homework.py | 23 ++ ...alendar_popover_delete_wizard_homework.xml | 25 ++ .../views/hr_applicant_views.xml | 2 +- .../tests/test_performance.py | 2 +- 44 files changed, 2020 insertions(+), 142 deletions(-) create mode 100644 addons/hr/static/src/components/radio_image_field/radio_image_field.js create mode 100644 addons/hr/static/src/components/radio_image_field/radio_image_field.xml create mode 100644 addons/hr_homeworking/__init__.py create mode 100644 addons/hr_homeworking/__manifest__.py create mode 100644 addons/hr_homeworking/data/hr_homeworking_demo.xml create mode 100644 addons/hr_homeworking/models/__init__.py create mode 100644 addons/hr_homeworking/models/hr_employee.py create mode 100644 addons/hr_homeworking/models/hr_homeworking.py create mode 100644 addons/hr_homeworking/models/res_partner.py create mode 100644 addons/hr_homeworking/models/res_users.py create mode 100644 addons/hr_homeworking/security/ir.model.access.csv create mode 100644 addons/hr_homeworking/security/security.xml create mode 100644 addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.js create mode 100644 addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.scss create mode 100644 addons/hr_homeworking/static/src/calendar/common/calendar_model.js create mode 100644 addons/hr_homeworking/static/src/calendar/common/calender_common_renderer.xml create mode 100644 addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.js create mode 100644 addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.xml create mode 100644 addons/hr_homeworking/static/src/calendar/hr_homeworking_calendar_controller.js create mode 100644 addons/hr_homeworking/static/src/im_status_patch.xml create mode 100644 addons/hr_homeworking/static/tests/hr_org_chart_tests.js create mode 100644 addons/hr_homeworking/tests/__init__.py create mode 100644 addons/hr_homeworking/tests/common.py create mode 100644 addons/hr_homeworking/tests/test_hr_employee_location.py create mode 100644 addons/hr_homeworking/views/hr_employee_views.xml create mode 100644 addons/hr_homeworking/views/hr_homeworking_views.xml create mode 100644 addons/hr_homeworking/views/res_users.xml create mode 100644 addons/hr_homeworking/wizard/__init__.py create mode 100644 addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.py create mode 100644 addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.xml diff --git a/addons/hr/data/hr_data.xml b/addons/hr/data/hr_data.xml index fc9d9d5942c..a6487ffe2b7 100644 --- a/addons/hr/data/hr_data.xml +++ b/addons/hr/data/hr_data.xml @@ -109,5 +109,23 @@ days -1 + + + Home + home + + + + + Office + office + + + + + Other + other + + diff --git a/addons/hr/data/hr_demo.xml b/addons/hr/data/hr_demo.xml index 0ce3e518753..a42f208acc5 100644 --- a/addons/hr/data/hr_demo.xml +++ b/addons/hr/data/hr_demo.xml @@ -145,10 +145,11 @@ If you have development competencies, we can propose you specific traineeships - - Building 1, Second Floor - - + + Building 1, Second Floor + office + + diff --git a/addons/hr/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py index c89a72dec6c..9b6378038be 100644 --- a/addons/hr/models/hr_employee_base.py +++ b/addons/hr/models/hr_employee_base.py @@ -31,9 +31,7 @@ class HrEmployeeBase(models.AbstractModel): mobile_phone = fields.Char('Work Mobile', compute="_compute_work_contact_details", store=True, inverse='_inverse_work_contact_details') work_email = fields.Char('Work Email', compute="_compute_work_contact_details", store=True, inverse='_inverse_work_contact_details') work_contact_id = fields.Many2one('res.partner', 'Work Contact', copy=False) - work_location_id = fields.Many2one('hr.work.location', 'Work Location', compute="_compute_work_location_id", store=True, readonly=False, - check_company=True, - domain="[('address_id', '=', address_id)]") + work_location_id = fields.Many2one('hr.work.location', 'Work Location') user_id = fields.Many2one('res.users') resource_id = fields.Many2one('resource.resource') resource_calendar_id = fields.Many2one('resource.calendar', check_company=True) @@ -212,11 +210,6 @@ class HrEmployeeBase(models.AbstractModel): employee.hr_icon_display = icon employee.show_hr_icon_display = show_icon - @api.depends('address_id') - def _compute_work_location_id(self): - to_reset = self.filtered(lambda e: e.address_id != e.work_location_id.address_id) - to_reset.work_location_id = False - @api.model def _get_employee_working_now(self): working_now = [] diff --git a/addons/hr/models/hr_work_location.py b/addons/hr/models/hr_work_location.py index e576bfda468..bbba32028e2 100644 --- a/addons/hr/models/hr_work_location.py +++ b/addons/hr/models/hr_work_location.py @@ -12,5 +12,9 @@ class WorkLocation(models.Model): active = fields.Boolean(default=True) name = fields.Char(string="Work Location", required=True) company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) + location_type = fields.Selection([ + ('home', 'Home'), + ('office', 'Office'), + ('other', 'Other')], string='Cover Image', default='office', required=True) address_id = fields.Many2one('res.partner', required=True, string="Work Address", check_company=True) location_number = fields.Char() diff --git a/addons/hr/populate/hr.py b/addons/hr/populate/hr.py index b0cc09926a8..c32f5eab19e 100644 --- a/addons/hr/populate/hr.py +++ b/addons/hr/populate/hr.py @@ -55,7 +55,6 @@ class HrWorkLocation(models.Model): _inherit = 'hr.work.location' _populate_sizes = {'small': 2, 'medium': 5, 'large': 20} - def _populate_factories(self): address_id = self.env.ref('base.main_partner').id diff --git a/addons/hr/static/src/components/radio_image_field/radio_image_field.js b/addons/hr/static/src/components/radio_image_field/radio_image_field.js new file mode 100644 index 00000000000..3733e37a2f5 --- /dev/null +++ b/addons/hr/static/src/components/radio_image_field/radio_image_field.js @@ -0,0 +1,17 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { RadioField, preloadRadio, radioField } from "@web/views/fields/radio/radio_field"; + +class RadioImageField extends RadioField {} +RadioImageField.template = "hr_homeworking.RadioImageField"; + +registry.category("fields").add("hr_homeworking_radio_image", { + ...radioField, + component: RadioImageField, +}); + +registry.category("preloadedData").add("hr_homeworking_radio_image", { + loadOnTypes: ["many2one"], + preload: preloadRadio, +}); diff --git a/addons/hr/static/src/components/radio_image_field/radio_image_field.xml b/addons/hr/static/src/components/radio_image_field/radio_image_field.xml new file mode 100644 index 00000000000..9c79b2ed3b3 --- /dev/null +++ b/addons/hr/static/src/components/radio_image_field/radio_image_field.xml @@ -0,0 +1,36 @@ + + + + +
+ + +
+ + + + + + + + + + +
+
+
+
+
+ + +
diff --git a/addons/hr/static/src/scss/hr.scss b/addons/hr/static/src/scss/hr.scss index 32c9599234f..f45a7deb9ab 100644 --- a/addons/hr/static/src/scss/hr.scss +++ b/addons/hr/static/src/scss/hr.scss @@ -52,6 +52,16 @@ } } +.o_hr_kanban_record { + .fa-2x { + font-size: 1.6em; + } + .fa-2x.fa-circle, + .fa-2x.fa-building { + font-size: 1.3em; + } +} + .o_hr_narrow_field { width: 8rem!important; max-width: 8rem!important; diff --git a/addons/hr/views/hr_employee_public_views.xml b/addons/hr/views/hr_employee_public_views.xml index ef7df51cd3b..8658473cd5f 100644 --- a/addons/hr/views/hr_employee_public_views.xml +++ b/addons/hr/views/hr_employee_public_views.xml @@ -140,47 +140,47 @@
- -
- - - -
-
- - - -
-
- - - -
- -
- - -
-
- +
  • -
  • -
  • +
  • + +
    + + + + + + + + +
    +
    +
  • +
  • + + +
  • +
  • + + +
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml index d58af7979b4..461a973c146 100644 --- a/addons/hr/views/hr_employee_views.xml +++ b/addons/hr/views/hr_employee_views.xml @@ -290,60 +290,51 @@
-
- -
- - - -
-
- - - -
-
- - - -
-
- - - -
-
-
  • -
  • -
  • +
  • + +
    + + + + + + + + +
    +
    +
  • +
  • + + +
  • +
  • + + +
diff --git a/addons/hr/views/hr_work_location_views.xml b/addons/hr/views/hr_work_location_views.xml index 4e22d8fdcc5..2b0c3504d7b 100644 --- a/addons/hr/views/hr_work_location_views.xml +++ b/addons/hr/views/hr_work_location_views.xml @@ -8,26 +8,24 @@ - + - - hr.work.location.view.form hr.work.location
- - - + @@ -37,7 +35,6 @@
- Work Locations hr.work.location diff --git a/addons/hr_attendance/models/hr_employee_base.py b/addons/hr_attendance/models/hr_employee_base.py index df3e8134e08..940c537f264 100644 --- a/addons/hr_attendance/models/hr_employee_base.py +++ b/addons/hr_attendance/models/hr_employee_base.py @@ -20,7 +20,7 @@ class HrEmployeeBase(models.AbstractModel): working_now_list = employee_to_check_working._get_employee_working_now() for employee in employees: if employee.attendance_state == "checked_out" and employee.hr_presence_state == "to_define" and \ - employee.id not in working_now_list: + employee.id in working_now_list: employee.hr_presence_state = "absent" elif employee.attendance_state == "checked_in": employee.hr_presence_state = "present" diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index 54b48f0094a..0e61117f9db 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -80,9 +80,10 @@ - + + diff --git a/addons/hr_holidays/views/hr_views.xml b/addons/hr_holidays/views/hr_views.xml index 922ef963e12..402afce9e0f 100644 --- a/addons/hr_holidays/views/hr_views.xml +++ b/addons/hr_holidays/views/hr_views.xml @@ -77,8 +77,10 @@ hr.employee - - + + + + @@ -92,21 +94,20 @@ - - -
- - -
- -
- -
+ + + + + + + On Time Off + @@ -123,29 +124,21 @@
- - - -
- - -
- - -
- -
-
- - - + + + + + + + diff --git a/addons/hr_homeworking/__init__.py b/addons/hr_homeworking/__init__.py new file mode 100644 index 00000000000..2ae6446f9dc --- /dev/null +++ b/addons/hr_homeworking/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import wizard diff --git a/addons/hr_homeworking/__manifest__.py b/addons/hr_homeworking/__manifest__.py new file mode 100644 index 00000000000..c9ab3369586 --- /dev/null +++ b/addons/hr_homeworking/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Homeworking', + 'version': '1.0', + 'category': 'Human Resources/Homeworking', + 'depends': ['hr', 'calendar'], + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'views/hr_employee_views.xml', + 'views/hr_homeworking_views.xml', + 'views/res_users.xml', + 'wizard/calendar_popover_delete_wizard_homework.xml', + ], + 'demo': [ + 'data/hr_homeworking_demo.xml', + ], + 'installable': True, + 'assets': { + 'web.assets_backend': [ + 'hr_homeworking/static/src/**/*', + ], + 'web.qunit_suite_tests': [ + 'hr_homeworking/static/tests/**/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/hr_homeworking/data/hr_homeworking_demo.xml b/addons/hr_homeworking/data/hr_homeworking_demo.xml new file mode 100644 index 00000000000..0f1102b3ef0 --- /dev/null +++ b/addons/hr_homeworking/data/hr_homeworking_demo.xml @@ -0,0 +1,10 @@ + + + + + GR2 + office + + + + diff --git a/addons/hr_homeworking/models/__init__.py b/addons/hr_homeworking/models/__init__.py new file mode 100644 index 00000000000..f4dc23efd03 --- /dev/null +++ b/addons/hr_homeworking/models/__init__.py @@ -0,0 +1,6 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import hr_employee +from . import res_partner +from . import res_users +from . import hr_homeworking diff --git a/addons/hr_homeworking/models/hr_employee.py b/addons/hr_homeworking/models/hr_employee.py new file mode 100644 index 00000000000..20f444d5d39 --- /dev/null +++ b/addons/hr_homeworking/models/hr_employee.py @@ -0,0 +1,184 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from collections import defaultdict +from datetime import timedelta +from dateutil.rrule import rrule, WEEKLY + +from odoo import api, fields, models + +from .hr_homeworking import DAYS + + +class HrEmployeeBase(models.AbstractModel): + _inherit = "hr.employee.base" + + monday_location_id = fields.Many2one('hr.work.location', string='Monday') + tuesday_location_id = fields.Many2one('hr.work.location', string='Tuesday') + wednesday_location_id = fields.Many2one('hr.work.location', string='Wednesday') + thursday_location_id = fields.Many2one('hr.work.location', string='Thursday') + friday_location_id = fields.Many2one('hr.work.location', string='Friday') + saturday_location_id = fields.Many2one('hr.work.location', string='Saturday') + sunday_location_id = fields.Many2one('hr.work.location', string='Sunday') + hr_icon_display = fields.Selection(selection_add=[('presence_home', 'At Home'), + ('presence_office', 'At Office'), + ('presence_other', 'At Other')]) + name_work_location_display = fields.Char(compute="_compute_name_work_location_display") + today_location_name = fields.Char() + + @api.model + def _get_current_day_location_field(self): + return DAYS[fields.Date.today().weekday()] + + # hack to allow groupby on today's location. Since there are 7 different fields, we have to use a placeholder + # in the search view and replace it with the correct field every time the views are fetched. + @api.model + def get_views(self, views, options=None): + res = super().get_views(views, options) + dayfield = self._get_current_day_location_field() + if 'search' in res['views']: + res['views']['search']['arch'] = res['views']['search']['arch'].replace('today_location_name', dayfield) + if 'list' in res['views']: + res['views']['list']['arch'] = res['views']['list']['arch'].replace('name_work_location_display', dayfield) + return res + + def _compute_name_work_location_display(self): + dayfield = self._get_current_day_location_field() + for employee in self: + employee.name_work_location_display = employee[dayfield].name + + @api.depends(*DAYS) + def _compute_presence_icon(self): + super()._compute_presence_icon() + dayfield = self._get_current_day_location_field() + for employee in self: + today_employee_location_id = employee[dayfield] + if not today_employee_location_id or employee.hr_icon_display.startswith('presence_holiday'): + continue + employee.hr_icon_display = f'presence_{today_employee_location_id.location_type}' + employee.show_hr_icon_display = True + + def _get_worklocation(self, start_date, end_date): + worklocations = self.env['hr.employee.location'].search([ + ('employee_id', 'in', self.ids), + '|', + '&', '&', + ('start_date', '<=', end_date), + ('start_date', '>=', start_date), + ('weekly', '=', False), + '&', '&', + ('start_date', '<=', end_date), + '|', + ('end_date', '>=', start_date), + ('end_date', '=', False), + ('weekly', '=', True), + ], order='start_date ASC') + worklocation_default = worklocations.filtered('weekly') + worklocations_exception = worklocations - worklocation_default + week_start = int(self.env['res.lang']._lang_get(self.env.user.lang).week_start) - 1 # week_start is 1 indexed + + # 1. Get all exceptional dates in the range + worklocation_data = defaultdict(list) + date_exception = defaultdict(set) + for index, worklocation in enumerate(worklocations_exception): + if not worklocation.removed: + worklocation_data[worklocation.employee_id.id].append({ + 'resModel': "hr.employee.location", + 'idInDB': worklocation.id, + 'id': index, + 'title': worklocation.work_location_name, + 'date': worklocation.start_date, + 'location_id': worklocation.work_location_id.id, + 'weekly': False, + 'icon': worklocation.work_location_type, + 'userId': worklocation.employee_id.user_id.id, + 'partner_id': [worklocation.employee_id.work_contact_id.id] # frontend expects a list (google_calendar) + }) + date_exception[worklocation.employee_id.id].add(worklocation.start_date) + + # 2. Get all weekly (default) dates in the range + index = len(worklocations_exception.ids) + start_date = fields.Datetime.to_datetime(start_date).date() + end_date = fields.Datetime.to_datetime(end_date).date() + for worklocation in worklocation_default: + start = max(worklocation.start_date, start_date) # the recurring location might have been set before our date_range, discard locations before that + end = worklocation.end_date or end_date + days_list = list(rrule(WEEKLY, start, byweekday=worklocation.start_date.weekday(), wkst=week_start, until=end)) + if not days_list: + continue + days_list = [x.date() for x in days_list] + final_days_list = [] + for day in days_list: + if day not in date_exception[worklocation.employee_id.id]: + final_days_list.append(day) + for index_intern, final_day in enumerate(final_days_list): + worklocation_data[worklocation.employee_id.id].append({ + 'resModel': "hr.employee.location", + 'idInDB': worklocation.id, + 'id': index_intern + index, + 'title': worklocation.work_location_name, + 'date': final_day, + 'location_id': worklocation.work_location_id.id, + 'weekly': True, + 'icon': worklocation.work_location_type, + 'userId': worklocation.employee_id.user_id.id, + 'partner_id': [worklocation.employee_id.work_contact_id.id] + }) + index += len(final_days_list) + + for employee in worklocation_data: + worklocation_data[employee].sort(key=lambda wl: (wl['title'], wl['date'])) + return worklocation_data + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + today = fields.Date.today() + homeworking_vals = [] + for employee in res: + for day_index, field_name in enumerate(DAYS): + if not employee[field_name]: + continue + day_offset = (day_index - today.weekday() + 7) % 7 + date = today + timedelta(days=day_offset) + homeworking_vals.append({ + 'employee_id': employee.id, + 'work_location_id': employee[field_name].id, + 'weekly': True, + 'start_date': date + }) + self.env['hr.employee.location'].create(homeworking_vals) + return res + + def write(self, values): + if self.env.context.get('no_loop'): + return super().write(values) + homeworking_vals = [] + employee_locations_to_remove = defaultdict(set) + today = fields.Date.today() + for day_index, field_name in enumerate(DAYS): + if field_name not in values: + continue + if not values[field_name]: + for employee in self: + employee_locations_to_remove[employee.id].add(day_index) + else: + day_offset = (day_index - today.weekday() + 7) % 7 + date = today + timedelta(days=day_offset) + for employee in self: + homeworking_vals.append({ + 'employee_id': employee.id, + 'work_location_id': values[field_name], + 'weekly': True, + 'start_date': date + }) + if homeworking_vals: + self.env['hr.employee.location'].create(homeworking_vals) + if employee_locations_to_remove: + read_group = self.env['hr.employee.location']._read_group([ + ('current_location', '=', True), ('employee_id', 'in', list(employee_locations_to_remove.keys()))], + groupby=['employee_id'], aggregates=['id:recordset'] + ) + for employee, locations in read_group: + if employee.id in employee_locations_to_remove: + locations.filtered(lambda l: l.weekday in employee_locations_to_remove[employee.id]).unlink() + return super().write(values) diff --git a/addons/hr_homeworking/models/hr_homeworking.py b/addons/hr_homeworking/models/hr_homeworking.py new file mode 100644 index 00000000000..270ccf38a81 --- /dev/null +++ b/addons/hr_homeworking/models/hr_homeworking.py @@ -0,0 +1,305 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models +from datetime import timedelta +from odoo.addons.resource.models.utils import string_to_datetime, datetime_to_string +from odoo.exceptions import ValidationError +from dateutil.rrule import rrule, DAILY + +DAYS = ['monday_location_id', 'tuesday_location_id', 'wednesday_location_id', 'thursday_location_id', 'friday_location_id', 'saturday_location_id', 'sunday_location_id'] +DAYS_STRING = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] + + +class HrEmployeeLocation(models.Model): + _name = "hr.employee.location" + _description = "Employee Location" + + work_location_id = fields.Many2one('hr.work.location', required=True, string="Location") + work_location_name = fields.Char(related='work_location_id.name', string="Location name") + work_location_type = fields.Selection(related="work_location_id.location_type") + employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id, required=True, ondelete="cascade") + employee_name = fields.Char(related="employee_id.name") + weekday = fields.Integer(compute="_compute_weekday", store=True) + weekly = fields.Boolean(default=False) + start_date = fields.Date(string="Start Date") + today_next_date = fields.Date(compute="_compute_today_next_date") + day_week_string = fields.Char(compute="_compute_day_week_string") + end_date_create = fields.Date(help="used in computations on employee location pop-up in calendar view.") + removed = fields.Boolean(default=False, help="User removed worklocation on one day that has a weekly location") + parent_default_homeworking_id = fields.Many2one("hr.employee.location", help="original weekly worklocation on a removed worklocation", ondelete="cascade") + child_removed_ids = fields.One2many("hr.employee.location", 'parent_default_homeworking_id') + current_location = fields.Boolean(default=True, help="Whether the record is the most recent weekly worklocation") + end_date = fields.Date(help="end date of a previous weekly worklocation (as opposed to current worklocation)") + + _sql_constraints = [ + ('uniq_exceptional_per_day', 'unique(employee_id, weekly, start_date, current_location)', 'Only one default work location and one exceptional work location per day per employee.'), + ] + + @api.constrains("employee_id", "weekday", "current_location") + def _check_current(self): + for record in self: + if record.current_location: + if self.search_count([('employee_id', '=', record.employee_id.id), ('weekday', '=', record.weekday), ('current_location', '=', True)]) > 1: + raise ValidationError(_('Only one current default work location per day per employee. can have some previous default work location')) + + @api.depends('start_date', 'weekly') + def _compute_weekday(self): + for record in self: + if not record.weekly: + continue + if record.start_date: + record.weekday = record.start_date.weekday() + + @api.depends('start_date', 'end_date_create') + def _compute_today_next_date(self): + today = fields.Date().today() + for record in self: + if record.start_date < today: + if record.end_date_create >= today: + record.today_next_date = today + else: + if record.start_date.weekday() <= today.weekday() and today.weekday() <= record.end_date_create.weekday(): + record.today_next_date = today + else: + offset = (record.start_date.weekday() - fields.Date.today().weekday() + 7) % 7 + record.today_next_date = today + timedelta(days=offset) + else: + record.today_next_date = record.start_date + + @api.depends('start_date', 'end_date_create', 'weekly') + def _compute_day_week_string(self): + for record in self: + if record.weekly and record.end_date_create - record.start_date > timedelta(days=7): + raise ValidationError(_('A weekly location cannot have a duration of more than 7 days')) + if (record.start_date > record.end_date_create): + record.end_date_create = record.start_date + weekdays_name = "" + if record.start_date.weekday() > record.end_date_create.weekday(): + for i in range(0, record.end_date_create.weekday()): + weekdays_name = weekdays_name + ", " + DAYS_STRING[i] + for i in range(record.start_date.weekday(), 7): + weekdays_name = weekdays_name + ", " + DAYS_STRING[i] + else: + for i in range(record.start_date.weekday(), record.end_date_create.weekday() + 1): + weekdays_name = weekdays_name + ", " + DAYS_STRING[i] + record.day_week_string = weekdays_name + + def _save_previous_default_worklocation(self): + self.ensure_one() + if self.start_date >= fields.Date().today(): + self.unlink() + else: + self.current_location = False + self.end_date = fields.Date().today() - timedelta(days=1) + self.child_removed_ids.filtered(lambda child: child.start_date >= fields.Date.today()).unlink() + return False + + def _next_weekday_date_after_today(self, date): + today = fields.Date().today() + offset = (date.weekday() - fields.Date.today().weekday() + 7) % 7 + return today + timedelta(days=offset) + + def _removed_worklocation_to_active_worklocation(self, work_location_id): + self.update({ + "removed": False, + "parent_default_homeworking_id": False, + "work_location_id": work_location_id, + }) + + def _check_exceptional_work_location(self, vals): + date = vals.get("start_date", self.start_date) + work = vals.get("work_location_id", self.work_location_id.id) + employee_id = vals.get("employee_id", self.employee_id.id) + exception_work_location = self.env['hr.employee.location'].search([ + ('start_date', '=', date), + ('employee_id.id', '=', employee_id), + ('weekly', '=', False), + ('id', '!=', self.id), + ]) + return exception_work_location, date, work, employee_id + + def add_exceptional_worklocation(self, vals): + exceptional, date, work, employee_id = self._check_exceptional_work_location(vals) + exceptional.ensure_one() + if exceptional: + exceptional.unlink() + self.env['hr.employee.location'].create({ + 'work_location_id': work, + 'employee_id': employee_id, + 'start_date': date, + }) + return True + + def exceptional_worklocation_to_default_worklocation(self, vals, unlink=True): + default_date = vals.get("start_date") + if default_date: + default_date = string_to_datetime(default_date).date() + else: + default_date = self.start_date + date = default_date + default_work = self.env['hr.employee.location'].search([ + ("employee_id.id", "=", self.employee_id.id), + ("weekday", "=", default_date.weekday()), + ("weekly", "=", True), + ("current_location", "=", True) + ]) + self.env['hr.employee.location'].search([ + ("employee_id.id", "=", self.employee_id.id), + ("start_date", "=", date), + ("removed", "=", True), + ]).unlink() + if default_date < fields.Date().today(): + default_date = self._next_weekday_date_after_today(date) + if default_work: + default_work._save_previous_default_worklocation() + if date != default_date: + self.env['hr.employee.location'].create({ + 'employee_id': self.employee_id.id, + 'start_date': default_date, + 'work_location_id': vals.get("work_location_id", self.work_location_id.id), + 'weekly': True + }) + vals["weekly"] = False + if unlink: + self.unlink() + else: + if vals.get("work_location_id"): + self.work_location_id = vals["work_location_id"] + return vals + + def add_removed_work_location(self, date): + self.ensure_one() + self.env['hr.employee.location'].create({ + 'work_location_id': self.work_location_id.id, + 'employee_id': self.employee_id.id, + 'start_date': date, + 'removed': True, + 'parent_default_homeworking_id': self.id, + }) + + def delete_default_worklocation(self): + self.ensure_one() + self._save_previous_default_worklocation() + + def _get_new_worklocation_vals(self, vals): + date = vals.get("start_date") + except_vals = { + "start_date": date, + "work_location_id": vals.get("work_location_id", self.work_location_id.id), + "weekly": vals.get("weekly", self.weekly), + "employee_id": vals.get("employee_id", self.employee_id.id), + } + # to not rewrite the past + self._save_previous_default_worklocation() + values = vals + employeeLocation = self.env['hr.employee.location'] + if date < fields.Date().today(): + employeeLocation.add_exceptional_worklocation(except_vals) + values["start_date"] = employeeLocation._next_weekday_date_after_today(date) + return values + + # create a default worklocation with values in vals and manages the link between the different records + def create_default_worklocation(self, vals, from_create=False, unlink=True): + date = vals.get("start_date", self.start_date) + employee_id = vals.get("employee_id", self.employee_id.id) + work_location_id = vals.get("work_location_id", self.work_location_id.id) + default_work_location = self.env['hr.employee.location'].search([ + ("employee_id.id", "=", employee_id), + ("weekly", "=", True), + ("weekday", "=", date.weekday()), + ("current_location", "=", True), + ("id", "!=", self.id) + ]) + if default_work_location: + val = default_work_location._get_new_worklocation_vals({ + "start_date": date, + "employee_id": employee_id, + "work_location_id": work_location_id + }) + vals["start_date"] = val.get("start_date", date) + if from_create: + return vals + else: + values = { + "start_date": val.get("start_date", date), + "employee_id": employee_id, + "work_location_id": work_location_id, + "weekly": True, + } + return super().create(values) + else: + exceptions, dummy, dummy, dummy = self._check_exceptional_work_location(vals) + if exceptions.filtered(lambda excep: excep.id != self.id): + exceptions.unlink() + if date < fields.Date().today(): + new_values = { + 'start_date': date, + 'employee_id': employee_id, + 'work_location_id': work_location_id, + } + if from_create or (exceptions and exceptions.filtered(lambda excep: excep.id != self.id)): + super().create(new_values) + else: + super().write(new_values) + vals["start_date"] = self._next_weekday_date_after_today(date) + if from_create: + return vals + else: + vals.update({ + "work_location_id": work_location_id, + "weekly": True, + "start_date": date, + "employee_id": employee_id, + }) + return super().create(vals) + + # clean values in vals_list to manage the link between record + def _clean_values_to_create(self, vals_list): + clean_vals_list = [] + for vals in vals_list: + date = string_to_datetime(vals.get("start_date")).date() + employee_id = vals.get("employee_id") or self.env.context.get('default_employee_id') or self.env.user.employee_id.id + if vals.get("weekly"): + clean_vals_list.append(self.create_default_worklocation({ + 'start_date': date, + 'employee_id': employee_id, + 'work_location_id': vals["work_location_id"], + 'weekly': True + }, from_create=True)) + else: + exceptions_worklocation = self.env['hr.employee.location'].search([ + ("employee_id.id", "=", employee_id), + ("weekly", "=", False), + ("start_date", "=", date), + ]) + worklocation_exception = exceptions_worklocation.filtered(lambda a: not a.removed) + if worklocation_exception: + worklocation_exception.work_location_id = vals.get("work_location_id") + else: + removed_worklocation_exception = exceptions_worklocation.filtered(lambda a: a.removed) + if removed_worklocation_exception: + removed_worklocation_exception._removed_worklocation_to_active_worklocation(vals.get("work_location_id")) + else: + clean_vals_list.append(vals) + return clean_vals_list + + @api.model_create_multi + def create(self, vals_list): + extended_vals_list = [] + for vals in vals_list: + if vals.get("end_date_create"): + start_date = string_to_datetime(vals.get("start_date")).date() + end_date = string_to_datetime(vals.get("end_date_create")).date() + days_list = list(rrule(DAILY, start_date, until=end_date)) + vals.pop("end_date_create") + for day in days_list: + extended_vals_list.append({**vals, **{"start_date": datetime_to_string(day)}}) + else: + extended_vals_list.append(vals) + res = super().create(self._clean_values_to_create(extended_vals_list)) + for wl in res: + if wl.weekly: + wl.employee_id.sudo().with_context(no_loop=True).write({DAYS[wl.weekday]: wl.work_location_id}) + if len(vals_list) == 1 and len(extended_vals_list) != 1: + return res[0] + return res diff --git a/addons/hr_homeworking/models/res_partner.py b/addons/hr_homeworking/models/res_partner.py new file mode 100644 index 00000000000..99ab4c03195 --- /dev/null +++ b/addons/hr_homeworking/models/res_partner.py @@ -0,0 +1,25 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + +class ResPartner(models.Model): + _inherit = 'res.partner' + + def _compute_im_status(self): + super()._compute_im_status() + users = self.env["res.users"].search([ + ('partner_id.id', 'in', self.ids)]) + for user in users: + dayfield = self.env['hr.employee']._get_current_day_location_field() + location_type = user[dayfield].location_type + if not location_type: + continue + im_status = user.partner_id.im_status + if im_status == "online" or im_status == "away" or im_status == "offline": + user.partner_id.im_status = location_type + "_" + im_status + + def get_worklocation(self, start_date, end_date): + employee_id = self.env['hr.employee'].search([ + ('work_contact_id.id', 'in', self.ids), + ('company_id.id', '=', self.env.company.id)]) + return employee_id._get_worklocation(start_date, end_date) diff --git a/addons/hr_homeworking/models/res_users.py b/addons/hr_homeworking/models/res_users.py new file mode 100644 index 00000000000..620e177f7ed --- /dev/null +++ b/addons/hr_homeworking/models/res_users.py @@ -0,0 +1,26 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import models, fields + +from .hr_homeworking import DAYS + +class User(models.Model): + _inherit = ['res.users'] + + monday_location_id = fields.Many2one("hr.work.location", related="employee_id.monday_location_id", readonly=False, string='Monday') + tuesday_location_id = fields.Many2one("hr.work.location", related="employee_id.tuesday_location_id", readonly=False, string='Tuesday') + wednesday_location_id = fields.Many2one("hr.work.location", related="employee_id.wednesday_location_id", readonly=False, string='Wednesday') + thursday_location_id = fields.Many2one("hr.work.location", related="employee_id.thursday_location_id", readonly=False, string='Thursday') + friday_location_id = fields.Many2one("hr.work.location", related="employee_id.friday_location_id", readonly=False, string='Friday') + saturday_location_id = fields.Many2one("hr.work.location", related="employee_id.saturday_location_id", readonly=False, string='Saturday') + sunday_location_id = fields.Many2one("hr.work.location", related="employee_id.sunday_location_id", readonly=False, string='Sunday') + + def _get_employee_fields_to_sync(self): + return super()._get_employee_fields_to_sync() + DAYS + + @property + def SELF_READABLE_FIELDS(self): + return super().SELF_READABLE_FIELDS + DAYS + + @property + def SELF_WRITEABLE_FIELDS(self): + return super().SELF_WRITEABLE_FIELDS + DAYS diff --git a/addons/hr_homeworking/security/ir.model.access.csv b/addons/hr_homeworking/security/ir.model.access.csv new file mode 100644 index 00000000000..a356940e04a --- /dev/null +++ b/addons/hr_homeworking/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_employee_location,hr.employee.location,model_hr_employee_location,hr.group_hr_user,1,1,1,1 +access_user_employee_location,hr.employee.location,model_hr_employee_location,base.group_user,1,1,1,1 +acces_hr_popover_delete_wizard,calendar.popover.delete.wizard.homework,model_calendar_popover_delete_wizard_homework,hr.group_hr_user,1,1,1,1 +acces_user_popover_delete_wizard,calendar.popover.delete.wizard.homework,model_calendar_popover_delete_wizard_homework,base.group_user,1,1,1,1 diff --git a/addons/hr_homeworking/security/security.xml b/addons/hr_homeworking/security/security.xml new file mode 100644 index 00000000000..fe8f1fa9475 --- /dev/null +++ b/addons/hr_homeworking/security/security.xml @@ -0,0 +1,40 @@ + + + + + + + homeworking: own + + + [ + ('employee_id', '=', user.employee_id.id) + ] + + + + homeworking: admin + + + [(1, '=', 1)] + + + + homeworking wizard: own + + + [ + ('hr_employee_location_id.employee_id', '=', user.employee_id.id) + ] + + + + homeworking wizard: admin + + + [(1, '=', 1)] + + + + + diff --git a/addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.js b/addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.js new file mode 100644 index 00000000000..a61514b663b --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.js @@ -0,0 +1,204 @@ +/** @odoo-module **/ + +import { AttendeeCalendarCommonRenderer } from "@calendar/views/attendee_calendar/common/attendee_calendar_common_renderer" +import { patch } from "@web/core/utils/patch"; +import { renderToString } from "@web/core/utils/render"; +patch(AttendeeCalendarCommonRenderer.prototype, "hr_homeworking_calendar_common_renderer", { + get options(){ + let a = Object.assign(this._super(), { + columnHeaderHtml: function(date) { + if (this.props.model.scale === 'week'){ + return "
" + moment(date).format("ddd DD") +"
" + renderToString(this.constructor.HeaderCalendarTemplate, {date: date, today : new Date()}); + } + if (this.props.model.scale === 'day'){ + return "
" + moment(date).format("MMMM DD, YYYY") + "
" + renderToString(this.constructor.HeaderCalendarTemplate, {date: date, today : new Date()}); + } + return "
" + moment(date).format("dddd") + "
"; + } + }); + return Object.assign(a, { + eventOrder: function(event1, event2){ + if (event1.extendedProps.worklocation){ + return -1; + } else { + if(event2.extendedProps.worklocation){ + return 1; + } else { + return event1.title.localeCompare(event2.title); + } + } + } + }) + }, + fcEventToRecord(event) { + const res = this._super(...arguments); + res.homework = event.homework; + return res; + }, + filterWorkRecord(arr, modelUserId){ + let events = [] + if (arr.length === 1){ + events.push(this.convertWorkRecordToEvent(arr[0])) + return events; + } + // sort ASC by title location. If it's equals put the event of the current user at first + arr.sort((a,b)=> (a.title > b.title) ? 1 : (a.title < b.title) ? -1 : a.userId === modelUserId ? -1 : b.userId === modelUserId ? 1 : 0) + let previous_title = ""; + for (const event of arr) { + if (previous_title !== event.title) { + events.push(this.convertWorkRecordToEvent(event)) + previous_title = event.title + } + } + return events; + }, + mapRecordsToEvents() { + let event = this._super(...arguments); + let work = []; + if (this.props.model.multiCalendar) { + for (const day in this.props.model.worklocations) { + let events = []; + const home = this.props.model.worklocations[day].home; + const office = this.props.model.worklocations[day].office; + const other = this.props.model.worklocations[day].other; + const modelUserId = this.env.searchModel.userService.userId; + if (home && home.length > 0) { + events = events.concat(this.filterWorkRecord(home,modelUserId)) + } + if (office && office.length > 0) { + events = events.concat(this.filterWorkRecord(office,modelUserId)) + } + if (other && other.length > 0) { + events = events.concat(this.filterWorkRecord(other,modelUserId)) + } + work.push(...events); + } + } else { + work = Object.values(this.props.model.worklocations) + .filter((wl) => wl.display) + .map((wl) => this.convertWorkRecordToEvent(wl)); + } + return event.concat(work); + }, + convertWorkRecordToEvent(record) { + return { + id: record.id, + title: record.title, + start: record.start.toISO(), + end: record.end.plus({ days: 1}).toISO(), + allDay: record.isAllDay, + icon: record.icon, + colorIndex: record.colorIndex, + worklocation: true, + // to avoid to drag location event + editable: false, + }; + }, + onDayRender(info){ + if (this.props.model.scale === 'week' || this.props.model.scale === 'day'){ + var ParseDate = (date) => date.getFullYear() + "-" +(date.getMonth() <10 ? "0" + (date.getMonth() + 1) : (date.getMonth()+1)) + "-" + (date.getDate() <10 ?"0" + date.getDate() : date.getDate()); + let button = info.view.context.calendar.el.querySelector(".fc-day-header[data-date='" + ParseDate(info.date) + "'] .o_month") + let line = info.view.context.calendar.el.querySelector(".fc-day-header[data-date='" + ParseDate(info.date) + "'] .line") + if (!button || !line) + return; + info.homework = true; + button.onclick = () =>this.onDateClick(info) + line.onclick = () =>this.onDateClick(info) + } + if (this.props.model.scale === 'month'){ + var ParseDate = (date) => date.getFullYear() + "-" +(date.getMonth() <10 ? "0" + (date.getMonth() + 1) : (date.getMonth()+1)) + "-" + (date.getDate() <10 ?"0" + date.getDate() : date.getDate()); + let box = info.view.el.querySelector(".fc-day-top[data-date='" + ParseDate(info.date) + "']") + if (!box) + return; + const content = renderToString(this.constructor.ButtonWorklocationTemplate, {}) + const {children } = new DOMParser().parseFromString(content, "text/html").body + box.appendChild(...children) + } + this._super(...arguments); + }, + onEventRender(info) { + const { el, event } = info; + if (event.extendedProps.worklocation) { + el.classList.add("o_homework_event"); + const multiCalendar = this.props.model.multiCalendar; + let injectedContentStr = ""; + const icon = event.extendedProps.icon; + if (multiCalendar) { + const records = this.props.model.worklocations[info.event.start][icon].filter((rec) => rec.title === event.title); + let iconStr; + if (icon === "home") { + iconStr = "fa-home"; + } + else if (icon === "office") { + iconStr = "fa-building"; + } + else { + iconStr = "fa-map-marker"; + } + if (records) { + const obj = {records: records, iconStr: iconStr, multiCalendar: multiCalendar}; + injectedContentStr = renderToString(this.constructor.WorklocationTemplate, obj); + } + } else { + const record = this.props.model.worklocations[info.event.id]; + if (record) { + injectedContentStr = renderToString(this.constructor.WorklocationTemplate, {...record}); + } + } + const domParser = new DOMParser(); + const { children } = domParser.parseFromString(injectedContentStr, "text/html").body; + el.querySelector(".fc-content").replaceWith(...children); + } else { + this._super(...arguments); + } + }, + onDblClick(info) { + if (info.event.extendedProps.worklocation) { + this.onClick(info); + } else { + this._super(...arguments); + } + }, + onClick(info){ + if (info.event.extendedProps.worklocation){ + const elems = document.elementsFromPoint(info.jsEvent.x, info.jsEvent.y) + const day_elem = elems.find((elem) => elem.classList.contains("fc-day","fc-widget-content")) + const day_elem_date = day_elem.getAttribute("data-date") + let wl; + if (this.props.model.multiCalendar) { + const elem = elems.find((elem) => elem.classList.contains("o_homework_content")) + if (!elem){ + return; + } + const id = parseInt(elem.getAttribute("data-id")); + const icon = info.event.extendedProps.icon; + const date = new Date(day_elem_date + " ") // the empty string set's the time to 0 + wl = this.props.model.worklocations[date][icon].find((location) => location.id === id); + } else { + for (const location of Object.values(this.props.model.worklocations)) { + if (location.start.toISODate() === day_elem_date) { + wl = location; + continue; + } + } + } + this.openPopover(info.el, wl); + this.highlightEvent(info.event, "o_cw_custom_highlight"); + } else { + this._super(...arguments); + } + }, + onDateClick(info){ + if (info.jsEvent && info.jsEvent.target.tagName ==="BUTTON") { + info.homework = true + this.props.createRecord(this.fcEventToRecord(info)); + } else { + this._super(...arguments) + } + } +}, + +AttendeeCalendarCommonRenderer.WorklocationTemplate = "hr.homeworking.CalendarCommonRenderer.worklocation", +AttendeeCalendarCommonRenderer.HeaderCalendarTemplate = "hr.homeworking.CalendarCommonRenderer.buttonWorklocation", +AttendeeCalendarCommonRenderer.ButtonWorklocationTemplate = "hr.homeworking.CalendarCommonRenderer.buttonWorklocation", +) diff --git a/addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.scss b/addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.scss new file mode 100644 index 00000000000..e764d799b24 --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/common/calendar_common_renderer.scss @@ -0,0 +1,89 @@ +.btnWorklocation{ + display:flex ; + align-items: center; + opacity: 0; + &:hover { + opacity: 100; + } + .o_month{ + box-shadow: 0.1em 0.1em 0.1em rgb(131, 126, 126); + border-radius: 100px; + font-size: 0.75em; + width: 50%; + border-color: transparent; + } + + .line{ + box-shadow: 0.1em 0.1em 0.1em rgb(131, 126, 126); + border: none; + border-color: transparent; + height: 0.3px; + width:100%; + } +} + +.fc-event { + box-shadow: none !important; +} + +.fc-event.o_homework_event { + background-color: transparent; + border-color: transparent; + box-shadow: none; + cursor: pointer; + .o_homework_single { + display:flex; + align-items: center; + width: 100%; + .fc-content { + margin: 0 !important; + padding: 3px 6px 3px 6px; + border-radius: 100px; + width: auto; + overflow: visible; + opacity: 1; + font-weight: 600; + } + .line{ + height: 0.3em; + width: 100%; + } + } + + .o_homework_multi { + display: flex; + .fc-content .fa-2x { + display: flex; + align-content: center; + flex-wrap: wrap; + justify-content: center; + height: 20px; + width: 20px; + border-radius: 100%; + font-size: 1.2em; + &.fa-building { + font-size: 1.0em; + } + } + } +} + +@for $i from 1 through length($o-colors-complete) { + $color: nth($o-colors-complete, $i); + .o_popover{ + .wl_color_#{$i - 1} { + color: $color; + } + } + .o_homework_multi { + .wl_color_#{$i - 1} { + color: $color; + background-color: scale-color($color, $lightness: 85%); + } + } + .o_homework_single { + .wl_color_#{$i - 1} { + background-color: scale-color($color, $lightness: 50%); + } + } +} \ No newline at end of file diff --git a/addons/hr_homeworking/static/src/calendar/common/calendar_model.js b/addons/hr_homeworking/static/src/calendar/common/calendar_model.js new file mode 100644 index 00000000000..ce5b449e4c7 --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/common/calendar_model.js @@ -0,0 +1,99 @@ +/** @odoo-module **/ + +import { AttendeeCalendarModel } from "@calendar/views/attendee_calendar/attendee_calendar_model"; +import { serializeDateTime, deserializeDate } from "@web/core/l10n/dates"; +import { patch } from "@web/core/utils/patch"; + +patch(AttendeeCalendarModel.prototype, "calendar_model_calendarApp", { + fetchEventLocation(data) { + let attendeeIds; + const filters = data.filterSections.partner_ids.filters; + if (filters[filters.length - 1].type === "all" && filters[filters.length - 1].active) { + attendeeIds = Object.keys(this.partnerColorMap); + } else { + attendeeIds = data.filterSections.partner_ids.filters + .filter(filter => filter.type !== "all" && filter.value && filter.active) + .map(filter => filter.value) + } + return this.orm.call('res.partner', "get_worklocation", [ + attendeeIds, + serializeDateTime(data.range.start), + serializeDateTime(data.range.end), + ]); + }, + + // returns a map of worklocations, display is used to mark the events that are to be shown in the view. + async loadWorkLocations(data) { + const res = await this.fetchEventLocation(data) + this.multiCalendar = Object.keys(res).length > 1; + const merged = this.multiCalendar ? {} : []; + let previousIcon; + let previousTitle; + let previous = 0; + for (const key in res) { + const rawRecords = res[key]; + for (const rawRecord of rawRecords) { + const normalizedRecord = this.normalizeLocalisationRecord(rawRecord) + if (this.multiCalendar) { + const startDate = normalizedRecord.start.toJSDate() + if (!merged[startDate]) { + merged[startDate] = {} + } + if (!merged[startDate][normalizedRecord.icon]) { + merged[startDate][normalizedRecord.icon] = [] + } + merged[startDate][normalizedRecord.icon].push(normalizedRecord); + } else { + if (rawRecord.icon === previousIcon && rawRecord.title === previousTitle && normalizedRecord.start.diff(merged[previous].end, "days").days == 1) { + merged[previous].end = merged[previous].end.plus({days: 1}); + normalizedRecord.display = false; + } else { + previousIcon = rawRecord.icon; + previousTitle = rawRecord.title; + previous = merged.length; + } + merged.push(normalizedRecord); + } + } + } + return this.multiCalendar ? merged : Object.assign({}, ...merged.map((x) => ({[x.id]: x}))); + }, + + normalizeLocalisationRecord(rawRecord) { + return { + id : rawRecord['id'], + title : rawRecord['title'], + isAllDay: true, + start : deserializeDate(rawRecord['date']), + end : deserializeDate(rawRecord['date']), + duration : 1, + isHatched: false, + isStriked: false, + display: true, + multiCalendar: this.multiCalendar, + icon: rawRecord.icon, + userId: rawRecord.userId, + partner_id: rawRecord.partner_id[0], + colorIndex: this.partnerColorMap[rawRecord.partner_id], + rawRecord, + }; + }, + + get worklocations() { + return this.data.worklocations; + }, + mapPartnersToColor(data) { + return data.filterSections.partner_ids.filters + .filter(filter => filter.type !== "all" && filter.value) + .reduce((map, partner) => ({ ...map, [partner.value]: partner.colorIndex}), {}) + }, + + /** + * @override + */ + async updateData(data){ + await this._super(...arguments) + this.partnerColorMap = this.mapPartnersToColor(data); + data.worklocations = await this.loadWorkLocations(data); + } +}) diff --git a/addons/hr_homeworking/static/src/calendar/common/calender_common_renderer.xml b/addons/hr_homeworking/static/src/calendar/common/calender_common_renderer.xml new file mode 100644 index 00000000000..b7337f66ec9 --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/common/calender_common_renderer.xml @@ -0,0 +1,43 @@ + + + + + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + +
+ + +
+ + +
+ +
+
+ + + +
+ +
+
+ \ No newline at end of file diff --git a/addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.js b/addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.js new file mode 100644 index 00000000000..24cada5a218 --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.js @@ -0,0 +1,50 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { AttendeeCalendarCommonPopover } from "@calendar/views/attendee_calendar/common/attendee_calendar_common_popover"; +import { Field } from "@web/views/fields/field" + +patch(AttendeeCalendarCommonPopover.prototype, "hr_homeworking_calendar_common_popover",{ + setup() { + this.fieldNames = ["work_location_id", "work_location_name", "work_location_type", "employee_id", "weekday", "weekly", "start_date", "employee_name"]; + this._super(...arguments) + }, + isWorkLocationEvent(){ + return this.props.record.rawRecord['resModel'] === 'hr.employee.location'; + }, + get hasFooter() { + return !this.isWorkLocationEvent() || this.props.record.userId === this.user.userId + }, + isCurrentUserIsOwnerWorklocation(){ + return this.isWorkLocationEvent() && this.props.record.userId === this.slot.record.model.user.userId; + }, + get isEventEditable() { + return ('resModel' in this.props.record.rawRecord) || this._super(...arguments) + }, + get isEventViewable() { + return !('resModel' in this.props.record.rawRecord) || this._super(...arguments) + }, + get isEventDeletable() { + return ('resModel' in this.props.record.rawRecord) || this._super(...arguments) + }, + get displayAttendeeAnswerChoice() { + return !('resModel' in this.props.record.rawRecord) && this._super(...arguments) + }, + get isCurrentUserAttendee() { + return !('resModel' in this.props.record.rawRecord) && this._super(...arguments) + } +}) + +AttendeeCalendarCommonPopover.template = "homework.AttendeeCalendarCommonPopover" + + +AttendeeCalendarCommonPopover.subTemplates = { + ...AttendeeCalendarCommonPopover.subTemplates, + body: "homework.AttendeeCalendarCommonPopover.body", + footer: "homework.AttendeeCalendarCommonPopover.footer", +} + +AttendeeCalendarCommonPopover.components = { + ...AttendeeCalendarCommonPopover.components, + Field +} diff --git a/addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.xml b/addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.xml new file mode 100644 index 00000000000..96623bca081 --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/common/popover/calendar_common_popover.xml @@ -0,0 +1,75 @@ + + + + + +
+

+ + + + + + + + + + + + +

+ + + +
+
+ + +
+
+
+ + + + + + + +
    +
    + + + - + + + + + +
    +
    + + +
    +
+
+
+ + + + + + + + + + + + + + + + + +
diff --git a/addons/hr_homeworking/static/src/calendar/hr_homeworking_calendar_controller.js b/addons/hr_homeworking/static/src/calendar/hr_homeworking_calendar_controller.js new file mode 100644 index 00000000000..46ad4db6e34 --- /dev/null +++ b/addons/hr_homeworking/static/src/calendar/hr_homeworking_calendar_controller.js @@ -0,0 +1,82 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { useService } from "@web/core/utils/hooks"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; +import { AttendeeCalendarController } from "@calendar/views/attendee_calendar/attendee_calendar_controller" +import { serializeDate} from "@web/core/l10n/dates"; + +patch(AttendeeCalendarController.prototype, "hr_homeworking_calendar_controller",{ + setup() { + this._super() + this.action = useService("action"); + }, + async editRecord(record, context = {}, shouldFetchFormViewId = true) { + if ('rawRecord' in record && 'date' in record.rawRecord) { + return this.action.doAction('hr_homeworking.hr_employee_location_action',{ + name: this.env._t("Edit Record"), + additionalContext: { + 'default_start_date': serializeDate(record.start), + 'default_end_date_create': serializeDate(record.end), + 'default_work_location_id' : record.rawRecord.location_id, + 'dialog_size': 'medium', + }, + onClose: async (closeInfo) => { + this.model.load() + }, + }); + } + return this._super(...arguments) + }, + createRecord(record) { + if (record.homework) { + return this.action.doAction('hr_homeworking.hr_employee_location_action',{ + name: this.env._t("Create Record"), + additionalContext: { + 'default_start_date': serializeDate(record.start), + 'default_end_date_create': serializeDate(record.start), + 'dialog_size': 'medium', + }, + onClose: async (closeInfo) => { + this.model.load() + }, + }); + } else { + return this._super(record); + } + }, + deleteRecord(record) { + if ('rawRecord' in record && 'date' in record.rawRecord) { + if (record.rawRecord.weekly) { + this.openRecurringDeletionWizardForHomework(record); + } else { + this.displayDialog(ConfirmationDialog, { + title: this.env._t("Confirmation"), + body: this.env._t("Are you sure you want to delete this work location?"), + confirm: () => { + this.orm.call('hr.employee.location', "unlink", [ + record.rawRecord.idInDB, + ]); + this.model.load(); + }, + cancel: () => { + }, + }); + } + } else { + this._super(...arguments) + } + }, + + openRecurringDeletionWizardForHomework(record) { + this.action.doAction('hr_homeworking.hr_popover_delete_homework_action',{ + additionalContext: { + 'default_hr_employee_location_id': record.rawRecord.idInDB, + 'default_start_date': serializeDate(record.start) + }, + onClose: async (closeInfo) => { + this.model.load() + }, + }); + } +}) diff --git a/addons/hr_homeworking/static/src/im_status_patch.xml b/addons/hr_homeworking/static/src/im_status_patch.xml new file mode 100644 index 00000000000..ee2fe7a83a0 --- /dev/null +++ b/addons/hr_homeworking/static/src/im_status_patch.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + $0 + + $0 + + $0 + + + + + + + + + + + + + + + + + $0 + + $0 + + $0 + + + \ No newline at end of file diff --git a/addons/hr_homeworking/static/tests/hr_org_chart_tests.js b/addons/hr_homeworking/static/tests/hr_org_chart_tests.js new file mode 100644 index 00000000000..e01d7c24a83 --- /dev/null +++ b/addons/hr_homeworking/static/tests/hr_org_chart_tests.js @@ -0,0 +1,13 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; + +patch(MockServer.prototype, "hr_homeworking", { + async _performRPC(route, args) { + if (args.method === "get_worklocation") { + return Promise.resolve() + } + return this._super(...arguments); + }, +}); \ No newline at end of file diff --git a/addons/hr_homeworking/tests/__init__.py b/addons/hr_homeworking/tests/__init__.py new file mode 100644 index 00000000000..bc9a0e215dc --- /dev/null +++ b/addons/hr_homeworking/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_employee_location diff --git a/addons/hr_homeworking/tests/common.py b/addons/hr_homeworking/tests/common.py new file mode 100644 index 00000000000..0e946e55f68 --- /dev/null +++ b/addons/hr_homeworking/tests/common.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.tests import common + + +class TestHrHomeworkingCommon(common.TransactionCase): + + @classmethod + def setUpClass(cls): + super(TestHrHomeworkingCommon, cls).setUpClass() + cls.env.user.tz = 'Europe/Brussels' + + # Test users to use through the various tests + cls.user_hruser = mail_new_test_user(cls.env, login='armande', groups='base.group_user,hr.group_hr_user') + cls.user_hruser_id = cls.user_hruser.id + + cls.user_hrmanager = mail_new_test_user(cls.env, login='bastien', groups='base.group_user,hr.group_hr_manager') + cls.user_hrmanager_id = cls.user_hrmanager.id + cls.user_hrmanager.tz = 'Europe/Brussels' + + cls.user_employee = mail_new_test_user(cls.env, login='david', groups='base.group_user') + cls.user_employee_id = cls.user_employee.id + + # Hr Data + Department = cls.env['hr.department'].with_context(tracking_disable=True) + WorkLocation = cls.env['hr.work.location'].with_context(tracking_disable=True) + main_partner_id = cls.env.ref('base.main_partner') + cls.hr_dept = Department.create({ + 'name': 'Human Resources', + }) + cls.rd_dept = Department.create({ + 'name': 'Research and devlopment', + }) + + cls.work_office_1 = WorkLocation.create({ + 'name': "Bureau 1", + 'location_type': "office", + 'address_id': main_partner_id.id, + }) + + cls.work_office_2 = WorkLocation.create({ + 'name': "Bureau 2", + 'location_type': "office", + 'address_id': main_partner_id.id, + }) + + cls.work_home = WorkLocation.create({ + 'name': "Maison", + 'location_type': "home", + 'address_id': main_partner_id.id, + }) + + cls.employee_emp = cls.env['hr.employee'].create({ + 'name': 'David Employee', + 'user_id': cls.user_employee_id, + 'department_id': cls.rd_dept.id, + }) + cls.employee_emp_id = cls.employee_emp.id + + cls.employee_hruser = cls.env['hr.employee'].create({ + 'name': 'Armande HrUser', + 'user_id': cls.user_hruser_id, + 'department_id': cls.rd_dept.id, + }) + cls.employee_hruser_id = cls.employee_hruser.id + + cls.employee_hrmanager = cls.env['hr.employee'].create({ + 'name': 'Bastien HrManager', + 'user_id': cls.user_hrmanager_id, + 'department_id': cls.hr_dept.id, + 'parent_id': cls.employee_hruser_id, + }) + cls.employee_hrmanager_id = cls.employee_hrmanager.id + + cls.rd_dept.write({'manager_id': cls.employee_hruser_id}) diff --git a/addons/hr_homeworking/tests/test_hr_employee_location.py b/addons/hr_homeworking/tests/test_hr_employee_location.py new file mode 100644 index 00000000000..0f17378492a --- /dev/null +++ b/addons/hr_homeworking/tests/test_hr_employee_location.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.hr_homeworking.tests.common import TestHrHomeworkingCommon + +from odoo.tests import tagged +from freezegun import freeze_time +from datetime import datetime + +@tagged('post_install', '-at_install', "homeworking_tests") +class TestHrHomeworkingHrEmployeeLocation(TestHrHomeworkingCommon): + @classmethod + def setUpClass(cls): + super(TestHrHomeworkingHrEmployeeLocation, cls).setUpClass() + cls.HrEmployeeLocation = cls.env['hr.employee.location'] + + @freeze_time('2023-05-16') + def test_register_location_in_the_futur_with_a_previous_worklocation(self): + with freeze_time('2023-05-01'): + self.HrEmployeeLocation.create({ + # tuesday + 'start_date': '2023-05-02', + 'end_date_create': '2023-05-02', + 'work_location_id': self.work_office_1.id, + 'employee_id': self.employee_hruser_id, + 'weekly': True, + }) + self.HrEmployeeLocation.create({ + # tuesday + 'start_date': '2023-05-16', + 'end_date_create': '2023-05-16', + 'work_location_id': self.work_office_1.id, + 'employee_id': self.employee_hruser_id, + 'weekly': True, + }) + self.env['hr.employee.location'].flush_model(['weekday']) + location_for_emp = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_hruser_id), + ]) + self.assertEqual(len(location_for_emp.ids), 2) + location_for_emp_weekly = location_for_emp.filtered(lambda wl: wl.weekly) + self.assertEqual(len(location_for_emp_weekly.ids), 2) + @freeze_time('2023-05-16') + def test_register_location_in_the_futur(self): + # register a location for just one day (wednesday > today) + self.HrEmployeeLocation.create({ + # wednesday + 'start_date': '2023-05-17', + 'end_date_create': '2023-05-17', + 'work_location_id': self.work_office_1.id, + 'employee_id': self.employee_emp_id, + }) + self.env['hr.employee.location'].flush_model(['weekday']) + location_for_emp = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_emp_id), + ]) + self.assertEqual(len(location_for_emp.ids), 1) + + # register a location for just two das (thursday, friday > today) + self.HrEmployeeLocation.create({ + # thursday + 'start_date': '2023-05-18', + # friday + 'end_date_create': '2023-05-19', + 'work_location_id': self.work_office_2.id, + 'employee_id': self.employee_emp_id, + }) + self.env['hr.employee.location'].flush_model(['weekday']) + location_for_emp = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_emp_id), + ]) + self.assertEqual(len(location_for_emp.ids), 3) + + # register a location for every saturday (saturday > today) + self.HrEmployeeLocation.create({ + # saturday + 'start_date': '2023-05-20', + 'end_date_create': '2023-05-20', + 'work_location_id': self.work_office_1.id, + 'employee_id': self.employee_emp_id, + 'weekly': True, + }) + self.env['hr.employee.location'].flush_model(['weekday']) + location_for_emp = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_emp_id), + ]) + self.assertEqual(len(location_for_emp.ids), 4) + self.assertFalse(self.employee_emp.monday_location_id) + self.assertFalse(self.employee_emp.tuesday_location_id) + self.assertFalse(self.employee_emp.wednesday_location_id) + self.assertFalse(self.employee_emp.thursday_location_id) + self.assertFalse(self.employee_emp.friday_location_id) + self.assertEqual(self.employee_emp.saturday_location_id, self.work_office_1) + self.assertFalse(self.employee_hruser.sunday_location_id) + + @freeze_time('2023-05-05') + def test_register_location_for_today(self): + # register a location for just one day (friday == today) + record_N_weekly_one_day = self.HrEmployeeLocation.create({ + # friday + 'start_date': '2023-05-05', + 'end_date_create': '2023-05-05', + 'work_location_id': self.work_office_1.id, + 'employee_id': self.employee_hruser_id, + }) + self.env['hr.employee.location'].flush_model(['weekday']) + self.assertFalse(self.employee_hruser.monday_location_id) + self.assertFalse(self.employee_hruser.tuesday_location_id) + self.assertFalse(self.employee_hruser.wednesday_location_id) + self.assertFalse(self.employee_hruser.thursday_location_id) + self.assertFalse(self.employee_hruser.friday_location_id) + self.assertFalse(self.employee_hruser.saturday_location_id) + self.assertFalse(self.employee_hruser.sunday_location_id) + self.assertFalse(record_N_weekly_one_day.weekday) + self.assertFalse(record_N_weekly_one_day.weekly) + + # register a location for 3 days (friday == today) + self.HrEmployeeLocation.create({ + # friday + 'start_date': '2023-05-05', + # sunday + 'end_date_create': '2023-05-07', + 'work_location_id': self.work_office_2.id, + 'employee_id': self.employee_hruser_id, + }) + self.env['hr.employee.location'].flush_model(['weekday']) + location_for_emp_hruser = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_hruser_id), + ]) + friday_location = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_hruser_id), + ("start_date", "=", datetime(2023, 5, 5)), + ]).work_location_id + self.assertEqual(friday_location, self.work_office_2) + self.assertEqual(len(location_for_emp_hruser.ids), 3) + + # register a location for every friday (friday == today) + self.HrEmployeeLocation.create({ + # friday + 'start_date': '2023-05-05', + 'end_date_create': '2023-05-05', + 'employee_id': self.employee_hruser_id, + 'work_location_id': self.work_home.id, + 'weekly': True + }) + self.env['hr.employee.location'].flush_model(['weekday']) + self.assertFalse(self.employee_hruser.monday_location_id) + self.assertFalse(self.employee_hruser.tuesday_location_id) + self.assertFalse(self.employee_hruser.wednesday_location_id) + self.assertFalse(self.employee_hruser.thursday_location_id) + self.assertEqual(self.employee_hruser.friday_location_id, self.work_home) + self.assertFalse(self.employee_hruser.saturday_location_id) + self.assertFalse(self.employee_hruser.sunday_location_id) + location_for_emp_hruser = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_hruser_id), + ]) + friday_locations = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_hruser_id), + ("start_date", "=", datetime(2023, 5, 5)), + ]) + # the exceptional worlocation is replaced by the default worklocation + self.assertEqual(len(location_for_emp_hruser.ids), 3) + self.assertEqual(len(friday_locations.ids), 1) + + # register a location for every wednesday, thursday and friday (the start of wednesday and thursday are in the past) + # expected behavior: + # 3 weekly record (for wednesday, thurday, friday) + # 4 exceptionnal record (wednesday and thursday before today) + (saturday and sunday) + self.HrEmployeeLocation.create({ + # wednesday + 'start_date': '2023-05-03', + # friday + 'end_date_create': '2023-05-05', + 'employee_id': self.employee_hruser_id, + 'work_location_id': self.work_office_1.id, + 'weekly': True + }) + self.env['hr.employee.location'].flush_model(['weekday']) + self.assertFalse(self.employee_hruser.monday_location_id) + self.assertFalse(self.employee_hruser.tuesday_location_id) + self.assertEqual(self.employee_hruser.wednesday_location_id, self.work_office_1) + self.assertEqual(self.employee_hruser.thursday_location_id, self.work_office_1) + self.assertEqual(self.employee_hruser.friday_location_id, self.work_office_1) + self.assertFalse(self.employee_hruser.saturday_location_id) + self.assertFalse(self.employee_hruser.sunday_location_id) + location_for_emp_hruser = self.HrEmployeeLocation.search([ + ("employee_id", "=", self.employee_hruser_id), + ]) + location_exceptional = location_for_emp_hruser.filtered(lambda wl: not wl.weekly) + self.assertEqual(len(location_for_emp_hruser.ids), 7) + self.assertEqual(len(location_exceptional), 4) diff --git a/addons/hr_homeworking/views/hr_employee_views.xml b/addons/hr_homeworking/views/hr_employee_views.xml new file mode 100644 index 00000000000..15341767ca7 --- /dev/null +++ b/addons/hr_homeworking/views/hr_employee_views.xml @@ -0,0 +1,98 @@ + + + + view.employee.form.inherit.hr + hr.employee + + + + + + + + + + view.employee.form.inherit.hr + hr.employee + + + + 1 + + + + Specify your default work location for each day of the week. This schedules will repeat itself each week. + + + + + + + + + + + + + + hr.employee.tree.timesheet + hr.employee + + + + + + + 1 + + + + + + hr.employee.kanban.inherit.homeworking + hr.employee + + + + + + + + + + + + + + + + + + + + + hr.employee.kanban.inherit.homeworking + hr.employee.public + + + + + + + + + + + + + + + + + diff --git a/addons/hr_homeworking/views/hr_homeworking_views.xml b/addons/hr_homeworking/views/hr_homeworking_views.xml new file mode 100644 index 00000000000..9f1a28ef57a --- /dev/null +++ b/addons/hr_homeworking/views/hr_homeworking_views.xml @@ -0,0 +1,40 @@ + + + + hr.employee.location.view.form + hr.employee.location + +
+ + +
+
+
+
+
+ + + hr.employee.location + form + + + new + +
diff --git a/addons/hr_homeworking/views/res_users.xml b/addons/hr_homeworking/views/res_users.xml new file mode 100644 index 00000000000..74bb1836203 --- /dev/null +++ b/addons/hr_homeworking/views/res_users.xml @@ -0,0 +1,24 @@ + + + + + res.users.preferences.form.inherit + res.users + + + + + Specify your default work location for each day of the week. This schedules will repeat itself each week. + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/hr_homeworking/wizard/__init__.py b/addons/hr_homeworking/wizard/__init__.py new file mode 100644 index 00000000000..db9df116b87 --- /dev/null +++ b/addons/hr_homeworking/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import calendar_popover_delete_wizard_homework diff --git a/addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.py b/addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.py new file mode 100644 index 00000000000..8d078f7ba19 --- /dev/null +++ b/addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class CalendarPopoverDeleteWizardHomework(models.TransientModel): + _name = 'calendar.popover.delete.wizard.homework' + _description = 'Calendar Popover Delete Wizard Homework' + + hr_employee_location_id = fields.Many2one('hr.employee.location', required=True) + start_date = fields.Date(required=True) + delete = fields.Selection([ + ('one', "Delete only this day's work location"), + ('all', "Delete this work location for everyweek")], + default='one', required=True) + + def remove_default_work_location(self): + self.ensure_one() + if self.delete == 'one': + self.hr_employee_location_id.add_removed_work_location(self.start_date) + else: + self.hr_employee_location_id.delete_default_worklocation() diff --git a/addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.xml b/addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.xml new file mode 100644 index 00000000000..e7f5697b1d3 --- /dev/null +++ b/addons/hr_homeworking/wizard/calendar_popover_delete_wizard_homework.xml @@ -0,0 +1,25 @@ + + + + calendar.popover.delete.wizard.homework.view.form + calendar.popover.delete.wizard.homework + +
+ +
+
+ +
+
+ + + Location wizard + calendar.popover.delete.wizard.homework + form + + + new + +
diff --git a/addons/hr_recruitment/views/hr_applicant_views.xml b/addons/hr_recruitment/views/hr_applicant_views.xml index ac346bafd88..5f98f59ae7d 100644 --- a/addons/hr_recruitment/views/hr_applicant_views.xml +++ b/addons/hr_recruitment/views/hr_applicant_views.xml @@ -582,7 +582,7 @@ - + diff --git a/addons/test_discuss_full/tests/test_performance.py b/addons/test_discuss_full/tests/test_performance.py index 0c691a9f844..bc396434783 100644 --- a/addons/test_discuss_full/tests/test_performance.py +++ b/addons/test_discuss_full/tests/test_performance.py @@ -1016,4 +1016,4 @@ class TestDiscussFullPerformance(TransactionCase): Returns the expected query count. The point of having a separate getter is to allow it to be overriden. """ - return 66 + return 68