# -*- 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',
)