# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. from odoo import api, fields, models class HrEmployee(models.Model): """Tag employees with the shop roles they can perform. An employee with role 'masking' receives the masking steps when WOs are generated; an employee with multiple roles receives WOs for all of them. A small shop where the owner wears every hat just tags themselves with every role. Lead hands are a separate per-role list — they don't have to be primary owners of those roles, but they're authorised to step in when the regular owner is absent or behind. The Manager Desk promotes lead hands above other workers in its dropdown for any role they cover. """ _inherit = 'hr.employee' x_fc_work_role_ids = fields.Many2many( 'fp.work.role', 'fp_employee_work_role_rel', 'employee_id', 'role_id', string='Shop Roles', help='Which shop roles this employee performs. Used by the ' 'Manager Desk and auto-assignment on WO generation. ' 'Roles are added automatically when an employee completes ' 'a task that meets the role mastery threshold.', ) # Per-role lead-hand list. Sarah might be a lead hand for masking + # racking but not for plating; Mike might cover everything during # a graveyard shift. Stored on a separate relation table so the # primary "Shop Roles" list stays distinct from the cover-anything # authority. x_fc_lead_hand_role_ids = fields.Many2many( 'fp.work.role', 'fp_employee_lead_hand_role_rel', 'employee_id', 'role_id', string='Lead Hand For', help='Roles where this employee is authorised to lead or cover ' 'for an absent operator. Lead hands are surfaced first in ' 'the Manager Desk worker picker for these roles.', ) x_fc_proficiency_ids = fields.One2many( 'fp.operator.proficiency', 'employee_id', string='Task Proficiency', help='Per-role completion tally. Workers earn one count per WO ' 'they finish on a given role. Once the count crosses the ' "role's mastery threshold the role is added to their " 'Shop Roles list automatically.', ) # ------------------------------------------------------------------ # Attendance helpers — used by the Manager Desk to show who is # currently clocked in. Works with vanilla hr_attendance or the # full fusion_clock module — both store an open record (no # check_out) for as long as the employee is on shift. # ------------------------------------------------------------------ x_fc_is_clocked_in = fields.Boolean( string='Clocked In', compute='_compute_x_fc_is_clocked_in', search='_search_x_fc_is_clocked_in', help='True if this employee currently has an open hr.attendance ' 'record (clocked in but not clocked out).', ) def _compute_x_fc_is_clocked_in(self): """Compute attendance status from hr.attendance. Batched so the manager dashboard doesn't issue one query per employee — important when the shop has dozens of operators. """ if not self: return Att = self.env.get('hr.attendance') if Att is None: for emp in self: emp.x_fc_is_clocked_in = False return # One read for the whole recordset. open_emp_ids = set(Att.sudo().search([ ('employee_id', 'in', self.ids), ('check_out', '=', False), ]).mapped('employee_id').ids) for emp in self: emp.x_fc_is_clocked_in = emp.id in open_emp_ids def _search_x_fc_is_clocked_in(self, *args): """Lets `[('x_fc_is_clocked_in', '=', True)]` work as a domain. Two compounding gotchas surfaced after fusion_clock auto-closed the demo open attendances: 1. Odoo 19 normalises ``('=', True)`` into ``('in', OrderedSet([True]))`` before invoking the search method. The previous code only handled ``=`` / ``!=`` and fell through to ``return []`` for ``in`` / ``not in`` — which Odoo treats as "no constraint" and matches every row. 2. ``('id', 'in', [])`` is also treated as no-constraint in some Odoo versions; replaced with a ``[0]`` sentinel so the empty-open-list case correctly matches nothing. Strategy: reduce caller intent to a *match_set* of booleans (which values of ``x_fc_is_clocked_in`` should match), flip on negative operators, then translate into ``id IN`` / ``NOT IN`` on the cached open-attendance employee ids. Variable signature future-proofs against Odoo's compute-field API shifting again. """ # Variable signature — Odoo 19 may pass (records, op, val). if len(args) == 3: _records, operator, value = args elif len(args) == 2: operator, value = args else: return [('id', '=', False)] Att = self.env.get('hr.attendance') if Att is None: return [('id', '=', False)] if operator in ('=', '!='): match_set = {bool(value)} elif operator in ('in', 'not in'): match_set = set(map(bool, value)) else: return [('id', '=', False)] # Negated operators flip the match set. if operator in ('!=', 'not in'): match_set = {True, False} - match_set if not match_set: return [('id', '=', False)] if match_set == {True, False}: return [] # every row matches open_emp_ids = Att.sudo().search( [('check_out', '=', False)] ).employee_id.ids ids_term = open_emp_ids or [0] return [('id', 'in' if True in match_set else 'not in', ids_term)] @api.model def _fp_clocked_in_user_ids(self): """Return the set of res.users.ids whose linked employee is on shift. Used by the Manager Desk controller to short-circuit the worker dropdown to "present today" without an N+1 attendance query per worker. """ Att = self.env.get('hr.attendance') if Att is None: return set() emps = Att.sudo().search([ ('check_out', '=', False), ]).mapped('employee_id') return set(emps.user_id.ids)