Files
Odoo-Modules/fusion_plating/fusion_plating/models/fp_rack.py
gsinghpal d9ae45ce9b feat(sub12b): extend fusion.plating.rack — racking_state + tags + capacity
The existing 'state' field tracks wear (active/needs_strip/stripping/
retired). Sub 12b adds an orthogonal 'racking_state' (empty/loading/
loaded/in_use/awaiting_unrack/out_of_service) for the load lifecycle
— a rack can be wear-active AND racking-loaded simultaneously.

New fields:
  racking_state        — operational lifecycle
  tag_ids (M2M)        — fp.rack.tag chips on plant overview
  capacity_count       — soft warn (distinct from existing 'capacity')
  current_job_step_id  — compute, derived from latest fp.job.step.move
  current_tank_id      — compute
  current_part_count   — compute

Form view picks up the new fields under Sub-12b group + a 'Current
Use' panel that hides when racking_state is empty / out_of_service.

The compute references fp.job.step.move which lands in Task 4 — the
module won't load cleanly on entech until Tasks 4-5 ship; that's
expected for batch deployment at the end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:03:09 -04:00

182 lines
6.9 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, _
class FpRack(models.Model):
"""Plating rack / barrel / fixture.
Racks carry parts through baths and accumulate nickel themselves over
time. Once the rack's metal turnover (MTO) count exceeds the strip
interval, the rack must be stripped before re-use to avoid bald spots
on parts.
"""
_name = 'fusion.plating.rack'
_description = 'Fusion Plating — Rack / Fixture'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, rack_type, name'
name = fields.Char(string='Rack ID', required=True, tracking=True)
rack_type = fields.Selection(
[('rack', 'Rack'), ('barrel', 'Barrel'),
('fixture', 'Fixture'), ('basket', 'Basket')],
string='Type', required=True, default='rack',
)
facility_id = fields.Many2one(
'fusion.plating.facility', string='Facility', required=True, tracking=True,
)
company_id = fields.Many2one(
'res.company', related='facility_id.company_id', store=True, readonly=True,
)
capacity = fields.Integer(
string='Capacity (parts)',
help='Max parts per load. Used for batch planning.',
)
contact_points = fields.Integer(
string='Contact Points',
help='Number of clips/tips that touch parts. Wear points for re-stripping.',
)
# --- Wear tracking ---
mto_count = fields.Float(
string='MTO (current)', default=0.0, tracking=True,
help='Metal turnover accumulated since last strip.',
)
strip_interval_mto = fields.Float(
string='Strip After (MTO)', default=3.0,
help='When MTO crosses this value, rack needs stripping.',
)
last_stripped_date = fields.Datetime(string='Last Stripped', tracking=True)
last_stripped_by_id = fields.Many2one(
'res.users', string='Stripped By', tracking=True,
)
strips_count = fields.Integer(string='Total Strips', default=0, readonly=True)
state = fields.Selection(
[('active', 'Active'),
('needs_strip', 'Needs Strip'),
('stripping', 'Stripping'),
('retired', 'Retired')],
string='Status', default='active', required=True, tracking=True,
compute='_compute_state', store=True, readonly=False,
)
status_color = fields.Integer(compute='_compute_status_color')
notes = fields.Html(string='Notes')
active = fields.Boolean(default=True)
_sql_constraints = [
('fp_rack_facility_name_uniq', 'unique(facility_id, name)',
'Rack ID must be unique per facility.'),
]
# ------------------------------------------------------------------
# Computes
# ------------------------------------------------------------------
@api.depends('mto_count', 'strip_interval_mto')
def _compute_state(self):
for rec in self:
if rec.state in ('stripping', 'retired'):
continue # Manually set — don't override
if rec.strip_interval_mto and rec.mto_count >= rec.strip_interval_mto:
rec.state = 'needs_strip'
elif rec.state != 'active':
rec.state = 'active'
@api.depends('state')
def _compute_status_color(self):
mapping = {'active': 4, 'needs_strip': 3, 'stripping': 2, 'retired': 10}
for rec in self:
rec.status_color = mapping.get(rec.state, 0)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_start_strip(self):
self.write({'state': 'stripping'})
def action_mark_stripped(self):
for rec in self:
rec.write({
'state': 'active',
'mto_count': 0.0,
'last_stripped_date': fields.Datetime.now(),
'last_stripped_by_id': self.env.user.id,
'strips_count': rec.strips_count + 1,
})
rec.message_post(body=_('Rack stripped and returned to service.'))
def action_retire(self):
self.write({'state': 'retired', 'active': False})
def _increment_mto(self, delta=1.0):
"""Add `delta` to the rack's MTO count. Called by the WO finish hook."""
for rec in self:
rec.mto_count = (rec.mto_count or 0.0) + delta
# ===== Sub 12b — racking lifecycle (orthogonal to wear-tracking state) =
racking_state = fields.Selection(
[
('empty', 'Empty'),
('loading', 'Loading'),
('loaded', 'Loaded'),
('in_use', 'In Use'),
('awaiting_unrack', 'Awaiting Unrack'),
('out_of_service', 'Out of Service'),
],
string='Racking State', default='empty', tracking=True,
help='Operational state in the rack→step→tank flow. Distinct '
'from the wear-tracking `state` (active/needs_strip/...).',
)
tag_ids = fields.Many2many(
'fp.rack.tag',
'fp_rack_tag_rel', 'rack_id', 'tag_id',
string='Tags',
)
capacity_count = fields.Integer(
string='Capacity (parts) — soft warn',
help='Soft warning threshold — runtime informs operator when '
'rack is loaded beyond this. Not enforced. Distinct from '
'`capacity` field (planning capacity).',
)
current_job_step_id = fields.Many2one(
'fp.job.step', string='Current Step',
compute='_compute_current_use', store=True,
)
current_tank_id = fields.Many2one(
'fusion.plating.tank', string='Current Tank',
compute='_compute_current_use', store=True,
)
current_part_count = fields.Integer(
string='Parts on Rack',
compute='_compute_current_use', store=True,
)
@api.depends('racking_state')
def _compute_current_use(self):
# Walks the most recent fp.job.step.move row per rack to derive
# current step + tank + part count. For racks not currently in
# use, all values are blank.
Move = self.env['fp.job.step.move']
for rack in self:
if rack.racking_state in ('empty', 'out_of_service'):
rack.current_job_step_id = False
rack.current_tank_id = False
rack.current_part_count = 0
continue
recent = Move.search(
[('rack_id', '=', rack.id)],
order='move_datetime desc',
limit=1,
)
rack.current_job_step_id = recent.to_step_id if recent else False
# current_tank_id pulls from the destination step's tank if set
rack.current_tank_id = (
recent.to_tank_id or
(recent.to_step_id.tank_id if recent and recent.to_step_id else False)
) if recent else False
rack.current_part_count = recent.qty_moved if recent else 0