Skip to content

Commit 030dfe4

Browse files
committed
[ADD] shift_change_portal
1 parent 2f164f6 commit 030dfe4

29 files changed

Lines changed: 1312 additions & 62 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../shift_change_portal

setup/shift_change_portal/setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import setuptools
2+
3+
setuptools.setup(
4+
setup_requires=['setuptools-odoo'],
5+
odoo_addon=True,
6+
)

shift_change/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
15
from . import models

shift_change/__manifest__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Copyright 2022 Coop IT Easy SC
2-
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
34

45
{
56
"name": "Shift Change",
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!--
3+
SPDX-FileCopyrightText: 2026 Coop IT Easy SC
4+
5+
SPDX-License-Identifier: AGPL-3.0-or-later
6+
-->
17
<odoo noupdate="1">
2-
<record id="enable_shift_changes" model="ir.config_parameter" forcecreate="False">
3-
<field name="key">shift_change.enable_shift_changes</field>
4-
<field name="value">True</field>
5-
</record>
6-
<record id="hour_limit_change" model="ir.config_parameter" forcecreate="False">
8+
<record id="hour_limit_change" model="ir.config_parameter">
79
<field name="key">shift_change.hour_limit_change</field>
810
<field name="value">24</field>
911
</record>
10-
<record id="enable_solidarity" model="ir.config_parameter" forcecreate="False">
11-
<field name="key">beesdoo_shift.enable_solidarity</field>
12-
<field name="value">True</field>
12+
<record id="same_shift_change_max" model="ir.config_parameter">
13+
<field name="key">shift_change.same_shift_change_max</field>
14+
<field name="value">3</field>
1315
</record>
1416
</odoo>

shift_change/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
15
from . import shift_change
26
from . import res_config_settings
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
15
from odoo import fields, models
26

37

48
class ResConfigSettings(models.TransientModel):
59
_inherit = "res.config.settings"
610

7-
# enable_exchanges
8-
enable_shift_changes = fields.Boolean(
9-
string="Activate shift changes",
10-
config_parameter="shift_change.enable_shift_changes",
11-
)
12-
# day_limit_swap
1311
hour_limit_change = fields.Integer(
1412
string="Number of hours above which a cooperator cannot change his shift",
1513
config_parameter="shift_change.hour_limit_change",
1614
)
15+
same_shift_change_max = fields.Integer(
16+
string="Number of time the same shift can be changed",
17+
config_parameter="shift_change.same_shift_change_max",
18+
)

shift_change/models/shift_change.py

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
15
from datetime import datetime, timedelta
26

37
from odoo import _, api, fields, models
@@ -32,50 +36,112 @@ def create(self, vals_list):
3236
to the old shift and subscribe him/her to the new one
3337
"""
3438
res = super().create(vals_list)
35-
res._unsubscribe_old_shift()
3639
res._subscribe_new_shift()
40+
res._unsubscribe_old_shift()
3741
return res
3842

3943
def _unsubscribe_old_shift(self):
4044
"""
4145
Unsubscribe self.worker_id from old_shift
42-
Return True is unsubscription is successful.
43-
:return: Boolean
46+
Raise error if not possible.
4447
"""
45-
if (
46-
self.old_shift_id.worker_id
47-
and self.old_shift_id.worker_id == self.worker_id
48-
):
49-
self.old_shift_id.worker_id = False
50-
else:
51-
raise ValidationError(_("You can't change shift that your are not worker."))
48+
self._check_old_shift(self.old_shift_id, self.worker_id)
49+
self.old_shift_id.write(
50+
{
51+
"worker_id": False,
52+
"is_regular": False,
53+
"is_compensation": False,
54+
}
55+
)
5256

5357
def _subscribe_new_shift(self):
5458
"""
5559
Subscribe self.worker_id to the new shift
56-
Return True is subscription is successful.
57-
:return: Boolean
60+
Raise error if not possible.
5861
"""
62+
self._check_new_shift(self.new_shift_id)
63+
self.new_shift_id.write(
64+
{
65+
"worker_id": self.worker_id.id,
66+
"is_regular": self.old_shift_id.is_regular,
67+
"is_compensation": self.old_shift_id.is_compensation,
68+
}
69+
)
70+
71+
@api.model
72+
def _check_old_shift(self, old_shift_id, worker_id):
73+
"""Check if old shift can be changed"""
74+
try:
75+
hour_limit_change = int(
76+
self.env["ir.config_parameter"].get_param(
77+
"shift_change.hour_limit_change"
78+
)
79+
)
80+
except ValueError:
81+
# fall back to a default value
82+
hour_limit_change = 0
83+
if not old_shift_id.worker_id or old_shift_id.worker_id != worker_id:
84+
raise ValidationError(_("You can't change shift that your are not worker."))
85+
if old_shift_id.start_time <= datetime.now():
86+
raise ValidationError(_("You can't change shift that is in the past."))
87+
if old_shift_id.start_time <= datetime.now() + timedelta(
88+
hours=hour_limit_change
89+
):
90+
raise ValidationError(_("You can't change a shift so close in the futur."))
91+
try:
92+
same_shift_change_max = int(
93+
self.env["ir.config_parameter"].get_param(
94+
"shift_change.same_shift_change_max"
95+
)
96+
)
97+
except ValueError:
98+
# fall back to a default value
99+
same_shift_change_max = 0
100+
if same_shift_change_max:
101+
same_shift_change_nb = 0
102+
tmp_old_shift_id = old_shift_id
103+
while tmp_old_shift_id:
104+
change = self.search(
105+
[
106+
("new_shift_id", "=", tmp_old_shift_id.id),
107+
("worker_id", "=", worker_id.id),
108+
],
109+
limit=1,
110+
)
111+
if change:
112+
same_shift_change_nb += 1
113+
tmp_old_shift_id = change.old_shift_id
114+
else:
115+
tmp_old_shift_id = None
116+
if same_shift_change_nb >= same_shift_change_max:
117+
raise ValidationError(
118+
_(
119+
"You can't change the same shift more than"
120+
f"{same_shift_change_max} times."
121+
)
122+
)
123+
124+
@api.model
125+
def _check_new_shift(self, new_shift_id):
126+
"""Check if shift can be changed or not"""
59127
try:
60128
hour_limit_change = int(
61129
self.env["ir.config_parameter"].get_param(
62130
"shift_change.hour_limit_change"
63131
)
64132
)
65133
except ValueError:
66-
# Fall back to a default value
134+
# fall back to a default value
67135
hour_limit_change = 0
68-
if self.new_shift_id.worker_id:
136+
if new_shift_id.worker_id:
69137
raise ValidationError(
70138
_("You can't subscribe to a shift assigned to someone else.")
71139
)
72-
elif self.new_shift_id.start_time <= datetime.now():
140+
if new_shift_id.start_time <= datetime.now():
73141
raise ValidationError(_("You can't subscribe to a shift in the past."))
74-
elif self.new_shift_id.start_time <= datetime.now() + timedelta(
142+
if new_shift_id.start_time <= datetime.now() + timedelta(
75143
hours=hour_limit_change
76144
):
77145
raise ValidationError(
78146
_("You can't subscribe to a shift so close in the futur.")
79147
)
80-
else:
81-
self.new_shift_id.worker_id = self.worker_id

shift_change/tests/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
15
from . import test_shift_change

shift_change/tests/test_shift_change.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
# SPDX-FileCopyrightText: 2026 Coop IT Easy SC
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
15
from datetime import date, datetime, timedelta
26

7+
from psycopg2.errors import IntegrityError
8+
39
from odoo.exceptions import AccessError, ValidationError
410
from odoo.tests.common import TransactionCase
11+
from odoo.tools import mute_logger
512

613

714
class TestShiftChange(TransactionCase):
@@ -29,8 +36,8 @@ def setUp(self):
2936
{
3037
"name": "shift_1",
3138
"task_template_id": self.task_template_1.id,
32-
"start_time": self.now,
33-
"end_time": self.now,
39+
"start_time": self.now + timedelta(days=2),
40+
"end_time": self.now + timedelta(days=2),
3441
"is_regular": True,
3542
"worker_id": self.worker_regular_1.id,
3643
}
@@ -39,8 +46,8 @@ def setUp(self):
3946
{
4047
"name": "shift_2",
4148
"task_template_id": self.task_template_2.id,
42-
"start_time": self.now,
43-
"end_time": self.now,
49+
"start_time": self.now + timedelta(days=2),
50+
"end_time": self.now + timedelta(days=2),
4451
"is_regular": True,
4552
"worker_id": self.worker_regular_2.id,
4653
}
@@ -49,16 +56,16 @@ def setUp(self):
4956
{
5057
"name": "shift_3",
5158
"task_template_id": self.task_template_2.id,
52-
"start_time": self.now + timedelta(days=2),
53-
"end_time": self.now + timedelta(days=2),
59+
"start_time": self.now + timedelta(days=4),
60+
"end_time": self.now + timedelta(days=4),
5461
"worker_id": False,
5562
}
5663
)
5764
self.shift_4 = self.shift_model.create(
5865
{
5966
"name": "shift_4",
6067
"task_template_id": self.task_template_2.id,
61-
"start_time": self.now - timedelta(days=2),
68+
"start_time": self.now - timedelta(days=4),
6269
"end_time": self.now,
6370
"worker_id": False,
6471
}
@@ -67,7 +74,7 @@ def setUp(self):
6774
{
6875
"name": "shift_5",
6976
"task_template_id": self.task_template_2.id,
70-
"start_time": self.now + timedelta(days=2),
77+
"start_time": self.now + timedelta(days=4),
7178
"end_time": self.now,
7279
"worker_id": False,
7380
}
@@ -85,6 +92,11 @@ def setUp(self):
8592
# Set context to avoid shift generation in the past
8693
self.env.context = dict(self.env.context, visualize_date=date.today())
8794

95+
# Set maximum change shift for testing
96+
self.env["ir.config_parameter"].set_param(
97+
"shift_change.same_shift_change_max", 1
98+
)
99+
88100
def test_shift_change(self):
89101
"""Test change a shift"""
90102
self.assertEqual(self.shift_1.worker_id, self.worker_regular_1)
@@ -99,6 +111,29 @@ def test_shift_change(self):
99111
self.assertFalse(self.shift_1.worker_id)
100112
self.assertEqual(self.shift_3.worker_id, self.worker_regular_1)
101113

114+
def test_shift_change_max(self):
115+
"""Test change a shift several times"""
116+
self.assertEqual(self.shift_1.worker_id, self.worker_regular_1)
117+
self.assertFalse(self.shift_3.worker_id)
118+
self.assertFalse(self.shift_5.worker_id)
119+
self.shift_change_model.create(
120+
{
121+
"worker_id": self.worker_regular_1.id,
122+
"old_shift_id": self.shift_1.id,
123+
"new_shift_id": self.shift_3.id,
124+
}
125+
)
126+
self.assertFalse(self.shift_1.worker_id)
127+
self.assertEqual(self.shift_3.worker_id, self.worker_regular_1)
128+
with self.assertRaises(ValidationError):
129+
self.shift_change_model.create(
130+
{
131+
"worker_id": self.worker_regular_1.id,
132+
"old_shift_id": self.shift_3.id,
133+
"new_shift_id": self.shift_5.id,
134+
}
135+
)
136+
102137
def test_shift_change_not_empty(self):
103138
"""Test changing a shift to a non empty shift"""
104139
self.assertEqual(self.shift_1.worker_id, self.worker_regular_1)
@@ -142,12 +177,13 @@ def test_shift_change_wrong_worker(self):
142177

143178
def test_shift_change_missing_required_fields(self):
144179
"""Test creating a shift with missing fields"""
145-
with self.assertRaises(ValidationError):
146-
self.shift_change_model.create(
147-
{
148-
"worker_id": self.worker_regular_1.id,
149-
}
150-
)
180+
with self.assertRaises(IntegrityError):
181+
with mute_logger("odoo.sql_db"):
182+
self.shift_change_model.create(
183+
{
184+
"worker_id": self.worker_regular_1.id,
185+
}
186+
)
151187

152188
def test_shift_change_writing(self):
153189
"""Test that writing to a shift fails"""

0 commit comments

Comments
 (0)