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:
@@ -115,6 +115,17 @@ class FpManagerDashboardController(http.Controller):
|
||||
w.x_fc_assigned_user_id.name or ''
|
||||
if w.x_fc_assigned_user_id else ''
|
||||
),
|
||||
# Role required by this step. Used by the
|
||||
# Manager Desk worker dropdown to surface
|
||||
# qualified operators first.
|
||||
'role_id': (
|
||||
w.x_fc_work_role_id.id
|
||||
if w.x_fc_work_role_id else False
|
||||
),
|
||||
'role_name': (
|
||||
w.x_fc_work_role_id.name or ''
|
||||
if w.x_fc_work_role_id else ''
|
||||
),
|
||||
}
|
||||
for w in wos
|
||||
],
|
||||
@@ -161,11 +172,43 @@ class FpManagerDashboardController(http.Controller):
|
||||
'avatar_url': f'/web/image/res.users/{user.id}/avatar_128',
|
||||
})
|
||||
|
||||
# ---- Pickers: operators, tanks, work centres ------------------
|
||||
operators = [
|
||||
{'id': u.id, 'name': u.name}
|
||||
for u in (operator_group.user_ids if operator_group else env['res.users'])
|
||||
]
|
||||
# ---- Pickers: operators (with presence + role data) -----------
|
||||
# We send richer operator records so the Manager Desk dropdown can
|
||||
# group qualified-and-present at the top, then lead hands, then
|
||||
# off-shift workers (greyed). Without this the manager has to
|
||||
# remember who's clocked in and who can do what.
|
||||
clocked_in_user_ids = (
|
||||
env['hr.employee']._fp_clocked_in_user_ids()
|
||||
if 'hr.employee' in env and hasattr(
|
||||
env['hr.employee'], '_fp_clocked_in_user_ids',
|
||||
)
|
||||
else set()
|
||||
)
|
||||
operator_users = (
|
||||
operator_group.user_ids if operator_group else env['res.users']
|
||||
)
|
||||
operators = []
|
||||
for u in operator_users:
|
||||
emp = u.employee_id
|
||||
role_ids = emp.x_fc_work_role_ids.ids if emp else []
|
||||
lead_role_ids = (
|
||||
emp.x_fc_lead_hand_role_ids.ids
|
||||
if emp and 'x_fc_lead_hand_role_ids' in emp._fields
|
||||
else []
|
||||
)
|
||||
operators.append({
|
||||
'id': u.id,
|
||||
'name': u.name,
|
||||
'is_clocked_in': u.id in clocked_in_user_ids,
|
||||
'role_ids': role_ids,
|
||||
'lead_hand_role_ids': lead_role_ids,
|
||||
})
|
||||
# Headline counts so the manager sees at-a-glance who's on shift.
|
||||
present_count = sum(1 for o in operators if o['is_clocked_in'])
|
||||
presence = {
|
||||
'clocked_in': present_count,
|
||||
'total': len(operators),
|
||||
}
|
||||
Tank = env.get('fusion.plating.tank')
|
||||
tanks = [
|
||||
{
|
||||
@@ -214,6 +257,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
'active': active_cards,
|
||||
'team': team,
|
||||
'operators': operators,
|
||||
'presence': presence,
|
||||
'tanks': tanks,
|
||||
'user_name': env.user.name,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user