feat(fusion_clock): native shift roles (fusion.clock.role) [A1-A3]
Replaces Odoo Planning's planning.role: name+colour model with the same 1-11 palette, employee default/allowed role fields, Employee Roles editor, role_id on shift template + schedule with default resolution, ACLs, menus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from . import clock_report
|
||||
from . import res_config_settings
|
||||
from . import clock_activity_log
|
||||
from . import clock_leave_request
|
||||
from . import clock_role
|
||||
from . import clock_shift
|
||||
from . import clock_schedule
|
||||
from . import clock_correction
|
||||
|
||||
46
fusion_clock/models/clock_role.py
Normal file
46
fusion_clock/models/clock_role.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Native shift role. Re-implements the small, useful subset of Odoo
|
||||
# Enterprise ``planning.role`` (name + colour) so Fusion Clock can colour and
|
||||
# label shifts on the portal without depending on the Enterprise Planning app.
|
||||
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FusionClockRole(models.Model):
|
||||
_name = 'fusion.clock.role'
|
||||
_description = 'Clock Shift Role'
|
||||
_order = 'sequence, name'
|
||||
_rec_name = 'name'
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
color = fields.Integer(default=_get_default_color)
|
||||
active = fields.Boolean(default=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
|
||||
# Kanban colour code (1-11) -> hex, mirroring planning.role._get_color_from_code
|
||||
# so the portal Schedule tab shows the same palette Planning used.
|
||||
_COLOR_HEX = {
|
||||
0: '#008784', 1: '#EE4B39', 2: '#F29648', 3: '#F4C609',
|
||||
4: '#55B7EA', 5: '#71405B', 6: '#E86869', 7: '#008784',
|
||||
8: '#267283', 9: '#BF1255', 10: '#2BAF73', 11: '#8754B0',
|
||||
}
|
||||
|
||||
def _get_color_from_code(self, is_open_shift=False):
|
||||
"""Return a hex colour for this role. Open shifts get an '80' alpha
|
||||
suffix (matching Planning's open-shift transparency convention)."""
|
||||
self.ensure_one()
|
||||
hex_value = self._COLOR_HEX.get(self.color, '#008784')
|
||||
return hex_value + ('80' if is_open_shift else '')
|
||||
@@ -58,6 +58,20 @@ class FusionClockSchedule(models.Model):
|
||||
store=True,
|
||||
)
|
||||
note = fields.Char(string='Note')
|
||||
role_id = fields.Many2one(
|
||||
'fusion.clock.role',
|
||||
string='Role',
|
||||
help="Shift role — drives the colour/label shown on the employee's "
|
||||
"portal schedule. Defaults from the shift template or the "
|
||||
"employee's Default Shift Role.",
|
||||
)
|
||||
recurrence_id = fields.Many2one(
|
||||
'fusion.clock.schedule.recurrence',
|
||||
string='Recurrence',
|
||||
ondelete='set null',
|
||||
index=True,
|
||||
help="Set when this entry was generated by a recurring rule.",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
@@ -292,10 +306,21 @@ class FusionClockSchedule(models.Model):
|
||||
new_schedule = self.browse()
|
||||
new_value = ''
|
||||
else:
|
||||
# Resolve the role: explicit payload role wins, then the shift
|
||||
# template's role, then the employee's default role.
|
||||
role_id = payload.get('role_id')
|
||||
if not role_id:
|
||||
shift_id = parsed.get('shift_id')
|
||||
shift = self.env['fusion.clock.shift'].browse(shift_id) if shift_id else None
|
||||
if shift and shift.role_id:
|
||||
role_id = shift.role_id.id
|
||||
elif employee.x_fclk_default_role_id:
|
||||
role_id = employee.x_fclk_default_role_id.id
|
||||
vals = {
|
||||
'employee_id': employee.id,
|
||||
'schedule_date': date_obj,
|
||||
'shift_id': parsed.get('shift_id') or False,
|
||||
'role_id': int(role_id) if role_id else False,
|
||||
'is_off': bool(parsed.get('is_off')),
|
||||
'start_time': parsed.get('start_time') or 0.0,
|
||||
'end_time': parsed.get('end_time') or 0.0,
|
||||
@@ -349,6 +374,9 @@ class FusionClockSchedule(models.Model):
|
||||
'hours': schedule.planned_hours,
|
||||
'hours_display': Schedule.fclk_hours_display(schedule.planned_hours),
|
||||
'note': schedule.note or '',
|
||||
'role_id': schedule.role_id.id or False,
|
||||
'role_name': schedule.role_id.name or '',
|
||||
'role_color': schedule.role_id._get_color_from_code() if schedule.role_id else '',
|
||||
}
|
||||
|
||||
plan = employee._get_fclk_day_plan(date_obj)
|
||||
@@ -366,6 +394,9 @@ class FusionClockSchedule(models.Model):
|
||||
'hours': plan.get('hours') or 0.0,
|
||||
'hours_display': Schedule.fclk_hours_display(plan.get('hours') or 0.0),
|
||||
'note': '',
|
||||
'role_id': False,
|
||||
'role_name': '',
|
||||
'role_color': '',
|
||||
}
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -42,6 +42,12 @@ class FusionClockShift(models.Model):
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
color = fields.Char(string='Color', default='#3B82F6')
|
||||
role_id = fields.Many2one(
|
||||
'fusion.clock.role',
|
||||
string='Default Role',
|
||||
help="Role assigned to shifts created from this template "
|
||||
"(drives the colour/label on the employee's portal schedule).",
|
||||
)
|
||||
|
||||
# Weekday pattern — which days this recurring shift applies as the baseline
|
||||
# when there is no posted planner entry for the day. Default Mon-Fri.
|
||||
|
||||
@@ -33,6 +33,21 @@ class HrEmployee(models.Model):
|
||||
help="Assigned shift schedule. Leave empty to use global defaults.",
|
||||
)
|
||||
|
||||
# Shift roles (native replacement for Odoo Planning's employee role fields)
|
||||
x_fclk_default_role_id = fields.Many2one(
|
||||
'fusion.clock.role',
|
||||
string='Default Shift Role',
|
||||
help="Pre-fills the role on every new shift created for this employee.",
|
||||
)
|
||||
x_fclk_role_ids = fields.Many2many(
|
||||
'fusion.clock.role',
|
||||
'fclk_employee_role_rel',
|
||||
'employee_id',
|
||||
'role_id',
|
||||
string='Allowed Shift Roles',
|
||||
help="Roles this employee is allowed to be scheduled for.",
|
||||
)
|
||||
|
||||
# Pending reason enforcement
|
||||
x_fclk_pending_reason = fields.Boolean(
|
||||
string='Pending Reason Required',
|
||||
|
||||
Reference in New Issue
Block a user