feat(plating): in-Odoo notifications, timer audit, presence-aware Manager Desk, auto-promotion
End-to-end workflow tightening + the team / skills system. Three
phases bundled because they share the same touchpoints (button_start /
button_finish / Manager Desk dropdown).
PHASE 1 — In-Odoo notifications + timer audit
=============================================
Workers now get a bell-icon notification (Odoo Discuss inbox) the
moment a manager assigns them a WO. No email — operators check Discuss
between jobs, and the customer-facing notification dispatcher stays
out of the worker loop.
- mrp.workorder.write() override fires message_notify(message_type=
'user_notification') only when x_fc_assigned_user_id transitions to
a non-empty value (clearing or no-op writes don't ping)
- 4 new fields on the WO header surface what was previously buried in
time_ids: x_fc_started_by_user_id, x_fc_started_at,
x_fc_finished_by_user_id, x_fc_finished_at
- button_start stamps started_* once (subsequent pause/resume cycles
preserve the original); button_finish stamps finished_* every time
the WO closes
- New "Timer Audit" group on the WO form (Time & Cost tab)
PHASE 2 — Presence-aware Manager Desk
=====================================
Manager Desk now knows who's clocked in. Works with vanilla
hr_attendance and fusion_clock — both expose hr.attendance with an
open record while the operator is on shift.
- bridge_mrp depends on hr_attendance
- hr.employee.x_fc_is_clocked_in computed field (batched query — one
DB hit for the whole employee set, not N+1)
- hr.employee._fp_clocked_in_user_ids() classmethod for the dashboard
- manager_controller sends operators with is_clocked_in / role_ids /
lead_hand_role_ids per worker, plus presence dict {clocked_in: N,
total: M}; each WO carries role_id/role_name so the dropdown can
match qualified operators
Manager Desk OWL:
- Header gets a "Present 7 / 12" pill chip; tap to toggle hideOffShift
(off-shift hidden when active, accent colour when filter is on)
- New operatorsForWO(wo) helper sorts dropdown options into 4 buckets:
qualified+clocked-in → lead-hand+clocked-in → clocked-in untrained
(training mode) → off-shift (greyed; only shown when hideOffShift
is false). Each option carries a ●/○ dot prefix and a soft suffix.
PHASE 3 — Skills, lead-hand-per-role, auto-promotion
====================================================
The team grows organically: managers assign training tasks, operators
finish them, the system auto-promotes after N successful runs.
- fp.work.role.mastery_required (integer, default reads from the
company-level Default Mastery Threshold). Each role can override —
masking might need 1 success, electroless nickel 5.
- res.company.x_fc_default_mastery_threshold + res.config.settings
exposure under "Workforce Settings" in the Fusion Plating settings
block (default 3)
- hr.employee.x_fc_lead_hand_role_ids m2m, separate from
x_fc_work_role_ids — Sarah can be a lead hand for masking + racking
even if those aren't her primary roles. Manager-only group access.
- New fp.operator.proficiency model (one row per employee+role) with
completed_count, first/last_completed_at, promoted, promoted_at,
progress_label compute. SQL-unique on (employee, role).
- mrp.workorder.button_finish increments the (employee, role)
counter, then if count >= role.mastery_required AND not promoted,
adds the role to x_fc_work_role_ids and posts a "🎉 Promoted"
chatter line on the employee record. Wrapped in try/except so a
tracker glitch never blocks production.
- Promotion uses the WO's assigned_user_id, NOT env.user — credit
goes to the operator who was supposed to do it, even if a manager
finished on their behalf.
Employee form gets a "Shop Roles" tab (supervisor+):
- "Tasks This Operator Can Do" m2m
- "Lead Hand For" m2m (manager-only)
- Read-only Task Proficiency list with progress / promotion badges
Verified on odoo-entech: all fields land, default threshold = 3,
asset bundle regenerated as 9f38f05.
Module bumps: fusion_plating 19.0.4.0.0,
fusion_plating_bridge_mrp 19.0.4.0.0,
fusion_plating_shopfloor 19.0.11.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
@@ -13,6 +13,12 @@ class HrEmployee(models.Model):
|
||||
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'
|
||||
|
||||
@@ -20,5 +26,94 @@ class HrEmployee(models.Model):
|
||||
'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.',
|
||||
'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, operator, value):
|
||||
"""Lets `[('x_fc_is_clocked_in', '=', True)]` work as a domain."""
|
||||
Att = self.env.get('hr.attendance')
|
||||
if Att is None:
|
||||
return [('id', '=', False)]
|
||||
open_ids = Att.sudo().search([
|
||||
('check_out', '=', False),
|
||||
]).mapped('employee_id').ids
|
||||
if operator in ('=', '!='):
|
||||
wanted = bool(value)
|
||||
if operator == '!=':
|
||||
wanted = not wanted
|
||||
return [('id', 'in' if wanted else 'not in', open_ids)]
|
||||
return []
|
||||
|
||||
@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)
|
||||
|
||||
Reference in New Issue
Block a user