Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions hr_shift_pattern/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
=========================
Employees Shifts Patterns
=========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:00a5304a942cfc0b9b0d0154486df5b8f92887df53fb5d8ff43cea23d5b3efc8
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fshift--planning-lightgray.png?logo=github
:target: https://github.com/OCA/shift-planning/tree/18.0/hr_shift_pattern
:alt: OCA/shift-planning
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/shift-planning-18-0/shift-planning-18-0-hr_shift_pattern
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/shift-planning&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds reusable week templates and multi-week rotation cycles
on top of *hr_shift*. A week template (a *semaine type*) maps each
worked day to a shift template. A cycle is an ordered list of week
templates. On a planning, each employee is placed on a week template,
and their days are filled from it.

**Table of contents**

.. contents::
:local:

Usage
=====

Configuration:

1. In *Shifts > Configuration > Week templates*, create a week template
and add one line per worked day (the day and the shift template that
applies).
2. In *Shifts > Configuration > Cycles*, create a cycle and order its
week templates.
3. On each employee, set the cycle and the week they start it on.

On a planning:

1. Generate the shifts: every employee is filled from the cycle step
matching that planning week.
2. The *Shifts* button shows the shifts grouped by week template; drag
an employee to another column for a one-off change of that week.

Changing an employee's cycle or start week re-fills their future
plannings; the current and past weeks are left untouched.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/shift-planning/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/shift-planning/issues/new?body=module:%20hr_shift_pattern%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* INVITU

Contributors
------------

- Cyril VINH-TUNG <cyril@invitu.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/shift-planning <https://github.com/OCA/shift-planning/tree/18.0/hr_shift_pattern>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
3 changes: 3 additions & 0 deletions hr_shift_pattern/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright 2026 INVITU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models
21 changes: 21 additions & 0 deletions hr_shift_pattern/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2026 INVITU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Employees Shifts Patterns",
"summary": "Build reusable week templates and multi-week rotation cycles",
"version": "18.0.1.0.0",
"author": "INVITU, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/shift-planning",
"category": "Human Resources/Shifts",
"depends": ["hr_shift"],
"demo": ["demo/demo.xml"],
"data": [
"security/ir.model.access.csv",
"views/shift_pattern_week_views.xml",
"views/shift_pattern_views.xml",
"views/hr_employee_views.xml",
"views/shift_planning_views.xml",
"views/menus.xml",
],
}
79 changes: 79 additions & 0 deletions hr_shift_pattern/demo/demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="week_mornings" model="hr.shift.pattern.week">
<field name="name">Mornings week</field>
<field name="color">10</field>
</record>
<record id="week_mornings_line_mon" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_mornings" />
<field name="day_number">0</field>
<field name="template_id" ref="hr_shift.template_morning" />
</record>
<record id="week_mornings_line_tue" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_mornings" />
<field name="day_number">1</field>
<field name="template_id" ref="hr_shift.template_morning" />
</record>
<record id="week_mornings_line_wed" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_mornings" />
<field name="day_number">2</field>
<field name="template_id" ref="hr_shift.template_morning" />
</record>
<record id="week_mornings_line_thu" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_mornings" />
<field name="day_number">3</field>
<field name="template_id" ref="hr_shift.template_morning" />
</record>
<record id="week_mornings_line_fri" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_mornings" />
<field name="day_number">4</field>
<field name="template_id" ref="hr_shift.template_morning" />
</record>
<record id="week_afternoons" model="hr.shift.pattern.week">
<field name="name">Afternoons week</field>
<field name="color">4</field>
</record>
<record id="week_afternoons_line_mon" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_afternoons" />
<field name="day_number">0</field>
<field name="template_id" ref="hr_shift.template_afternoon" />
</record>
<record id="week_afternoons_line_tue" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_afternoons" />
<field name="day_number">1</field>
<field name="template_id" ref="hr_shift.template_afternoon" />
</record>
<record id="week_afternoons_line_wed" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_afternoons" />
<field name="day_number">2</field>
<field name="template_id" ref="hr_shift.template_afternoon" />
</record>
<record id="week_afternoons_line_thu" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_afternoons" />
<field name="day_number">3</field>
<field name="template_id" ref="hr_shift.template_afternoon" />
</record>
<record id="week_afternoons_line_fri" model="hr.shift.pattern.week.line">
<field name="week_id" ref="week_afternoons" />
<field name="day_number">4</field>
<field name="template_id" ref="hr_shift.template_afternoon" />
</record>
<record id="pattern_morn_aft" model="hr.shift.pattern">
<field name="name">Mornings/Afternoons rotation</field>
<field name="color">7</field>
</record>
<record id="pattern_morn_aft_step_1" model="hr.shift.pattern.step">
<field name="pattern_id" ref="pattern_morn_aft" />
<field name="sequence">10</field>
<field name="week_id" ref="week_mornings" />
</record>
<record id="pattern_morn_aft_step_2" model="hr.shift.pattern.step">
<field name="pattern_id" ref="pattern_morn_aft" />
<field name="sequence">20</field>
<field name="week_id" ref="week_afternoons" />
</record>
<record id="hr.employee_admin" model="hr.employee">
<field name="shift_pattern_id" ref="pattern_morn_aft" />
<field name="shift_pattern_start_date">2025-01-06</field>
</record>
</odoo>
7 changes: 7 additions & 0 deletions hr_shift_pattern/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright 2026 INVITU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import shift_pattern_week
from . import shift_pattern
from . import hr_employee
from . import shift_planning_shift
from . import shift_planning
70 changes: 70 additions & 0 deletions hr_shift_pattern/models/hr_employee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2026 INVITU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import timedelta

from odoo import _, api, fields, models


class HrEmployee(models.Model):
_inherit = "hr.employee"

shift_pattern_id = fields.Many2one(
comodel_name="hr.shift.pattern",
string="Shift cycle",
help="Rotation cycle the employee follows.",
)
shift_pattern_start_date = fields.Date(
string="Cycle start week",
help="Week the employee starts on the first cycle step (set to its Monday).",
)
shift_pattern_start_week = fields.Char(
string="ISO week", compute="_compute_shift_pattern_start_week"
)

@api.depends("shift_pattern_start_date")
def _compute_shift_pattern_start_week(self):
for employee in self:
date = employee.shift_pattern_start_date
if date:
iso = date.isocalendar()
employee.shift_pattern_start_week = _(
"Week %(week)s / %(year)s", week=iso[1], year=iso[0]
)
else:
employee.shift_pattern_start_week = ""

@api.onchange("shift_pattern_start_date")
def _onchange_shift_pattern_start_date(self):
if self.shift_pattern_start_date:
date = self.shift_pattern_start_date
self.shift_pattern_start_date = date - timedelta(days=date.weekday())

@api.model
def _normalize_start(self, vals):
if vals.get("shift_pattern_start_date"):
date = fields.Date.to_date(vals["shift_pattern_start_date"])
vals["shift_pattern_start_date"] = date - timedelta(days=date.weekday())

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._normalize_start(vals)
return super().create(vals_list)

def write(self, vals):
self._normalize_start(vals)
res = super().write(vals)
if {"shift_pattern_id", "shift_pattern_start_date"} & set(vals):
self._recompute_future_shifts()
return res

def _recompute_future_shifts(self):
today = fields.Date.context_today(self)
monday = today - timedelta(days=today.weekday())
shifts = self.env["hr.shift.planning.shift"].search(
[
("employee_id", "in", self.ids),
("planning_id.start_date", ">", monday),
]
)
shifts._apply_cycle()
27 changes: 27 additions & 0 deletions hr_shift_pattern/models/shift_pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 INVITU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models


class ShiftPattern(models.Model):
_name = "hr.shift.pattern"
_description = "Shift rotation cycle"

name = fields.Char(required=True)
color = fields.Integer()
step_ids = fields.One2many(
comodel_name="hr.shift.pattern.step", inverse_name="pattern_id"
)
active = fields.Boolean(default=True)


class ShiftPatternStep(models.Model):
_name = "hr.shift.pattern.step"
_description = "Shift rotation cycle step"
_order = "pattern_id, sequence, id"

pattern_id = fields.Many2one(
comodel_name="hr.shift.pattern", required=True, ondelete="cascade"
)
sequence = fields.Integer(default=10)
week_id = fields.Many2one(comodel_name="hr.shift.pattern.week", required=True)
39 changes: 39 additions & 0 deletions hr_shift_pattern/models/shift_pattern_week.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2026 INVITU
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models

from odoo.addons.hr_shift.models.shift_template import WEEK_DAYS_SELECTION


class ShiftPatternWeek(models.Model):
_name = "hr.shift.pattern.week"
_description = "Shift week template"

name = fields.Char(required=True)
color = fields.Integer()
line_ids = fields.One2many(
comodel_name="hr.shift.pattern.week.line", inverse_name="week_id"
)
active = fields.Boolean(default=True)


class ShiftPatternWeekLine(models.Model):
_name = "hr.shift.pattern.week.line"
_description = "Shift week template day"
_order = "week_id, day_number"

week_id = fields.Many2one(
comodel_name="hr.shift.pattern.week", required=True, ondelete="cascade"
)
day_number = fields.Selection(
selection=WEEK_DAYS_SELECTION, string="Day", required=True
)
template_id = fields.Many2one(comodel_name="hr.shift.template", required=True)

_sql_constraints = [
(
"day_unique_per_week",
"unique(week_id, day_number)",
"Each day can only appear once per week template.",
)
]
Loading
Loading