diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py b/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py index bf5d4acc..e5b94ff6 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/hr_employee.py @@ -22,3 +22,136 @@ class HrEmployee(models.Model): 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