This commit is contained in:
gsinghpal
2026-05-25 08:17:29 -04:00
parent 5d5964a327
commit 80887d6098
19 changed files with 117 additions and 558 deletions

View File

@@ -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.',

View File

@@ -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"/>

View File

@@ -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',

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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."
)

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_fp_bake_window_operator fp.bake.window.operator model_fusion_plating_bake_window fusion_plating.group_fp_technician 1 1 1 0
9 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
10 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
11 access_fp_operator_queue_operator fp.operator.queue.operator model_fusion_plating_operator_queue fusion_plating.group_fp_technician 1 1 1 1
12 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
13 access_fp_operator_queue_manager fp.operator.queue.manager model_fusion_plating_operator_queue fusion_plating.group_fp_manager 1 1 1 1

View File

@@ -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) {

View File

@@ -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')"/>

View File

@@ -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 -->

View File

@@ -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>

View File

@@ -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"

View File

@@ -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([])}")

View File

@@ -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)