diff --git a/hr_shift/README.rst b/hr_shift/README.rst new file mode 100644 index 0000000..e2df3c7 --- /dev/null +++ b/hr_shift/README.rst @@ -0,0 +1,78 @@ +================ +Employees Shifts +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:267f057335dc26361bc24fcc7b9ad4f01ba39496cccb443c54a519cc725c3524 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/14.0/hr_shift + :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-14-0/shift-planning-14-0-hr_shift + :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=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Assign shifts to employees. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - David Vidal + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_shift/__init__.py b/hr_shift/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/hr_shift/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_shift/__manifest__.py b/hr_shift/__manifest__.py new file mode 100644 index 0000000..219156d --- /dev/null +++ b/hr_shift/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Employees Shifts", + "summary": "Define shifts for employees", + "version": "14.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/shift-planning", + "category": "Marketing", + "depends": ["hr_holidays_public", "base_sparse_field"], + "data": [ + "security/ir.model.access.csv", + "views/shift_planning_views.xml", + "views/shift_template_views.xml", + "views/res_config_settings_views.xml", + "views/assets.xml", + ], +} diff --git a/hr_shift/models/__init__.py b/hr_shift/models/__init__.py new file mode 100644 index 0000000..825b418 --- /dev/null +++ b/hr_shift/models/__init__.py @@ -0,0 +1,6 @@ +from . import res_company +from . import resource_calendar +from . import res_config_settings +from . import shift_planning +from . import shift_template +from . import hr_employee diff --git a/hr_shift/models/hr_employee.py b/hr_shift/models/hr_employee.py new file mode 100644 index 0000000..33c9ca0 --- /dev/null +++ b/hr_shift/models/hr_employee.py @@ -0,0 +1,42 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class HrEmployeeBase(models.AbstractModel): + _inherit = "hr.employee.base" + + current_shift_id = fields.Many2one( + comodel_name="hr.shift.planning.line", compute="_compute_current_shift_id" + ) + + def _shift_of_date(self, min_time, max_time): + return ( + self.env["hr.shift.planning.line"] + .sudo() + .search( + [ + ("employee_id", "=", self.id), + ("state", "=", "assigned"), + ("start_time", ">=", min_time), + ("end_time", "<=", max_time), + ] + ) + ) + + def _compute_current_shift_id(self): + """Current shift for a given employee if any""" + today = fields.Date.today() + now = fields.Datetime.now() + min_time = fields.datetime.combine(today, now.min.time()) + max_time = fields.datetime.combine(today, now.max.time()) + for employee in self: + employee.current_shift_id = employee._shift_of_date(min_time, max_time) + + def _get_employee_working_now(self): + # Get shift info if available + employees_in_current_shift = self.filtered("current_shift_id") + others = super( + HrEmployeeBase, (self - employees_in_current_shift) + )._get_employee_working_now() + return others + employees_in_current_shift.ids diff --git a/hr_shift/models/res_company.py b/hr_shift/models/res_company.py new file mode 100644 index 0000000..3752221 --- /dev/null +++ b/hr_shift/models/res_company.py @@ -0,0 +1,13 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + +from .shift_template import WEEK_DAYS_SELECTION + + +class ResCompany(models.Model): + _inherit = "res.company" + + # Default from monday to friday + shift_start_day = fields.Selection(selection=WEEK_DAYS_SELECTION, default="0") + shift_end_day = fields.Selection(selection=WEEK_DAYS_SELECTION, default="4") diff --git a/hr_shift/models/res_config_settings.py b/hr_shift/models/res_config_settings.py new file mode 100644 index 0000000..3eaa2ed --- /dev/null +++ b/hr_shift/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + shift_start_day = fields.Selection( + related="company_id.shift_start_day", readonly=False + ) + shift_end_day = fields.Selection(related="company_id.shift_end_day", readonly=False) diff --git a/hr_shift/models/resource_calendar.py b/hr_shift/models/resource_calendar.py new file mode 100644 index 0000000..ef18603 --- /dev/null +++ b/hr_shift/models/resource_calendar.py @@ -0,0 +1,57 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import datetime + +from odoo import api, models +from odoo.tools import groupby + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + @api.model + def _resource_shift_for_datetime_range(self, start_dt, end_dt, resources, tz=None): + min_time = datetime.combine(start_dt, start_dt.min.time()) + max_time = datetime.combine(end_dt, end_dt.max.time()) + shifts = self.env["hr.shift.planning.line"].search( + [ + ("resource_id", "in", resources.ids), + ("state", "=", "assigned"), + ("start_time", ">=", min_time), + ("end_time", "<=", max_time), + ] + ) + return shifts + + def _attendance_intervals_batch( + self, start_dt, end_dt, resources=None, domain=None, tz=None + ): + # Override calendar intervals when a shift is found and substitute those + # intervals with the ones on the shift + # TODO: deal with TZ! + res = super()._attendance_intervals_batch( + start_dt, end_dt, resources, domain, tz + ) + if resources: + shift_ids = self._resource_shift_for_datetime_range( + start_dt, end_dt, resources, tz=tz + ) + for resource, shifts in groupby(shift_ids, lambda x: x.resource_id): + intervals_to_add = [] + intervals_to_remove = [] + resource_intervals = res[resource.id]._items + for shift in shifts: + intervals_to_remove += [ + (start, end, resource_item) + for start, end, resource_item in resource_intervals + if ( + shift.start_time + >= datetime.combine(start, start.min.time()) + and shift.end_time >= datetime.combine(end, end.min.time()) + ) + ] + intervals_to_add.append((shift.start_time, shift.end_time, shift)) + res[resource.id]._items = [ + x for x in resource_intervals if x not in intervals_to_remove + ] + intervals_to_add + return res diff --git a/hr_shift/models/shift_planning.py b/hr_shift/models/shift_planning.py new file mode 100644 index 0000000..a588a43 --- /dev/null +++ b/hr_shift/models/shift_planning.py @@ -0,0 +1,402 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from .shift_template import WEEK_DAYS_SELECTION + + +class ShiftPlanning(models.Model): + _name = "hr.shift.planning" + _description = "Shift plannings" + _order = "end_date desc" + + year = fields.Integer(required=True) + week_number = fields.Integer(required=True) + start_date = fields.Date( + compute="_compute_dates", inverse="_inverse_start_date", store=True + ) + end_date = fields.Date(compute="_compute_dates", readonly=True, store=True) + shift_ids = fields.One2many( + comodel_name="hr.shift.planning.shift", inverse_name="planning_id" + ) + shifts_count = fields.Integer(compute="_compute_shifts_count") + issued_shift_ids = fields.One2many( + comodel_name="hr.shift.planning.shift", + compute="_compute_issued_shift_ids", + ) + issued_shifts_count = fields.Integer(compute="_compute_issued_shift_ids") + state = fields.Selection( + selection=[ + ("new", "New"), + ("assignment", "Assignment"), + ("planned", "Planned"), + ], + default="new", + ) + days_data = fields.Serialized(default={}, compute="_compute_days_data") + # Decidir cómo mostrar # nº asignados por turno, nº sin asignar + # summary = fields.Html() + + _sql_constraints = [ + ( + "unique_year_week", + "unique(year,week_number)", + "You can't plan the same week twice!", + ) + ] + + def default_get(self, fields_list): + # Get the last plan and start from there + result = super().default_get(fields_list) + last_plan = self.search([], limit=1) + if not last_plan or result.get("year") or result.get("week_number"): + return result + year, week_number, *_ = ( + last_plan.end_date + relativedelta(days=1) + ).isocalendar() + result.update({"year": year, "week_number": week_number}) + return result + + def name_get(self): + result = [ + ( + planning.id, + ( + f"{planning.year} {_('Week')} {planning.week_number} " + f"({planning.start_date} - {planning.end_date})" + ), + ) + for planning in self + ] + return result + + @api.depends("shift_ids") + def _compute_shifts_count(self): + for plan in self: + plan.shifts_count = len(plan.shift_ids) + + @api.depends("week_number", "year") + def _compute_dates(self): + for planning in self.filtered(lambda x: x.week_number and x.year): + planning.start_date = datetime.fromisocalendar( + planning.year, planning.week_number, 1 + ) + planning.end_date = datetime.fromisocalendar( + planning.year, planning.week_number, 7 + ) + + def _inverse_start_date(self): + for planning in self.filtered("start_date"): + planning.year, planning.week_number, *_ = planning.start_date.isocalendar() + + @api.depends("shift_ids") + def _compute_issued_shift_ids(self): + for plan in self: + plan.issued_shift_ids = ( + self.env["hr.shift.planning.line"] + .search( + [ + ("shift_id", "in", plan.shift_ids.ids), + ("state", "=", "on_leave"), + ] + ) + .shift_id + ) + plan.issued_shifts_count = len(plan.issued_shift_ids) + + def _compute_days_data(self): + """Used in the Kanban view""" + self.days_data = {} + for plan in self.filtered(lambda x: x.start_date and x.end_date): + dates = self.env["hr.shift.template"]._explode_date_range( + plan.start_date, plan.end_date + ) + plan.days_data = { + date["weekday"]: { + "weekday": dict(WEEK_DAYS_SELECTION).get(str(date["weekday"])), + "weekday_number": str(date["weekday"]), + "plan": plan.id, + "day": date["date"].day, + } + for date in dates + } + + def generate_shifts(self): + self.ensure_one() + available_employes = self.env["hr.employee"].search([]) + shifts = self.env["hr.shift.planning.shift"].create( + [ + { + "planning_id": self.id, + "employee_id": employee.id, + } + for employee in (available_employes - self.shift_ids.employee_id) + ] + ) + shifts._generate_shift_lines() + self.state = "assignment" + + # TODO: re-generar turnos + + def action_view_shifts(self): + action = self.env["ir.actions.act_window"]._for_xml_id( + "hr_shift.shift_planning_shift_action" + ) + action["display_name"] = f"{_('Shifts for')} {self.display_name}" + return action + + def action_view_issued_shifts(self): + action = self.action_view_shifts() + action["domain"] = [("id", "in", self.issued_shift_ids.ids)] + action["display_name"] = f"{_('Issues for')} {self.display_name}" + return action + + def action_view_day_shifts(self): + action = self.env["ir.actions.act_window"]._for_xml_id( + "hr_shift.shift_planning_day_detail_action" + ) + weekday_number = str(self.env.context.get("weekday_number", "0")) + action["domain"] = [ + ("shift_id.planning_id", "=", self.id), + ("day_number", "=", str(weekday_number)), + ] + action["context"] = { + "multi_employee_mode": True, + "group_by": "template_id", + } + action["display_name"] = _( + "%(day)s shifts of %(planning)s", + day=dict(WEEK_DAYS_SELECTION).get(weekday_number), + planning=self.display_name, + ) + return action + + +class ShiftPlanningShift(models.Model): + _name = "hr.shift.planning.shift" + _description = "Shift of the week for a given employee" + + planning_id = fields.Many2one(comodel_name="hr.shift.planning") + employee_id = fields.Many2one(comodel_name="hr.employee") + image_128 = fields.Image(related="employee_id.image_128") + template_id = fields.Many2one( + comodel_name="hr.shift.template", group_expand="_group_expand_template_id" + ) + color = fields.Integer(related="template_id.color") + line_ids = fields.One2many( + comodel_name="hr.shift.planning.line", inverse_name="shift_id" + ) + lines_data = fields.Serialized(default={}, compute="_compute_lines_data") + state = fields.Selection( + selection=[ + ("available", "Fully available"), + ("partial", "Partially available"), + ("unavailable", "Unavailable"), + ], + ) + + _sql_constraints = [ + ( + "unique_planning_employee", + "unique(planning_id,employee_id)", + "You can't assign an employee twice to the same plan!", + ) + ] + + @api.model + def _group_expand_template_id(self, templates, domain, order): + return self.env["hr.shift.template"].search([]) + + @api.depends("line_ids") + def _compute_lines_data(self): + for shift in self.filtered("line_ids"): + shift.lines_data = { + line.id: { + "day": dict(WEEK_DAYS_SELECTION).get(line.day_number), + "template": line.template_id.name, + "state": line.state, + "color": line.color, + } + for line in shift.line_ids + } + + def _generate_shift_lines(self): + for shift in self: + dates = shift.template_id._explode_date_range( + shift.planning_id.start_date, shift.planning_id.end_date + ) + shift_lines = [] + for shift_date in dates: + shift_lines.append( + { + "shift_id": shift.id, + "day_number": str(shift_date["weekday"]), + } + ) + lines = shift.line_ids.create(shift_lines) + lines._compute_state() + + def write(self, vals): + if "template_id" not in vals: + return super().write(vals) + template = self.env["hr.shift.template"].browse(vals["template_id"] or 0) + self.filtered(lambda x: x.template_id != template).line_ids.unlink() + res = super().write(vals) + self._generate_shift_lines() + return res + + def action_view_shift_details(self): + action = self.env["ir.actions.act_window"]._for_xml_id( + "hr_shift.shift_planning_line_action" + ) + if self.env.context.get("shift_line_id"): + action["view_mode"] = "form" + action["views"] = [(False, "form")] + action["res_id"] = self.env.context.get("shift_line_id") + action["target"] = "new" + action["display_name"] = f"{_('Details for')} {self.employee_id.name}" + return action + + +class ShiftPlanningLine(models.Model): + _name = "hr.shift.planning.line" + _description = "Shift of the day for the employee" + _order = "shift_id desc, day_number asc" + + shift_id = fields.Many2one(comodel_name="hr.shift.planning.shift") + template_id = fields.Many2one( + comodel_name="hr.shift.template", + store=True, + readonly=False, + compute="_compute_template_id", + group_expand="_group_expand_template_id", + ) + start_hour = fields.Float(string="Start hour", related="template_id.start_time") + end_hour = fields.Float(string="End hour", related="template_id.end_time") + color = fields.Integer(related="template_id.color") + planning_id = fields.Many2one(related="shift_id.planning_id") + employee_id = fields.Many2one(related="shift_id.employee_id") + resource_id = fields.Many2one(related="employee_id.resource_id", store=True) + day_number = fields.Selection(string="Day", selection=WEEK_DAYS_SELECTION) + start_time = fields.Datetime(compute="_compute_shift_time", store=True) + end_time = fields.Datetime(compute="_compute_shift_time", store=True) + start_date = fields.Date(string="Date", compute="_compute_start_date") + state = fields.Selection( + selection=[ + ("assigned", "Assigned"), + ("on_leave", "On leave"), + ("unassigned", "Unassigned"), + ("holiday", "Holiday"), + ], + compute="_compute_state", + readonly=False, + store=True, + ) + + @api.constrains("template_id") + def _constrain_template_id(self): + for record in self.filtered("template_id"): + if record.state == "holiday": + raise UserError( + _( + "This is a public holiday and the employee isn't available " + "for this shift" + ) + ) + elif record.state == "on_leave": + raise UserError( + _("This employee is on leave so can't be assigned to this shift") + ) + + @api.depends("template_id") + def _compute_state(self): + for shift in self: + if shift._is_public_holiday(): + shift.state = "holiday" + elif shift._is_on_leave(): + shift.state = "on_leave" + elif shift.template_id: + shift.state = "assigned" + else: + shift.state = "unassigned" + + @api.depends("shift_id.template_id", "state") + def _compute_template_id(self): + for line in self: + if line.state in {"assigned", "unassigned"}: + line.template_id = line.shift_id.template_id + if line.state in {"holiday", "on_leave"}: + line.template_id = False + + @api.model + def _group_expand_template_id(self, templates, domain, order): + return self.env["hr.shift.template"].search([]) + + def name_get(self): + result = [ + ( + line.id, + ( + f"{dict(WEEK_DAYS_SELECTION).get(line.day_number)} " + f"{line.template_id.name or line.state}" + ), + ) + for line in self + ] + return result + + @api.depends("planning_id", "day_number", "template_id") + def _compute_shift_time(self): + # TODO: Unify this calculations as we're repeating them several times + for shift in self.filtered("shift_id"): + shift_date = shift.template_id._get_weekdate( + shift.planning_id.start_date, int(shift.day_number) + ) + start_time = shift.template_id._prepare_time()["start_time"] + end_time = shift.template_id._prepare_time()["end_time"] + shift.start_time = datetime.combine( + shift_date, + datetime.min.time().replace( + hour=start_time["hour"], minute=start_time["minute"] + ), + ) + shift.end_time = datetime.combine( + shift_date, + datetime.min.time().replace( + hour=end_time["hour"], minute=end_time["minute"] + ), + ) + + def _compute_start_date(self): + for shift in self: + shift.start_date = shift.start_time + + def _is_public_holiday(self): + if not (self.start_date and self.employee_id): + return False + return self.env["hr.holidays.public"].is_public_holiday( + self.start_date, self.employee_id.id + ) + + def _is_on_leave(self): + if not (self.start_time and self.end_time and self.employee_id): + return False + return bool( + self.env["resource.calendar.leaves"] + .sudo() + .search( + [ + ("resource_id", "=", self.employee_id.resource_id.id), + ("date_from", "<=", self.start_time), + ("date_to", ">=", self.end_time), + ] + ) + ) + + def action_unassign_shift(self): + self.template_id = False diff --git a/hr_shift/models/shift_template.py b/hr_shift/models/shift_template.py new file mode 100644 index 0000000..c2bd926 --- /dev/null +++ b/hr_shift/models/shift_template.py @@ -0,0 +1,70 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models + +WEEK_DAYS_SELECTION = [ + ("0", _("Monday")), + ("1", _("Tuesday")), + ("2", _("Wednesday")), + ("3", _("Thursday")), + ("4", _("Friday")), + ("5", _("Saturday")), + ("6", _("Sunday")), +] + + +class ShiftTemplate(models.Model): + _name = "hr.shift.template" + _description = "Shifts" + + name = fields.Char() + day_of_week_start = fields.Selection(selection=WEEK_DAYS_SELECTION) + day_of_week_end = fields.Selection(selection=WEEK_DAYS_SELECTION) + start_time = fields.Float() + end_time = fields.Float() + color = fields.Integer() + + def _prepare_time(self): + def _parse_float_time(float_time): + hour, minute = divmod(abs(float_time) * 60, 60) + return { + "hour": int(hour), + "minute": int(minute), + } + + return { + "start_time": _parse_float_time(self.start_time), + "end_time": _parse_float_time(self.end_time), + } + + @api.model + def _get_weekdate(self, date_start, weekday): + delta_days = (weekday - date_start.weekday() + 7) % 7 + return date_start + relativedelta(days=delta_days) + + def _explode_date_range(self, date_start, date_end): + """Based on the record values, it returns a list of dicts containing a start + datetime, an end datetime, and the weekday for the start datetime. The range + can be wider or shorter than the template week days span, but we'll only return + those within the template's week day span.""" + date_list = [] + current_date = date_start + day_of_week_start = int( + self.day_of_week_start or self.env.company.shift_start_day + ) + day_of_week_end = int(self.day_of_week_end or self.env.company.shift_end_day) + while current_date <= date_end: + weekday = current_date.weekday() + if day_of_week_start <= weekday <= day_of_week_end: + date_list.append( + { + "date": current_date, + "weekday": weekday, + } + ) + current_date += timedelta(days=1) + return date_list diff --git a/hr_shift/readme/CONTRIBUTORS.md b/hr_shift/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..73dc1db --- /dev/null +++ b/hr_shift/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Tecnativa](https://tecnativa.com): + - David Vidal diff --git a/hr_shift/readme/DESCRIPTION.md b/hr_shift/readme/DESCRIPTION.md new file mode 100644 index 0000000..17eae62 --- /dev/null +++ b/hr_shift/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Assign shifts to employees. diff --git a/hr_shift/security/ir.model.access.csv b/hr_shift/security/ir.model.access.csv new file mode 100644 index 0000000..e170b7c --- /dev/null +++ b/hr_shift/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_shift_template,access_shift_template,model_hr_shift_template,base.group_user,1,1,1,1 +access_shift_planning,access_shift_planning,model_hr_shift_planning,base.group_user,1,1,1,1 +access_shift_planning_shift,access_shift_planning_shift,model_hr_shift_planning_shift,base.group_user,1,1,1,1 +access_shift_planning_line,access_shift_planning_line,model_hr_shift_planning_line,base.group_user,1,1,1,1 diff --git a/hr_shift/static/description/icon.png b/hr_shift/static/description/icon.png new file mode 100644 index 0000000..48f71bf Binary files /dev/null and b/hr_shift/static/description/icon.png differ diff --git a/hr_shift/static/description/index.html b/hr_shift/static/description/index.html new file mode 100644 index 0000000..8ef0326 --- /dev/null +++ b/hr_shift/static/description/index.html @@ -0,0 +1,426 @@ + + + + + +Employees Shifts + + + +
+

Employees Shifts

+ + +

Beta License: AGPL-3 OCA/shift-planning Translate me on Weblate Try me on Runboat

+

Assign shifts to employees.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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 project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/hr_shift/static/src/scss/shift.scss b/hr_shift/static/src/scss/shift.scss new file mode 100644 index 0000000..7a6ea1d --- /dev/null +++ b/hr_shift/static/src/scss/shift.scss @@ -0,0 +1,14 @@ +@mixin o-kanban-button-color { + @for $size from 1 through length($o-colors) { + // Note: the first color is supposed to be invisible if there is a color + // field but it is used as a default color when there is no color field + .o_button_color_#{$size - 1} { + background-color: nth($o-colors, $size); + color: white; + } + } +} + +.o_kanban_view { + @include o-kanban-button-color; +} diff --git a/hr_shift/views/assets.xml b/hr_shift/views/assets.xml new file mode 100644 index 0000000..1e74291 --- /dev/null +++ b/hr_shift/views/assets.xml @@ -0,0 +1,12 @@ + + + + diff --git a/hr_shift/views/res_config_settings_views.xml b/hr_shift/views/res_config_settings_views.xml new file mode 100644 index 0000000..7550976 --- /dev/null +++ b/hr_shift/views/res_config_settings_views.xml @@ -0,0 +1,59 @@ + + + + res.config.settings + + + +
+
+
Default working week + +
+
+
+ Set default company's default working start and end day to be used in shifts span +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hr_shift/views/shift_planning_views.xml b/hr_shift/views/shift_planning_views.xml new file mode 100644 index 0000000..a01e786 --- /dev/null +++ b/hr_shift/views/shift_planning_views.xml @@ -0,0 +1,516 @@ + + + + hr.shift.planning + +
+
+ +
+ +
+ + +
+ + + + + + + + + + +
+
+
+
+ + hr.shift.planning + + + + + + + + +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+

+ + +

+
+
+
+
+
+
+
+
+ + hr.shift.planning + + + + + + + + hr.shift.planning + + + + + + + + + + + hr.shift.planning.shift + + + + + + + + + + hr.shift.planning.shift + + + + + + + + + + hr.shift.planning.shift + + + + + + + + + + + +
+
+
+ +
+
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+
+
+
+
+
+ + hr.shift.planning.line + + + + + + + + + + hr.shift.planning.line + + + + + + + + + + + + + + hr.shift.planning.line + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + +
+ +
+
+ +
+
+
+
+

+ + + + +

+
+
+
+
+
+
+
+
+ + hr.shift.planning.line + + + + + + + + + hr.shift.planning.line + +
+
+ +
+ + + + + + + + + + + + + + +
+
+
+ + hr.shift.planning.shift + kanban,tree,form + {'group_by': 'template_id'} + [('planning_id', '=', active_id)] + + + hr.shift.planning.line + kanban,calendar,tree,form + {'group_by': 'template_id'} + [('shift_id', '=', active_id)] + + + hr.shift.planning.line + kanban,tree,form + {'group_by': 'template_id'} + [('shift_id.planning_id', '=', active_id)] + + + hr.shift.planning.line + calendar,tree + {'group_by': 'template_id'} + [('employee_id.user_id', '=', uid), ('state', '!=', 'unassigned')] + + + hr.shift.planning + kanban,tree,calendar,form + + + + +
diff --git a/hr_shift/views/shift_template_views.xml b/hr_shift/views/shift_template_views.xml new file mode 100644 index 0000000..649d8fd --- /dev/null +++ b/hr_shift/views/shift_template_views.xml @@ -0,0 +1,50 @@ + + + + hr.shift.template + +
+ + + + + + + + + + + + + + + + +
+
+
+ + hr.shift.template + + + + + + + + + + + + + hr.shift.template + tree,form + + +
diff --git a/setup/hr_shift/odoo/addons/hr_shift b/setup/hr_shift/odoo/addons/hr_shift new file mode 120000 index 0000000..87d8524 --- /dev/null +++ b/setup/hr_shift/odoo/addons/hr_shift @@ -0,0 +1 @@ +../../../../hr_shift \ No newline at end of file diff --git a/setup/hr_shift/setup.py b/setup/hr_shift/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/hr_shift/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)