diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py
index f9d74830bd0ae..14a81f967dedf 100644
--- a/addons/hr/models/hr_employee.py
+++ b/addons/hr/models/hr_employee.py
@@ -579,8 +579,7 @@ def _compute_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:
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 8abdd473fe7e2..0c640d5b88d4c 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 @@
+ t-ref="datetime-picker-target-new-contract" t-if="props.record.resId">New Contract
diff --git a/addons/hr/tests/test_hr_version.py b/addons/hr/tests/test_hr_version.py
index d23cfcbf63131..92fb938aa5863 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 8b7d13fa38ee9..fa457799d554f 100644
--- a/addons/hr/views/hr_employee_views.xml
+++ b/addons/hr/views/hr_employee_views.xml
@@ -360,7 +360,7 @@
to
-
+
diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py
index 98fddbbd93ddf..a5441a565e9b4 100644
--- a/addons/hr_holidays/models/hr_leave_type.py
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -275,14 +275,15 @@ def _search_max_leaves(self, operator, value):
return [('id', 'in', valid_leaves)]
def _search_virtual_remaining_leaves(self, operator, value):
- def is_valid(leave_type):
- return not leave_type.requires_allocation or op(leave_type.virtual_remaining_leaves, value)
op = PY_OPERATORS.get(operator)
if not op:
return NotImplemented
if operator != 'in':
value = float(value)
leave_types = self.env['hr.leave.type'].search([])
+
+ def is_valid(leave_type):
+ return not leave_type.requires_allocation or op(leave_type.virtual_remaining_leaves, value)
return [('id', 'in', leave_types.filtered(is_valid).ids)]
@api.depends_context('uid', 'employee_id', 'default_employee_id', 'leave_date_from', 'default_date_from')
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 9cec634e1b2dd..5ea81942c9be6 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
index 05288069a543b..5795a61d7a354 100644
--- a/addons/hr_holidays/static/src/components/day_selection/day_selection.js
+++ b/addons/hr_holidays/static/src/components/day_selection/day_selection.js
@@ -1,23 +1,19 @@
import { registry } from "@web/core/registry";
-import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field";
+import { selectionField, SelectionField } from "@web/views/fields/selection/selection_field";
export class DaySelectionField extends SelectionField {
static props = {
...SelectionField.props,
- monthField: String,
+ monthField: { type: 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;
+ const options = super.options;
+ const monthNumber = this.props.record.data[this.props.monthField];
+
+ // Use 2024 to get 29 days for February since it's a leap year
+ const lastDay = new Date(2024, parseInt(monthNumber), 0).getDate();
+ return options.filter((options) => parseInt(options[0]) <= lastDay);
}
}
@@ -30,12 +26,6 @@ export const daySelectionField = {
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/tests/tours/time_off_accrual_date_filter_tour.js b/addons/hr_holidays/static/tests/tours/time_off_accrual_date_filter_tour.js
new file mode 100644
index 0000000000000..3446296d79b6d
--- /dev/null
+++ b/addons/hr_holidays/static/tests/tours/time_off_accrual_date_filter_tour.js
@@ -0,0 +1,189 @@
+import { registry } from "@web/core/registry";
+import { stepUtils } from "@web_tour/tour_utils";
+
+registry.category("web_tour.tours").add("time_off_accrual_date_filter_tour", {
+ url: "/odoo",
+ steps: () => [
+ stepUtils.showAppsMenuItem(),
+ // Open Time Off app and create new accrual plan
+ {
+ content: "Open Time Off app",
+ trigger: '.o_app[data-menu-xmlid="hr_holidays.menu_hr_holidays_root"]',
+ run: "click",
+ },
+ {
+ content: "Open Configuration",
+ trigger:
+ '.o_menu_sections [data-menu-xmlid="hr_holidays.menu_hr_holidays_configuration"]',
+ run: "click",
+ },
+ {
+ content: "Open Accrual Plans",
+ trigger:
+ '.dropdown-item[data-menu-xmlid="hr_holidays.hr_holidays_accrual_menu_configuration"]',
+ run: "click",
+ },
+ {
+ content: "Click the New button to create a plan",
+ trigger: ".o_list_button_add",
+ run: "click",
+ },
+ {
+ content: "Add a milestone",
+ trigger: 'button[name="action_create_accrual_plan_level"]',
+ run: "click",
+ },
+ {
+ content: "Open the frequency dropdown menu",
+ trigger: '.o_field_widget[name="frequency"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Wait for the menu to open",
+ trigger: ".o_select_menu_menu",
+ },
+ {
+ content: "Select the Yearly frequency type",
+ trigger: '.o_select_menu_item[data-choice-index="6"]',
+ run: "click",
+ },
+ // Check the number of days available for January (1-31)
+ {
+ content: "Open the month selection menu",
+ trigger: '.o_field_widget[name="yearly_month"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Select January from the list of months",
+ trigger: '.o_select_menu_item[data-choice-index="0"]',
+ run: function (actions) {
+ actions.click();
+ },
+ },
+ {
+ content: "Wait for dropdown to disappear",
+ trigger: "body:not(:has(.o_select_menu_menu))",
+ },
+ {
+ content: "Open the days menu",
+ trigger: '.o_field_widget[name="yearly_day"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Verify January shows day 31",
+ trigger: ".o_select_menu_menu:has(.o_select_menu_item:contains('31'))",
+ },
+ {
+ content: "Verify January shows day 30",
+ trigger: ".o_select_menu_menu:has(.o_select_menu_item:contains('30'))",
+ },
+ {
+ content: "Select the last day from the list of months",
+ trigger: '.o_select_menu_item[data-choice-index="30"]',
+ run: function (actions) {
+ actions.click();
+ },
+ },
+ {
+ content: "Wait for dropdown to disappear",
+ trigger: "body:not(:has(.o_select_menu_menu))",
+ },
+ // Check the number of days available for February (1-29)
+ {
+ content: "Open the month selection menu",
+ trigger: '.o_field_widget[name="yearly_month"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Select February from the list of months",
+ trigger: '.o_select_menu_item[data-choice-index="1"]',
+ run: function (actions) {
+ actions.click();
+ },
+ },
+ {
+ content: "Wait for dropdown to disappear",
+ trigger: "body:not(:has(.o_select_menu_menu))",
+ },
+ {
+ content: "Open the days menu",
+ trigger: '.o_field_widget[name="yearly_day"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Verify that 29 is the selected item in the open menu",
+ trigger: '.o_select_menu_menu .o_select_menu_item.selected:contains("29")',
+ },
+ {
+ content: "Check that 30 is missing",
+ trigger: ".o_select_menu_menu:not(:has(.o_select_menu_item:contains('30')))",
+ },
+ {
+ content: "Check that 31 is missing",
+ trigger: ".o_select_menu_menu:not(:has(.o_select_menu_item:contains('31')))",
+ },
+ {
+ content: "Select a day from the list of months",
+ trigger: '.o_select_menu_item[data-choice-index="28"]',
+ run: function (actions) {
+ actions.click();
+ },
+ },
+ {
+ content: "Wait for dropdown to disappear",
+ trigger: "body:not(:has(.o_select_menu_menu))",
+ },
+ // Check the number of days available for April (1-30)
+ {
+ content: "Open the month selection menu",
+ trigger: '.o_field_widget[name="yearly_month"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Select April from the list of months",
+ trigger: '.o_select_menu_item[data-choice-index="3"]',
+ run: function (actions) {
+ actions.click();
+ },
+ },
+ {
+ content: "Wait for dropdown to disappear",
+ trigger: "body:not(:has(.o_select_menu_menu))",
+ },
+ {
+ content: "Open the days menu",
+ trigger: '.o_field_widget[name="yearly_day"] .o_select_menu_toggler',
+ run: "click",
+ },
+ {
+ content: "Check that 31 is missing",
+ trigger: ".o_select_menu_menu:not(:has(.o_select_menu_item:contains('31')))",
+ },
+ {
+ content: "Select a day from the list of months",
+ trigger: '.o_select_menu_item[data-choice-index="28"]',
+ run: function (actions) {
+ actions.click();
+ },
+ },
+ {
+ content: "Wait for RPC and UI to settle",
+ trigger: "body:not(.o_rpc_waiting)",
+ },
+ {
+ trigger: 'button[special="save"]',
+ content: "Save the Accrual Level Modal",
+ run: "click",
+ },
+ {
+ content: "Type the name of the accrual plan",
+ trigger: '.o_field_char[name="name"] input',
+ run: "fill Test Accrual plan",
+ },
+ {
+ content: "Click the cloud save button",
+ trigger: "button.o_form_button_save:has(i.fa-cloud-upload)",
+ run: "click",
+ },
+ ],
+});
diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py
index 14964fc0304be..59b97da3dab6e 100644
--- a/addons/hr_holidays/tests/__init__.py
+++ b/addons/hr_holidays/tests/__init__.py
@@ -36,3 +36,4 @@
from . import test_time_off_allocation_tour
from . import test_flexible_resource_calendar
from . import test_calendar_leaves_count
+from . import test_time_off_accrual_date_filter_tour
diff --git a/addons/hr_holidays/tests/test_hr_leave_type.py b/addons/hr_holidays/tests/test_hr_leave_type.py
index 69992303378e1..20b4a19cfab4d 100644
--- a/addons/hr_holidays/tests/test_hr_leave_type.py
+++ b/addons/hr_holidays/tests/test_hr_leave_type.py
@@ -124,3 +124,46 @@ def test_users_tz_shift_back(self):
).search([('has_valid_allocation', '=', True)], limit=1)
self.assertFalse(leave_types, "Got valid leaves outside vaild period")
+
+ def test_search_virtual_remaining_leaves(self):
+ """Test the search implementation for virtual remaining leaves.
+ Verify that the search correctly identifies:
+ 1. Allocated leave type with available balance as valid.
+ 2. Allocated leave type with zero balance as invalid.
+ 3. Unallocated leave type as valid.
+ """
+ employee = self.env['hr.employee'].create({'name': 'Test Employee'})
+
+ type_allocated_available = self.env['hr.leave.type'].create({
+ 'name': 'Allocated Time Off - Days Available',
+ 'unit_of_measure': 'day',
+ 'requires_allocation': True,
+ })
+
+ type_allocated_unavailable = self.env['hr.leave.type'].create({
+ 'name': 'Allocated Time Off - Days Unavailable',
+ 'unit_of_measure': 'day',
+ 'requires_allocation': True,
+ })
+
+ type_unallocated_available = self.env['hr.leave.type'].create({
+ 'name': 'Unallocated Time Off',
+ 'unit_of_measure': 'day',
+ 'requires_allocation': False,
+ })
+
+ self.env['hr.leave.allocation'].sudo().create({
+ 'name': 'Test Allocation',
+ 'state': 'confirm',
+ 'holiday_status_id': type_allocated_available.id,
+ 'employee_id': employee.id,
+ 'number_of_days': 10,
+ }).action_approve()
+
+ available_leave_types = self.env['hr.leave.type'].with_context(
+ employee_id=employee.id
+ ).search([('virtual_remaining_leaves', '>', 1)])
+
+ self.assertIn(type_allocated_available, available_leave_types, "Leave type with sufficient allocation was incorrectly excluded from search results.")
+ self.assertNotIn(type_allocated_unavailable, available_leave_types, "Leave type with zero remaining leaves should not be present in the search results.")
+ self.assertIn(type_unallocated_available, available_leave_types, "Leave types that don't require allocation should always be visible.")
diff --git a/addons/hr_holidays/tests/test_time_off_accrual_date_filter_tour.py b/addons/hr_holidays/tests/test_time_off_accrual_date_filter_tour.py
new file mode 100644
index 0000000000000..e67da93b20631
--- /dev/null
+++ b/addons/hr_holidays/tests/test_time_off_accrual_date_filter_tour.py
@@ -0,0 +1,9 @@
+from odoo.tests import HttpCase, tagged, users
+
+
+@tagged('post_install', '-at_install')
+class TestTimeOffAccrualDateFilterTour(HttpCase):
+
+ @users('admin')
+ def test_time_off_accrual_date_filter_tour(self):
+ self.start_tour('/odoo', 'time_off_accrual_date_filter_tour', login='admin')
diff --git a/addons/hr_holidays/views/hr_leave_accrual_views.xml b/addons/hr_holidays/views/hr_leave_accrual_views.xml
index 12a8650c1e058..1be2698d3a6e4 100644
--- a/addons/hr_holidays/views/hr_leave_accrual_views.xml
+++ b/addons/hr_holidays/views/hr_leave_accrual_views.xml
@@ -191,9 +191,9 @@
widget="radio_followed_by_element"
options="{'links': {'other': 'carryover_custom_date'}, 'observe': 'carryover'}"/>
- : the
-
+ : the
of
diff --git a/addons/hr_skills/report/__init__.py b/addons/hr_skills/report/__init__.py
index 8e82b9a1d6f11..bad5c5e9967fe 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 0000000000000..b70b034e5db95
--- /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 0000000000000..079ef1f790fd9
--- /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 86b997da5ffbb..bddca497eb6ef 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 3c16c4c9fecb3..37903bb69394b 100644
--- a/addons/hr_skills/views/hr_views.xml
+++ b/addons/hr_skills/views/hr_views.xml
@@ -581,7 +581,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
+
+