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>
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
def _fp_tz_get(self):
|
|
"""Same selection list Odoo uses on res.partner.tz."""
|
|
import pytz
|
|
return [(tz, tz) for tz in pytz.all_timezones]
|
|
|
|
|
|
class ResCompany(models.Model):
|
|
_inherit = 'res.company'
|
|
|
|
# ----- Fusion Plating default timezone --------------------------------
|
|
# Used as the fallback whenever a user's res.users.tz is empty (cron
|
|
# jobs, batch emails, headless contexts). Auto-populated by the
|
|
# post_init_hook in fusion_plating/__init__.py.
|
|
x_fc_default_tz = fields.Selection(
|
|
_fp_tz_get,
|
|
string='Fusion Plating Timezone',
|
|
default=lambda self: self.env.user.tz or 'America/Toronto',
|
|
help='Timezone used for plating dashboards, reports, and emails when '
|
|
'a user has no personal timezone set. Detected automatically on '
|
|
'install; the admin can change it any time from '
|
|
'Settings > Fusion Plating.',
|
|
)
|
|
|
|
# ----- Worker auto-promotion default -----------------------------------
|
|
# Default number of successful WO completions a worker needs on a role
|
|
# before it's auto-added to their Shop Roles. Each role can override
|
|
# via fp.work.role.mastery_required.
|
|
x_fc_default_mastery_threshold = fields.Integer(
|
|
string='Default Mastery Threshold',
|
|
default=3,
|
|
help='How many successful WO completions an operator needs on a '
|
|
"task before it's added to their Shop Roles automatically. "
|
|
'New roles inherit this number; managers can override per '
|
|
'role on the role form. 1 = promote on first success; 3 = '
|
|
'solid baseline; 5+ for tasks that need real practice.',
|
|
)
|
|
|
|
# ----- Facility footprint for this legal entity ----------------------
|
|
x_fc_facility_ids = fields.One2many(
|
|
'fusion.plating.facility',
|
|
'company_id',
|
|
string='Plating Facilities',
|
|
)
|
|
x_fc_facility_count = fields.Integer(
|
|
string='# Facilities',
|
|
compute='_compute_x_fc_facility_count',
|
|
)
|
|
x_fc_default_facility_id = fields.Many2one(
|
|
'fusion.plating.facility',
|
|
string='Default Facility',
|
|
help='Facility used when the context does not specify one (single-site shops).',
|
|
)
|
|
|
|
def _compute_x_fc_facility_count(self):
|
|
for rec in self:
|
|
rec.x_fc_facility_count = len(rec.x_fc_facility_ids)
|
|
|
|
# =====================================================================
|
|
# CoC / Certificate report settings
|
|
# =====================================================================
|
|
x_fc_owner_user_id = fields.Many2one(
|
|
'res.users',
|
|
string='Certificate Owner (Default Signer)',
|
|
help='Quality manager / owner whose signature appears on Certificates '
|
|
'of Conformance by default. Signature is pulled from their linked '
|
|
'HR Employee record.',
|
|
)
|
|
x_fc_coc_signature_override = fields.Binary(
|
|
string='Signature Override Image',
|
|
help='Optional. Upload a pre-scanned signature image to use on '
|
|
'Certificates of Conformance. Overrides the Owner user\'s '
|
|
'employee signature when set. Useful if the owner doesn\'t have '
|
|
'an HR record or wants a different signature for plating certs.',
|
|
)
|
|
|
|
# --- Accreditation logos shown in CoC header ---
|
|
x_fc_nadcap_logo = fields.Binary(string='Nadcap Logo')
|
|
x_fc_nadcap_active = fields.Boolean(
|
|
string='Nadcap Accredited',
|
|
help='Show the Nadcap logo on certificates.',
|
|
)
|
|
x_fc_as9100_logo = fields.Binary(string='AS9100 / ISO 9001 Logo')
|
|
x_fc_as9100_active = fields.Boolean(
|
|
string='AS9100 / ISO 9001 Certified',
|
|
help='Show the AS9100 / ISO 9001 logo on certificates.',
|
|
)
|
|
x_fc_cgp_logo = fields.Binary(string='Controlled Goods Program Logo')
|
|
x_fc_cgp_active = fields.Boolean(
|
|
string='CGP Registered',
|
|
help='Show the Controlled Goods Program logo on certificates.',
|
|
)
|