# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. """Operator proficiency tracker — counts successful WO completions per (employee, role) pair and auto-promotes the employee once the role's mastery threshold is crossed. The promotion mechanic lets managers casually train workers on the job: they assign someone a task they've never done, the worker finishes it successfully, and after N successes the role is added to the employee's Shop Roles automatically. The operator never has to fill in a form; their growing skill set just unlocks itself. """ from markupsafe import Markup from odoo import _, api, fields, models class FpOperatorProficiency(models.Model): _name = 'fp.operator.proficiency' _description = 'Fusion Plating — Operator Task Proficiency' _rec_name = 'display_name' _order = 'employee_id, role_id' employee_id = fields.Many2one( 'hr.employee', string='Operator', required=True, ondelete='cascade', index=True, ) role_id = fields.Many2one( 'fp.work.role', string='Role', required=True, ondelete='cascade', index=True, ) completed_count = fields.Integer( string='Completions', default=0, help='Number of times this operator has successfully finished a ' 'WO that required this role.', ) first_completed_at = fields.Datetime( string='First Success', help='When the operator finished their first WO for this role.', ) last_completed_at = fields.Datetime( string='Last Success', help='Most recent WO completion against this role.', ) promoted = fields.Boolean( string='Promoted', default=False, index=True, help='True once the role has been added to the operator\'s Shop ' 'Roles automatically. Stays True even if a manager removes ' 'the role afterwards — the count and promotion history are ' 'preserved as a training record.', ) promoted_at = fields.Datetime( string='Promoted On', help='When the auto-promotion fired (count crossed the role\'s ' 'mastery threshold).', ) display_name = fields.Char( compute='_compute_display_name', store=True, ) progress_label = fields.Char( compute='_compute_progress_label', help='"3 / 5" style indicator of how close this operator is to ' 'mastery.', ) _sql_constraints = [ ('fp_proficiency_uniq', 'unique(employee_id, role_id)', 'There is already a proficiency record for this operator and role.'), ] @api.depends('employee_id.name', 'role_id.name') def _compute_display_name(self): for rec in self: rec.display_name = ( f'{rec.employee_id.name or "?"} — {rec.role_id.name or "?"}' ) @api.depends('completed_count', 'role_id.mastery_required') def _compute_progress_label(self): for rec in self: target = rec.role_id.mastery_required or 0 rec.progress_label = ( f'{rec.completed_count} / {target}' if target else str(rec.completed_count) ) # ------------------------------------------------------------------ # API used by mrp.workorder.button_finish (via _fp_record_proficiency). # ------------------------------------------------------------------ @api.model def _record_completion(self, employee, role): """Increment the (employee, role) tally and promote if at threshold. Idempotent for the (employee, role) pair — if no record exists, we create one. Always uses sudo() because the worker may not have write access to their own profile. """ if not employee or not role: return self.browse() rec = self.sudo().search([ ('employee_id', '=', employee.id), ('role_id', '=', role.id), ], limit=1) now = fields.Datetime.now() if rec: new_count = rec.completed_count + 1 rec.write({ 'completed_count': new_count, 'last_completed_at': now, }) else: rec = self.sudo().create({ 'employee_id': employee.id, 'role_id': role.id, 'completed_count': 1, 'first_completed_at': now, 'last_completed_at': now, }) rec._maybe_promote() return rec def _maybe_promote(self): """Promote the employee if they've crossed the role's threshold. - Already promoted: no-op (history is preserved but no duplicate chatter spam). - Already in Shop Roles (e.g. manager added it manually): mark promoted but don't post chatter. - Below threshold: nothing to do. - At/above threshold AND not on Shop Roles yet: add the role and post a celebratory chatter line on the employee. """ for rec in self: if rec.promoted: continue target = rec.role_id.mastery_required or 0 if target <= 0: continue # Auto-promotion disabled for this role if rec.completed_count < target: continue employee = rec.employee_id role = rec.role_id already_assigned = role in employee.x_fc_work_role_ids rec.sudo().write({ 'promoted': True, 'promoted_at': fields.Datetime.now(), }) if already_assigned: # Manager pre-added the role; don't double-announce. continue # Add to Shop Roles + announce on the employee chatter. employee.sudo().write({ 'x_fc_work_role_ids': [(4, role.id)], }) employee.message_post( body=Markup(_( '🎉 %(name)s promoted — qualified for ' '%(role)s after %(count)s successful ' 'completions.' )) % { 'name': employee.name, 'role': role.name, 'count': rec.completed_count, }, subtype_xmlid='mail.mt_note', )