changes
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.10.30.0',
|
||||
'version': '19.0.10.31.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
Add "All Jobs" and "Steps" as children of fusion_plating_shopfloor's
|
||||
Add "Work Orders" and "Steps" as children of fusion_plating_shopfloor's
|
||||
Shop Floor menu. We can reference shopfloor's xmlid here because
|
||||
fusion_plating_jobs declares it as a depend.
|
||||
|
||||
Sequences fit between Tablet Station (10) and Bake Windows (20)
|
||||
in shopfloor's existing fp_menu.xml.
|
||||
|
||||
Renamed 2026-05-25 — "All Jobs" → "Work Orders" for consistency
|
||||
with the rest of the UI (SO smart button is "WO", tablet cards
|
||||
show "WO # 00001", KPI tile says "Work Orders"). xmlid kept
|
||||
as menu_fp_jobs_all_jobs so bookmarks and inherits don't break.
|
||||
-->
|
||||
|
||||
<menuitem id="menu_fp_jobs_all_jobs"
|
||||
name="All Jobs"
|
||||
name="Work Orders"
|
||||
parent="fusion_plating_shopfloor.menu_fp_shopfloor"
|
||||
action="fusion_plating.action_fp_job"
|
||||
sequence="15"/>
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.33.1.13',
|
||||
'version': '19.0.33.2.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||
'first-piece inspection gates.',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||
'description': """
|
||||
Fusion Plating — Shop Floor
|
||||
===========================
|
||||
@@ -53,7 +52,6 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'views/res_users_views.xml',
|
||||
'views/fp_bake_oven_views.xml',
|
||||
'views/fp_bake_window_views.xml',
|
||||
'views/fp_first_piece_gate_views.xml',
|
||||
'views/fp_plant_overview_views.xml',
|
||||
'views/tank_status_template.xml',
|
||||
'views/fp_tablet_session_event_views.xml',
|
||||
|
||||
@@ -641,8 +641,8 @@ class FpShopfloorController(http.Controller):
|
||||
# /fp/landing/kanban. The Tablet Station menu now points at the new
|
||||
# surface. This endpoint stays live as long as the legacy
|
||||
# fp_shopfloor_tablet OWL component is still registered — it consumes
|
||||
# the rich payload (my_queue, active_wo, baths, bake_windows, gates,
|
||||
# holds, pending_qcs, stations). Phase 5 cleanup will retire both the
|
||||
# the rich payload (my_queue, active_wo, baths, bake_windows, holds,
|
||||
# pending_qcs, stations). Phase 5 cleanup will retire both the
|
||||
# legacy component and this endpoint together.
|
||||
@http.route('/fp/shopfloor/tablet_overview', type='jsonrpc', auth='user')
|
||||
def tablet_overview(self, station_id=None, facility_id=None):
|
||||
@@ -673,7 +673,6 @@ class FpShopfloorController(http.Controller):
|
||||
|
||||
Step = env['fp.job.step']
|
||||
BakeWindow = env['fusion.plating.bake.window']
|
||||
Gate = env['fusion.plating.first.piece.gate']
|
||||
Hold = env['fusion.plating.quality.hold']
|
||||
|
||||
def _fac_dom(dom):
|
||||
@@ -704,7 +703,6 @@ class FpShopfloorController(http.Controller):
|
||||
awaiting = BakeWindow.search_count(bake_dom + [('state', '=', 'awaiting_bake')])
|
||||
in_progress_bakes = BakeWindow.search_count(bake_dom + [('state', '=', 'bake_in_progress')])
|
||||
missed = BakeWindow.search_count(bake_dom + [('state', '=', 'missed_window')])
|
||||
pending_gates = Gate.search_count(_fac_dom([('result', '=', 'pending')]))
|
||||
hold_dom = [('state', 'in', ('on_hold', 'under_review'))]
|
||||
if my_job_ids_for_kpi and 'x_fc_job_id' in Hold._fields:
|
||||
hold_dom.append(('x_fc_job_id', 'in', my_job_ids_for_kpi))
|
||||
@@ -715,7 +713,6 @@ class FpShopfloorController(http.Controller):
|
||||
{'label': 'In Progress', 'value': steps_progress, 'tone': 'success', 'icon': 'fa-cogs'},
|
||||
{'label': 'Awaiting Bake', 'value': awaiting, 'tone': 'warning', 'icon': 'fa-fire'},
|
||||
{'label': 'Missed Windows', 'value': missed, 'tone': 'danger' if missed else 'muted', 'icon': 'fa-exclamation-triangle'},
|
||||
{'label': 'First-Piece', 'value': pending_gates, 'tone': 'info', 'icon': 'fa-flag-checkered'},
|
||||
{'label': 'Quality Holds', 'value': open_holds, 'tone': 'danger' if open_holds else 'muted', 'icon': 'fa-pause-circle'},
|
||||
]
|
||||
|
||||
@@ -873,23 +870,6 @@ class FpShopfloorController(http.Controller):
|
||||
for bw in bws
|
||||
]
|
||||
|
||||
# -- First-piece gates -------------------------------------------
|
||||
gate_domain = _fac_dom([('result', 'in', ('pending', 'fail'))])
|
||||
gates = Gate.search(gate_domain, order='first_piece_produced desc', limit=6)
|
||||
gates_data = [
|
||||
{
|
||||
'id': g.id,
|
||||
'name': g.name,
|
||||
'part_ref': g.part_ref or '',
|
||||
'customer': g.customer_ref or '',
|
||||
'bath': g.bath_id.name or '',
|
||||
'result': g.result,
|
||||
'first_piece': fp_format(request.env, g.first_piece_produced),
|
||||
'inspector': g.inspector_id.name or '',
|
||||
}
|
||||
for g in gates
|
||||
]
|
||||
|
||||
# -- Quality holds -----------------------------------------------
|
||||
# v19.0.24.3.0 — scope holds to operator's jobs so Carlos's
|
||||
# sidebar isn't flooded with plant-wide HOLD-XXXX from other
|
||||
@@ -986,7 +966,6 @@ class FpShopfloorController(http.Controller):
|
||||
'active_wo': active_wo,
|
||||
'baths': baths_data,
|
||||
'bake_windows': bw_data,
|
||||
'gates': gates_data,
|
||||
'holds': holds_data,
|
||||
'pending_qcs': pending_qcs,
|
||||
'stations': stations,
|
||||
@@ -1007,26 +986,6 @@ class FpShopfloorController(http.Controller):
|
||||
})
|
||||
return {'ok': True}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Mark a first-piece gate result from the tablet
|
||||
# ----------------------------------------------------------------------
|
||||
@http.route('/fp/shopfloor/mark_gate', type='jsonrpc', auth='user')
|
||||
def mark_gate(self, gate_id, result):
|
||||
env = request.env
|
||||
gate = env['fusion.plating.first.piece.gate'].browse(int(gate_id))
|
||||
if not gate.exists():
|
||||
return {'ok': False, 'error': 'Gate not found.'}
|
||||
try:
|
||||
if result == 'pass':
|
||||
gate.action_mark_pass()
|
||||
elif result == 'fail':
|
||||
gate.action_mark_fail()
|
||||
else:
|
||||
return {'ok': False, 'error': f'Unknown result {result}'}
|
||||
except UserError as e:
|
||||
return {'ok': False, 'error': str(e.args[0]) if e.args else str(e)}
|
||||
return {'ok': True, 'state': gate.result}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Operator queue snapshot (legacy fusion.plating.operator.queue helper)
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -1403,7 +1362,6 @@ class FpShopfloorController(http.Controller):
|
||||
'due_today': 25,
|
||||
'priority': 20,
|
||||
'due_soon': 15,
|
||||
'first_piece': 10,
|
||||
'normal': 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -120,39 +120,8 @@
|
||||
<field name="notes" type="html"><p>Window missed — operator shift change, parts left on rack. Flagged for quality review.</p></field>
|
||||
</record>
|
||||
|
||||
<!-- ========== FIRST PIECE GATES ========== -->
|
||||
<record id="demo_fpg_1" model="fusion.plating.first.piece.gate">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_en_mp"/>
|
||||
<field name="part_ref">P/N 4422-B — Hydraulic Cylinder Rod</field>
|
||||
<field name="customer_ref">WO-8841</field>
|
||||
<field name="routing_first_run" eval="True"/>
|
||||
<field name="first_piece_produced" eval="(DateTime.now() - timedelta(hours=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="first_piece_inspected" eval="(DateTime.now() - timedelta(hours=2, minutes=30)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="result">pass</field>
|
||||
<field name="rest_of_lot_released" eval="True"/>
|
||||
<field name="notes" type="html"><p>Thickness: 0.0005" ± 0.0001" — within spec. Adhesion bend test passed. Lot released for full production.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="demo_fpg_2" model="fusion.plating.first.piece.gate">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_cr_hard"/>
|
||||
<field name="part_ref">P/N 7810-A — Landing Gear Pin</field>
|
||||
<field name="customer_ref">WO-8835</field>
|
||||
<field name="routing_first_run" eval="False"/>
|
||||
<field name="first_piece_produced" eval="(DateTime.now() - timedelta(hours=4)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="result">pending</field>
|
||||
<field name="rest_of_lot_released" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_fpg_3" model="fusion.plating.first.piece.gate">
|
||||
<field name="bath_id" ref="fusion_plating.demo_bath_an_typeii"/>
|
||||
<field name="part_ref">P/N 3300-F — Enclosure Panel</field>
|
||||
<field name="customer_ref">WO-8830</field>
|
||||
<field name="routing_first_run" eval="True"/>
|
||||
<field name="first_piece_produced" eval="(DateTime.now() - timedelta(days=1, hours=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="first_piece_inspected" eval="(DateTime.now() - timedelta(days=1, hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="result">fail</field>
|
||||
<field name="rest_of_lot_released" eval="False"/>
|
||||
<field name="notes" type="html"><p>Colour variation on test coupon — dye bath concentration too low. Bath adjusted and retested before proceeding.</p></field>
|
||||
</record>
|
||||
<!-- First-piece gate demo records retired with the fp.first.piece.gate
|
||||
model removal (19.0.33.2.0, 2026-05-25). The feature was a skeleton
|
||||
(manual create, no enforcement, no job link) with 0 entech rows. -->
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -14,13 +14,4 @@
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_fp_first_piece_gate" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: First-Piece Gate</field>
|
||||
<field name="code">fusion.plating.first.piece.gate</field>
|
||||
<field name="prefix">FPG/%(year)s/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""19.0.33.2.0 — Drop fp.first.piece.gate model + all dependents.
|
||||
|
||||
The first-piece gate model was a skeleton: manual-create only, no
|
||||
enforcement gate, no FK to fp.job, 0 production rows on entech after
|
||||
months. Audit on 2026-05-25 concluded REMOVE.
|
||||
|
||||
Per Rule "Removing menus/records — Odoo does NOT auto-delete orphans":
|
||||
deleting <menuitem> / <record> tags from XML does NOT remove the
|
||||
corresponding DB rows. We have to explicitly drop them here.
|
||||
|
||||
Pre-migrate (vs post-migrate): runs BEFORE the new XML loads, so
|
||||
there's no window where the loader tries to reference the deleted
|
||||
model/action/view records and fails. Also runs before model __init__,
|
||||
so the registry never sees the (now-removed) Python class try to
|
||||
match against an existing-but-renamed DB table.
|
||||
"""
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
_logger.info("Dropping fp.first.piece.gate model + all dependents")
|
||||
|
||||
# ---- 1. Drop the table (CASCADE wipes any inbound FKs we missed) ----
|
||||
cr.execute("""
|
||||
DROP TABLE IF EXISTS fusion_plating_first_piece_gate CASCADE
|
||||
""")
|
||||
|
||||
# ---- 2. ir.model row (cascades to ir.model.fields, ir.model.access,
|
||||
# and ir.model.constraint via Odoo's own FK rules) -----------
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model
|
||||
WHERE model = 'fusion.plating.first.piece.gate'
|
||||
""")
|
||||
|
||||
# ---- 3. Orphan ir.ui.menu — the menuitem was removed from
|
||||
# fp_menu.xml; without explicit delete it'd linger ----------
|
||||
cr.execute("""
|
||||
DELETE FROM ir_ui_menu
|
||||
WHERE id IN (
|
||||
SELECT res_id FROM ir_model_data
|
||||
WHERE module = 'fusion_plating_shopfloor'
|
||||
AND model = 'ir.ui.menu'
|
||||
AND name = 'menu_fp_shopfloor_first_piece'
|
||||
)
|
||||
""")
|
||||
|
||||
# ---- 4. Orphan ir.actions.act_window ------------------------------
|
||||
cr.execute("""
|
||||
DELETE FROM ir_act_window
|
||||
WHERE id IN (
|
||||
SELECT res_id FROM ir_model_data
|
||||
WHERE module = 'fusion_plating_shopfloor'
|
||||
AND model = 'ir.actions.act_window'
|
||||
AND name = 'action_fp_first_piece_gate'
|
||||
)
|
||||
""")
|
||||
|
||||
# ---- 5. Drop the ir.sequence row + its companion postgres
|
||||
# sequence (Odoo creates a real PG seq named "fusion_plating_first_piece_gate") ----
|
||||
cr.execute("""
|
||||
DELETE FROM ir_sequence
|
||||
WHERE code = 'fusion.plating.first.piece.gate'
|
||||
""")
|
||||
cr.execute("""
|
||||
DROP SEQUENCE IF EXISTS fusion_plating_first_piece_gate
|
||||
""")
|
||||
|
||||
# ---- 6. Drop ALL remaining ir.model.data rows tagged for the
|
||||
# retired model so noupdate=1 demo seeds, view xmlids, etc.
|
||||
# don't ghost-haunt future upgrades ------------------------
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_data
|
||||
WHERE module = 'fusion_plating_shopfloor'
|
||||
AND ( name = 'menu_fp_shopfloor_first_piece'
|
||||
OR name = 'action_fp_first_piece_gate'
|
||||
OR name = 'seq_fp_first_piece_gate'
|
||||
OR name LIKE 'view_fp_first_piece%'
|
||||
OR name LIKE 'demo_fpg_%')
|
||||
""")
|
||||
|
||||
_logger.info(
|
||||
"fp.first.piece.gate removal complete: table dropped, ir.model "
|
||||
"row gone, menu/action/sequence/views/demo xmlids purged."
|
||||
)
|
||||
@@ -5,7 +5,6 @@
|
||||
from . import fp_shopfloor_station
|
||||
from . import fp_bake_oven
|
||||
from . import fp_bake_window
|
||||
from . import fp_first_piece_gate
|
||||
from . import fp_operator_queue
|
||||
from . import fp_tank
|
||||
from . import res_users
|
||||
|
||||
@@ -1,128 +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 FpFirstPieceGate(models.Model):
|
||||
"""First-piece inspection gate per routing.
|
||||
|
||||
Aerospace, nuclear and many automotive customers require that the FIRST
|
||||
piece off a routing be inspected and dispositioned BEFORE the rest of
|
||||
the lot is allowed to run. This model captures that gate.
|
||||
"""
|
||||
_name = 'fusion.plating.first.piece.gate'
|
||||
_description = 'Fusion Plating — First-Piece Inspection Gate'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'first_piece_produced desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
bath_id = fields.Many2one(
|
||||
'fusion.plating.bath',
|
||||
string='Bath',
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
related='bath_id.facility_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
part_ref = fields.Char(
|
||||
string='Part Reference',
|
||||
tracking=True,
|
||||
)
|
||||
customer_ref = fields.Char(
|
||||
string='Customer Reference',
|
||||
tracking=True,
|
||||
)
|
||||
routing_first_run = fields.Boolean(
|
||||
string='First Run of Routing',
|
||||
help='Tick if this is the first time this routing runs this part.',
|
||||
)
|
||||
|
||||
first_piece_produced = fields.Datetime(
|
||||
string='First Piece Produced',
|
||||
tracking=True,
|
||||
)
|
||||
first_piece_inspected = fields.Datetime(
|
||||
string='First Piece Inspected',
|
||||
tracking=True,
|
||||
)
|
||||
inspector_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Inspector',
|
||||
tracking=True,
|
||||
)
|
||||
result = fields.Selection(
|
||||
[
|
||||
('pending', 'Pending'),
|
||||
('pass', 'Pass'),
|
||||
('fail', 'Fail'),
|
||||
],
|
||||
string='Result',
|
||||
default='pending',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
rest_of_lot_released = fields.Boolean(
|
||||
string='Rest of Lot Released',
|
||||
tracking=True,
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Notes',
|
||||
)
|
||||
status_color = fields.Integer(
|
||||
compute='_compute_status_color',
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.first.piece.gate')
|
||||
return seq or '/'
|
||||
|
||||
@api.depends('result', 'rest_of_lot_released')
|
||||
def _compute_status_color(self):
|
||||
for rec in self:
|
||||
if rec.result == 'fail':
|
||||
rec.status_color = 1 # red
|
||||
elif rec.result == 'pass' and rec.rest_of_lot_released:
|
||||
rec.status_color = 4 # green
|
||||
elif rec.result == 'pass':
|
||||
rec.status_color = 3 # yellow — passed but not released
|
||||
else:
|
||||
rec.status_color = 5 # purple — pending
|
||||
|
||||
# ==========================================================================
|
||||
# Actions
|
||||
# ==========================================================================
|
||||
def action_mark_pass(self):
|
||||
self.write({
|
||||
'result': 'pass',
|
||||
'first_piece_inspected': fields.Datetime.now(),
|
||||
'inspector_id': self.env.user.id,
|
||||
})
|
||||
|
||||
def action_mark_fail(self):
|
||||
self.write({
|
||||
'result': 'fail',
|
||||
'first_piece_inspected': fields.Datetime.now(),
|
||||
'inspector_id': self.env.user.id,
|
||||
})
|
||||
|
||||
def action_release_lot(self):
|
||||
self.filtered(lambda r: r.result == 'pass').write({
|
||||
'rest_of_lot_released': True,
|
||||
})
|
||||
@@ -65,20 +65,7 @@ class FpOperatorQueue(models.TransientModel):
|
||||
'source_id': bw.id,
|
||||
})
|
||||
|
||||
gate_domain = [('result', '=', 'pending')]
|
||||
if facility_id:
|
||||
gate_domain.append(('facility_id', '=', facility_id))
|
||||
gates = self.env['fusion.plating.first.piece.gate'].search(gate_domain)
|
||||
for g in gates:
|
||||
rows.append({
|
||||
'operator_id': user_id,
|
||||
'facility_id': g.facility_id.id,
|
||||
'label': f"First piece: {g.name}",
|
||||
'description': f"Part {g.part_ref or '?'}",
|
||||
'priority': 80,
|
||||
'source_model': 'fusion.plating.first.piece.gate',
|
||||
'source_id': g.id,
|
||||
})
|
||||
# First-piece-gate aggregation retired (19.0.33.2.0) with the model.
|
||||
|
||||
# ----- MRP work orders (if fusion_plating_bridge_mrp installed) -----
|
||||
# Show two buckets, in this order:
|
||||
|
||||
@@ -17,27 +17,22 @@ class FpTank(models.Model):
|
||||
x_fp_shopfloor_queue_size = fields.Integer(
|
||||
string='Shopfloor Queue',
|
||||
compute='_compute_shopfloor_queue_size',
|
||||
help='Number of bake windows + first-piece gates currently waiting on '
|
||||
'this tank\'s current bath.',
|
||||
help='Number of bake windows currently waiting on this tank\'s '
|
||||
'current bath.',
|
||||
)
|
||||
|
||||
def _compute_shopfloor_queue_size(self):
|
||||
# First-piece-gate contribution dropped 19.0.33.2.0 with the model.
|
||||
BakeWindow = self.env['fusion.plating.bake.window']
|
||||
Gate = self.env['fusion.plating.first.piece.gate']
|
||||
for rec in self:
|
||||
bath_ids = rec.bath_ids.ids
|
||||
if not bath_ids:
|
||||
rec.x_fp_shopfloor_queue_size = 0
|
||||
continue
|
||||
count = BakeWindow.search_count([
|
||||
rec.x_fp_shopfloor_queue_size = BakeWindow.search_count([
|
||||
('bath_id', 'in', bath_ids),
|
||||
('state', 'in', ('awaiting_bake', 'bake_in_progress')),
|
||||
])
|
||||
count += Gate.search_count([
|
||||
('bath_id', 'in', bath_ids),
|
||||
('result', '=', 'pending'),
|
||||
])
|
||||
rec.x_fp_shopfloor_queue_size = count
|
||||
|
||||
def action_open_tablet_view(self):
|
||||
"""Open the tablet client action focused on this tank."""
|
||||
|
||||
@@ -8,9 +8,6 @@ access_fp_bake_oven_manager,fp.bake.oven.manager,model_fusion_plating_bake_oven,
|
||||
access_fp_bake_window_operator,fp.bake.window.operator,model_fusion_plating_bake_window,fusion_plating.group_fp_technician,1,1,1,0
|
||||
access_fp_bake_window_supervisor,fp.bake.window.supervisor,model_fusion_plating_bake_window,fusion_plating.group_fp_shop_manager_v2,1,1,1,0
|
||||
access_fp_bake_window_manager,fp.bake.window.manager,model_fusion_plating_bake_window,fusion_plating.group_fp_manager,1,1,1,1
|
||||
access_fp_first_piece_gate_operator,fp.first.piece.gate.operator,model_fusion_plating_first_piece_gate,fusion_plating.group_fp_technician,1,1,1,0
|
||||
access_fp_first_piece_gate_supervisor,fp.first.piece.gate.supervisor,model_fusion_plating_first_piece_gate,fusion_plating.group_fp_shop_manager_v2,1,1,1,0
|
||||
access_fp_first_piece_gate_manager,fp.first.piece.gate.manager,model_fusion_plating_first_piece_gate,fusion_plating.group_fp_manager,1,1,1,1
|
||||
access_fp_operator_queue_operator,fp.operator.queue.operator,model_fusion_plating_operator_queue,fusion_plating.group_fp_technician,1,1,1,1
|
||||
access_fp_operator_queue_supervisor,fp.operator.queue.supervisor,model_fusion_plating_operator_queue,fusion_plating.group_fp_shop_manager_v2,1,1,1,1
|
||||
access_fp_operator_queue_manager,fp.operator.queue.manager,model_fusion_plating_operator_queue,fusion_plating.group_fp_manager,1,1,1,1
|
||||
|
||||
|
@@ -239,16 +239,8 @@ export class ShopfloorTablet extends Component {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async onGateResult(gateId, result) {
|
||||
try {
|
||||
await rpc("/fp/shopfloor/mark_gate", { gate_id: gateId, result });
|
||||
this.setMessage(`First-piece marked ${result.toUpperCase()}.`,
|
||||
result === "pass" ? "success" : "danger");
|
||||
} catch (err) {
|
||||
this.setMessage(`Gate update failed: ${err.message || err}`, "danger");
|
||||
}
|
||||
await this.refresh();
|
||||
}
|
||||
// onGateResult / /fp/shopfloor/mark_gate retired with the
|
||||
// fp.first.piece.gate model removal (19.0.33.2.0).
|
||||
|
||||
async onQueueItemClick(row) {
|
||||
if (row.source_model && row.source_id) {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<!-- KPI strip -->
|
||||
<div t-if="state.data" class="kpi-strip">
|
||||
<FpKpiTile value="state.data.kpis.active_jobs"
|
||||
label="'Active Jobs'"
|
||||
label="'Work Orders'"
|
||||
kind="'good'"
|
||||
active="!!state.filters.all"
|
||||
onClick="() => this.toggleFilter('all')"/>
|
||||
|
||||
@@ -342,53 +342,9 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="o_fp_panel">
|
||||
<div class="o_fp_panel_head">
|
||||
<h3><i class="fa fa-flag-checkered"/>First-Piece Gates</h3>
|
||||
<span class="o_fp_panel_count"><t t-esc="state.overview.gates.length"/></span>
|
||||
</div>
|
||||
<div t-if="!state.overview.gates.length" class="o_fp_empty">
|
||||
<i class="fa fa-flag-checkered"/>
|
||||
<div>No pending first-piece inspections.</div>
|
||||
</div>
|
||||
<ul class="o_fp_bake_list" t-if="state.overview.gates.length">
|
||||
<t t-foreach="state.overview.gates" t-as="g" t-key="g.id">
|
||||
<li class="o_fp_bake_row" t-att-data-state="g.result">
|
||||
<div class="o_fp_bake_main">
|
||||
<div class="o_fp_bake_name">
|
||||
<t t-esc="g.name"/>
|
||||
<span class="text-muted ms-1"> — <t t-esc="g.part_ref"/></span>
|
||||
</div>
|
||||
<div class="o_fp_bake_meta">
|
||||
<t t-esc="g.customer"/>
|
||||
<t t-if="g.bath"> · Bath <t t-esc="g.bath"/></t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_bake_time">
|
||||
<span t-att-class="'o_fp_chip o_fp_chip_' + stateBadge(g.result)">
|
||||
<t t-esc="g.result"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_fp_bake_actions">
|
||||
<button t-if="g.result === 'pending'"
|
||||
class="btn btn-success"
|
||||
t-on-click="() => this.onGateResult(g.id, 'pass')">
|
||||
Pass
|
||||
</button>
|
||||
<button t-if="g.result === 'pending'"
|
||||
class="btn btn-danger"
|
||||
t-on-click="() => this.onGateResult(g.id, 'fail')">
|
||||
Fail
|
||||
</button>
|
||||
<button class="btn btn-light"
|
||||
t-on-click="() => this.openRecord('fusion.plating.first.piece.gate', g.id)">
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</section>
|
||||
<!-- First-piece gate panel retired with the fp.first.piece.gate
|
||||
model removal (19.0.33.2.0). The feature was never wired
|
||||
up — manual create, no enforcement, no rows in production. -->
|
||||
|
||||
<!-- ===== Pending QC banner (S19 follow-up) ===== -->
|
||||
<!-- Shows whenever Carlos's job has an open QC. Tap -->
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_first_piece_gate_list" model="ir.ui.view">
|
||||
<field name="name">fp.first.piece.gate.list</field>
|
||||
<field name="model">fusion.plating.first.piece.gate</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="First-Piece Gates"
|
||||
decoration-success="result == 'pass' and rest_of_lot_released"
|
||||
decoration-warning="result == 'pass' and not rest_of_lot_released"
|
||||
decoration-danger="result == 'fail'"
|
||||
decoration-muted="result == 'pending'">
|
||||
<field name="name"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="part_ref"/>
|
||||
<field name="customer_ref" optional="show"/>
|
||||
<field name="routing_first_run" widget="boolean_toggle" optional="hide"/>
|
||||
<field name="first_piece_produced" optional="show"/>
|
||||
<field name="first_piece_inspected" optional="show"/>
|
||||
<field name="inspector_id" optional="show"/>
|
||||
<field name="result" widget="badge"
|
||||
decoration-success="result == 'pass'"
|
||||
decoration-danger="result == 'fail'"
|
||||
decoration-muted="result == 'pending'"/>
|
||||
<field name="rest_of_lot_released" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_first_piece_gate_form" model="ir.ui.view">
|
||||
<field name="name">fp.first.piece.gate.form</field>
|
||||
<field name="model">fusion.plating.first.piece.gate</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="First-Piece Gate">
|
||||
<header>
|
||||
<button name="action_mark_pass" string="Mark Pass" type="object"
|
||||
class="oe_highlight" invisible="result != 'pending'"/>
|
||||
<button name="action_mark_fail" string="Mark Fail" type="object"
|
||||
invisible="result != 'pending'"/>
|
||||
<button name="action_release_lot" string="Release Lot" type="object"
|
||||
invisible="result != 'pass' or rest_of_lot_released"/>
|
||||
<field name="result" widget="statusbar"
|
||||
statusbar_visible="pending,pass"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Job">
|
||||
<field name="bath_id"/>
|
||||
<field name="facility_id" readonly="1"/>
|
||||
<field name="part_ref"/>
|
||||
<field name="customer_ref"/>
|
||||
<field name="routing_first_run"/>
|
||||
</group>
|
||||
<group string="Inspection">
|
||||
<field name="first_piece_produced"/>
|
||||
<field name="first_piece_inspected"/>
|
||||
<field name="inspector_id"/>
|
||||
<field name="rest_of_lot_released"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Notes"/>
|
||||
<field name="notes"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Kanban rebuilt 2026-04 to match the Plant Overview card design.
|
||||
Shared base styles in fp_kanbans.scss (.o_fp_kcard); the
|
||||
wrapping .o_fp_fpg_kanban scopes per-result stripe colours.
|
||||
-->
|
||||
<record id="view_fp_first_piece_gate_kanban" model="ir.ui.view">
|
||||
<field name="name">fp.first.piece.gate.kanban</field>
|
||||
<field name="model">fusion.plating.first.piece.gate</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="result" class="o_fp_fpg_kanban">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="part_ref"/>
|
||||
<field name="customer_ref"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="first_piece_produced"/>
|
||||
<field name="inspector_id"/>
|
||||
<field name="result"/>
|
||||
<field name="rest_of_lot_released"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_fp_kcard"
|
||||
t-att-data-result="record.result.raw_value">
|
||||
<div class="o_fp_kcard_title">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
<div class="o_fp_kcard_sub" t-if="record.part_ref.raw_value">
|
||||
<field name="part_ref"/>
|
||||
</div>
|
||||
<div class="o_fp_kcard_meta">
|
||||
<span t-if="record.bath_id.raw_value">
|
||||
<i class="fa fa-flask me-1"/><field name="bath_id"/>
|
||||
</span>
|
||||
<span class="o_fp_kcard_meta_sep"
|
||||
t-if="record.bath_id.raw_value and record.customer_ref.raw_value">·</span>
|
||||
<span t-if="record.customer_ref.raw_value">
|
||||
<field name="customer_ref"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_fp_kcard_meta"
|
||||
t-if="record.inspector_id.raw_value or record.first_piece_produced.raw_value">
|
||||
<span t-if="record.inspector_id.raw_value">
|
||||
<i class="fa fa-user me-1"/><field name="inspector_id"/>
|
||||
</span>
|
||||
<span class="o_fp_kcard_meta_sep"
|
||||
t-if="record.inspector_id.raw_value and record.first_piece_produced.raw_value">·</span>
|
||||
<span t-if="record.first_piece_produced.raw_value">
|
||||
<field name="first_piece_produced"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_fp_kcard_footer">
|
||||
<span t-att-class="'o_fp_kcard_chip ' + (
|
||||
record.result.raw_value === 'pass' ? 'tone-success'
|
||||
: record.result.raw_value === 'fail' ? 'tone-danger'
|
||||
: record.result.raw_value === 'pending' ? 'tone-warning'
|
||||
: 'tone-muted')">
|
||||
<field name="result"/>
|
||||
</span>
|
||||
<span class="o_fp_fpg_released"
|
||||
t-if="record.rest_of_lot_released.raw_value">
|
||||
<i class="fa fa-check"/> Released
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_fp_first_piece_gate_search" model="ir.ui.view">
|
||||
<field name="name">fp.first.piece.gate.search</field>
|
||||
<field name="model">fusion.plating.first.piece.gate</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="First-Piece Gates">
|
||||
<field name="name"/>
|
||||
<field name="part_ref"/>
|
||||
<field name="customer_ref"/>
|
||||
<field name="bath_id"/>
|
||||
<field name="inspector_id"/>
|
||||
<separator/>
|
||||
<filter string="Pending" name="pending" domain="[('result','=','pending')]"/>
|
||||
<filter string="Passed" name="passed" domain="[('result','=','pass')]"/>
|
||||
<filter string="Failed" name="failed" domain="[('result','=','fail')]"/>
|
||||
<separator/>
|
||||
<filter string="Lot Released" name="released" domain="[('rest_of_lot_released','=',True)]"/>
|
||||
<filter string="Lot On Hold" name="on_hold"
|
||||
domain="[('result','=','pass'),('rest_of_lot_released','=',False)]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Result" name="group_result" context="{'group_by':'result'}"/>
|
||||
<filter string="Customer" name="group_customer" context="{'group_by':'customer_ref'}"/>
|
||||
<filter string="Inspector" name="group_inspector" context="{'group_by':'inspector_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_first_piece_gate" model="ir.actions.act_window">
|
||||
<field name="name">First-Piece Gates</field>
|
||||
<field name="res_model">fusion.plating.first.piece.gate</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_first_piece_gate_search"/>
|
||||
<field name="context">{'search_default_pending': 1}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -43,12 +43,6 @@
|
||||
action="action_fp_bake_window"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_shopfloor_first_piece"
|
||||
name="First-Piece Gates"
|
||||
parent="menu_fp_shopfloor"
|
||||
action="action_fp_first_piece_gate"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Phase 2 — both under Shop Setup. -->
|
||||
<menuitem id="menu_fp_shopfloor_stations_cfg"
|
||||
name="Shopfloor Stations"
|
||||
|
||||
@@ -1054,54 +1054,7 @@ if env['fusion.plating.bake.window'].search_count([]) < 6:
|
||||
})
|
||||
LOG(" +3 additional bake windows (awaiting / in-progress)")
|
||||
|
||||
# First-piece inspection gates — seed 4 variants
|
||||
Gate = env['fusion.plating.first.piece.gate']
|
||||
if Gate.search_count([]) < 4:
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'HW-TOR-5501',
|
||||
'customer_ref': 'Honeywell Toronto',
|
||||
'routing_first_run': True,
|
||||
'first_piece_produced': datetime.now() - timedelta(minutes=35),
|
||||
'result': 'pending',
|
||||
})
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'AP-WGL-7200',
|
||||
'customer_ref': 'Amphenol Canada',
|
||||
'routing_first_run': False,
|
||||
'first_piece_produced': datetime.now() - timedelta(hours=2),
|
||||
'first_piece_inspected': datetime.now() - timedelta(hours=1, minutes=40),
|
||||
'inspector_id': env.user.id,
|
||||
'result': 'pass',
|
||||
'rest_of_lot_released': True,
|
||||
})
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'MG-WG-8801',
|
||||
'customer_ref': 'Magellan Aerospace',
|
||||
'routing_first_run': True,
|
||||
'first_piece_produced': datetime.now() - timedelta(hours=4),
|
||||
'first_piece_inspected': datetime.now() - timedelta(hours=3, minutes=30),
|
||||
'inspector_id': env.user.id,
|
||||
'result': 'pass',
|
||||
'rest_of_lot_released': False, # passed but awaiting release
|
||||
'notes': '<p>Thickness 1.95 mils — within tolerance. Lot released pending planner signoff.</p>',
|
||||
})
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'CY-STR-240',
|
||||
'customer_ref': 'Cyclone Manufacturing',
|
||||
'routing_first_run': True,
|
||||
'first_piece_produced': datetime.now() - timedelta(hours=6),
|
||||
'first_piece_inspected': datetime.now() - timedelta(hours=5, minutes=30),
|
||||
'inspector_id': env.user.id,
|
||||
'result': 'fail',
|
||||
'notes': '<p>Thickness 0.8 mils — below spec (min 1.2). Rework required.</p>',
|
||||
})
|
||||
LOG(" 4 first-piece gates: 1 pending / 1 passed+released / 1 passed-holding / 1 failed")
|
||||
else:
|
||||
LOG(f" Already has {Gate.search_count([])} first-piece gates — skipping")
|
||||
# First-piece-gate seeding retired with the model (19.0.33.2.0).
|
||||
|
||||
# Quality holds on active MOs — gives the Shop Floor quality-holds panel content
|
||||
Hold = env['fusion.plating.quality.hold']
|
||||
@@ -1374,7 +1327,6 @@ LOG(f" Bath logs: {env['fusion.plating.bath.log'].search_count([])}")
|
||||
LOG(f" Replenishments: {env['fusion.plating.bath.replenishment.suggestion'].search_count([])}")
|
||||
LOG(f" Bake windows: {env['fusion.plating.bake.window'].search_count([])}")
|
||||
LOG(f" Stations: {env['fusion.plating.shopfloor.station'].search_count([])}")
|
||||
LOG(f" First-piece: {env['fusion.plating.first.piece.gate'].search_count([])}")
|
||||
LOG(f" Quality holds: {env['fusion.plating.quality.hold'].search_count([])}")
|
||||
LOG(f" Racks: {env['fusion.plating.rack'].search_count([])}")
|
||||
LOG(f" Operator certs: {env['fp.operator.certification'].search_count([])}")
|
||||
|
||||
@@ -853,17 +853,7 @@ if BakeWin is not None and job:
|
||||
'bake window auto-created',
|
||||
f'{len(bw)} record(s) for {job.name}')
|
||||
|
||||
# First-piece gate auto-created?
|
||||
FPG = env.get('fusion.plating.first.piece.gate')
|
||||
if FPG is not None:
|
||||
# FPG model may not have production_id either; try common link fields
|
||||
fpg = FPG.search([]) # take any recent
|
||||
fpg_for_mo = fpg.filtered(
|
||||
lambda g: getattr(g, 'production_id', False) and g.production_id.id == mo.id
|
||||
) if 'production_id' in FPG._fields else fpg.browse([])
|
||||
finding('PASS' if fpg_for_mo else 'WARN',
|
||||
'first-piece gate',
|
||||
f'{len(fpg_for_mo)} for MO (coating-driven; OK if 0)')
|
||||
# First-piece-gate check retired with the model (19.0.33.2.0).
|
||||
|
||||
# Each operator can see their OWN assigned WOs via the tablet
|
||||
# (queue is a TransientModel; tablet calls build_for_user on load)
|
||||
|
||||
Reference in New Issue
Block a user