chore(plating): retire fusion_plating_culture — not a priority
Culture/values/recognitions framework was shipping zero data and zero workflow integration for this client. It's a people-ops concern (peer kudos, "Fundamental of the Week" rotations) with no overlap with the technical plating pipeline — no interaction with process recipes, quality holds, sensors, or compliance. Verified zero data on entech before uninstalling: fusion.plating.value 0 records fusion.plating.value.set 0 fusion.plating.value.recognition 0 fusion.plating.value.rotation 0 Clean uninstall on entech, module dir removed from disk. The Culture top-level menu disappears. If a future client wants it back, the module is easy to re-author — nothing we built on top of it depends on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import fp_value_set
|
||||
from . import fp_value
|
||||
from . import fp_value_rotation
|
||||
from . import fp_value_recognition
|
||||
from . import hr_employee
|
||||
@@ -1,100 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class FpValue(models.Model):
|
||||
"""A single value, fundamental, core behaviour, or belief.
|
||||
|
||||
Values are grouped under a fusion.plating.value.set. Each value has a
|
||||
short title, an optional display number (e.g. "Fundamental #3"), a
|
||||
long-form description, stories / examples of living the value, and an
|
||||
icon for display in kanban tiles.
|
||||
"""
|
||||
_name = 'fusion.plating.value'
|
||||
_description = 'Fusion Plating — Value'
|
||||
_order = 'set_id, sequence, number, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Title',
|
||||
required=True,
|
||||
help='Short, memorable title for the value, e.g. '
|
||||
'"Do What\'s Best for the Customer".',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
help='Display order within the set.',
|
||||
)
|
||||
number = fields.Integer(
|
||||
string='Number',
|
||||
help='Optional display number (e.g. 1, 2, 3). Shows on kanban '
|
||||
'tiles and in rotation schedules.',
|
||||
)
|
||||
set_id = fields.Many2one(
|
||||
'fusion.plating.value.set',
|
||||
string='Value Set',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
related='set_id.company_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
description = fields.Html(
|
||||
string='Description',
|
||||
help='Long-form explanation of what this value means and how it '
|
||||
'shows up in daily work.',
|
||||
)
|
||||
examples = fields.Html(
|
||||
string='Stories & Examples',
|
||||
help='Stories, anecdotes, and concrete examples of people living '
|
||||
'this value.',
|
||||
)
|
||||
icon = fields.Char(
|
||||
string='Icon',
|
||||
default='fa-star',
|
||||
help='Font Awesome icon class (without the "fa " prefix), '
|
||||
'e.g. "fa-heart", "fa-bolt".',
|
||||
)
|
||||
color = fields.Integer(
|
||||
string='Color Index',
|
||||
default=0,
|
||||
help='Kanban colour index (0-11).',
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
recognition_ids = fields.One2many(
|
||||
'fusion.plating.value.recognition',
|
||||
'value_id',
|
||||
string='Recognitions',
|
||||
)
|
||||
recognition_count = fields.Integer(
|
||||
string='Recognition Count',
|
||||
compute='_compute_recognition_count',
|
||||
)
|
||||
|
||||
@api.depends('recognition_ids', 'recognition_ids.state')
|
||||
def _compute_recognition_count(self):
|
||||
for rec in self:
|
||||
rec.recognition_count = len(rec.recognition_ids.filtered(
|
||||
lambda r: r.state == 'published'
|
||||
))
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for rec in self:
|
||||
if rec.number:
|
||||
result.append((rec.id, f'#{rec.number} — {rec.name}'))
|
||||
else:
|
||||
result.append((rec.id, rec.name))
|
||||
return result
|
||||
@@ -1,120 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class FpValueRecognition(models.Model):
|
||||
"""A peer recognition tied to a specific value.
|
||||
|
||||
Example: "I saw Sarah living Value #3 yesterday when she stayed late
|
||||
to re-mask a customer part after first-article inspection flagged a
|
||||
nick."
|
||||
|
||||
Recognitions start in draft, move to published, and can be archived
|
||||
when they roll off a stand-up wall or a newsletter.
|
||||
"""
|
||||
_name = 'fusion.plating.value.recognition'
|
||||
_description = 'Fusion Plating — Value Recognition'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'nomination_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Title',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Short headline for the recognition, e.g. '
|
||||
'"Sarah stayed late for the customer".',
|
||||
default=lambda self: _('New Recognition'),
|
||||
)
|
||||
reference = fields.Char(
|
||||
string='Reference',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
value_id = fields.Many2one(
|
||||
'fusion.plating.value',
|
||||
string='Value',
|
||||
required=True,
|
||||
tracking=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
set_id = fields.Many2one(
|
||||
'fusion.plating.value.set',
|
||||
string='Value Set',
|
||||
related='value_id.set_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
recognized_employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Recognized',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='The person being recognized.',
|
||||
)
|
||||
nominated_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Nominated By',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
nomination_date = fields.Datetime(
|
||||
string='Nominated On',
|
||||
default=fields.Datetime.now,
|
||||
tracking=True,
|
||||
)
|
||||
story = fields.Html(
|
||||
string='Story',
|
||||
help='What happened? Describe the moment you saw the value being '
|
||||
'lived.',
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('draft', 'Draft'),
|
||||
('published', 'Published'),
|
||||
('archived', 'Archived'),
|
||||
],
|
||||
string='Status',
|
||||
default='draft',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
color = fields.Integer(
|
||||
string='Color Index',
|
||||
default=0,
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('reference') or vals['reference'] == _('New'):
|
||||
vals['reference'] = self.env['ir.sequence'].next_by_code(
|
||||
'fusion.plating.value.recognition'
|
||||
) or _('New')
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_publish(self):
|
||||
for rec in self:
|
||||
rec.state = 'published'
|
||||
rec.message_post(body=_('Recognition published.'))
|
||||
return True
|
||||
|
||||
def action_archive_recognition(self):
|
||||
for rec in self:
|
||||
rec.state = 'archived'
|
||||
rec.message_post(body=_('Recognition archived.'))
|
||||
return True
|
||||
|
||||
def action_reset_to_draft(self):
|
||||
for rec in self:
|
||||
rec.state = 'draft'
|
||||
return True
|
||||
@@ -1,149 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class FpValueRotation(models.Model):
|
||||
"""A schedule that rotates a value to the front of the shop's attention.
|
||||
|
||||
Used to drive "Fundamental of the Day/Week/Month" programs. The admin
|
||||
creates a rotation, picks a set and a period, and enables it. This
|
||||
module does NOT ship an ir.cron — the shop admin wires a cron to call
|
||||
`_cron_advance_rotation` if they want automatic advancement. Many
|
||||
shops prefer to advance the rotation manually at a weekly stand-up.
|
||||
"""
|
||||
_name = 'fusion.plating.value.rotation'
|
||||
_description = 'Fusion Plating — Value Rotation Schedule'
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Display name of the rotation, e.g. "Weekly Fundamental".',
|
||||
)
|
||||
set_id = fields.Many2one(
|
||||
'fusion.plating.value.set',
|
||||
string='Value Set',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
tracking=True,
|
||||
)
|
||||
rotation_period = fields.Selection(
|
||||
[
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
('monthly', 'Monthly'),
|
||||
],
|
||||
string='Rotation Period',
|
||||
required=True,
|
||||
default='weekly',
|
||||
tracking=True,
|
||||
)
|
||||
current_value_id = fields.Many2one(
|
||||
'fusion.plating.value',
|
||||
string='Current Value',
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
help='The value currently surfaced by this rotation. Updated by '
|
||||
'_cron_advance_rotation.',
|
||||
domain="[('set_id', '=', set_id)]",
|
||||
)
|
||||
last_rotation_date = fields.Date(
|
||||
string='Last Rotated On',
|
||||
readonly=True,
|
||||
)
|
||||
next_rotation_date = fields.Date(
|
||||
string='Next Rotation',
|
||||
compute='_compute_next_rotation_date',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
related='set_id.company_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
|
||||
@api.depends('last_rotation_date', 'rotation_period')
|
||||
def _compute_next_rotation_date(self):
|
||||
for rec in self:
|
||||
if not rec.last_rotation_date:
|
||||
rec.next_rotation_date = fields.Date.context_today(rec)
|
||||
continue
|
||||
delta_days = {
|
||||
'daily': 1,
|
||||
'weekly': 7,
|
||||
'monthly': 30,
|
||||
}.get(rec.rotation_period, 7)
|
||||
rec.next_rotation_date = rec.last_rotation_date + timedelta(
|
||||
days=delta_days
|
||||
)
|
||||
|
||||
def action_advance(self):
|
||||
"""Manually advance the rotation to the next value.
|
||||
|
||||
Wired to a form button so a shop that doesn't want cron-driven
|
||||
rotation can move the fundamental forward at a stand-up meeting.
|
||||
"""
|
||||
for rec in self:
|
||||
rec._advance_one()
|
||||
return True
|
||||
|
||||
def _advance_one(self):
|
||||
"""Move this rotation to the next value in the set, wrapping."""
|
||||
self.ensure_one()
|
||||
values = self.set_id.value_ids.filtered('active').sorted(
|
||||
key=lambda v: (v.sequence, v.number, v.id)
|
||||
)
|
||||
if not values:
|
||||
return False
|
||||
if not self.current_value_id:
|
||||
next_value = values[0]
|
||||
else:
|
||||
try:
|
||||
idx = list(values).index(self.current_value_id)
|
||||
next_idx = (idx + 1) % len(values)
|
||||
next_value = values[next_idx]
|
||||
except ValueError:
|
||||
next_value = values[0]
|
||||
self.write({
|
||||
'current_value_id': next_value.id,
|
||||
'last_rotation_date': fields.Date.context_today(self),
|
||||
})
|
||||
self.message_post(
|
||||
body=_('Rotation advanced to %(value)s.') % {
|
||||
'value': next_value.display_name,
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _cron_advance_rotation(self):
|
||||
"""Cron entry point.
|
||||
|
||||
A shop admin can wire an ir.cron to this method. The module
|
||||
intentionally does not ship one — different shops want different
|
||||
cadences. Only advances rotations whose next_rotation_date is
|
||||
today or earlier.
|
||||
"""
|
||||
today = fields.Date.context_today(self)
|
||||
rotations = self.search([('active', '=', True)])
|
||||
for rotation in rotations:
|
||||
if (
|
||||
not rotation.last_rotation_date
|
||||
or rotation.next_rotation_date
|
||||
and rotation.next_rotation_date <= today
|
||||
):
|
||||
rotation._advance_one()
|
||||
return True
|
||||
@@ -1,111 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class FpValueSet(models.Model):
|
||||
"""A named collection of values owned by a company.
|
||||
|
||||
Each shop loads its own set. One shop might call theirs
|
||||
"Entech Fundamentals" with 24 items, another "Our Way" with 10. A shop
|
||||
may have several sets (historical, draft, archived) but usually only
|
||||
one is marked primary.
|
||||
|
||||
The module ships NO seed data — this is a generic framework.
|
||||
"""
|
||||
_name = 'fusion.plating.value.set'
|
||||
_description = 'Fusion Plating — Value Set'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'is_primary desc, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Display name of the set, e.g. "Entech Fundamentals", "Our Way".',
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Unique slug identifier, e.g. "entech_fundamentals".',
|
||||
)
|
||||
description = fields.Html(
|
||||
string='Description',
|
||||
help='Long-form description of what this set represents and how '
|
||||
'the shop uses it.',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
tracking=True,
|
||||
)
|
||||
value_ids = fields.One2many(
|
||||
'fusion.plating.value',
|
||||
'set_id',
|
||||
string='Values',
|
||||
)
|
||||
value_count = fields.Integer(
|
||||
string='Value Count',
|
||||
compute='_compute_value_count',
|
||||
)
|
||||
is_primary = fields.Boolean(
|
||||
string='Primary Set',
|
||||
default=False,
|
||||
tracking=True,
|
||||
help='Marks this as the shop\'s primary culture set. Only one set '
|
||||
'per company should be primary at a time.',
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_value_set_code_company_uniq',
|
||||
'unique(code, company_id)',
|
||||
'Value set code must be unique within a company.',
|
||||
),
|
||||
]
|
||||
|
||||
@api.depends('value_ids')
|
||||
def _compute_value_count(self):
|
||||
for rec in self:
|
||||
rec.value_count = len(rec.value_ids)
|
||||
|
||||
def write(self, vals):
|
||||
"""When flipping a set to primary, demote any other primary set in
|
||||
the same company so there is only one primary per company.
|
||||
"""
|
||||
res = super().write(vals)
|
||||
if vals.get('is_primary'):
|
||||
for rec in self:
|
||||
if rec.is_primary:
|
||||
others = self.search([
|
||||
('id', '!=', rec.id),
|
||||
('company_id', '=', rec.company_id.id),
|
||||
('is_primary', '=', True),
|
||||
])
|
||||
if others:
|
||||
others.write({'is_primary': False})
|
||||
return res
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for rec in recs:
|
||||
if rec.is_primary:
|
||||
others = self.search([
|
||||
('id', '!=', rec.id),
|
||||
('company_id', '=', rec.company_id.id),
|
||||
('is_primary', '=', True),
|
||||
])
|
||||
if others:
|
||||
others.write({'is_primary': False})
|
||||
return recs
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
"""Extend hr.employee with culture recognition totals.
|
||||
|
||||
Uses the x_fc_ prefix per the Fusion Central field-naming convention
|
||||
for base Odoo model extensions.
|
||||
"""
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
x_fc_recognition_ids = fields.One2many(
|
||||
'fusion.plating.value.recognition',
|
||||
'recognized_employee_id',
|
||||
string='Recognitions',
|
||||
)
|
||||
x_fc_recognition_count = fields.Integer(
|
||||
string='Recognition Count',
|
||||
compute='_compute_x_fc_recognition_stats',
|
||||
)
|
||||
x_fc_last_recognized_date = fields.Datetime(
|
||||
string='Last Recognized',
|
||||
compute='_compute_x_fc_recognition_stats',
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'x_fc_recognition_ids',
|
||||
'x_fc_recognition_ids.state',
|
||||
'x_fc_recognition_ids.nomination_date',
|
||||
)
|
||||
def _compute_x_fc_recognition_stats(self):
|
||||
for rec in self:
|
||||
published = rec.x_fc_recognition_ids.filtered(
|
||||
lambda r: r.state == 'published'
|
||||
)
|
||||
rec.x_fc_recognition_count = len(published)
|
||||
if published:
|
||||
rec.x_fc_last_recognized_date = max(
|
||||
published.mapped('nomination_date')
|
||||
)
|
||||
else:
|
||||
rec.x_fc_last_recognized_date = False
|
||||
Reference in New Issue
Block a user