Computes for the Manager At-Risk heatmap (Phase 4 tablet redesign). Non-stored — recomputed on /fp/manager/at_risk read; that endpoint caches its full payload for 60s so the cost is bounded. bottleneck_score = active_step_count * avg_wait_minutes avg_wait_minutes = rolling-7-day avg of (date_started - create_date) Work centres with high score show red in the heatmap — combination of queue length AND average wait time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
#
|
|
# fp.work.centre — native plating work-centre model.
|
|
#
|
|
# Replaces mrp.workcenter for the plating flow. Plating work centres
|
|
# are domain-specific (a tank line, a bake oven, a rack station — not
|
|
# assembly cells). Each centre has a 'kind' that drives release-ready
|
|
# validation on fp.job.step (e.g. wet_line -> bath+tank required).
|
|
|
|
from odoo import fields, models
|
|
|
|
|
|
class FpWorkCentre(models.Model):
|
|
"""Routing station for a job step — replaces mrp.workcenter for
|
|
plating after the Sub 11 MRP cutout.
|
|
|
|
Each routing station has a `kind` (wet_line / bake / mask / rack /
|
|
inspect / other) that drives release-ready validation on
|
|
`fp.job.step` (e.g. wet_line requires bath+tank to be set before
|
|
the step can start). Costable via `cost_per_hour`.
|
|
|
|
Distinct from `fusion.plating.work.center` (Production Line),
|
|
which is the physical shop-layout grouping that owns tanks.
|
|
A Production Line typically contains many Routing Stations.
|
|
"""
|
|
_name = 'fp.work.centre'
|
|
_description = 'Plating Routing Station'
|
|
_order = 'sequence, code, name'
|
|
|
|
name = fields.Char(string='Routing Station', required=True)
|
|
code = fields.Char(required=True, help='Short code used on stickers and reports.')
|
|
sequence = fields.Integer(default=10)
|
|
facility_id = fields.Many2one(
|
|
'fusion.plating.facility',
|
|
string='Facility',
|
|
)
|
|
kind = fields.Selection(
|
|
[
|
|
('wet_line', 'Wet Line'),
|
|
('bake', 'Bake Oven'),
|
|
('mask', 'Masking'),
|
|
('rack', 'Racking'),
|
|
('inspect', 'Inspection'),
|
|
('other', 'Other'),
|
|
],
|
|
required=True,
|
|
default='other',
|
|
)
|
|
cost_per_hour = fields.Monetary(
|
|
currency_field='currency_id',
|
|
help='Used for fp.job.step cost rollups.',
|
|
)
|
|
currency_id = fields.Many2one(
|
|
'res.currency',
|
|
default=lambda self: self.env.company.currency_id,
|
|
)
|
|
default_bath_id = fields.Many2one('fusion.plating.bath')
|
|
default_tank_id = fields.Many2one('fusion.plating.tank')
|
|
# NOTE: `default_oven_id` from the spec/plan is omitted here — the
|
|
# `fusion.plating.bake.oven` model lives in fusion_plating_shopfloor,
|
|
# which the core module cannot depend on. The bridge module that
|
|
# introduces fp.job/fp.job.step (Task 1.x) can re-introduce this
|
|
# field via _inherit if/when the bake-oven coupling is needed.
|
|
active = fields.Boolean(default=True)
|
|
|
|
# Phase 4 tablet redesign — Manager At-Risk heatmap inputs.
|
|
# Non-stored (recomputed on every read by /fp/manager/at_risk; the
|
|
# endpoint caches the payload for 60s anyway so the cost is bounded).
|
|
bottleneck_score = fields.Float(
|
|
compute='_compute_bottleneck',
|
|
string='Bottleneck Score',
|
|
help='active_step_count * avg_wait_minutes (rolling 7-day). '
|
|
'Drives the Manager At-Risk heatmap — work centres with '
|
|
'high score have queue + wait pressure.',
|
|
)
|
|
avg_wait_minutes = fields.Float(
|
|
compute='_compute_bottleneck',
|
|
string='Avg Wait (min)',
|
|
help='Average minutes that steps at this work centre waited in '
|
|
'ready state before starting, over the last 7 days.',
|
|
)
|
|
|
|
def _compute_bottleneck(self):
|
|
from datetime import timedelta
|
|
Step = self.env['fp.job.step']
|
|
now = fields.Datetime.now()
|
|
seven_days_ago = now - timedelta(days=7)
|
|
for wc in self:
|
|
active_n = Step.search_count([
|
|
('work_centre_id', '=', wc.id),
|
|
('state', 'in', ('ready', 'in_progress')),
|
|
])
|
|
# Avg wait: recent steps where date_started is set; approximate
|
|
# "ready since" as create_date when no explicit ready timestamp
|
|
# is recorded. Bounded set (last 7 days) keeps the search cheap.
|
|
recent = Step.search([
|
|
('work_centre_id', '=', wc.id),
|
|
('date_started', '>=', seven_days_ago),
|
|
('date_started', '!=', False),
|
|
])
|
|
waits = []
|
|
for s in recent:
|
|
if s.create_date and s.date_started:
|
|
waits.append(
|
|
(s.date_started - s.create_date).total_seconds() / 60.0
|
|
)
|
|
avg = (sum(waits) / len(waits)) if waits else 0.0
|
|
wc.avg_wait_minutes = avg
|
|
wc.bottleneck_score = active_n * avg
|
|
|
|
_sql_constraints = [
|
|
('unique_code', 'UNIQUE(code)', 'Work centre code must be unique.'),
|
|
]
|