feat(fusion_clock): planning -> native data migration [A8]
post-migrate(19.0.5.0.0) -> fusion.clock.schedule._fclk_port_planning_data: planning.role -> fusion.clock.role, employee default/allowed roles, and planning.slot -> fusion.clock.schedule (local date+float, role map, posted if published, open if unassigned). Guarded (no-op on Community), idempotent (marker), per-row savepoints. Integration test runs on Enterprise clones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -516,6 +516,83 @@ class FusionClockSchedule(models.Model):
|
||||
notified += 1
|
||||
return posted, notified
|
||||
|
||||
@api.model
|
||||
def _fclk_port_planning_data(self):
|
||||
"""Port Odoo Planning data (roles, employee roles, slots) into the
|
||||
native models. Safe no-op when planning is not installed. Returns a
|
||||
dict of counts. Called by the 19.0.5.0.0 migration and by tests."""
|
||||
import pytz
|
||||
|
||||
counts = {'roles': 0, 'employees': 0, 'slots': 0, 'skipped': 0}
|
||||
env = self.env
|
||||
has_roles = 'planning.role' in env
|
||||
has_slots = 'planning.slot' in env
|
||||
if not has_roles and not has_slots:
|
||||
return counts
|
||||
|
||||
Role = env['fusion.clock.role'].sudo()
|
||||
role_map = {}
|
||||
if has_roles:
|
||||
for prole in env['planning.role'].sudo().with_context(active_test=False).search([]):
|
||||
target = Role.with_context(active_test=False).search(
|
||||
[('name', '=ilike', prole.name)], limit=1) or Role.create({
|
||||
'name': prole.name, 'color': prole.color or 1, 'active': prole.active})
|
||||
role_map[prole.id] = target.id
|
||||
counts['roles'] = len(role_map)
|
||||
|
||||
Employee = env['hr.employee'].sudo().with_context(active_test=False)
|
||||
for emp in Employee.search([]):
|
||||
vals = {}
|
||||
if emp._fields.get('default_planning_role_id') and emp.default_planning_role_id:
|
||||
mapped = role_map.get(emp.default_planning_role_id.id)
|
||||
if mapped:
|
||||
vals['x_fclk_default_role_id'] = mapped
|
||||
if emp._fields.get('planning_role_ids') and emp.planning_role_ids:
|
||||
mapped_ids = [role_map[r.id] for r in emp.planning_role_ids if r.id in role_map]
|
||||
if mapped_ids:
|
||||
vals['x_fclk_role_ids'] = [(6, 0, mapped_ids)]
|
||||
if vals:
|
||||
emp.write(vals)
|
||||
counts['employees'] += 1
|
||||
|
||||
if has_slots:
|
||||
Schedule = self.sudo()
|
||||
for slot in env['planning.slot'].sudo().search([], order='start_datetime'):
|
||||
if not slot.start_datetime or not slot.end_datetime:
|
||||
counts['skipped'] += 1
|
||||
continue
|
||||
employee = slot.employee_id if 'employee_id' in slot._fields else False
|
||||
tz_name = ((employee.tz if employee else False)
|
||||
or (slot.resource_id.tz if slot.resource_id else False)
|
||||
or env.company.partner_id.tz or 'UTC')
|
||||
try:
|
||||
tz = pytz.timezone(tz_name)
|
||||
except Exception:
|
||||
tz = pytz.UTC
|
||||
local_start = pytz.utc.localize(slot.start_datetime).astimezone(tz)
|
||||
local_end = pytz.utc.localize(slot.end_datetime).astimezone(tz)
|
||||
span_hours = (slot.end_datetime - slot.start_datetime).total_seconds() / 3600.0
|
||||
allocated = slot.allocated_hours if 'allocated_hours' in slot._fields else span_hours
|
||||
vals = {
|
||||
'employee_id': employee.id if employee else False,
|
||||
'is_open': not bool(employee),
|
||||
'schedule_date': local_start.date(),
|
||||
'start_time': round(local_start.hour + local_start.minute / 60.0, 2),
|
||||
'end_time': round(local_end.hour + local_end.minute / 60.0, 2),
|
||||
'break_minutes': round(max(0.0, span_hours - (allocated or span_hours)) * 60.0, 0),
|
||||
'role_id': role_map.get(slot.role_id.id) if slot.role_id else False,
|
||||
'note': slot.name or False,
|
||||
'state': 'posted' if slot.state == 'published' else 'draft',
|
||||
}
|
||||
with env.cr.savepoint():
|
||||
try:
|
||||
Schedule.create(vals)
|
||||
counts['slots'] += 1
|
||||
except Exception as exc:
|
||||
counts['skipped'] += 1
|
||||
_logger.warning("Fusion Clock: skip planning.slot %s (%s).", slot.id, exc)
|
||||
return counts
|
||||
|
||||
|
||||
class FusionClockScheduleAudit(models.Model):
|
||||
_name = 'fusion.clock.schedule.audit'
|
||||
|
||||
Reference in New Issue
Block a user