# -*- 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 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. """ _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.', ) <<<<<<< Updated upstream ======= # 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. Odoo 19 normalises the equality term ``('=', True)`` into ``('in', OrderedSet([True]))`` before calling this method, so we have to handle ``in`` / ``not in`` as well as the bare ``=`` / ``!=``. The signature is also variadic so a future Odoo refactor that prepends a ``records`` argument doesn't break us. Strategy: 1. Reduce the caller's intent to a *match_set* of booleans — which values of ``x_fc_is_clocked_in`` should match. 2. Negative operators flip that set. 3. Translate the set into an ``id IN`` (or ``NOT IN``) term on the cached open-attendance employee ids. """ # 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)] # Build the set of bool values the caller wants. if operator in ('=', '!='): match_set = {bool(value)} elif operator in ('in', 'not in'): # value is a (possibly OrderedSet) iterable of bools. match_set = set(map(bool, value)) else: return [('id', '=', False)] # Negated operators flip the match set so we can reason in # purely positive terms below. if operator in ('!=', 'not in'): match_set = {True, False} - match_set if not match_set: return [('id', '=', False)] if match_set == {True, False}: # No filter at all — every employee matches. return [] open_emp_ids = Att.sudo().search( [('check_out', '=', False)] ).employee_id.ids # Sentinel guards against an empty list — Odoo treats # `('id', 'in', [])` as no constraint in some versions and # ends up matching every row. 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) >>>>>>> Stashed changes