diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index 4c6f4cbf43082f..2da6a5f0d87ec1 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -222,13 +222,9 @@ def _lang_get(self): } """ - permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user", tracking=True) visa_no = fields.Char('Visa No', groups="hr.group_hr_user", tracking=True) visa_expire = fields.Date('Visa Expiration Date', groups="hr.group_hr_user", tracking=True) - work_permit_expiration_date = fields.Date('Work Permit Expiration Date', groups="hr.group_hr_user", tracking=True) has_work_permit = fields.Binary(string="Work Permit", groups="hr.group_hr_user") - work_permit_scheduled_activity = fields.Boolean(default=False, groups="hr.group_hr_user") - work_permit_name = fields.Char('work_permit_name', compute='_compute_work_permit_name', groups="hr.group_hr_user") certificate = fields.Selection(selection='_get_certificate_selection', string='Certificate Level', groups="hr.group_hr_user", tracking=True) study_field = fields.Char("Field of Study", groups="hr.group_hr_user", tracking=True) emergency_contact = fields.Char(groups="hr.group_hr_user", tracking=True) @@ -649,8 +645,7 @@ def _compute_split_legal_name(self): @api.depends('current_version_id') @api.depends_context('version_id') def _compute_version_id(self): - context_version_id = self.env.context.get('version_id', False) - context_version = self.env['hr.version'].browse(context_version_id).exists() if context_version_id else self.env['hr.version'] + context_version = self.env['hr.version'].browse(self.env.context.get('version_id', False)) for employee in self: if context_version.employee_id == self: @@ -1197,13 +1192,6 @@ def _compute_birthday_public_display_string(self): else: employee.birthday_public_display_string = "hidden" - @api.depends('name', 'permit_no') - def _compute_work_permit_name(self): - for employee in self: - name = employee.name.replace(' ', '_') + '_' if employee.name else '' - permit_no = '_' + employee.permit_no if employee.permit_no else '' - employee.work_permit_name = "%swork_permit%s" % (name, permit_no) - def _get_partner_count_depends(self): return ['user_id'] diff --git a/addons/hr/models/hr_version.py b/addons/hr/models/hr_version.py index 9e2b824741da36..f064d51f7b606e 100644 --- a/addons/hr/models/hr_version.py +++ b/addons/hr/models/hr_version.py @@ -88,6 +88,11 @@ def _get_hr_responsible_domain(self): ('other', 'Other'), ], groups="hr.group_hr_user", tracking=1, help="This is the legal sex as recognized by the state, used for official and statutory purposes.") + permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user") + work_permit_expiration_date = fields.Date('Work Permit Expiration Date', groups="hr.group_hr_user") + work_permit_scheduled_activity = fields.Boolean(default=False, groups="hr.group_hr_user") + work_permit_name = fields.Char('work_permit_name', compute='_compute_work_permit_name', groups="hr.group_hr_user") + private_street = fields.Char(string="Private Street", groups="hr.group_hr_user", tracking=1) private_street2 = fields.Char(string="Private Street2", groups="hr.group_hr_user", tracking=1) private_city = fields.Char(string="Private City", groups="hr.group_hr_user", tracking=1) @@ -243,6 +248,13 @@ def _compute_allowed_country_state_ids(self): for version in versions_without_countries: version.allowed_country_state_ids = states + @api.depends('employee_id.name', 'permit_no') + def _compute_work_permit_name(self): + for version in self: + emp_name = version.employee_id.name.replace(' ', '_') + '_' if version.employee_id and version.employee_id.name else '' + permit_no = '_' + version.permit_no if version.permit_no else '' + version.work_permit_name = f"{emp_name}work_permit{permit_no}" + @api.constrains('employee_id', 'contract_date_start', 'contract_date_end') def _check_dates(self): version_read_group = self.env['hr.version'].sudo()._read_group( diff --git a/addons/hr/static/src/components/button_new_contract/button_new_contract.xml b/addons/hr/static/src/components/button_new_contract/button_new_contract.xml index c3109a1bfee67c..8ba725c044d98e 100644 --- a/addons/hr/static/src/components/button_new_contract/button_new_contract.xml +++ b/addons/hr/static/src/components/button_new_contract/button_new_contract.xml @@ -3,7 +3,7 @@ New Contract + t-ref="this.datetimePickerTargetRef" t-if="this.props.record.resId">New Contract diff --git a/addons/hr/tests/test_hr_version.py b/addons/hr/tests/test_hr_version.py index 2e6c241a4a615e..26190aa4d07201 100644 --- a/addons/hr/tests/test_hr_version.py +++ b/addons/hr/tests/test_hr_version.py @@ -523,27 +523,6 @@ def test_multi_edit_other_and_contract_date_sync(self): self.assertEqual(version.job_id.id, jobB.id) self.assertEqual(version.contract_date_end, date(2020, 9, 30)) - def test_delete_version(self): - employee = self.env['hr.employee'].create({ - 'name': 'John Doe', - 'date_version': '2020-01-01', - }) - v1 = employee.version_id - v2 = employee.create_version({ - 'date_version': '2021-01-01', - }) - v3 = employee.create_version({ - 'date_version': '2022-01-01', - }) - self.assertEqual(employee.current_version_id, v3) - - v3.unlink() - self.assertEqual(employee.current_version_id, v2) - v1.unlink() - self.assertEqual(employee.current_version_id, v2) - with self.assertRaises(ValidationError): - v2.unlink() - def test_multi_edit_multi_employees_no_contract(self): """ Test the multi-edit when there is one version per employee, without contract diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml index 6bc745d2a06895..69fda10997f59c 100644 --- a/addons/hr/views/hr_employee_views.xml +++ b/addons/hr/views/hr_employee_views.xml @@ -383,7 +383,7 @@ to - + diff --git a/addons/hr_holidays/static/src/components/accrual_level/accrual_levels.scss b/addons/hr_holidays/static/src/components/accrual_level/accrual_levels.scss index 8e052ffe5bbf7d..f6a930fd221024 100644 --- a/addons/hr_holidays/static/src/components/accrual_level/accrual_levels.scss +++ b/addons/hr_holidays/static/src/components/accrual_level/accrual_levels.scss @@ -10,7 +10,7 @@ } .o_accrual { - .o_field_accrual, .o_field_selection, .o_field_day_selection, .o_field_filterable_selection { + .o_field_accrual, .o_field_selection, .o_field_filterable_selection { width: fit-content !important; &:not(.o_readonly_modifier) > *:first-child { @@ -20,7 +20,7 @@ field-sizing: content; } - &:not(.o_field_selection, .o_field_day_selection, .o_field_filterable_selection) > *:first-child { + &:not(.o_field_selection, .o_field_filterable_selection) > *:first-child { max-width: 8ch; } } diff --git a/addons/hr_holidays/static/src/components/day_selection/day_selection.js b/addons/hr_holidays/static/src/components/day_selection/day_selection.js deleted file mode 100644 index 5a599d35ae7ab2..00000000000000 --- a/addons/hr_holidays/static/src/components/day_selection/day_selection.js +++ /dev/null @@ -1,46 +0,0 @@ -import { t, useProps } from "@odoo/owl"; -import { registry } from "@web/core/registry"; -import { - SelectionField, - selectionField, - selectionFieldProps, -} from "@web/views/fields/selection/selection_field"; - -export class DaySelectionField extends SelectionField { - props = useProps({ - ...selectionFieldProps, - monthField: t.string(), - }); - /** - * @override - * return the available days in the carryover_month - * e.g. February -> [1, 29], april -> [1, 30] - */ - get options() { - let options = super.options; - const carryover_month = this.props.record.data[this.props.monthField]; - // lastDay is the last day of the current_month for the leap year 2020 - const lastDay = new Date(2020, carryover_month, 0).getDate(); - options = options.filter((option) => option[0] <= lastDay); - return options; - } -} - -export const daySelectionField = { - ...selectionField, - component: DaySelectionField, - extractProps({ attrs }) { - return { - ...selectionField.extractProps(...arguments), - monthField: attrs.month_field, - }; - }, - fieldDependencies: ({ attrs }) => [ - { - name: attrs.month_field, - type: "selection", - }, - ], -}; - -registry.category("fields").add("day_selection", daySelectionField); diff --git a/addons/hr_holidays/static/src/components/dynamic_day_selection/dynamic_day_selection.js b/addons/hr_holidays/static/src/components/dynamic_day_selection/dynamic_day_selection.js new file mode 100644 index 00000000000000..1f2d478c2ce1f3 --- /dev/null +++ b/addons/hr_holidays/static/src/components/dynamic_day_selection/dynamic_day_selection.js @@ -0,0 +1,55 @@ +import { t, useProps } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { SelectionField, selectionField, selectionFieldProps } from "@web/views/fields/selection/selection_field"; + +export class DynamicDaySelectionField extends SelectionField { + props = useProps({ + ...selectionFieldProps, + monthFieldName: t.string().optional(), + }); + + get options() { + const allOptions = super.options; + const monthFieldName = this.props.monthFieldName; + const monthValue = this.props.record.data[monthFieldName]; + + if (!monthValue) { + return allOptions; + } + + // Leap year so that February always gets 29 days. + const maxDays = new Date(2004, monthValue, 0).getDate(); + + const filteredOptions = allOptions.filter(option => { + if (!option[0]) { + return true; + } + return parseInt(option[0]) <= maxDays; + }); + + return filteredOptions; + } +} + +export const dynamicDaySelection = { + ...selectionField, + component: DynamicDaySelectionField, + + extractProps({ attrs }) { + return { + ...selectionField.extractProps(...arguments), + monthFieldName: attrs.month_field, + }; + }, + + fieldDependencies: ({ attrs }) => { + return [ + { + name: attrs.month_field, + type: "selection", + }, + ]; + }, +}; + +registry.category("fields").add("dynamic_day_selection", dynamicDaySelection); diff --git a/addons/hr_holidays/tests/test_hr_work_entry_type.py b/addons/hr_holidays/tests/test_hr_work_entry_type.py index aca98678c86190..035000638c6d0b 100644 --- a/addons/hr_holidays/tests/test_hr_work_entry_type.py +++ b/addons/hr_holidays/tests/test_hr_work_entry_type.py @@ -232,3 +232,25 @@ def test_change_count_days_as(self): with self.assertRaises(ValidationError): work_entry_type.count_days_as = 'working' + + def test_time_off_type_dropdown_search(self): + """ Test that loading the time off types (hr.work.entry.type) dropdown list + does not raise any traceback during search or selection. + """ + self.env['hr.work.entry.type'].create({ + 'name': 'Test Paid Time Off', + 'code': 'TPTO', + 'requires_allocation': True, + 'request_unit': 'day', + 'unit_of_measure': 'day', + }) + + domain = [('virtual_remaining_leaves', '>', 0)] + + res = self.env['hr.work.entry.type'].with_user(self.user_employee_id).name_search( + name='', + domain=domain, + operator='ilike', + limit=8, + ) + self.assertIsNotNone(res) diff --git a/addons/hr_holidays/views/hr_leave_accrual_views.xml b/addons/hr_holidays/views/hr_leave_accrual_views.xml index 3fe5368e17ac9a..e769c93e773281 100644 --- a/addons/hr_holidays/views/hr_leave_accrual_views.xml +++ b/addons/hr_holidays/views/hr_leave_accrual_views.xml @@ -36,17 +36,17 @@ on the - + of and the - + of on the - + of @@ -204,8 +204,8 @@ options="{'links': {'other': 'carryover_custom_date'}, 'observe': 'carryover'}"/> : the - + of diff --git a/addons/hr_skills/__manifest__.py b/addons/hr_skills/__manifest__.py index b696abb334f5d9..c0d52672c3dc32 100644 --- a/addons/hr_skills/__manifest__.py +++ b/addons/hr_skills/__manifest__.py @@ -22,6 +22,7 @@ 'data/ir_actions_server_data.xml', 'data/ir_cron_data.xml', 'data/report_paperformat.xml', + 'report/hr_employee_certification_report_views.xml', 'report/hr_employee_skill_history_report_views.xml', 'report/hr_employee_skill_report_views.xml', 'report/hr_employee_cv_report.xml', diff --git a/addons/hr_skills/report/__init__.py b/addons/hr_skills/report/__init__.py index 8e82b9a1d6f113..bad5c5e9967fe7 100644 --- a/addons/hr_skills/report/__init__.py +++ b/addons/hr_skills/report/__init__.py @@ -1,5 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import hr_employee_certification_report from . import hr_employee_cv_report from . import hr_employee_skill_history_report from . import hr_employee_skill_report diff --git a/addons/hr_skills/report/hr_employee_certification_report.py b/addons/hr_skills/report/hr_employee_certification_report.py new file mode 100644 index 00000000000000..b70b034e5db953 --- /dev/null +++ b/addons/hr_skills/report/hr_employee_certification_report.py @@ -0,0 +1,47 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, tools + + +class HrEmployeeCertificationReport(models.BaseModel): + _name = 'hr.employee.certification.report' + _auto = False + _inherit = ["hr.manager.department.report"] + _description = 'Employee Certification Report' + _order = 'employee_id, level_progress desc' + + company_id = fields.Many2one('res.company', readonly=True) + department_id = fields.Many2one('hr.department', readonly=True) + + skill_id = fields.Many2one('hr.skill', readonly=True) + skill_type_id = fields.Many2one('hr.skill.type', readonly=True) + skill_level = fields.Char(readonly=True) + level_progress = fields.Float(readonly=True, aggregator='avg') + active = fields.Boolean(readonly=False) + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + + self.env.cr.execute(""" + CREATE OR REPLACE VIEW %(table)s AS ( + SELECT + row_number() OVER () AS id, + e.id AS employee_id, + e.company_id AS company_id, + v.department_id AS department_id, + s.skill_id AS skill_id, + s.skill_type_id AS skill_type_id, + sl.level_progress / 100.0 AS level_progress, + sl.name AS skill_level, + (s.valid_to IS NULL OR s.valid_to >= '%(date)s') AND s.valid_from <= '%(date)s' AS active + FROM hr_employee e + LEFT JOIN hr_version v ON e.current_version_id = v.id + LEFT OUTER JOIN hr_employee_skill s ON e.id = s.employee_id + LEFT OUTER JOIN hr_skill_level sl ON sl.id = s.skill_level_id + LEFT OUTER JOIN hr_skill_type st ON st.id = sl.skill_type_id + WHERE e.active AND st.active IS True AND st.is_certification IS TRUE + ) + """ % { + 'table': self._table, + 'date': fields.Date.context_today(self) + }) diff --git a/addons/hr_skills/report/hr_employee_certification_report_views.xml b/addons/hr_skills/report/hr_employee_certification_report_views.xml new file mode 100644 index 00000000000000..079ef1f790fd91 --- /dev/null +++ b/addons/hr_skills/report/hr_employee_certification_report_views.xml @@ -0,0 +1,71 @@ + + + + hr.employee.certification.report + + + + + + + + + + + + hr.employee.certification.report + + + + + + + + + + + + + hr.employee.certification.report + + + + + + + + + + + + + + + + + + + + Certification + hr.employee.certification.report + + list,pivot + { + 'search_default_employee': 1, + } + + + + This report will give you an overview of the certification per Employee. + Create them in configuration and add them on the Employee. + + + + + + diff --git a/addons/hr_skills/report/hr_employee_skill_report_views.xml b/addons/hr_skills/report/hr_employee_skill_report_views.xml index 86b997da5ffbbe..bddca497eb6efa 100644 --- a/addons/hr_skills/report/hr_employee_skill_report_views.xml +++ b/addons/hr_skills/report/hr_employee_skill_report_views.xml @@ -89,6 +89,6 @@ id="hr_employee_skill_inventory_report_menu" name="Skills Inventory" action="hr_employee_skill_report_action" - parent="hr.hr_menu_hr_reports" + parent="hr_skills.hr_employee_skill_report_menu" sequence="15"/> diff --git a/addons/hr_skills/views/hr_views.xml b/addons/hr_skills/views/hr_views.xml index 618511b765c046..a59eff8f1fdd11 100644 --- a/addons/hr_skills/views/hr_views.xml +++ b/addons/hr_skills/views/hr_views.xml @@ -595,7 +595,7 @@ hr.employee.skill certifications [('is_certification', '=', True), ('company_id', 'in', allowed_company_ids)] - {'show_employee': True} + {'show_employee': True, 'search_default_group_by_type': 1} list,form + +
+
+ This report will give you an overview of the certification per Employee. + Create them in configuration and add them on the Employee. +