19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -1,9 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_attendances
from . import test_calendar_sync
from . import test_hr_employee
from . import test_hr_employee_public
from . import test_channel
from . import test_self_user_access
from . import test_mail_activity_plan
from . import test_mail_features
from . import test_multi_company
from . import test_payroll_fields_access
from . import test_resource
from . import test_ui
from . import test_scenario
from . import test_hr_department
from . import test_hr_version
from . import test_hr_contract_versions
from . import test_flexible_resource_calendar
from . import test_multiple_bank_accounts

View file

@ -9,6 +9,26 @@ class TestHrCommon(common.TransactionCase):
@classmethod
def setUpClass(cls):
super(TestHrCommon, cls).setUpClass()
super().setUpClass()
cls.res_users_hr_officer = mail_new_test_user(cls.env, login='hro', groups='base.group_user,hr.group_hr_user', name='HR Officer', email='hro@example.com')
cls.res_users_hr_officer = mail_new_test_user(
cls.env,
email='hro@example.com',
login='hro',
groups='base.group_user,hr.group_hr_user,base.group_partner_manager',
name='HR Officer',
)
cls.res_users_hr_manager = mail_new_test_user(
cls.env,
email='manager@example.com',
login='manager',
groups='base.group_user,hr.group_hr_manager,base.group_partner_manager',
name='HR Admin',
)
cls.employee = cls.env['hr.employee'].create({
'name': 'Richard',
'sex': 'male',
'country_id': cls.env.ref('base.be').id,
})

View file

@ -0,0 +1,77 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from pytz import timezone
from datetime import datetime, date
from odoo.addons.hr.tests.common import TestHrCommon
class TestAttendances(TestHrCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.company.resource_calendar_id.tz = "Europe/Brussels"
contract_now = cls.employee.create_version({
'wage': 1,
'contract_date_start': date(2024, 6, 1),
'date_version': date(2024, 6, 1),
})
resource_calendar_half_time = cls.env['resource.calendar'].create([{
'name': "Test Calendar: Half Time",
'company_id': cls.env.company.id,
'tz': "Europe/Brussels",
'two_weeks_calendar': False,
'attendance_ids': [(5, 0, 0)] + [(0, 0, {
'name': "Attendance",
'dayofweek': dayofweek,
'hour_from': hour_from,
'hour_to': hour_to,
'day_period': day_period,
}) for dayofweek, hour_from, hour_to, day_period in [
("0", 8.0, 12.0, "morning"),
("0", 12.0, 13.0, "lunch"),
("0", 13.0, 16.6, "afternoon"),
("1", 8.0, 12.0, "morning"),
("1", 12.0, 13.0, "lunch"),
("1", 13.0, 16.6, "afternoon"),
("2", 8.0, 11.8, "morning"),
]],
}])
cls.employee.create_version({
'resource_calendar_id': resource_calendar_half_time.id,
'contract_date_start': date(2024, 6, 1),
'contract_date_end': date(2024, 7, 31),
'wage': 1,
'date_version': date(2024, 7, 1),
})
cls.employee.create_version({
'contract_date_start': date(2024, 8, 1),
'wage': 1,
'date_version': date(2024, 9, 1),
})
cls.employee.resource_calendar_id = contract_now.resource_calendar_id
def test_incoming_overlapping_contract(self):
tz = timezone("Europe/Brussels")
check_in_tz = datetime.combine(datetime(2024, 6, 1), datetime.min.time()).astimezone(tz)
check_out_tz = datetime.combine(datetime(2024, 6, 30), datetime.max.time()).astimezone(tz)
intervals = self.employee._employee_attendance_intervals(check_in_tz, check_out_tz, lunch=False)
self.assertEqual(len(intervals), 40)
check_in_tz = datetime.combine(datetime(2024, 7, 1), datetime.min.time()).astimezone(tz)
check_out_tz = datetime.combine(datetime(2024, 7, 31), datetime.max.time()).astimezone(tz)
intervals = self.employee._employee_attendance_intervals(check_in_tz, check_out_tz, lunch=False)
self.assertEqual(len(intervals), 25)
check_in_tz = datetime.combine(datetime(2024, 8, 1), datetime.min.time()).astimezone(tz)
check_out_tz = datetime.combine(datetime(2024, 8, 31), datetime.max.time()).astimezone(tz)
intervals = self.employee._employee_attendance_intervals(check_in_tz, check_out_tz, lunch=False)
self.assertEqual(len(intervals), 20)

View file

@ -0,0 +1,106 @@
from odoo.fields import Datetime, Date
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.tests import Form
class TestContractCalendars(TestHrCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.calendar_richard = cls.env['resource.calendar'].create({'name': 'Calendar of Richard'})
cls.employee.resource_calendar_id = cls.calendar_richard
# force old date to test new version to be created.
cls.employee.version_id.date_version = Date.to_date('2015-01-01')
cls.calendar_35h = cls.env['resource.calendar'].create({'name': '35h calendar'})
cls.contract_cdd_values = {
'date_version': Date.to_date('2016-01-01'),
'date_start': Date.to_date('2016-01-01'),
'name': 'First CDD Contract for Richard',
'resource_calendar_id': cls.calendar_35h.id,
'wage': 5000.0,
}
cls.contract_fully_flexible_values = {
'date_version': Date.to_date('2017-01-01'),
'date_start': Date.to_date('2017-01-01'),
'name': 'Fully Flexible Contract for Richard',
'resource_calendar_id': False,
'wage': 5000.0,
}
def test_contract_state_incoming_to_open(self):
# Employee's calendar should change
self.assertEqual(self.employee.resource_calendar_id, self.calendar_richard)
cdd = self.employee.create_version(self.contract_cdd_values)
self.assertEqual(self.employee.version_id.id, cdd.id, "The version of the employee should be updated to the last version.")
self.assertEqual(self.employee.resource_calendar_id, cdd.resource_calendar_id, "The employee should have the calendar of its contract.")
def test_set_fully_flexible_contract_should_change_resource_calendar(self):
# Setting a running contract with fully flexible calendar should set the employee's calendar to False (fully flexible)
self.assertEqual(self.employee.resource_calendar_id, self.calendar_richard)
flexijob = self.employee.create_version(self.contract_fully_flexible_values)
self.assertEqual(self.employee.version_id.id, flexijob.id, "The version of the employee should be updated to the last version.")
self.assertFalse(self.employee.resource_calendar_id, "The employee should have a fully flexible calendar.")
def test_contract_transfer_leaves(self):
def create_calendar_leave(start, end, resource=None):
return self.env['resource.calendar.leaves'].create({
'name': 'leave name',
'date_from': start,
'date_to': end,
'resource_id': resource.id if resource else None,
'calendar_id': self.employee.resource_calendar_id.id,
'time_type': 'leave',
})
start = Datetime.to_datetime('2015-11-17 07:00:00')
end = Datetime.to_datetime('2015-11-20 18:00:00')
leave1 = create_calendar_leave(start, end, resource=self.employee.resource_id)
start = Datetime.to_datetime('2015-11-25 07:00:00')
end = Datetime.to_datetime('2015-11-28 18:00:00')
leave2 = create_calendar_leave(start, end, resource=self.employee.resource_id)
# global leave
start = Datetime.to_datetime('2015-11-25 07:00:00')
end = Datetime.to_datetime('2015-11-28 18:00:00')
leave3 = create_calendar_leave(start, end)
self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=self.employee.resource_id, from_date=Date.to_date('2015-11-21'))
self.assertEqual(leave1.calendar_id, self.calendar_richard, "It should stay in Richard's calendar")
self.assertEqual(leave3.calendar_id, self.calendar_richard, "Global leave should stay in original calendar")
self.assertEqual(leave2.calendar_id, self.calendar_35h, "It should be transferred to the other calendar")
# Transfer global leaves
self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=None, from_date=Date.to_date('2015-11-21'))
self.assertEqual(leave3.calendar_id, self.calendar_35h, "Global leave should be transfered")
def test_calendar_no_desync(self):
""" resource_calendar_id cannot be desync between employee and version last version """
self.employee.create_version(self.contract_cdd_values)
self.assertEqual(self.employee.resource_calendar_id, self.calendar_35h)
self.assertEqual(self.employee.version_id.resource_calendar_id, self.calendar_35h)
self.assertEqual(self.employee.version_ids[0].resource_calendar_id, self.calendar_richard)
calendar_38h = self.env['resource.calendar'].create({'name': '38h calendar'})
self.employee.resource_calendar_id = calendar_38h
self.assertEqual(self.employee.version_id.resource_calendar_id, calendar_38h)
self.assertEqual(self.employee.version_ids[0].resource_calendar_id, self.calendar_richard)
def test_employee_resource_contract_without_and_with_date_from(self):
"""
Test setting the resource with an employee contract on resource leave without and with start date.
"""
leave_form = Form(self.env['resource.calendar.leaves'])
leave_form.date_from = False
leave_form.resource_id = self.employee.resource_id
self.assertFalse(leave_form.calendar_id)
leave_form.date_from = Datetime.to_datetime('2018-01-01 07:00:00')
self.assertEqual(leave_form.calendar_id, self.employee.version_id.resource_calendar_id)

View file

@ -1,16 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestChannel(TestHrCommon):
@classmethod
def setUpClass(cls):
super(TestChannel, cls).setUpClass()
cls.channel = cls.env['mail.channel'].create({'name': 'Test'})
cls.channel = cls.env['discuss.channel'].create({'name': 'Test'})
emp0 = cls.env['hr.employee'].create({
'user_id': cls.res_users_hr_officer.id,

View file

@ -0,0 +1,123 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime, date
from odoo.tests.common import TransactionCase
UTC = pytz.timezone('UTC')
class TestFlexibleResourceCalendar(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.calendar_40h_flex = cls.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'flexible_hours': True,
})
cls.flex_resource, cls.fully_flex_resource = cls.env['resource.resource'].create([{
'name': 'Flex',
'tz': 'UTC',
'calendar_id': cls.calendar_40h_flex.id,
}, {
'name': 'fully flex',
'tz': 'UTC',
'calendar_id': False,
}])
def test_flexible_resource_work_intervals_with_contracts(self):
flex_employee, fully_flex_employee = self.env['hr.employee'].create([{
'name': "flex employee",
'date_version': date(2025, 1, 1),
'contract_date_start': date(2025, 1, 1),
'contract_date_end': date(2025, 7, 29),
'wage': 10,
'resource_calendar_id': self.calendar_40h_flex.id,
'resource_id': self.flex_resource.id,
}, {
'name': "fully flex employee",
'date_version': date(2025, 1, 1),
'contract_date_start': date(2025, 1, 1),
'contract_date_end': date(2025, 7, 29),
'wage': 10,
'resource_calendar_id': False,
'resource_id': self.fully_flex_resource.id,
}])
flex_employee.create_version({
'date_version': date(2025, 8, 2),
'contract_date_start': date(2025, 8, 2),
'wage': 10,
'resource_calendar_id': self.calendar_40h_flex.id,
})
fully_flex_employee.create_version({
'date_version': date(2025, 8, 2),
'contract_date_start': date(2025, 8, 2),
'wage': 10,
'resource_calendar_id': False,
})
start_dt = datetime(2025, 7, 28).astimezone(UTC)
end_dt = datetime(2025, 8, 3, 17).astimezone(UTC)
resources = self.flex_resource | self.fully_flex_resource
work_intervals, hours_per_day, hours_per_week = resources._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
self.maxDiff = None
for resource in resources:
self.assertEqual(work_intervals[resource.id]._items, [
(datetime(2025, 7, 28, 0, 0, tzinfo=UTC), datetime(2025, 7, 28, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 29, 0, 0, tzinfo=UTC), datetime(2025, 7, 29, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 2, 0, 0, tzinfo=UTC), datetime(2025, 8, 2, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 3, 0, 0, tzinfo=UTC), datetime(2025, 8, 3, 17, 0, tzinfo=UTC), self.env['resource.calendar.attendance']),
], "work intervals should be inside contract 1 and 2 periods, no contracts on 30, 31, 1")
self.assertDictEqual(hours_per_day[self.flex_resource.id], {
date(2025, 7, 28): 8.0,
date(2025, 7, 29): 8.0,
date(2025, 8, 2): 8.0,
date(2025, 8, 3): 8.0,
})
self.assertDictEqual(hours_per_week[self.flex_resource.id], {
(2025, 31): 32.0,
(2025, 32): 40.0,
}, "working day 27, 28, 29 and 02 on week 31, having a valid contract on week 32")
self.assertTrue(self.fully_flex_resource.id not in hours_per_day, "no date hours limit for fully flexible employees")
def test_flexible_resource_work_intervals_without_contracts(self):
start_dt = datetime(2025, 7, 28).astimezone(UTC)
end_dt = datetime(2025, 8, 3, 17).astimezone(UTC)
resources = self.flex_resource | self.fully_flex_resource
work_intervals, hours_per_day, hours_per_week = resources._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
self.maxDiff = None
for resource in resources:
self.assertEqual(work_intervals[resource.id]._items, [
(datetime(2025, 7, 28, 0, 0, tzinfo=UTC), datetime(2025, 7, 28, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 29, 0, 0, tzinfo=UTC), datetime(2025, 7, 29, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 30, 0, 0, tzinfo=UTC), datetime(2025, 7, 30, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 31, 0, 0, tzinfo=UTC), datetime(2025, 7, 31, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 1, 0, 0, tzinfo=UTC), datetime(2025, 8, 1, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 2, 0, 0, tzinfo=UTC), datetime(2025, 8, 2, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 3, 0, 0, tzinfo=UTC), datetime(2025, 8, 3, 17, 0, tzinfo=UTC), self.env['resource.calendar.attendance']),
], "when no contracts at all, we get the full period")
self.assertDictEqual(hours_per_day[self.flex_resource.id], {
date(2025, 7, 28): 8.0,
date(2025, 7, 29): 8.0,
date(2025, 7, 30): 8.0,
date(2025, 7, 31): 8.0,
date(2025, 8, 1): 8.0,
date(2025, 8, 2): 8.0,
date(2025, 8, 3): 8.0,
}, "when no contracts at all, we get the full period")
self.assertTrue(self.fully_flex_resource.id not in hours_per_day, "no date hours limit for fully flexible employees")
self.assertDictEqual(hours_per_week[self.flex_resource.id], {
(2025, 31): 40.0,
(2025, 32): 40.0,
})

View file

@ -0,0 +1,594 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('post_install', '-at_install')
class TestHrContractVersions(TransactionCase):
"""
TestHrContractVersions is responsible for testing the behavior and validity of versions
with contracts.
This class tests 2 helper methods: `_get_contract_versions()` and `_get_contracts()`
There are multiple scenarios:
- No contract, 1 version
- 1 contract with 1 version
- 1 contract with 5 versions
- 2 contracts with 1 version each
- 2 contracts with 3 versions each
For each of them, multiple tests are performed with different start_date, end_date and use_latest_version
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env['res.company'].create({
'name': 'Test Company',
'country_id': cls.env.ref('base.us').id,
})
cls.env.user.company_id = cls.company
cls.employee = cls.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2025-01-01'
})
def create_version(self, date_version):
return self.employee.create_version({
'date_version': date_version,
})
def create_versions(self, *dates):
res = self.env["hr.version"]
for date_version in dates:
res |= self.create_version(date_version)
return res
def assert_get_contract_versions(self, date_start, date_end, versions_per_contract_expected):
versions_per_contract = self.employee._get_contract_versions(date_start, date_end)[self.employee.id]
self.assertEqual(len(versions_per_contract), len(versions_per_contract_expected), "%s contract should be found" % len(versions_per_contract_expected))
for vpc, vpc_e in zip(versions_per_contract.values(), versions_per_contract_expected):
self.assertEqual(vpc, vpc_e, "invalid number of versions (%s instead of %s) for this contract : contract_date_start : %s" % (len(vpc), len(vpc_e), vpc_e[0].contract_date_start))
def assert_get_contracts(self, date_start, date_end, use_latest_version, contracts_expected):
contracts = self.employee._get_contracts(date_start, date_end, use_latest_version)[self.employee.id]
for c, c_e in zip(contracts, contracts_expected):
self.assertEqual(c, c_e, "invalid contracts")
def test_0contract_1version(self):
"""
We should retrieve no versions, because no contract is defined
"""
for date_version in ['2025-01-01', '2025-06-01', '2025-12-31']:
self.employee.date_version = date_version
for date_start in (None, date(2025, 3, 1)):
for date_end in (None, date(2025, 6, 15)):
for use_latest_version in (True, False):
self.assert_get_contract_versions(
date_start,
date_end,
[]
)
self.assert_get_contracts(
date_start,
date_end,
use_latest_version,
[]
)
""" Timeline for this test
V : versions
C : first version of the contract
= : contract
1/
04/01 7/31
2025|C---------=====================----------|
01/01 06/01 12/31
2/
04/01 7/31
2025|----------==========C==========----------|
01/01 06/01 12/31
3/
04/01 7/31
2025|----------=====================---------C|
01/01 06/01 12/31
"""
def test_1contract_1version(self):
"""
We should always retrieve the only existing version
"""
unique_version = self.employee.version_id
unique_version.contract_date_start = date(2025, 4, 1)
unique_version.contract_date_end = date(2025, 7, 31)
for date_version in ['2025-01-01', '2025-06-01', '2025-12-31']:
unique_version.date_version = date_version
for date_start in (None, date(2025, 3, 1), date(2025, 4, 1)):
for date_end in (None, date(2025, 4, 1), date(2025, 6, 15), date(2025, 7, 31)):
for use_latest_version in (True, False):
self.assert_get_contract_versions(
date_start,
date_end,
[unique_version]
)
self.assert_get_contracts(
date_start,
date_end,
use_latest_version,
unique_version
)
""" Timeline for this setup
V : versions
C : first version of the contract
= : contract
04/01=========================07/31
2025|C---------V---------------V------V--------VV---------|
01/01 04/01 06/01 07/01 07/31;08/01
"""
def setup_1contract_5version(self):
contract_versions = self.employee.version_id | self.create_versions(
date(2025, 4, 1),
date(2025, 6, 1),
date(2025, 7, 1),
date(2025, 7, 31)
)
contract_versions.contract_date_start = date(2025, 4, 1)
contract_versions.contract_date_end = date(2025, 7, 31)
versions_not_in_contract = self.create_version(date(2025, 8, 1))
return contract_versions, versions_not_in_contract
def test_1contract_5version(self):
# All versions of the contract should be retrieved
expected_contract_versions, _ = self.setup_1contract_5version()
self.assert_get_contract_versions(
None,
None,
[expected_contract_versions]
)
# The last version of the contract should be retrieved
self.assert_get_contracts(
None,
None,
True,
expected_contract_versions[-1]
)
# The first version of the contract should be retrieved
self.assert_get_contracts(
None,
None,
False,
expected_contract_versions[0]
)
def test_1contract_5version_w_date_start(self):
# All versions of the contract should be retrieved
expected_contract_versions, _ = self.setup_1contract_5version()
self.assert_get_contract_versions(
date(2025, 3, 1),
None,
[expected_contract_versions]
)
# The last version of the contract should be retrieved
self.assert_get_contracts(
date(2025, 3, 1),
None,
True,
expected_contract_versions[-1]
)
# We need to retrieve the version closest and before the start date. (first version of the contract)
self.assert_get_contracts(
date(2025, 3, 1),
None,
False,
expected_contract_versions[0]
)
# We need to retrieve the version closest and before the start date. (second version of the contract)
self.assert_get_contracts(
date(2025, 4, 1),
None,
False,
expected_contract_versions[0]
)
def test_1contract_5version_w_date_start_date_end(self):
# all versions of the contract should be retrieved (even if versions are not in the range)
expected_contract_versions, _ = self.setup_1contract_5version()
self.assert_get_contract_versions(
date(2025, 5, 15),
date(2025, 6, 15),
[expected_contract_versions]
)
# We need to retrieve the version closest and before the end date.
self.assert_get_contracts(
date(2025, 4, 15),
date(2025, 6, 15),
True,
expected_contract_versions[2]
)
# We need to retrieve the version closest and before the start date.
self.assert_get_contracts(
date(2025, 4, 15),
date(2025, 6, 15),
False,
expected_contract_versions[1]
)
# We need to retrieve the version closest and before the end date.
self.assert_get_contracts(
date(2025, 6, 15),
date(2025, 7, 15),
True,
expected_contract_versions[3]
)
# We need to retrieve the version closest and before the start date.
self.assert_get_contracts(
date(2025, 6, 15),
date(2025, 8, 15),
False,
expected_contract_versions[2]
)
def test_1contract_5version_w_date_end(self):
# All versions of the contract should be retrieved
expected_contract_versions, _ = self.setup_1contract_5version()
self.assert_get_contract_versions(
None,
date(2025, 8, 31),
[expected_contract_versions]
)
# We need to retrieve the version closest and before the end date. (last version of the contract)
self.assert_get_contracts(
None,
date(2025, 8, 31),
True,
expected_contract_versions[-1]
)
# We need to retrieve the version closest and before the end date. (last version of the contract)
self.assert_get_contracts(
None,
date(2025, 7, 31),
True,
expected_contract_versions[-1]
)
# We need to retrieve the version closest and before the end date. (second to last version of the contract)
self.assert_get_contracts(
None,
date(2025, 7, 30),
True,
expected_contract_versions[-2]
)
# The first version of the contract should be retrieved
self.assert_get_contracts(
None,
date(2025, 8, 31),
False,
expected_contract_versions[0]
)
""" Timeline for this setup
V : versions
C : first version of the contract
= : contract
4/01 5/15 6/15 7/31
2025|C---------====================-C-====================----------|
01/01 06/01
"""
def setup_2contract_1version_each(self):
contract_1_version = self.employee.version_id
contract_1_version.contract_date_start = date(2025, 4, 1)
contract_1_version.contract_date_end = date(2025, 5, 15)
contract_2_version = self.create_version(date(2025, 6, 1))
contract_2_version.contract_date_start = date(2025, 6, 15)
contract_2_version.contract_date_end = date(2025, 7, 31)
return contract_1_version, contract_2_version
def test_2contract_1version_each(self):
# all versions of all contracts
contract_1_version, contract_2_version = self.setup_2contract_1version_each()
self.assert_get_contract_versions(
None,
None,
[contract_1_version, contract_2_version]
)
# the latest version of each contract
self.assert_get_contracts(
None,
None,
True,
contract_1_version | contract_2_version
)
# the first version of each contract
self.assert_get_contracts(
None,
None,
False,
contract_1_version | contract_2_version
)
def test_2contract_1version_each_w_date_start(self):
# all versions of all contracts
contract_1_version, contract_2_version = self.setup_2contract_1version_each()
self.assert_get_contract_versions(
date(2025, 3, 1),
None,
[contract_1_version, contract_2_version]
)
# all versions of the second contract
self.assert_get_contract_versions(
date(2025, 6, 15),
None,
[contract_2_version]
)
# the latest version of each contract
self.assert_get_contracts(
date(2025, 3, 1),
None,
True,
contract_1_version | contract_2_version
)
# the first before the start date, of each contract
self.assert_get_contracts(
date(2025, 3, 1),
None,
False,
contract_1_version | contract_2_version
)
def test_2contract_1version_each_w_date_start_date_end(self):
# no versions, because no contract active in the range, even if versions are in the range
contract_1_version, contract_2_version = self.setup_2contract_1version_each()
self.assert_get_contract_versions(
date(2025, 5, 16),
date(2025, 6, 14),
[]
)
# all versions of each contract
self.assert_get_contract_versions(
date(2025, 5, 15),
date(2025, 6, 15),
[contract_1_version, contract_2_version]
)
# the first version before the end date, of the contract active in the range
self.assert_get_contracts(
date(2025, 5, 15),
date(2025, 6, 14),
True,
contract_1_version
)
# the first version before the start date, of the contract active in the range
self.assert_get_contracts(
date(2025, 5, 15),
date(2025, 6, 14),
False,
contract_1_version
)
# the first version before the end date, of the contract active in the range
self.assert_get_contracts(
date(2025, 5, 16),
date(2025, 6, 15),
True,
contract_2_version
)
# the first version before the start date, of the contract active in the range
self.assert_get_contracts(
date(2025, 5, 16),
date(2025, 6, 15),
False,
contract_2_version
)
# the first version before the end date, of the contract active in the range
self.assert_get_contracts(
date(2025, 5, 15),
date(2025, 6, 15),
True,
[contract_1_version, contract_2_version]
)
# the first version before the start date, of the contract active in the range
self.assert_get_contracts(
date(2025, 5, 15),
date(2025, 6, 15),
False,
[contract_1_version, contract_2_version]
)
def test_2contract_1version_each_w_date_end(self):
# all versions of all contracts
contract_1_version, contract_2_version = self.setup_2contract_1version_each()
self.assert_get_contract_versions(
None,
date(2025, 8, 31),
[contract_1_version, contract_2_version]
)
# all versions of the first contract
self.assert_get_contract_versions(
None,
date(2025, 6, 15),
[contract_1_version, contract_2_version]
)
# all versions of the first contract
self.assert_get_contract_versions(
None,
date(2025, 6, 14),
[contract_1_version]
)
# the first before the end date, of each contract
self.assert_get_contracts(
None,
date(2025, 8, 31),
True,
contract_1_version | contract_2_version
)
# the first version of each contract
self.assert_get_contracts(
None,
date(2025, 8, 31),
False,
contract_1_version | contract_2_version
)
""" Timeline for this setup
V : versions
C : first version of the contract
= : contract
4/01============5/15 6/15============7/31
2025|C---------V------------------VV------C-------V------------------VV---------|
1/1 4/1 5/15;5/16 6/1 6/15 7/31;8/1
"""
def setup_2contract_3version_each(self):
contract_1_versions = self.employee.version_id | self.create_versions(
date(2025, 4, 1),
date(2025, 5, 15),
)
contract_1_versions.contract_date_start = date(2025, 4, 1)
contract_1_versions.contract_date_end = date(2025, 5, 15)
versions_not_in_contract = self.create_version(date(2025, 5, 16))
contract_2_versions = self.create_versions(
date(2025, 6, 1),
date(2025, 6, 15),
date(2025, 7, 31),
)
contract_2_versions.contract_date_start = date(2025, 6, 15)
contract_2_versions.contract_date_end = date(2025, 7, 31)
versions_not_in_contract |= self.create_version(date(2025, 8, 1))
return contract_1_versions, contract_2_versions, versions_not_in_contract
def test_2contract_3version_each(self):
# all versions of all contracts
contract_1_version, contract_2_version, _ = self.setup_2contract_3version_each()
self.assert_get_contract_versions(
None,
None,
[contract_1_version, contract_2_version]
)
# the latest version of each contract
self.assert_get_contracts(
None,
None,
True,
contract_1_version[-1] | contract_2_version[-1]
)
# the first version of each contract
self.assert_get_contracts(
None,
None,
False,
contract_1_version[0] | contract_2_version[0]
)
def test_2contract_3version_each_w_date_start(self):
# all versions of all contracts
contract_1_version, contract_2_version, _ = self.setup_2contract_3version_each()
self.assert_get_contract_versions(
date(2025, 3, 1),
None,
[contract_1_version, contract_2_version]
)
# the latest version of each contract
self.assert_get_contracts(
date(2025, 3, 1),
None,
True,
contract_1_version[-1] | contract_2_version[-1]
)
# the first before the start date, of each contract
self.assert_get_contracts(
date(2025, 3, 1),
None,
False,
contract_1_version[0] | contract_2_version[0]
)
# the first before the start date, of each contract
self.assert_get_contracts(
date(2025, 4, 1),
None,
False,
contract_1_version[1] | contract_2_version[0]
)
def test_2contract_3version_each_w_date_start_date_end(self):
# all versions of all contracts
contract_1_version, contract_2_version, _ = self.setup_2contract_3version_each()
self.assert_get_contract_versions(
date(2025, 5, 16),
date(2025, 6, 14),
[]
)
# the first before the end date, of the contract active in the range
self.assert_get_contracts(
date(2025, 4, 15),
date(2025, 6, 15),
True,
[contract_1_version[-1], contract_2_version[1]]
)
# the first before the start date, of the contract active in the range
self.assert_get_contracts(
date(2025, 4, 15),
date(2025, 6, 15),
False,
[contract_1_version[0], contract_2_version[0]]
)
# the first before the end date, of the contract active in the range
self.assert_get_contracts(
date(2025, 6, 15),
date(2025, 7, 31),
True,
contract_2_version[-1]
)
# the first before the start date, of the contract active in the range
self.assert_get_contracts(
date(2025, 6, 15),
date(2025, 7, 31),
False,
contract_2_version[1]
)
def test_2contract_3version_each_w_date_end(self):
# all versions of all contracts
contract_1_version, contract_2_version, _ = self.setup_2contract_3version_each()
self.assert_get_contract_versions(
None,
date(2025, 8, 31),
[contract_1_version, contract_2_version]
)
# the first before the end date, of each contract
self.assert_get_contracts(
None,
date(2025, 8, 31),
True,
contract_1_version[-1] | contract_2_version[-1]
)
# the first before the end date, of each contract
self.assert_get_contracts(
None,
date(2025, 7, 31),
True,
contract_1_version[-1] | contract_2_version[-1]
)
# the first before the end date, of each contract
self.assert_get_contracts(
None,
date(2025, 7, 30),
True,
contract_1_version[-1] | contract_2_version[-2]
)
# the latest version of each contract
self.assert_get_contracts(
None,
date(2025, 8, 31),
False,
contract_1_version[0] | contract_2_version[0]
)

View file

@ -0,0 +1,62 @@
from odoo.addons.hr.tests.test_multi_company import TestMultiCompany
class TestHrDepartment(TestMultiCompany):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.department = cls.env['hr.department'].create({
'name': 'test department',
})
cls.employee_a.department_id = cls.department
cls.employee_other_a.department_id = cls.department
cls.employee_b.department_id = cls.department
def test_dapartment_total_employee_count(self):
'''
Test that employee_count has only the count of employees in the selected companies
'''
employee_count = self.department.with_company(self.company_a).total_employee # should only count the 2 employees in company_a
self.assertEqual(employee_count, 2)
self.department._compute_total_employee()
employee_count = self.department.total_employee # should count all 3 employees
self.assertEqual(employee_count, 3)
def test_department_company_id(self):
"""
When the parent exists and parent's company changes to non-empty company, the child's company must be equalized to the same company
If the parent's company changes to empty, the child's company should not get affected
"""
self.parent_department = self.env['hr.department'].create({
'name': 'parent of the test department',
'company_id': self.company_a.id,
})
self.department.company_id = self.company_b.id
self.assertTrue(self.department.company_id == self.company_b)
self.department.parent_id = self.parent_department.id
self.assertTrue(self.department.company_id == self.company_a)
self.parent_department.company_id = self.company_b
self.assertTrue(self.department.company_id == self.company_b)
self.parent_department.company_id = False
# Child's company should not change
self.assertTrue(self.department.company_id == self.company_b)
self.parents_parent_department = self.env['hr.department'].create({
'name': 'grandparent of test department',
'company_id': False,
})
self.parent_department.parent_id = self.parents_parent_department.id
# Since the company of grandparent is False, it will not affect childs.
self.assertFalse(self.parent_department.company_id)
self.assertTrue(self.department.company_id == self.company_b)
self.parents_parent_department.company_id = self.company_a.id
self.assertTrue(self.parent_department.company_id == self.company_a)
self.assertTrue(self.department.company_id == self.company_a)

View file

@ -1,9 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from psycopg2.errors import UniqueViolation
from freezegun import freeze_time
from odoo.tests import Form
from odoo import fields, Command
from odoo.fields import Domain
from odoo.tests import Form, users, new_test_user, HttpCase, tagged, TransactionCase
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.tools import mute_logger
from odoo.exceptions import ValidationError
from psycopg2.errors import NotNullViolation
class TestHrEmployee(TestHrCommon):
@ -21,6 +28,49 @@ class TestHrEmployee(TestHrCommon):
'image_1920': False
})
def test_employee_must_have_active_version(self):
employee = self.env['hr.employee'].create({
'name': 'Batman'
})
self.assertEqual(len(employee.version_ids), 1)
employee_version = employee.version_id
with self.assertRaises(ValidationError, msg="An employee should always have a version"):
employee.write({'version_ids': False})
with self.assertRaises(ValidationError, msg="An employee should always have a version"):
employee_version.unlink()
with self.assertRaises(ValidationError, msg="An employee should always have a version"):
employee_version.write({
'employee_id': self.employee_without_image.id
})
with self.assertRaises(ValidationError, msg="An employee should always have an active version"):
employee_version.write({'active': False})
def test_employee_smart_button_multi_company(self):
partner = self.env['res.partner'].create({'name': 'Partner Test'})
company_A = self.env['res.company'].create({'name': 'company_A'})
company_B = self.env['res.company'].create({'name': 'company_B'})
self.env['hr.employee'].create({
'name': 'employee_A',
'work_contact_id': partner.id,
'company_id': company_A.id,
})
self.env['hr.employee'].create({
'name': 'employee_B',
'work_contact_id': partner.id,
'company_id': company_B.id
})
partner.with_company(company_A)._compute_employees_count()
self.assertEqual(partner.employees_count, 1)
partner.with_company(company_B)._compute_employees_count()
self.assertEqual(partner.employees_count, 1)
single_company_action = partner.with_company(company_B).action_open_employees()
self.assertEqual(single_company_action.get('view_mode'), 'form')
partner.with_company(company_A).with_company(company_B)._compute_employees_count()
self.assertEqual(partner.employees_count, 2)
multi_company_action = partner.with_company(company_A).with_company(company_B).action_open_employees()
self.assertEqual(multi_company_action.get('view_mode'), 'kanban')
def test_employee_linked_partner(self):
user_partner = self.user_without_image.partner_id
work_contact = self.employee_without_image.work_contact_id
@ -36,6 +86,38 @@ class TestHrEmployee(TestHrCommon):
employee = employee_form.save()
self.assertEqual(employee.tz, _tz)
def test_employee_timezone(self):
self.res_users_hr_officer.tz = "Africa/Cairo"
Employee = self.env['hr.employee'].with_user(self.res_users_hr_officer)
employee_form = Form(Employee)
employee_form.user_id = self.res_users_hr_officer
employee_form.name = 'Youssef Ahmed'
employee_form.work_email = 'yoahm@example.com'
employee = employee_form.save()
# validate timezone sync between employee & user
self.assertEqual(employee.tz, self.res_users_hr_officer.tz)
# validate that we can change timezone on user
self.res_users_hr_officer.tz = "Europe/Brussels"
self.assertEqual(self.res_users_hr_officer.tz, employee.tz)
# validate that we can change timezone on employee
employee.tz = "Europe/London"
self.assertEqual(self.res_users_hr_officer.tz, employee.tz)
# Check False value on employee
with mute_logger('odoo.sql_db'), self.assertRaises(NotNullViolation):
employee.tz = False
# Check False value on user
with mute_logger('odoo.sql_db'), self.assertRaises(NotNullViolation):
self.res_users_hr_officer.tz = False
# Check None value on user's calendar
with mute_logger('odoo.sql_db'), self.assertRaises(NotNullViolation):
self.res_users_hr_officer.company_id.resource_calendar_id.write({'tz': None})
def test_employee_from_user(self):
_tz = 'Pacific/Apia'
_tz2 = 'America/Tijuana'
@ -51,18 +133,30 @@ class TestHrEmployee(TestHrCommon):
self.assertEqual(employee.work_email, self.res_users_hr_officer.email)
self.assertEqual(employee.tz, self.res_users_hr_officer.tz)
def test_employee_from_user_tz_no_reset(self):
def test_employee_computed_from_user(self):
self.res_users_hr_officer.name = 'Raoul Grosbedon'
self.res_users_hr_officer.email = 'raoul@example.com'
Employee = self.env['hr.employee']
employee_form = Form(Employee)
employee_form.user_id = self.res_users_hr_officer
self.assertEqual(employee_form.name, 'Raoul Grosbedon')
self.assertEqual(employee_form.work_email, 'raoul@example.com')
employee = employee_form.save()
self.assertEqual(employee.name, 'Raoul Grosbedon')
self.assertEqual(employee.work_email, 'raoul@example.com')
def test_employee_from_manager_tz_no_reset(self):
_tz = 'Pacific/Apia'
self.res_users_hr_officer.tz = False
Employee = self.env['hr.employee'].with_user(self.res_users_hr_officer)
self.res_users_hr_manager.tz = False
Employee = self.env['hr.employee'].with_user(self.res_users_hr_manager)
employee_form = Form(Employee)
employee_form.name = 'Raoul Grosbedon'
employee_form.work_email = 'raoul@example.com'
employee_form.tz = _tz
employee_form.user_id = self.res_users_hr_officer
employee_form.user_id = self.res_users_hr_manager
employee = employee_form.save()
self.assertEqual(employee.name, 'Raoul Grosbedon')
self.assertEqual(employee.work_email, self.res_users_hr_officer.email)
self.assertEqual(employee.work_email, self.res_users_hr_manager.email)
self.assertEqual(employee.tz, _tz)
def test_employee_has_avatar_even_if_it_has_no_image(self):
@ -126,16 +220,10 @@ class TestHrEmployee(TestHrCommon):
self.assertFalse(emp_parent.member_of_department)
employees = emp + emp_sub + emp_sub_sub + emp_other + emp_parent
self.assertEqual(
employees.filtered_domain(employees._search_part_of_department('=', True)),
employees.filtered_domain(employees.version_id._search_part_of_department('in', [True])),
emp + emp_sub + emp_sub_sub)
self.assertEqual(
employees.filtered_domain(employees._search_part_of_department('!=', False)),
emp + emp_sub + emp_sub_sub)
self.assertEqual(
employees.filtered_domain(employees._search_part_of_department('=', False)),
emp_other + emp_parent)
self.assertEqual(
employees.filtered_domain(employees._search_part_of_department('!=', True)),
employees.filtered_domain(['!'] + employees.version_id._search_part_of_department('in', [True])),
emp_other + emp_parent)
def test_employee_create_from_user(self):
@ -185,7 +273,7 @@ class TestHrEmployee(TestHrCommon):
def test_employee_update_work_contact_id(self):
"""
Check that the `work_contact_id` information is no longer
updated when an employee's `user_id` is removed.
updated when an employee's `user_id` is added to another employee.
"""
user = self.env['res.users'].create({
'name': 'Test',
@ -209,20 +297,550 @@ class TestHrEmployee(TestHrCommon):
employee_B.work_email = 'new_email@example.com'
self.assertEqual(employee_A.work_email, 'employee_A@example.com')
self.assertEqual(employee_B.work_email, 'new_email@example.com')
self.assertFalse(employee_A.work_contact_id)
self.assertEqual(employee_B.work_contact_id, user.partner_id)
def test_unlink_address(self):
employee = self.employee_without_image
partner = self.env["res.partner"].create({
"name": "Mr. Bean",
"street": "12 Arbour Road",
"city": "London"
})
employee.address_home_id = partner.id
bank = self.env['res.partner.bank'].create({
"acc_number": "123",
"partner_id": partner.id
})
employee.bank_account_id = bank.id
def test_availability_user_infos_employee(self):
""" Ensure that all the user infos needed to display the avatar popover card
are available on the model hr.employee.
"""
user = self.env['res.users'].create([{
'name': 'Test user',
'login': 'test',
'email': 'test@odoo.perso',
'phone': '+32488990011',
}])
employee = self.env['hr.employee'].create([{
'name': 'Test employee',
'user_id': user.id,
}])
user_fields = ['email', 'phone', 'im_status']
for field in user_fields:
self.assertEqual(employee[field], user[field])
employee.address_home_id = False
self.assertFalse(employee.address_home_id)
def test_set_user_on_new_employee(self):
test_company = self.env['res.company'].create({
'name': 'Test User Company',
})
self.env['hr.employee'].create({
'name': 'Hr Officer - employee',
'user_id': self.res_users_hr_officer.id,
'company_id': test_company.id,
})
self.res_users_hr_officer.write({'company_ids': test_company.ids, 'company_id': test_company.id})
# Try to set the user with existing employee in the company, on a new employee form
employee_form = Form(self.env['hr.employee'].with_user(self.res_users_hr_officer).with_company(company=test_company.id))
employee_form.name = "Second employee"
employee_form.user_id = self.res_users_hr_officer
with mute_logger('odoo.sql_db'), self.assertRaises(UniqueViolation), self.assertRaises(ValidationError):
employee_form.save()
employee_2 = self.env['hr.employee'].create({
'name': 'Hr 2 - employee',
'company_id': test_company.id,
})
# Try to set the user with existing employee in the company, on another existing employee
employee_2_form = Form(employee_2.with_user(self.res_users_hr_officer).with_company(company=test_company.id))
employee_2_form.user_id = self.res_users_hr_officer
with mute_logger('odoo.sql_db'), self.assertRaises(UniqueViolation), self.assertRaises(ValidationError):
employee_2_form.save()
@users('admin')
def test_change_user_on_employee(self):
test_other_user = self.env['res.users'].create({
'name': 'Test Other User',
'login': 'test_other_user',
})
test_other_user.partner_id.company_id = self.env.company
test_company = self.env['res.company'].create({
'name' : 'Test User Company',
})
self.env.user.write({'company_ids': test_company.ids, 'company_id': test_company.id})
test_user = self.env['res.users'].create({
'name': 'Test User',
'login': 'test_user',
})
test_user.partner_id.company_id = test_company
bank_account = self.env['res.partner.bank'].create({
'acc_number' : '1234567',
'partner_id' : test_user.partner_id.id,
})
test_employee = self.env['hr.employee'].create({
'name': 'Test User - employee',
'user_id': test_user.id,
'company_id': test_company.id,
'bank_account_ids': [Command.link(bank_account.id)],
})
# change user -> bank account change company
with Form(test_employee) as employee_form:
employee_form.user_id = test_other_user
# change user back -> check that there is no company error
with Form(test_employee) as employee_form:
employee_form.user_id = test_user
def test_change_user_on_employee_keep_partner(self):
"""
Check that removing user from employee keeps the link in
work_contact_id until the user is assigned to another employee.
"""
user = self.env['res.users'].create({
'name': 'Test User',
'login': 'test_user',
})
employee = self.env['hr.employee'].create({
'name': 'Test User - employee',
'user_id': user.id,
})
# remove user
employee.user_id = None
self.assertEqual(employee.work_contact_id, user.partner_id)
self.assertFalse(employee.user_id)
# create new employee from user
user._compute_company_employee()
user.action_create_employee()
self.assertTrue(len(user.employee_ids) == 1, "Test user should have exactly one employee associated with it")
# previous employee shouldn't have a work_contact_id anymore, as the partner is reassigned
self.assertFalse(employee.work_contact_id)
# the new employee should be associated to both the user and its partner
new_employee = user.employee_ids
self.assertEqual(new_employee.work_contact_id, user.partner_id)
self.assertEqual(new_employee.user_id, user)
def test_change_user_on_employee_multi_company(self):
"""
Removing user from employee keeps the link in work_contact_id in the correct company until the user
is assigned to another employee, and does not affect employees in other companies. When the unique
constraint of one employee per user in one company is triggered, the work_contact_id for the
existing employee is nor removed, and employees in other companies are not affected.
"""
company_A = self.env['res.company'].create({'name': 'company_A'})
company_B = self.env['res.company'].create({'name': 'company_B'})
user = self.env['res.users'].create({
'name': 'Test User',
'login': 'test_user',
})
partner = user.partner_id
employee_A = self.env['hr.employee'].create({
'name': 'employee_A',
'user_id': user.id,
'company_id': company_A.id,
})
employee_B = self.env['hr.employee'].create({
'name': 'employee_B',
'user_id': user.id,
'company_id': company_B.id
})
# Creating an employee in one company does not remove the link with employee in the other company
self.assertEqual(user.with_company(company_A).employee_id, employee_A)
self.assertEqual(user.with_company(company_B).employee_id, employee_B)
# Partner is linked to both employees
partner.with_company(company_A).with_company(company_B)._compute_employees_count()
self.assertEqual(partner.employees_count, 2)
# Remove user from employee in one company does not affect link user-employee in the other company
employee_A.user_id = None
self.assertEqual(user.with_company(company_A).employee_id.ids, [])
self.assertEqual(user.with_company(company_B).employee_id, employee_B)
# Partner still linked to both employees
partner.with_company(company_A).with_company(company_B)._compute_employees_count()
self.assertEqual(partner.employees_count, 2)
# Creating a new employee for a user in company A does not affect link user-employee in the other company
new_employee_A = self.env['hr.employee'].create({
'name': 'new_employee_A',
'user_id': user.id,
'company_id': company_A.id,
})
# User cannot be assigned to more than one employee in the same company. work_contact_id should not be removed.
with mute_logger('odoo.sql_db'), self.assertRaises(UniqueViolation), self.assertRaises(ValidationError):
self.env['hr.employee'].create({
'name': 'new_employee_B',
'user_id': user.id,
'company_id': company_B.id,
})
self.assertEqual(user.with_company(company_A).employee_id, new_employee_A)
self.assertEqual(user.with_company(company_B).employee_id, employee_B)
self.assertEqual(partner.employee_ids, employee_B + new_employee_A)
def test_avatar(self):
# Check simple employee has a generated image (initials)
employee_georgette = self.env['hr.employee'].create({'name': 'Georgette Pudubec'})
self.assertTrue(employee_georgette.image_1920)
self.assertTrue(employee_georgette.avatar_1920)
self.assertTrue(employee_georgette.work_contact_id)
self.assertTrue(employee_georgette.work_contact_id.image_1920)
self.assertTrue(employee_georgette.work_contact_id.avatar_1920)
# Check user has a generate image
user_norbert = self.env['res.users'].create({'name': 'Norbert Comidofisse', 'login': 'Norbert6870'})
self.assertTrue(user_norbert.image_1920)
self.assertTrue(user_norbert.avatar_1920)
# Check that linked employee got user image
employee_norbert = self.env['hr.employee'].create({'name': 'Norbert Employee', 'user_id': user_norbert.id})
self.assertEqual(employee_norbert.image_1920, user_norbert.image_1920)
self.assertEqual(employee_norbert.avatar_1920, user_norbert.avatar_1920)
def test_badge_validation(self):
# check employee's barcode should be a sequence of digits and alphabets
employee = self.env['hr.employee'].create({
'name': 'Badge Employee'
})
employee_form = Form(employee)
employee_form.barcode = 'Test@badge1'
with self.assertRaises(ValidationError):
employee_form.save()
employee_form.barcode = 'Testàë@badge'
with self.assertRaises(ValidationError):
employee_form.save()
employee_form.barcode = 'Testbadge2'
employee_form.save()
self.assertEqual(employee_form.barcode, 'Testbadge2')
def test_departure_wizard(self):
""" Test the archiving wizard in the case of multiple employees """
employee_A, employee_B, employee_C = self.env['hr.employee'].create([
{
'name': f'Employee {code}',
'user_id': False,
'work_email': f'employee_{code}@example.com',
} for code in ['A', 'B', 'C']
])
archiving_employees = [employee.id for employee in (employee_A, employee_C)]
wizard = self.env['hr.departure.wizard'].with_context(
employee_termination=True,
active_ids=archiving_employees,
).create({})
wizard.action_register_departure()
all_employees = employee_A | employee_B | employee_C
self.assertEqual(all_employees.filtered(lambda e: e.active), employee_B, "Employees should have been archived")
def test_search_hr_employee_no_access(self):
new_user = new_test_user(self.env, 'employee')
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
})
domain = Domain([
('name', '=', 'Test Employee'),
('active', '=', True)
]).optimize(self.env['hr.employee'])
with self.assertNoLogs('odoo.domains'):
self.assertEqual(
employee.ids,
self.env['hr.employee'].with_user(new_user).search(domain).ids,
)
def test_is_flexible(self):
employee = self.env['hr.employee'].create({
'name': 'Employee',
})
self.assertTrue(employee.resource_calendar_id)
self.assertFalse(employee.is_flexible)
self.assertFalse(employee.is_fully_flexible)
employee.resource_calendar_id.flexible_hours = True
self.assertTrue(employee.is_flexible)
self.assertFalse(employee.is_fully_flexible)
employee.resource_calendar_id = False
self.assertTrue(employee.is_flexible)
self.assertTrue(employee.is_fully_flexible)
def test_resource_calendar_sync_with_employee_one(self):
calendar = self.env['resource.calendar'].create({
'name': 'test calendar',
'flexible_hours': True,
})
self.assertTrue(self.employee.resource_id)
self.assertTrue(self.employee.resource_calendar_id)
self.assertEqual(self.employee.resource_calendar_id, self.employee.resource_id.calendar_id)
self.assertNotEqual(self.employee.resource_calendar_id, calendar)
self.assertTrue(self.employee.resource_calendar_id, self.employee.resource_id.calendar_id)
old_calendar = self.employee.resource_calendar_id
old_version = self.employee.version_id
old_version.date_version = old_version.date_version - relativedelta(days=1)
self.employee.resource_calendar_id = calendar
self.assertEqual(self.employee.resource_id.calendar_id, calendar)
version = self.employee.create_version({'resource_calendar_id': old_calendar.id, 'date_version': fields.Date.today()})
self.assertEqual(self.employee.current_version_id, version)
self.assertNotEqual(self.employee.current_version_id, old_version)
self.assertEqual(self.employee.resource_calendar_id, old_calendar)
self.assertEqual(self.employee.resource_id.calendar_id, old_calendar)
def test_job_title(self):
first_job = self.env['hr.job'].create({'name': "first job"})
second_job = self.env['hr.job'].create({'name': "second job"})
with Form(self.employee_without_image) as employee_form:
# Assign first job to employee, job title should be job name
employee_form.job_id = first_job
self.assertEqual(employee_form.job_title, first_job.name)
# Change job title, job name should not change
employee_form.job_title = "custom job title"
self.assertEqual(first_job.name, "first job")
# Change the name of the first job, job title should not be updated
first_job.name = "first job modified"
self.assertEqual(employee_form.job_title, "custom job title")
employee_form.save()
# Assign second job to employee, job title should be second job name
employee_form.job_id = second_job
self.assertEqual(employee_form.job_title, second_job.name)
# Switch back to first job, job title should be first job name
employee_form.job_id = first_job
self.assertEqual(employee_form.job_title, first_job.name)
def test_flexible_working_hours(self):
"""
Test to verifie that get_unusual_days() return false for flexible work schedule
"""
# Creating a flexible working schedule
calendar_flex = self.env['resource.calendar'].create([
{
'tz': "Europe/Brussels",
'name': 'flexible hours',
'flexible_hours': "True",
},
])
employeeA = self.env['hr.employee'].create({
'name': 'Employee',
})
# Testing employeA on regular working schedule
days = employeeA._get_unusual_days(str(datetime(2025, 1, 1)), str(datetime(2025, 12, 31)))
self.assertTrue(days)
self.assertTrue(days['2025-01-04'])
# Assigning flexible work hours to employeeA
employeeA.resource_calendar_id = calendar_flex.id
days = employeeA._get_unusual_days(str(datetime(2025, 1, 1)), str(datetime(2025, 12, 31)))
self.assertTrue(days)
self.assertFalse(days['2025-01-04'])
def test_user_creation_from_employee_with_invalid_email(self):
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'work_email': 'test'
})
action = employee.action_create_users()
self.assertEqual(action['params']['message'], f'You need to set a valid work email address for {employee.name}')
self.assertFalse(employee.user_id)
def test_user_creation_from_employee_multi_emails(self):
employees = self.env['hr.employee'].create([
{
'name': 'Existing Email Employee',
'work_email': self.user_without_image.email,
}, {
'name': 'New Employee',
'work_email': 'newuser@example.com',
}, {
'name': 'Invalid Email Employee',
'work_email': 'invalid-email',
}, {
'name': 'Without Email Employee',
'work_email': False,
}, {
'name': 'Formatted Email Employee',
'work_email': f'"John Doe" <{self.user_without_image.email_normalized}>',
}, {
'name': 'Multi Email Employee',
'work_email': '"Name1" <name@test.example.com>, "Name 2" <name2@test.example.com>',
},
])
# Add an existing employee who already has a user to the employee list
employees += self.employee_without_image
context = {'selected_ids': employees.ids}
confirmed_employees = self.env['hr.employee'].with_context(context).browse(employees.ids)
action = confirmed_employees.action_create_users()
params = action.get('params')
self.assertEqual(params.get('message'), f"User already exists with the same email for Employees {employees[0].name}, {employees[4].name}")
params = params.get('next').get('params')
self.assertEqual(params.get('message'), f"You need to set a valid work email address for {employees[2].name}, {employees[5].name}")
params = params.get('next').get('params')
self.assertEqual(params.get('message'), f"You need to set the work email address for {employees[3].name}")
params = params.get('next').get('params')
self.assertEqual(params.get('message'), f"User already exists for Those Employees {employees[6].name}")
params = params.get('next').get('params')
self.assertEqual(params.get('message'), f"Users {employees[1].name} creation successful")
self.assertTrue(employees[1].user_id)
def test_user_contact_phone_sync(self):
partner = self.env['res.partner'].create({'name': 'Partner Test'})
first_company = self.env['res.company'].create({'name': 'First Company'})
first_employee = self.env['hr.employee'].create({
'name': 'First Employee',
'work_contact_id': partner.id,
'company_id': first_company.id,
})
first_employee.write({'work_phone': '12345', 'work_email': 'first_employee@test.com'})
self.assertEqual(first_employee.work_phone, partner.phone)
self.assertEqual(first_employee.work_email, partner.email)
partner.write({'phone': '67890', 'email': 'partner@test.com'})
self.assertEqual(partner.phone, first_employee.work_phone)
self.assertEqual(partner.email, first_employee.work_email)
second_company = self.env['res.company'].create({'name': 'Second Company'})
second_employee = self.env['hr.employee'].create({
'name': 'Second Employee',
'work_contact_id': partner.id,
'company_id': second_company.id,
})
second_employee.write({'work_phone': '112233', 'work_email': 'second_employee@test.com'})
self.assertNotEqual(second_employee.work_phone, partner.phone)
self.assertNotEqual(second_employee.work_phone, first_employee.work_phone)
self.assertNotEqual(second_employee.work_email, partner.email)
self.assertNotEqual(second_employee.work_email, first_employee.work_email)
partner.write({'phone': '445566', 'email': 'partner_updated@test.com'})
self.assertNotEqual(partner.phone, second_employee.work_phone)
self.assertNotEqual(partner.phone, first_employee.work_phone)
self.assertNotEqual(partner.email, second_employee.work_email)
self.assertNotEqual(partner.email, first_employee.work_email)
@tagged('-at_install', 'post_install')
class TestHrEmployeeLinks(HttpCase):
def test_shared_private_link_permissions(self):
"""
Employees not part of group_hr_user are not supposed to be able to see
private employees pages (e.g.: from a shared link).
The tour will check if the correct redirection warning appears when such
case happens.
"""
user_amy = new_test_user(
self.env,
name="Amy Rose",
login='amy',
groups='base.group_user' # cannot access private employee profiles
)
employee_sonic = self.env['hr.employee'].create({
'name': 'Sonic the Hedgehog',
})
with mute_logger('odoo.http'): # ignore raised RedirectWarning
self.start_tour(
f"/odoo/employees/{employee_sonic.id}",
"check_public_employee_link_redirect",
login=user_amy.login,
)
@tagged('-at_install', 'post_install')
class TestVersionCron(TransactionCase):
"""Test the behavior of CRONs affecting hr.version"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Will be used for default employee version address (contains phone)
cls.env.user.company_id = cls.env['res.company'].create(
{'name': 'Pokémon Center', 'phone': '+32404040404'}
)
# Employee has a default version that will be overridden
with freeze_time("2020-10-07"):
cls.employee = cls.env['hr.employee'].create(
{
'name': 'Charizard',
'work_phone': '+32404040404',
"distance_home_work": 32,
"distance_home_work_unit": 'miles',
}
)
def test_version_cron_update_no_fields(self):
"""
Employees should not see their fields be updated if the CRON does not
change their version.
"""
with freeze_time('2023-10-06'):
self.employee.create_version(
{'date_version': '2023-10-07', "distance_home_work": 40}
)
# Saving current employee data to compare later on
employee_values = {}
# some fields cannot be accessed. We need to filter them out
employee_fields = [
field
for field in self.env['hr.employee']._fields
if hasattr(self.employee, field)
]
for field in employee_fields:
employee_values[field] = self.employee[field]
# Should not change to new version
with freeze_time('2023-10-06'):
self.env['hr.employee']._cron_update_current_version_id()
for field in employee_fields:
self.assertEqual(
employee_values[field],
self.employee[field],
f"""No field should change if _cron_update_current_version_id() does not change the version.
However, the field {field} changed""",
)
def test_version_cron_update_fields(self):
"""
Employees should see some of their field be changed if the CRON changes
their version.
"""
with freeze_time('2023-10-06'):
self.employee.create_version(
{'date_version': '2023-10-07', "distance_home_work": 40}
)
current_home_distance = self.employee.distance_home_work
current_version = self.employee.current_version_id
# Should change to new version
with freeze_time('2023-10-07'):
self.env['hr.employee']._cron_update_current_version_id()
self.assertNotEqual(
current_version,
self.employee.current_version_id,
"current_version_id should have changed after calling _cron_update_current_version_id()",
)
self.assertNotEqual(
current_home_distance,
self.employee.distance_home_work,
"distance_home_work should have changed after calling _cron_update_current_version_id()",
)
@tagged('-at_install', 'post_install')
class TestHrEmployeeWebJson(HttpCase):
def setUp(self):
super().setUp()
# JSON route needs to be enabled for the tests
self.env['ir.config_parameter'].sudo().set_param('web.json.enabled', True)
def test_webjson_employees(self):
# Check that json employees can be accessed
url = "/json/1/employees"
self.env['ir.config_parameter'].set_param('web.json.enabled', True)
self.authenticate('admin', 'admin')
CSRF_USER_HEADERS = {
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": 'none',
"Sec-Fetch-User": "?1",
}
res = self.url_open(url, headers=CSRF_USER_HEADERS)
self.assertEqual(res.status_code, 200)

View file

@ -0,0 +1,27 @@
# 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.addons.hr.tests.common import TestHrCommon
class TestHrEmployee(TestHrCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.res_users_without_hr_right = mail_new_test_user(
cls.env,
email='nhr@example.com',
login='nhr',
groups='base.group_user,base.group_partner_manager',
name='No HR Right',
)
def test_access_related_field_to_hr_employee(self):
# Check if a related field related to hr_employee is accessible.
self.env['hr.employee.public'].with_user(self.res_users_without_hr_right).search([("email", "!=", False)])
def test_access_search_on_users_department(self):
User = self.env['res.users'].with_user(self.res_users_without_hr_right)
User.search([('employee_id.department_id', '=', 1)])

View file

@ -0,0 +1,842 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from psycopg2.errors import CheckViolation
from odoo.tests import tagged
from odoo.tests.common import freeze_time
from odoo.exceptions import AccessError, ValidationError
from odoo.tools import mute_logger
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.addons.mail.tests.common import mail_new_test_user
@tagged('post_install', '-at_install')
class TestHrVersion(TestHrCommon):
def test_dates_constraints(self):
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01'
})
self.assertFalse(employee.contract_date_start)
self.assertFalse(employee.contract_date_end)
employee.write({
'contract_date_start': '2020-01-01',
'contract_date_end': False
})
self.assertEqual(employee.contract_date_start, date(2020, 1, 1))
self.assertFalse(employee.contract_date_end)
employee.write({
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31'
})
self.assertEqual(employee.contract_date_start, date(2020, 1, 1))
self.assertEqual(employee.contract_date_end, date(2020, 12, 31))
employee.write({
'contract_date_start': False,
'contract_date_end': False
})
self.assertFalse(employee.contract_date_start)
self.assertFalse(employee.contract_date_end)
with self.assertRaises(CheckViolation), mute_logger('odoo.sql_db'):
employee.write({
'contract_date_start': False,
'contract_date_end': '2020-12-31'
})
with self.assertRaises(ValidationError):
employee.write({
'contract_date_start': '2021-01-01',
'contract_date_end': '2020-12-31'
})
def test_contracts_no_overlap(self):
# Simple overlap cases
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31'
})
with self.assertRaises(ValidationError):
employee.create_version({
'date_version': '2019-06-01',
'contract_date_start': '2019-06-01',
'contract_date_end': '2020-05-31'
})
with self.assertRaises(ValidationError):
employee.create_version({
'date_version': '2020-06-01',
'contract_date_start': '2020-06-01',
'contract_date_end': '2021-05-31'
})
with self.assertRaises(ValidationError):
employee.create_version({
'date_version': '2020-02-01',
'contract_date_start': '2020-02-01',
'contract_date_end': '2020-10-31'
})
# It should not detect overlap with archived versions
employee.create_version({
'active': False,
'date_version': '2019-06-01',
'contract_date_start': '2019-06-01',
'contract_date_end': '2020-05-31'
})
def test_occupation_dates(self):
"""
Occupation dates are global for the employee, they are a list of intervals
(date_from, date_to) where the employee is in contract (date_from and date_to included).
"""
# A single version and no contract
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01'
})
self.assertEqual(employee._get_all_contract_dates(), [])
# A single version and contract
employee.write({
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31'
})
occupation_dates = [(date(2020, 1, 1), date(2020, 12, 31))]
self.assertEqual(employee._get_all_contract_dates(), occupation_dates)
# 2 versions with 1 contract each
employee.create_version({
'date_version': '2021-01-01',
'contract_date_start': '2021-01-01',
'contract_date_end': '2023-12-31'
})
occupation_dates = [
(date(2020, 1, 1), date(2020, 12, 31)),
(date(2021, 1, 1), date(2023, 12, 31)),
]
self.assertEqual(employee._get_all_contract_dates(), occupation_dates)
# 3 versions with 2 sharing the same contract
employee.create_version({
'date_version': '2022-01-01',
})
self.assertEqual(employee._get_all_contract_dates(), occupation_dates)
# 4 versions with 2 sharing the same contract, the last one is permanent
employee.create_version({
'date_version': '2025-01-01',
'contract_date_start': '2025-01-01',
})
occupation_dates = [
(date(2020, 1, 1), date(2020, 12, 31)),
(date(2021, 1, 1), date(2023, 12, 31)),
(date(2025, 1, 1), False),
]
self.assertEqual(employee._get_all_contract_dates(), occupation_dates)
def test_dates_new_version_out_of_contract(self):
"""
If the new version falls on a period out of contract, clear the dates
"""
# Create a new version after the end of the current contract
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31',
})
version = employee.create_version({
'date_version': '2021-01-01'
})
self.assertFalse(version.contract_date_start)
self.assertFalse(version.contract_date_end)
# Forcing the contract_date_start and or contract_date_end in the 'create' should override False
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31',
})
version = employee.create_version({
'date_version': '2021-01-01',
'contract_date_start': '2021-01-01',
'contract_date_end': '2021-12-31'
})
self.assertEqual(version.contract_date_start, date(2021, 1, 1))
self.assertEqual(version.contract_date_end, date(2021, 12, 31))
# Create a new version before the start of the current contract
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31',
})
version = employee.create_version({'date_version': '2019-01-01'})
self.assertFalse(version.contract_date_start)
self.assertFalse(version.contract_date_end)
# Create a new version between two contracts
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31'
})
employee.create_version({
'date_version': '2022-01-01',
'contract_date_start': '2022-01-01',
'contract_date_end': '2022-12-31'
})
version = employee.create_version({
'date_version': '2021-01-01',
})
self.assertFalse(version.contract_date_start)
self.assertFalse(version.contract_date_end)
def test_dates_new_version_in_contract(self):
"""
If the new version falls on some contract, copy its contract dates
"""
# Create a new version on a permanent contract
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
})
version = employee.create_version({
'date_version': '2021-01-01'
})
self.assertEqual(version.contract_date_start, date(2020, 1, 1))
self.assertFalse(version.contract_date_end)
# Create a new version on a fixed term contract
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'contract_date_start': '2020-01-01',
'contract_date_end': '2021-12-31'
})
version = employee.create_version({'date_version': '2021-01-01'})
self.assertEqual(version.contract_date_start, date(2020, 1, 1))
self.assertEqual(version.contract_date_end, date(2021, 12, 31))
# Create a new version on any contract interval regardless of the version valid at that date
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31'
})
employee.create_version({
'date_version': '2022-01-01',
'contract_date_start': '2021-01-01',
'contract_date_end': '2022-12-31'
})
version = employee.create_version({
'date_version': '2021-01-01',
})
self.assertEqual(version.contract_date_start, date(2021, 1, 1))
self.assertEqual(version.contract_date_end, date(2022, 12, 31))
def test_dates_synchronisation(self):
"""
All versions that share or will share (at the end of a 'write')
the same contract_date_start are synchronized.
"""
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2021-12-31',
})
v1 = employee.version_id
v2 = employee.create_version({
'date_version': '2021-01-01',
'contract_date_end': '2022-12-31',
})
self.assertEqual(v2.contract_date_end, date(2022, 12, 31))
self.assertEqual(v1.contract_date_end, v2.contract_date_end)
v1.write({
'contract_date_start': '2021-01-01',
})
self.assertEqual(v1.contract_date_start, date(2021, 1, 1))
self.assertEqual(v1.contract_date_start, v2.contract_date_start)
v2.write({
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31',
})
self.assertEqual(v2.contract_date_start, date(2020, 1, 1))
self.assertEqual(v1.contract_date_start, v2.contract_date_start)
self.assertEqual(v2.contract_date_end, date(2020, 12, 31))
self.assertEqual(v1.contract_date_end, v2.contract_date_end)
v3 = employee.create_version({
'date_version': '2030-01-01',
'contract_date_start': '2030-01-01',
})
versions = []
for i in range(10):
versions.append(employee.create_version({
'date_version': f'20{31 + i}-01-01',
}))
v3.write({
'contract_date_end': '2040-12-31',
})
for version in versions:
self.assertEqual(version.contract_date_end, date(2040, 12, 31))
def test_1_version_contract_synchronisation(self):
"""
When an employee has only one version, the contract_date_start should
be synchronized with the date_version.
"""
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
})
version = employee.version_id
employee.write({'contract_date_start': '2019-01-01'})
self.assertEqual(version.contract_date_start, version.date_version)
# date_version should not be reset if the contract_date_start is set to False
employee.write({'contract_date_start': False})
self.assertEqual(version.date_version, date(2019, 1, 1))
employee.write({'contract_date_start': '2021-01-01'})
self.assertEqual(version.contract_date_start, version.date_version)
def test_2_versions_contract_synchronisation(self):
"""
When an employee has two versions, the synchronisation should stop
"""
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
})
v1 = employee.version_id
employee.write({'contract_date_start': '2019-01-01'})
self.assertEqual(v1.contract_date_start, v1.date_version)
v2 = employee.create_version({'date_version': '2021-01-01'})
employee.write({'contract_date_start': '2020-01-01'})
self.assertEqual(v1.date_version, date(2019, 1, 1))
self.assertEqual(v2.date_version, date(2021, 1, 1))
# Archived versions do not count.
# So if we archive v2, the synchronisation should start again.
v2.active = False
employee.write({'contract_date_start': '2021-01-01'})
self.assertEqual(v1.contract_date_start, v1.date_version)
def test_in_out_contract(self):
"""
Check that an employee is in or out of the contract at a specific date.
"""
# If no contract dates are defined, the employee is not considered in contract
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
})
self.assertFalse(employee._is_in_contract(date(2010, 1, 1)))
self.assertFalse(employee._is_in_contract(date(2020, 1, 1)))
self.assertFalse(employee._is_in_contract(date(2030, 1, 1)))
# In a permanent contract, the employee is contract since the contract_date_start
employee.contract_date_start = '2020-01-01'
self.assertFalse(employee._is_in_contract(date(2010, 1, 1)))
self.assertTrue(employee._is_in_contract(date(2020, 1, 1)))
self.assertTrue(employee._is_in_contract(date(2030, 1, 1)))
# In a fixed term contract, the employee is contract in between the contract dates
employee.contract_date_end = '2029-12-31'
self.assertFalse(employee._is_in_contract(date(2010, 1, 1)))
self.assertTrue(employee._is_in_contract(date(2020, 1, 1)))
self.assertFalse(employee._is_in_contract(date(2030, 1, 1)))
def test_cron_update_current_version(self):
cron = self.env.ref('hr.ir_cron_data_employee_update_current_version')
with freeze_time(date(2020, 1, 1)), self.enter_registry_test_mode():
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
})
v1 = employee.version_id
v2 = employee.create_version({
'date_version': '2020-01-02'
})
self.assertEqual(v1, employee.current_version_id)
cron.method_direct_trigger()
self.assertEqual(v1, employee.current_version_id)
with freeze_time(date(2020, 1, 2)), self.enter_registry_test_mode():
self.assertEqual(v1, employee.current_version_id)
cron.method_direct_trigger()
self.assertEqual(v2, employee.current_version_id)
def test_related_fields_on_version(self):
""" Some groups have been added to avoid users with basic access to HR app see some critical (like wage field for instance)
This test makes sure the groups added in version fields is also in the employee fields related.
However, to define the same groups in employee fields, we have to redefine the related fields (readonly=False, related='version_id.{field_name})
Otherwise, the field we loose the linked with the version field and could be readonly instead of editable.
"""
version_fields = {
f_name: field
for f_name, field in self.env['hr.version']._fields.items()
if field.groups and field.groups not in ['hr.group_hr_user', 'base.group_user'] and not (field.related and field.related.startswith('employee_id'))
}
employee_fields = {
f_name: field
for f_name, field in self.env['hr.employee']._fields.items()
if f_name in version_fields
}
fields_without_group = []
fields_without_related = []
fields_readonly = []
for f_name, field in employee_fields.items():
v_field = version_fields[f_name]
if not (field.groups and field.groups != v_field):
fields_without_group.append(f_name)
elif not (field.related and field.related == f'version_id.{f_name}'):
fields_without_related.append(f_name)
elif field.readonly != v_field.readonly:
fields_readonly.append(f_name)
self.assertFalse(fields_without_group, "Inconsistency between some employee fields and version ones (those employees fields should have the same groups than related one in version")
self.assertFalse(fields_without_related, "Some employee fields have the same name than the version ones but they are not related")
self.assertFalse(fields_readonly, "(Readonly) Inconsistency between some employee fields and version ones, the both fields (in version and employee) have to be readonly or editable")
def test_multi_edit_contract_sync_same_contract(self):
"""
Test the multi-edit contract sync feature when the targeted versions share the same contract.
"""
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31',
})
versions = employee.version_id
versions |= employee.create_version({'date_version': '2020-04-01'})
versions |= employee.create_version({'date_version': '2020-08-01'})
versions[:2].contract_date_end = "2020-9-30"
for version in versions:
self.assertEqual(version.contract_date_end, date(2020, 9, 30))
def test_multi_edit_contract_sync_different_contract(self):
"""
Test the multi-edit contract sync feature when the targeted versions have different contracts.
"""
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-5-31',
})
versions = employee.version_id
versions |= employee.create_version({'date_version': '2020-04-01'})
versions |= employee.create_version({
'date_version': '2020-08-01',
'contract_date_start': '2020-08-01',
})
with self.assertRaises(ValidationError):
versions[:2].contract_date_end = "2020-9-30"
def test_multi_edit_other(self):
"""
Test the multi-edit when the targeted versions have different contracts
Different fields than contract_date_start and contract_date_end are changed.
"""
jobA = self.env['hr.job'].create({'name': "Job A"})
jobB = self.env['hr.job'].create({'name': "Job B"})
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-5-31',
'job_id': jobA.id,
})
versions = employee.version_id
versions |= employee.create_version({'date_version': '2020-04-01'})
versions |= employee.create_version({
'date_version': '2020-08-01',
'contract_date_start': '2020-08-01',
'job_id': jobA.id,
})
versions[1:].job_id = jobB.id
self.assertEqual(versions[0].job_id.id, jobA.id)
for version in versions[1:]:
self.assertEqual(version.job_id.id, jobB.id)
def test_multi_edit_other_and_contract_date_sync(self):
"""
Test the multi-edit when the targeted versions have the same contract
Different contract_dates and other fields are changed.
"""
jobA = self.env['hr.job'].create({'name': "Job A"})
jobB = self.env['hr.job'].create({'name': "Job B"})
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-12-31',
'job_id': jobA.id,
})
versions = employee.version_id
versions |= employee.create_version({'date_version': '2020-04-01'})
versions |= employee.create_version({'date_version': '2020-08-01'})
versions[1:].write({
"contract_date_end": "2020-9-30",
"job_id": jobB.id,
})
self.assertEqual(versions[0].job_id.id, jobA.id)
self.assertEqual(versions[0].contract_date_end, date(2020, 9, 30))
for version in versions[1:]:
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
"""
employee_john, employee_rob = self.env['hr.employee'].create([
{
'name': 'John Doe',
'date_version': '2020-01-01',
},
{
'name': 'Rob Carter',
'date_version': '2020-10-18',
}
])
versions = (employee_john | employee_rob).version_id
versions.write({
'contract_date_start': '2021-10-10'
})
self.assertEqual(versions[0].contract_date_start, date(2021, 10, 10))
self.assertEqual(versions[1].contract_date_start, date(2021, 10, 10))
def test_multi_edit_multi_employees_mix_contract(self):
"""
Test the multi-edit when there is one version per employee, some with contract
"""
employee_john, employee_rob = self.env['hr.employee'].create([
{
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
},
{
'name': 'Rob Carter',
'date_version': '2020-10-18',
}
])
versions = (employee_john | employee_rob).version_id
versions.write({
'contract_date_start': '2021-10-10'
})
self.assertEqual(versions[0].contract_date_start, date(2021, 10, 10))
self.assertEqual(versions[1].contract_date_start, date(2021, 10, 10))
def test_multi_edit_multi_employees_all_contract(self):
"""
Test the multi-edit when there is one version per employee, all with different contract
"""
employee_john, employee_rob = self.env['hr.employee'].create([
{
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
},
{
'name': 'Rob Carter',
'date_version': '2020-10-18',
'contract_date_start': '2020-10-18',
}
])
versions = (employee_john | employee_rob).version_id
versions |= employee_john.create_version({
'date_version': '2021-08-01',
'contract_date_start': '2020-01-01',
})
versions.write({
'contract_date_start': '2021-10-10'
})
self.assertEqual(versions[0].contract_date_start, date(2021, 10, 10))
self.assertEqual(versions[1].contract_date_start, date(2021, 10, 10))
self.assertEqual(versions[2].contract_date_start, date(2021, 10, 10))
def test_multi_edit_multi_employees_incompatible(self):
"""
Test the multi-edit when there is one version per employee, one with incompatible dates
"""
employee_john, employee_rob = self.env['hr.employee'].create([
{
'name': 'John Doe',
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2020-10-10'
},
{
'name': 'Rob Carter',
'date_version': '2020-10-18',
'contract_date_start': '2020-10-18',
}
])
versions = (employee_john | employee_rob).version_id
versions |= employee_john.create_version({
'date_version': '2021-08-01',
'contract_date_start': '2021-08-01',
})
with self.assertRaises(ValidationError):
versions.write({
'contract_date_start': '2021-10-10'
})
def test_hr_version_fields_tracking(self):
tracking_blacklist = {
"__last_update",
"active_employee",
"activity_ids",
"company_country_id",
"contract_wage",
"country_code",
"create_date",
"create_uid",
"currency_id",
"date_end",
"date_start",
"departure_description",
"display_name",
"id",
"is_current",
"is_flexible",
"is_fully_flexible",
"is_future",
"is_in_contract",
"is_past",
"job_title",
"last_modified_date",
"last_modified_on",
"last_modified_uid",
"member_of_department",
"message_follower_ids",
"message_ids",
"message_partner_ids",
"rating_ids",
"template_warning",
"tz",
"website_message_ids",
"work_location_name",
"work_location_type",
"write_date",
"write_uid",
}
hr_version_model = self.env['hr.version']
fields_without_tracking = []
for field_name, field in hr_version_model._fields.items():
if field_name in tracking_blacklist:
continue
if field.compute and not field.inverse:
continue
if field.related:
continue
if hasattr(field, 'store') and field.store is False:
continue
if hasattr(field, 'tracking') and not field.tracking:
fields_without_tracking.append(field_name)
self.assertFalse(
fields_without_tracking,
f"The following hr.version fields should have tracking=True: {fields_without_tracking}",
)
def test_related_fields_on_version_onchange(self):
""" This test is to ensure that each _onchange method on version has a corresponding _onchange on employee that calls it. """
version_methods = {method for method in dir(self.env['hr.version']) if method.startswith('_onchange')}
employee_methods = {method for method in dir(self.env['hr.employee']) if method.startswith('_onchange')}
not_implemented_onchanges = version_methods - employee_methods
self.assertFalse(
not_implemented_onchanges,
f"""The following _onchange methods on hr.version should have corresponding methods implemented on hr.employee: {not_implemented_onchanges}\n
You might need to implement methods with the same name on hr.employee and call the corresponding self.version_id._onchange inside"""
)
def test_search_on_version_fields(self):
Department = self.env['hr.department'].with_context(tracking_disable=True)
rd_dep = Department.create({
'name': 'Research and devlopment',
})
employee1, employee2 = employees = self.env['hr.employee'].create([
{
'contract_date_start': '2020-10-10',
'wage': 3000,
'name': 'Employee1',
'hr_responsible_id': self.res_users_hr_manager.id,
'department_id': rd_dep.id,
},
{
'contract_date_start': '2022-10-10',
'wage': 2000,
'name': 'Employee2',
},
])
internal_user = mail_new_test_user(
self.env,
email='internal_user@example.com',
login='internal_user',
name='Internal User',
)
self.employee.department_id = rd_dep
self.employee.user_id = internal_user
HrEmployeePublic_with_internal_user = self.env['hr.employee.public'].with_user(internal_user)
with self.assertRaises(AccessError, msg="Internal user should not be able to access to hr.employee model"):
HrEmployeePublic_with_internal_user.search([
('employee_id.contract_date_start', '<', '2022-01-01'),
('id', 'in', employees.ids),
])
with self.assertRaises(AccessError, msg="Internal user should not be able to access to hr.employee model"):
HrEmployeePublic_with_internal_user.search([('employee_id.wage', '=', 2000), ('id', 'in', employees.ids)])
with self.assertRaises(AccessError, msg="Internal user should not be able to access to hr.employee model"):
HrEmployeePublic_with_internal_user.search([('employee_id.version_id.wage', '=', 2000), ('id', 'in', employees.ids)])
self.assertEqual(
HrEmployeePublic_with_internal_user.search([('name', '=', 'Employee2'), ('id', 'in', employees.ids)]),
self.env['hr.employee.public'].browse(employee2.id),
)
self.assertEqual(
HrEmployeePublic_with_internal_user.search([('member_of_department', '=', True), ('id', 'in', employees.ids)]),
self.env['hr.employee.public'].browse(employee1.id),
)
HrEmployee_with_office_user = self.env['hr.employee'].with_user(self.res_users_hr_officer)
self.employee.user_id = self.res_users_hr_officer
with self.assertRaises(AccessError, msg="HR Officer should not be able to access to 'payroll fields'"):
HrEmployee_with_office_user.search([('contract_date_start', '<', '2022-01-01'), ('id', 'in', employees.ids)])
with self.assertRaises(AccessError, msg="HR Officer should not be able to access to 'payroll fields'"):
HrEmployee_with_office_user.search([('wage', '=', 2000), ('id', 'in', employees.ids)])
with self.assertRaises(AccessError, msg="HR Officer should not be able to access to 'payroll fields'"):
HrEmployee_with_office_user.search([('version_id.wage', '=', 2000), ('id', 'in', employees.ids)])
self.assertEqual(HrEmployee_with_office_user.search([('name', '=', 'Employee1'), ('id', 'in', employees.ids)]), employee1)
self.assertEqual(HrEmployee_with_office_user.search([('hr_responsible_id', '=', self.res_users_hr_manager.id), ('id', 'in', employees.ids)]), employee1)
self.assertEqual(HrEmployee_with_office_user.search([('version_id.hr_responsible_id', '=', self.res_users_hr_manager.id), ('id', 'in', employees.ids)]), employee1)
self.assertEqual(HrEmployee_with_office_user.search([('member_of_department', '=', True), ('id', 'in', employees.ids)]), employee1)
if payroll_group := self.env.ref('hr_payroll.group_hr_payroll_user', raise_if_not_found=False):
self.res_users_hr_manager.group_ids += payroll_group
HrEmployee_with_manager_user = self.env['hr.employee'].with_user(self.res_users_hr_manager)
self.employee.user_id = self.res_users_hr_manager
self.assertEqual(HrEmployee_with_manager_user.search([('contract_date_start', '<', '2022-01-01'), ('id', 'in', employees.ids)]), employee1)
self.assertEqual(HrEmployee_with_manager_user.search([('wage', '=', 2000), ('id', 'in', employees.ids)]), employee2)
self.assertEqual(HrEmployee_with_manager_user.search([('version_id.wage', '=', 2000), ('id', 'in', employees.ids)]), employee2)
self.assertEqual(HrEmployee_with_manager_user.search([('hr_responsible_id', '=', self.res_users_hr_manager.id), ('id', 'in', employees.ids)]), employee1)
self.assertEqual(HrEmployee_with_manager_user.search([('version_id.hr_responsible_id', '=', self.res_users_hr_manager.id), ('id', 'in', employees.ids)]), employee1)
self.assertEqual(HrEmployee_with_manager_user.search([('member_of_department', '=', True), ('id', 'in', employees.ids)]), employee1)
def test_archive_or_unassign_all_versions(self):
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
})
another_employee = self.env['hr.employee'].create({
'name': 'Jane Doe'
})
employee.create_version({
'date_version': '2021-01-01',
})
# make sure there are at least 2 versions
self.assertEqual(len(employee.version_ids), 2)
# attempt to archive all versions
with self.assertRaises(ValidationError):
employee.version_ids.action_archive()
# attempt to reassign all versions
with self.assertRaises(ValidationError):
employee.version_ids.write({"employee_id": another_employee.id})
def test_unlink_version_except_one(self):
employee = self.env['hr.employee'].create({
'name': 'John Doe',
'date_version': '2020-01-01',
})
version = employee.create_version({
'date_version': '2021-01-01',
})
self.assertEqual(len(employee.version_ids), 2)
version.unlink()
self.assertEqual(len(employee.version_ids), 1)
def test_date_version_sync_contract_date_start_for_single_version(self):
"""
This test is to ensure that in case when an employee has only one version, writing contract_date_start on the employee
will synchronize the version.date_version with that contract_date_start
"""
with freeze_time(date(2025, 12, 20)), self.enter_registry_test_mode():
employee = self.env['hr.employee'].create({
'name': 'John Doe',
})
version = employee.version_id
self.assertEqual(version.date_version, date(2025, 12, 20))
employee.write({'contract_date_start': '2025-12-10'})
self.assertEqual(version.contract_date_start, date(2025, 12, 10))
self.assertEqual(version.date_version, date(2025, 12, 10))
# date_version should not be reset if contract_date_start is cleared
employee.write({'contract_date_start': False})
self.assertEqual(version.date_version, date(2025, 12, 10))
# and setting it again should re-sync
employee.write({'contract_date_start': '2025-12-15'})
self.assertEqual(version.contract_date_start, date(2025, 12, 15))
self.assertEqual(version.date_version, date(2025, 12, 15))

View file

@ -0,0 +1,288 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo import fields, Command
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common_activity import ActivityScheduleCase
from odoo.exceptions import UserError, ValidationError
from odoo.tests import tagged, users
class ActivityScheduleHRCase(ActivityScheduleCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.plan_onboarding = cls.env['mail.activity.plan'].create({
'name': 'Test Onboarding',
'res_model': 'hr.employee',
'template_ids': [
Command.create({
'activity_type_id': cls.activity_type_todo.id,
'responsible_id': cls.user_admin.id,
'responsible_type': 'other',
'sequence': 10,
'summary': 'Plan training',
}), Command.create({
'activity_type_id': cls.activity_type_todo.id,
'responsible_id': False,
'responsible_type': 'coach',
'sequence': 20,
'summary': 'Training',
}),
]
})
cls.plan_party = cls.env['mail.activity.plan'].create({
'name': 'Test Party Plan',
'res_model': 'res.partner',
'template_ids': [
Command.create({
'activity_type_id': cls.activity_type_todo.id,
'responsible_id': cls.user_admin.id,
'responsible_type': 'on_demand',
'sequence': 10,
'summary': 'Party',
}),
]
})
cls.user_manager = mail_new_test_user(
cls.env,
email='test.manager@test.mycompany.com',
groups='base.group_user,hr.group_hr_manager',
login='test_manager',
name='Test Manager',
)
cls.user_coach = mail_new_test_user(
cls.env,
email='test.coach@test.mycompany.com',
groups='base.group_user,hr.group_hr_manager',
login='test_coach',
name='Test Coach',
)
cls.user_employee_1 = mail_new_test_user(
cls.env,
email='test.employee1@test.mycompany.com',
groups='base.group_user,hr.group_hr_manager',
login='test_employee1',
name='Test Employee 1',
)
cls.user_employee_2 = mail_new_test_user(
cls.env,
email='test.employee2@test.mycompany.com',
groups='base.group_user,hr.group_hr_manager',
login='test_employee2',
name='Test Employee 2',
)
cls.user_employee_dep_b = mail_new_test_user(
cls.env,
email='test.employeedepb@test.mycompany.com',
groups='base.group_user,hr.group_hr_manager',
login='test_employee_dep_b',
name='Test Employee DepB',
)
cls.users = cls.user_manager + cls.user_coach + cls.user_employee_1 + cls.user_employee_2 + cls.user_employee_dep_b
cls.user_internal_basic = mail_new_test_user(
cls.env,
email='non.employee@test.mycompany.com',
groups='base.group_user',
login='non_employee',
name='Non Employee',
)
cls.employees = cls.env['hr.employee'].create([
{
'name': user.name,
'user_id': user.id,
'work_email': user.email,
} for user in cls.users
])
cls.employee_manager, cls.employee_coach, cls.employee_1, cls.employee_2, cls.employee_dep_b = cls.employees
cls.department_a = cls.env['hr.department'].create({
'name': 'Test Department A',
'member_ids': [Command.link(employee.id) for employee in cls.employees - cls.employee_dep_b],
})
cls.department_b = cls.env['hr.department'].create({
'name': 'Test Department B',
'member_ids': [Command.link(cls.employee_dep_b.id)],
})
cls.employee_1.coach_id = cls.employee_coach
cls.employee_1.parent_id = cls.employee_manager
cls.employee_2.coach_id = cls.employee_coach
cls.employee_2.parent_id = cls.employee_manager
cls.employee_coach.parent_id = cls.employee_manager
cls.employee_dep_b.coach_id = cls.employee_coach
cls.employee_3 = cls.employee_coach
cls.employee_4 = cls.employee_manager
cls.employee_4.coach_id = cls.employee_coach
for employee, date_start in ((cls.employee_1, '2023-08-01'),
(cls.employee_2, '2023-09-01'),
(cls.employee_3, '2023-12-01'),
(cls.employee_4, '2024-01-01')):
employee.version_id.write({
'contract_date_end': fields.Date.from_string('2025-12-31'),
'contract_date_start': fields.Date.from_string(date_start),
'date_version': fields.Date.from_string(date_start),
'name': 'Contract',
'wage': 1,
})
@tagged('mail_activity', 'mail_activity_plan')
class TestActivitySchedule(ActivityScheduleHRCase):
@users('admin')
def test_department(self):
""" Check that the allowed plan are filtered according to the department. """
no_plan = self.env['mail.activity.plan']
plan_department_a, plan_department_b = self.env['mail.activity.plan'].create([
{
'department_id': department.id,
'name': f'plan {department.name}',
'res_model': 'hr.employee',
'template_ids': [(0, 0, {'activity_type_id': self.activity_type_todo.id})],
} for department in self.department_a + self.department_b
])
for employees, expected_department, authorized_plans, non_authorized_plans in (
(self.employee_1 + self.employee_dep_b, False, self.plan_onboarding, no_plan),
(self.employee_1 + self.employee_2, self.department_a, self.plan_onboarding + plan_department_a, plan_department_b),
(self.employee_1, self.department_a, self.plan_onboarding + plan_department_a, plan_department_b),
(self.employee_dep_b, self.department_b, self.plan_onboarding + plan_department_b, plan_department_a),
):
with self._instantiate_activity_schedule_wizard(employees) as form:
if expected_department:
self.assertEqual(form.department_id, expected_department)
else:
self.assertFalse(form.department_id)
for plan in non_authorized_plans:
self.assertNotIn(plan, form.plan_available_ids)
for plan in authorized_plans:
with self._instantiate_activity_schedule_wizard(employees) as form:
form.plan_id = plan
def test_res_model_compatibility(self):
""" Check that we cannot change the plan model to a model different
of employee if hr plan specific features are used. """
with self.assertRaises(
UserError,
msg="Coach, manager or employee can only be chosen as template responsible with employee plan."):
self.plan_onboarding.res_model = 'res.partner'
self.plan_onboarding.template_ids[1].responsible_type = 'on_demand'
self.plan_onboarding.res_model = 'res.partner'
self.plan_onboarding.res_model = 'hr.employee'
self.plan_onboarding.template_ids[1].responsible_type = 'manager'
with self.assertRaises(
UserError,
msg="Coach, manager or employee can only be chosen as template responsible with employee plan."):
self.plan_onboarding.res_model = 'res.partner'
self.plan_onboarding.template_ids[1].responsible_type = 'employee'
with self.assertRaises(
UserError,
msg="Coach, manager or employee can only be chosen as template responsible with employee plan."):
self.plan_onboarding.res_model = 'res.partner'
self.plan_onboarding.template_ids[1].responsible_type = 'on_demand'
self.plan_onboarding.res_model = 'res.partner'
self.plan_onboarding.res_model = 'hr.employee'
self.plan_onboarding.department_id = self.department_a
self.plan_onboarding.res_model = 'res.partner'
self.assertFalse(self.plan_onboarding.department_id)
def test_responsible(self):
""" Check that the responsible is correctly configured. """
self.plan_onboarding.template_ids[0].write({
'responsible_type': 'manager',
'responsible_id': False,
})
self.plan_onboarding.write({
'template_ids': [(0, 0, {
'activity_type_id': self.activity_type_todo.id,
'summary': 'Send feedback to the manager',
'responsible_type': 'employee',
'sequence': 30,
})],
})
for employees in (self.employee_1, self.employee_1 + self.employee_2):
# Happy case
form = self._instantiate_activity_schedule_wizard(employees)
form.plan_id = self.plan_onboarding
expected_summary_lines = [
('Plan training', self.user_manager.id if len(employees) == 1 else False),
('Training', self.user_coach.id if len(employees) == 1 else False),
('Send feedback to the manager', employees.user_id.id if len(employees) == 1 else False),
]
for summary_line, (expected_description, expected_responsible_id) in zip(
form.plan_schedule_line_ids._records, expected_summary_lines, strict=True
):
self.assertEqual(summary_line['line_description'], expected_description)
self.assertEqual(summary_line['responsible_user_id'], expected_responsible_id)
self.assertFalse(form.has_error)
wizard = form.save()
wizard.action_schedule_plan()
for employee in employees:
activities = self.get_last_activities(employee, 3)
self.assertEqual(len(activities), 3)
self.assertEqual(activities[0].user_id, self.user_manager)
self.assertEqual(activities[1].user_id, self.user_coach)
self.assertEqual(activities[2].user_id, employee.user_id)
# Cases with errors
self.employee_1.parent_id = False
self.employee_1.coach_id = False
form = self._instantiate_activity_schedule_wizard(employees)
form.plan_id = self.plan_onboarding
self.assertTrue(form.has_error)
n_error = form.error.count('<li>')
self.assertEqual(n_error, 2)
self.assertIn(f'Manager of employee {self.employee_1.name} is not set.', form.error)
self.assertIn(f'Coach of employee {self.employee_1.name} is not set.', form.error)
with self.assertRaises(ValidationError):
form.save()
self.employee_1.parent_id = self.employee_manager
self.employee_1.coach_id = self.employee_coach
self.employee_coach.user_id = False
self.employee_manager.user_id = False
form = self._instantiate_activity_schedule_wizard(employees)
form.plan_id = self.plan_onboarding
self.assertTrue(form.has_warning)
n_warning = form.warning.count('<li>')
self.assertEqual(n_warning, 2 * len(employees))
self.assertIn(f"The user of {self.employee_1.name}'s coach is not set.", form.warning)
self.assertIn(f'The manager of {self.employee_1.name} should be linked to a user.', form.warning)
if len(employees) > 1:
self.assertIn(f"The user of {self.employee_2.name}'s coach is not set.", form.warning)
self.assertIn(f'The manager of {self.employee_2.name} should be linked to a user.', form.warning)
# should save without error, with coach
form.save()
self.employee_coach.user_id = self.user_coach
self.employee_manager.user_id = self.user_manager
@freeze_time('2023-08-31')
@users('admin')
def test_default_due_date(self):
for employees, plan_date in (
(self.employee_1, '2023-09-30'),
(self.employee_2, '2023-09-30'),
(self.employee_3, '2023-12-01'),
(self.employee_4, '2024-01-01'),
(self.employee_1 + self.employee_2 + self.employee_3, '2023-09-30'),
(self.employee_2 + self.employee_3, '2023-09-30'),
(self.employee_1 + self.employee_3, '2023-09-30'),
(self.employee_3 + self.employee_4, '2023-12-01'),
(self.employee_4 + self.employee_3, '2023-12-01'),
):
with self._instantiate_activity_schedule_wizard(employees) as form:
form.plan_id = self.plan_onboarding
self.assertEqual(form.plan_date, fields.Date.from_string(plan_date))
# not applicable on other models
customers = self.env['res.partner'].create([
{'name': 'Customer1'},
{'name': 'Customer2'},
])
with self._instantiate_activity_schedule_wizard(customers) as form:
form.plan_id = self.plan_party
self.assertEqual(form.plan_date, fields.Date.from_string('2023-08-31'))

View file

@ -0,0 +1,69 @@
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests.common import tagged
@tagged('post_install', '-at_install', 'mail_flow')
class TestHrEmployeeMail(TestHrCommon, MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_template_employee = cls.env['mail.template'].with_user(cls.user_admin).create({
'auto_delete': True,
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'email_from': '{{ object.user_id.email_formatted or user.email_formatted or "" }}',
'model_id': cls.env['ir.model']._get_id('hr.employee'),
'name': 'Test Hr Template',
'subject': 'Test {{ object.name }}',
'use_default_to': True,
})
# note: email and phone are user related fields
cls.test_employee = cls.env['hr.employee'].create([
{
'company_id': cls.company_admin.id,
'country_id': cls.env.ref('base.be').id,
'name': 'QuickEmployee',
'work_email': 'quick.employee@test.example.com',
'work_phone': '+32455001122',
},
])
def test_assert_initial_values(self):
self.assertTrue(self.test_employee.work_contact_id)
self.assertFalse(self.test_employee.message_partner_ids)
self.assertFalse(self.test_employee.email)
self.assertFalse(self.test_employee.phone)
self.assertFalse(self.test_employee.user_id)
def test_employee_get_default_recipients(self):
employee = self.test_employee.with_user(self.res_users_hr_officer)
defaults = employee._message_get_default_recipients()
self.assertDictEqual(
defaults[employee.id],
{'email_cc': '', 'email_to': '', 'partner_ids': self.test_employee.work_contact_id.ids},
)
def test_employee_get_suggested_recipients(self):
employee = self.test_employee.with_user(self.res_users_hr_officer)
suggested = employee._message_get_suggested_recipients()
self.assertListEqual(suggested, [
{
'create_values': {},
'email': self.test_employee.work_contact_id.email_normalized,
'name': self.test_employee.work_contact_id.name,
'partner_id': self.test_employee.work_contact_id.id,
},
])
def test_employee_template(self):
employee, template = self.test_employee.with_user(self.res_users_hr_officer), self.test_template_employee.with_user(self.res_users_hr_officer)
message = employee.message_post_with_source(
template,
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
self.assertEqual(
message.notified_partner_ids, self.test_employee.work_contact_id,
'Matches suggested recipients',
)

View file

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.addons.base.models.ir_qweb import QWebException
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.exceptions import AccessError
class TestMultiCompany(TestHrCommon):
class TestMultiCompanyReport(TestHrCommon):
@classmethod
def setUpClass(cls):
@ -35,7 +37,70 @@ class TestMultiCompany(TestHrCommon):
self.assertIn(b'Machin', content)
def test_single_company_report(self):
with self.assertRaises(QWebException): # CacheMiss followed by AccessError
with self.assertRaises(AccessError): # CacheMiss followed by AccessError
self.env['ir.actions.report'].with_user(self.res_users_hr_officer).with_company(
self.company_1
)._render_qweb_pdf('hr.hr_employee_print_badge', res_ids=self.employees.ids)
class TestMultiCompany(TestHrCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company_a = cls.env['res.company'].create({'name': 'Company A'})
cls.company_b = cls.env['res.company'].create({'name': 'Company B'})
cls.user_a = mail_new_test_user(cls.env, login='user_a', company_id=cls.company_a.id, company_ids=(cls.company_a | cls.company_b).ids)
cls.user_b = mail_new_test_user(cls.env, login='user_b', company_id=cls.company_b.id)
cls.employee_a = cls.env['hr.employee'].create({
'name': 'Employee A',
'company_id': cls.company_a.id,
'user_id': cls.user_a.id,
})
cls.employee_other_a = cls.env['hr.employee'].create({
'name': 'Employee Other A',
'company_id': cls.company_a.id,
})
cls.employee_b = cls.env['hr.employee'].create({
'name': 'Employee B',
'company_id': cls.company_b.id,
'user_id': cls.user_b.id,
'parent_id': cls.employee_a.id,
})
cls.employee_other_b = cls.env['hr.employee'].create({
'name': 'Employee Other B',
'company_id': cls.company_b.id,
})
cls.env.flush_all()
cls.env.invalidate_all()
def test_read_manager_employee(self):
# UserB should be able to read its manager's record - without being connected
# on company A
self.employee_a.with_user(self.user_b).with_company(self.company_b).name
self.employee_b.with_user(self.user_a).with_company(self.company_a).name
# UserB should not be able to read other employees in that company
with self.assertRaises(AccessError):
self.employee_other_a.with_user(self.user_b).with_company(self.company_b).name
def test_read_no_manager_company(self):
self.employee_b.parent_id = False
with self.assertRaises(AccessError):
self.employee_a.with_user(self.user_b).name
def test_compute_presence_state(self):
self.user_a.company_ids = self.company_a
# user A should still read the employee since he is the manager of that employee
self.employee_b.with_user(self.user_a).with_company(self.company_a).name
# user A should still read hr_presence_state even if he does not have access to the company of the employee
self.employee_b.with_user(self.user_a).with_company(self.company_a).hr_presence_state

View file

@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged
@tagged('-at_install', 'post_install')
class TestEmployeeMultipleBanksUi(HttpCase):
def test_employee_profile_tour(self):
employee = self.env['hr.employee'].create({
'name': 'Johnny H.',
})
self.start_tour("/odoo", 'hr_employee_multiple_bank_accounts_tour', login="admin", timeout=200)
total = 0
for ba in employee.bank_account_ids:
ba_percentage = employee.salary_distribution[str(ba.id)]['amount']
ba_is_percentage = employee.salary_distribution[str(ba.id)]['amount_is_percentage']
self.assertEqual(ba_is_percentage, True)
self.assertAlmostEqual(ba_percentage, 33.33, delta=0.011)
total += ba_percentage
self.assertAlmostEqual(total, 100.0, "Total must amount to 100.")

View file

@ -0,0 +1,102 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestPayrollFieldsAccess(TransactionCase):
def test_related_fields_on_version(self):
""" Some groups have been added to avoid users with basic access to HR app see some critical (like wage field for instance)
This test makes sure the groups added in version fields is also in the employee fields related.
However, to define the same groups in employee fields, we have to redefine the related fields (readonly=False, related='version_id.{field_name})
Otherwise, the field we loose the linked with the version field and could be readonly instead of editable.
"""
version_fields = {
f_name: field
for f_name, field in self.env['hr.version']._fields.items()
if field.groups and field.groups not in ['hr.group_hr_user', 'base.group_user'] and not (field.related and field.related.startswith('employee_id'))
}
employee_fields = {
f_name: field
for f_name, field in self.env['hr.employee']._fields.items()
if f_name in version_fields
}
fields_without_group = []
fields_without_related = []
fields_readonly = []
for f_name, field in employee_fields.items():
v_field = version_fields[f_name]
if not (field.groups and field.groups != v_field):
fields_without_group.append(f_name)
elif not (field.related and field.related == f'version_id.{f_name}'):
fields_without_related.append(f_name)
elif field.readonly != v_field.readonly:
fields_readonly.append(f_name)
self.assertFalse(fields_without_group, "Inconsistency between some employee fields and version ones (those employees fields should have the same groups than related one in version")
self.assertFalse(fields_without_related, "Some employee fields have the same name than the version ones but they are not related")
self.assertFalse(fields_readonly, "(Readonly) Inconsistency between some employee fields and version ones, the both fields (in version and employee) have to be readonly or editable")
def _test_payroll_fields_are_hidden_to_non_payroll_users(self, model_name, view_id, payroll_page_name):
form_view = self.env.ref(view_id)
form_view_get_result = self.env['hr.employee'].get_view(form_view.id, 'form')
form_view_arch = form_view_get_result['arch']
node = etree.fromstring(form_view_arch)
self.assertTrue(node.xpath(f"//page[@name='{payroll_page_name}']"), f"[{model_name}] Payroll page should be found in the form view.")
payroll_field_node_list = node.xpath(f"//page[@name='{payroll_page_name}']//field[not(ancestor::field)]")
self.assertTrue(payroll_field_node_list, f"[{model_name}] At least one field should be found inside Payroll information page.")
payroll_field_names = [
payroll_field_node.attrib['name']
for payroll_field_node in payroll_field_node_list
]
current_payroll_field_names = {
f_name
for f_name, field in self.env[model_name]._fields.items()
if field.groups and ('hr.group_hr_manager' in field.groups or 'hr_payroll.group_hr_payroll_user' in field.groups)
}
whitelist_field_names = [
'resource_calendar_id',
'employee_type',
'tz',
'currency_id',
'lang',
'registration_number',
'standard_calendar_id',
'employee_age',
'distance_home_work',
'distance_home_work_unit',
'show_billable_time_target',
'billable_time_target',
'holidays',
'car_id',
'new_car',
'new_car_model_id',
'ordered_car_id',
'fuel_type',
'transport_mode_bike',
'bike_id',
'new_bike',
'new_bike_model_id',
'originated_offer_id',
'is_non_resident',
'structure_id'
]
missing_group_field_names = [
f_name
for f_name in payroll_field_names
if f_name not in current_payroll_field_names and f_name not in whitelist_field_names
]
self.assertFalse(
missing_group_field_names,
"[{}] Missing payroll group on following fields: \n - {}".format(
model_name,
'\n - '.join(missing_group_field_names),
),
)
def test_payroll_fields_are_hidden_to_non_payroll_users_in_employee_form_view(self):
self._test_payroll_fields_are_hidden_to_non_payroll_users('hr.employee', 'hr.view_employee_form', 'payroll_information')
def test_payroll_fields_are_hidden_to_non_payroll_users_in_version_form_view(self):
self._test_payroll_fields_are_hidden_to_non_payroll_users('hr.version', 'hr.hr_contract_template_form_view', 'information')

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from pytz import timezone, utc
from odoo.addons.resource.models.resource import Intervals, sum_intervals
from datetime import datetime, date
from pytz import utc, timezone
from odoo.tools.intervals import Intervals
from odoo.fields import Date
from odoo.tools.date_utils import sum_intervals
from .common import TestHrCommon
@ -23,6 +26,50 @@ class TestResource(TestHrCommon):
cls.env.cr.execute("UPDATE hr_employee SET create_date=%s WHERE id=%s",
(cls.employee_niv_create_date, cls.employee_niv.id))
cls.calendar_richard = cls.env['resource.calendar'].create({'name': 'Calendar of Richard'})
cls.employee.resource_calendar_id = cls.calendar_richard
cls.calendar_35h = cls.env['resource.calendar'].create({
'name': '35h calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Lunch', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Lunch', 'dayofweek': '1', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Lunch', 'dayofweek': '2', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Lunch', 'dayofweek': '4', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
],
})
cls.contract_cdd = cls.employee.version_id
cls.contract_cdd.write({
'date_version': Date.to_date('2021-09-01'),
'contract_date_start': Date.to_date('2021-09-01'),
'contract_date_end': Date.to_date('2021-10-31'),
'name': 'First CDD Contract for Richard',
'resource_calendar_id': cls.calendar_35h.id,
'wage': 5000.0,
'employee_id': cls.employee.id,
})
cls.contract_cdi_values = {
'date_version': Date.to_date('2021-11-01'),
'contract_date_start': Date.to_date('2021-11-01'),
'contract_date_end': False,
'name': 'CDI Contract for Richard',
'resource_calendar_id': cls.calendar_richard.id,
'wage': 5000.0,
'employee_id': cls.employee.id,
}
def test_calendars_validity_within_period_default(self):
calendars = self.employee_niv.resource_id._get_calendars_validity_within_period(
utc.localize(datetime(2021, 7, 1, 8, 0, 0)),
@ -55,3 +102,114 @@ class TestResource(TestHrCommon):
niv_entry = calendars[self.employee_niv.resource_id.id]
self.assertFalse(niv_entry[self.calendar_40h] - interval, "Interval should cover all calendar's validity")
self.assertFalse(interval - niv_entry[self.calendar_40h], "Calendar validity should cover all interval")
def test_availability_hr_infos_resource(self):
""" Ensure that all the hr infos needed to display the avatar popover card
are available on the model resource.resource, even if the employee is archived
"""
user = self.env['res.users'].create([{
'name': 'Test user',
'login': 'test',
'email': 'test@odoo.perso',
'phone': '+32488990011',
}])
department = self.env['hr.department'].create([{
'name': 'QA',
}])
resource = self.env['resource.resource'].create([{
'name': 'Test resource',
'user_id': user.id,
}])
employee = self.env['hr.employee'].create([{
'name': 'Test employee',
'active': False,
'user_id': user.id,
'job_title': 'Tester',
'department_id': department.id,
'work_email': 'test@odoo.pro',
'work_phone': '+32800100100',
'resource_id': resource.id,
}])
for field in 'email', 'phone', 'im_status':
self.assertEqual(resource[field], user[field])
for field in 'job_title', 'department_id', 'work_email', 'work_phone', 'show_hr_icon_display', 'hr_icon_display':
self.assertEqual(resource[field], employee[field])
def test_calendars_validity_within_period(self):
self.employee.create_version(self.contract_cdi_values)
tz = timezone(self.employee.tz)
calendars = self.employee.resource_id._get_calendars_validity_within_period(
tz.localize(datetime(2021, 10, 1, 0, 0, 0)),
tz.localize(datetime(2021, 12, 1, 0, 0, 0)),
)
interval_35h = Intervals([(
tz.localize(datetime(2021, 10, 1, 0, 0, 0)),
tz.localize(datetime.combine(date(2021, 10, 31), datetime.max.time())),
self.env['resource.calendar.attendance']
)])
interval_40h = Intervals([(
tz.localize(datetime(2021, 11, 1, 0, 0, 0)),
tz.localize(datetime(2021, 12, 1, 0, 0, 0)),
self.env['resource.calendar.attendance']
)])
self.assertEqual(1, len(calendars), "The dict returned by calendars validity should only have 1 entry")
self.assertEqual(2, len(calendars[self.employee.resource_id.id]), "Jean should only have one calendar")
richard_entries = calendars[self.employee.resource_id.id]
for calendar in richard_entries:
self.assertTrue(calendar in (self.calendar_35h | self.calendar_richard), "Each calendar should be listed")
if calendar == self.calendar_35h:
self.assertFalse(richard_entries[calendar] - interval_35h, "Interval 35h should cover all calendar 35h validity")
self.assertFalse(interval_35h - richard_entries[calendar], "Calendar 35h validity should cover all interval 35h")
elif calendar == self.calendar_richard:
self.assertFalse(richard_entries[calendar] - interval_40h, "Interval 40h should cover all calendar 40h validity")
self.assertFalse(interval_40h - richard_entries[calendar], "Calendar 40h validity should cover all interval 40h")
def test_queries(self):
employees_test = self.env['hr.employee'].create([{
'name': 'Employee ' + str(i),
} for i in range(0, 50)])
for emp in employees_test:
self.contract_cdd.copy({'employee_id': emp.id})
self.contract_cdi_values['employee_id'] = emp.id
self.employee.create_version(self.contract_cdi_values)
start = utc.localize(datetime(2021, 9, 1, 0, 0, 0))
end = utc.localize(datetime(2021, 11, 30, 23, 59, 59))
with self.assertQueryCount(165):
work_intervals, _ = (employees_test | self.employee).resource_id._get_valid_work_intervals(start, end)
self.assertEqual(len(work_intervals), 51)
def test_get_valid_work_intervals(self):
self.employee.create_version(self.contract_cdi_values)
start = timezone(self.employee.tz).localize(datetime(2021, 10, 24, 2, 0, 0))
end = timezone(self.employee.tz).localize(datetime(2021, 11, 6, 23, 59, 59))
work_intervals, _ = self.employee.resource_id._get_valid_work_intervals(start, end)
sum_work_intervals = sum_intervals(work_intervals[self.employee.resource_id.id])
self.assertEqual(75, sum_work_intervals, "Sum of the work intervals for the employee should be 35h+40h = 75h")
def test_multi_contract_attendance(self):
""" Verify whether retrieving an employee's calendar attendances can
handle multiple contracts with different calendars.
"""
date_from = utc.localize(datetime(2021, 10, 1, 0, 0, 0))
date_to = utc.localize(datetime(2021, 11, 30, 0, 0, 0))
attendances = self.employee._get_calendar_attendances(date_from, date_to)
self.assertEqual(21 * 7, attendances['hours'],
"Attendances should only include running or finished contracts.")
self.employee.create_version(self.contract_cdi_values)
attendances = self.employee._get_calendar_attendances(date_from, date_to)
self.assertEqual(21 * 7 + 21 * 8, attendances['hours'],
"Attendances should add up multiple contracts with varying work weeks.")
def test_alter_resource_calendar_of_resouce(self):
self.assertEqual(self.employee.resource_calendar_id, self.employee.resource_id.calendar_id)
self.assertEqual(self.employee.version_id.resource_calendar_id, self.employee.resource_id.calendar_id)
self.employee.resource_id.write({'calendar_id': self.calendar_40h})
self.assertEqual(self.employee.resource_calendar_id, self.employee.resource_id.calendar_id)
self.assertEqual(self.employee.version_id.resource_calendar_id, self.employee.resource_id.calendar_id)

View file

@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestHrScenario(TransactionCase):
def test_load_scenario(self):
self.env['hr.employee']._load_scenario()

View file

@ -5,55 +5,32 @@ from collections import OrderedDict
from itertools import chain
from lxml import etree
from odoo import Command
from odoo.addons.hr.tests.common import TestHrCommon
from odoo.tests import new_test_user, tagged, Form
from odoo.exceptions import AccessError
@tagged('post_install', '-at_install')
class TestSelfAccessProfile(TestHrCommon):
class TestSelfAccessPreferences(TestHrCommon):
def test_access_my_profile(self):
""" A simple user should be able to read all fields in his profile """
def test_access_preferences_view(self):
""" A simple user should be able to read all fields in the preferences form """
james = new_test_user(self.env, login='hel', groups='base.group_user', name='Simple employee', email='ric@example.com')
james = james.with_user(james)
james_bank_account = self.env['res.partner.bank'].create({'acc_number': 'BE1234567890', 'partner_id': james.partner_id.id})
self.env['hr.employee'].create({
'name': 'James',
'user_id': james.id,
'bank_account_id': self.env['res.partner.bank'].create({'acc_number': 'BE1234567890', 'partner_id': james.partner_id.id}).id
'bank_account_ids': [Command.link(james_bank_account.id)]
})
view = self.env.ref('hr.res_users_view_form_profile')
view = self.env.ref('hr.res_users_view_form_preferences')
view_infos = james.get_view(view.id)
fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')]
james.read(fields)
def test_readonly_fields(self):
""" Employee related fields should be readonly if self editing is not allowed """
self.env['ir.config_parameter'].sudo().set_param('hr.hr_employee_self_edit', False)
james = new_test_user(self.env, login='hel', groups='base.group_user', name='Simple employee', email='ric@example.com')
james = james.with_user(james)
self.env['hr.employee'].create({
'name': 'James',
'user_id': james.id,
})
view = self.env.ref('hr.res_users_view_form_profile')
fields = james._fields
view_infos = james.get_view(view.id)
employee_related_fields = {
el.get('name')
for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')
if fields[el.get('name')].related and fields[el.get('name')].related.split('.')[0] == 'employee_id'
}
form = Form(james, view=view)
for field in employee_related_fields:
with self.assertRaises(AssertionError, msg="Field '%s' should be readonly in the employee profile when self editing is not allowed." % field):
form.__setattr__(field, 'some value')
def test_profile_view_fields(self):
""" A simple user should see all fields in profile view, even if they are protected by groups """
view = self.env.ref('hr.res_users_view_form_profile')
def test_preferences_view_fields(self):
""" A simple user should see all fields in preferences view, even if they are protected by groups """
view = self.env.ref('hr.res_users_view_form_preferences')
# For reference, check the view with user with every groups protecting user fields
all_groups_xml_ids = chain(*[
@ -66,7 +43,7 @@ class TestSelfAccessProfile(TestHrCommon):
for xml_id in all_groups_xml_ids:
all_groups |= self.env.ref(xml_id.strip())
user_all_groups = new_test_user(self.env, groups='base.group_user', login='hel', name='God')
user_all_groups.write({'groups_id': [(4, group.id, False) for group in all_groups]})
user_all_groups.write({'group_ids': [(4, group.id, False) for group in all_groups]})
view_infos = self.env['res.users'].with_user(user_all_groups).get_view(view.id)
full_fields = [el.get('name') for el in etree.fromstring(view_infos['arch']).xpath('//field[not(ancestor::field)]')]
@ -78,7 +55,7 @@ class TestSelfAccessProfile(TestHrCommon):
# Compare both
self.assertEqual(full_fields, fields, "View fields should not depend on user's groups")
def test_access_my_profile_toolbar(self):
def test_access_preferences_view_toolbar(self):
""" A simple user shouldn't have the possibilities to see the 'Change Password' action"""
james = new_test_user(self.env, login='jam', groups='base.group_user', name='Simple employee', email='jam@example.com')
james = james.with_user(james)
@ -86,9 +63,8 @@ class TestSelfAccessProfile(TestHrCommon):
'name': 'James',
'user_id': james.id,
})
view = self.env.ref('hr.res_users_view_form_profile')
toolbar = james.get_views([(view.id, 'form')], {'toolbar': True})['views']['form']['toolbar']
available_actions = toolbar.get('action', [])
view = self.env.ref('hr.res_users_view_form_preferences')
available_actions = james.get_views([(view.id, 'form')], {'toolbar': True})['views']['form']['toolbar'].get('action', {})
change_password_action = self.env.ref("base.change_password_wizard_action")
self.assertFalse(any(x['id'] == change_password_action.id for x in available_actions))
@ -100,10 +76,30 @@ class TestSelfAccessProfile(TestHrCommon):
'name': 'John',
'user_id': john.id,
})
view = self.env.ref('hr.res_users_view_form_profile')
view = self.env.ref('hr.res_users_view_form_preferences')
available_actions = john.get_views([(view.id, 'form')], {'toolbar': True})['views']['form']['toolbar']['action']
self.assertTrue(any(x['id'] == change_password_action.id for x in available_actions))
def test_employee_fields_groups(self):
# Note: If this tests is crashing, this is probably because the linked field on the error
# message is defined on hr.employee only (and not on hr.employee.public) and has no group
# defined on it (at least hr.group_hr_user).
internal_user = new_test_user(self.env, login='mireille', groups='base.group_user', name='Mireille', email='mireille@example.com')
self.env['hr.employee'].with_user(internal_user).search([]).read([])
def test_open_preferences_with_group_without_external_id(self):
"""Test opening preferences when the user belongs to a group without an external ID."""
self.env['hr.employee'].create({
'name': 'John',
'user_id': self.env.user.id,
})
group = self.env['res.groups'].create({
'name': "Test Group",
})
self.env.user.group_ids = [Command.link(group.id)]
action = self.env.user.action_get()
self.assertEqual(action['type'], 'ir.actions.act_window')
self.assertEqual(action['display_name'], 'Change my Preferences')
class TestSelfAccessRights(TestHrCommon):
@ -114,13 +110,12 @@ class TestSelfAccessRights(TestHrCommon):
cls.richard_emp = cls.env['hr.employee'].create({
'name': 'Richard',
'user_id': cls.richard.id,
'address_home_id': cls.env['res.partner'].create({'name': 'Richard', 'phone': '21454', 'type': 'private'}).id,
'private_phone': '21454',
})
cls.hubert = new_test_user(cls.env, login='hub', groups='base.group_user', name='Simple employee', email='hub@example.com')
cls.hubert_emp = cls.env['hr.employee'].create({
'name': 'Hubert',
'user_id': cls.hubert.id,
'address_home_id': cls.env['res.partner'].create({'name': 'Hubert', 'type': 'private'}).id,
})
cls.protected_fields_emp = OrderedDict([(k, v) for k, v in cls.env['hr.employee']._fields.items() if v.groups == 'hr.group_hr_user'])
@ -140,6 +135,14 @@ class TestSelfAccessRights(TestHrCommon):
def testReadOtherEmployee(self):
with self.assertRaises(AccessError):
self.hubert_emp.with_user(self.richard).read(self.protected_fields_emp.keys())
# Check simple user can read all public fields of private employee
public_fields = [
field_name
for field_name in self.env['hr.employee.public']._fields
if field_name in self.env['hr.employee']._fields
]
res = self.hubert_emp.with_user(self.richard).read(public_fields)
self.assertEqual(len(public_fields), len(res[0]))
# Write hr.employee #
def testWriteSelfEmployee(self):
@ -161,56 +164,14 @@ class TestSelfAccessRights(TestHrCommon):
with self.assertRaises(AccessError):
self.hubert.with_user(self.richard).read(self.self_protected_fields_user)
# Write res.users #
def testWriteSelfUserEmployeeSettingFalse(self):
for f, v in self.self_protected_fields_user.items():
with self.assertRaises(AccessError):
self.richard.with_user(self.richard).write({f: 'dummy'})
def testWriteSelfUserEmployee(self):
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', True)
for f, v in self.self_protected_fields_user.items():
val = None
if v.type == 'char' or v.type == 'text':
val = '0000' if f == 'pin' else 'dummy'
val = '0000' if f in ['pin', 'barcode'] else 'dummy'
if val is not None:
self.richard.with_user(self.richard).write({f: val})
def testWriteSelfUserPreferencesEmployee(self):
# self should always be able to update non hr.employee fields if
# they are in SELF_READABLE_FIELDS
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False)
# should not raise
vals = [
{'tz': "Australia/Sydney"},
{'email': "new@example.com"},
{'signature': "<p>I'm Richard!</p>"},
{'notification_type': "email"},
]
for v in vals:
# should not raise
self.richard.with_user(self.richard).write(v)
def testWriteOtherUserPreferencesEmployee(self):
# self should always be able to update non hr.employee fields if
# they are in SELF_READABLE_FIELDS
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False)
vals = [
{'tz': "Australia/Sydney"},
{'email': "new@example.com"},
{'signature': "<p>I'm Richard!</p>"},
{'notification_type': "email"},
]
for v in vals:
with self.assertRaises(AccessError):
self.hubert.with_user(self.richard).write(v)
def testWriteSelfPhoneEmployee(self):
# phone is a related from res.partner (from base) but added in SELF_READABLE_FIELDS
self.env['ir.config_parameter'].set_param('hr.hr_employee_self_edit', False)
with self.assertRaises(AccessError):
self.richard.with_user(self.richard).write({'phone': '2154545'})
def testWriteOtherUserEmployee(self):
for f in self.self_protected_fields_user:
with self.assertRaises(AccessError):
@ -220,6 +181,28 @@ class TestSelfAccessRights(TestHrCommon):
# Searching user based on employee_id field should not raise bad query error
self.env['res.users'].with_user(self.richard).search([('employee_id', 'ilike', 'Hubert')])
# Write hr.department
def testWriteDepartmentEmployee(self):
with self.assertRaises(AccessError):
self.env['hr.department'].with_user(self.richard).create({'name': 'New Dept'})
dept = self.env['hr.department'].create({'name': 'New Dept'})
with self.assertRaises(AccessError):
dept.with_user(self.richard).write({'name': 'Renamed Dept'})
def test_onchange_readable_fields_with_no_access(self):
"""
The purpose is to test that the onchange logic takes into account `SELF_READABLE_FIELDS`.
The view contains fields that are in `SELF_READABLE_FIELDS` (example: `private_street`).
Even if the user does not have read access to the employee,
it should not cause an access error if these fields are in `SELF_READABLE_FIELDS`.
"""
self.env['res.lang']._activate_lang("fr_FR")
with Form(self.richard.with_user(self.richard), view='hr.res_users_view_form_preferences') as form:
# triggering an onchange should not trigger some access error
form.lang = "fr_FR"
form.tz = "Europe/Brussels"
def test_access_employee_account(self):
hubert = new_test_user(self.env, login='hubert', groups='base.group_user', name='Hubert Bonisseur de La Bath', email='hubert@oss.fr')
hubert = hubert.with_user(hubert)
@ -227,16 +210,14 @@ class TestSelfAccessRights(TestHrCommon):
hubert_emp = self.env['hr.employee'].create({
'name': 'Hubert',
'user_id': hubert.id,
'bank_account_id': hubert_acc.id
'bank_account_ids': [Command.link(hubert_acc.id)]
})
hubert.partner_id.sudo().employee_ids = hubert_emp
self.assertFalse(hubert.user_has_groups('hr.group_hr_user'))
self.assertFalse(hubert.env.user.has_group('hr.group_hr_user'))
self.assertFalse(hubert.env.su)
self.assertEqual(hubert.read(['employee_bank_account_id'])[0]['employee_bank_account_id'][1], 'FR******7890')
self.assertEqual(hubert.sudo().employee_bank_account_id.display_name, 'FR******7890')
self.assertEqual(hubert_emp.with_user(hubert).sudo().bank_account_id.display_name, 'FR******7890')
self.assertEqual(hubert.sudo().employee_bank_account_ids.display_name, 'FR******7890')
self.assertEqual(hubert_emp.with_user(hubert).sudo().bank_account_ids.display_name, 'FR******7890')
hubert_acc.invalidate_recordset(["display_name"])
self.assertEqual(hubert_emp.with_user(hubert).sudo().bank_account_id.sudo(False).display_name, 'FR******7890')
self.assertEqual(hubert_emp.with_user(hubert).sudo().bank_account_ids.sudo(False).display_name, 'FR******7890')

View file

@ -1,17 +1,47 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged, new_test_user
from odoo.tests import HttpCase, freeze_time, tagged, new_test_user
@tagged('-at_install', 'post_install')
@tagged('-at_install', 'post_install', 'is_tour')
class TestEmployeeUi(HttpCase):
def test_employee_profile_tour(self):
user = new_test_user(self.env, login='davidelora', groups='base.group_user')
johnny_user = new_test_user(self.env, login="johnny", name="Johnny H.")
self.env['hr.employee'].create([{
'name': 'Johnny H.',
"user_id": johnny_user.id,
}, {
'name': 'David Elora',
'user_id': user.id,
}])
self.start_tour("/web", 'hr_employee_tour', login="davidelora")
self.start_tour("/odoo", 'hr_employee_tour', login="davidelora")
@freeze_time('2024-01-01')
def test_version_timeline_auto_save_tour(self):
# as payroll tap access will be overridden by hr_payroll
is_payroll_installed = self.env['ir.module.module'].search_count([
('name', '=', 'hr_payroll'), ('state', '=', 'installed')])
group = 'hr_payroll.group_hr_payroll_manager' if is_payroll_installed else 'hr.group_hr_manager'
user = new_test_user(self.env, login='alice', groups=group)
bob_user = new_test_user(self.env, login="Bob", name="Bob M.")
self.env['hr.employee'].create([{
'name': 'Alice',
'user_id': user.id,
}])
bob_employee = self.env['hr.employee'].create([{
'name': 'Bob M.',
"user_id": bob_user.id,
}])
bob_employee.write({
'contract_date_start': '2024-01-01',
'contract_date_end': False,
})
self.start_tour("/odoo", 'version_timeline_auto_save_tour', login="alice")
self.assertFalse(bob_employee.version_ids[-1].contract_date_start)