This commit is contained in:
gsinghpal
2026-04-12 09:09:50 -04:00
parent d07159b9b5
commit be611876ad
470 changed files with 41761 additions and 51 deletions

View File

@@ -0,0 +1,6 @@
# -*- 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 models

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
{
'name': 'Fusion Plating — MRP Bridge',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """
Fusion Plating — MRP Bridge
============================
Part of the Fusion Plating product family by Nexa Systems Inc.
Links Fusion Plating infrastructure (facilities, work centres, baths, tanks)
to Odoo's native MRP manufacturing orders and work orders so shops can:
* Assign a plating facility and FP work centre to an MRP work centre.
* Tag each work order with the specific bath, tank, rack/fixture, target
thickness, and dwell time for traceability.
* Attach a customer specification and facility to a manufacturing order.
* Create an MRP work centre directly from a Fusion Plating work centre.
* Link a portal job to a manufacturing order for customer visibility.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
""",
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'maintainer': 'Nexa Systems Inc.',
'support': 'support@nexasystems.ca',
'license': 'OPL-1',
'price': 0.00,
'currency': 'CAD',
'depends': [
'fusion_plating',
'fusion_plating_portal',
'fusion_plating_quality',
'fusion_plating_logistics',
'fusion_plating_batch',
'mrp',
'mrp_workorder',
'mrp_account',
'sale_mrp',
'account',
],
'data': [
'security/ir.model.access.csv',
'views/mrp_workcenter_views.xml',
'views/mrp_workorder_views.xml',
'views/mrp_production_views.xml',
'views/fp_quality_hold_views.xml',
'views/fp_batch_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
}

View File

@@ -0,0 +1,14 @@
# -*- 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 mrp_workcenter
from . import mrp_workorder
from . import mrp_production
from . import fp_work_center
from . import fp_portal_job
from . import fp_quality_hold
from . import fp_delivery
from . import fp_batch
from . import account_move

View File

@@ -0,0 +1,53 @@
# -*- 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 models
class AccountMove(models.Model):
"""Extend invoice to auto-complete portal job when posted.
GAP 7: Invoice posted → find portal job for same customer/SO
→ set state "complete" + invoice_ref.
"""
_inherit = 'account.move'
def action_post(self):
"""Override to cascade invoice posting to portal job completion."""
res = super().action_post()
PortalJob = self.env.get('fusion.plating.portal.job')
if PortalJob is None:
return res
for invoice in self:
if invoice.move_type != 'out_invoice':
continue
# Find portal jobs for this customer that are shipped but not complete
# Match by SO origin from the invoice lines
origin = invoice.invoice_origin or ''
jobs = PortalJob.browse()
if origin:
# Try to find MO linked to this SO, then its portal job
mos = self.env['mrp.production'].search(
[('origin', '=', origin)],
)
for mo in mos:
if mo.x_fc_portal_job_id and mo.x_fc_portal_job_id.state == 'shipped':
jobs |= mo.x_fc_portal_job_id
# Fallback: find shipped jobs for same partner
if not jobs:
jobs = PortalJob.search([
('partner_id', '=', invoice.partner_id.id),
('state', '=', 'shipped'),
('invoice_ref', '=', False),
], limit=1)
for job in jobs:
job.write({
'state': 'complete',
'invoice_ref': invoice.name,
})
job.message_post(
body='Invoice %s posted — job complete.' % invoice.name,
)
return res

View File

@@ -0,0 +1,32 @@
# -*- 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 fields, models
class FpBatch(models.Model):
"""Extend batch with M2M link to MRP work orders.
GAP 6: Batch ↔ Work Order linkage so the shop knows which WOs
were processed in which tank batch (rack/barrel load).
"""
_inherit = 'fusion.plating.batch'
workorder_ids = fields.Many2many(
'mrp.workorder',
'fp_batch_mrp_workorder_rel',
'batch_id',
'workorder_id',
string='Work Orders',
help='MRP work orders processed in this batch.',
)
workorder_count = fields.Integer(
string='WO Count',
compute='_compute_workorder_count',
)
def _compute_workorder_count(self):
for rec in self:
rec.workorder_count = len(rec.workorder_ids)

View File

@@ -0,0 +1,36 @@
# -*- 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 fields, models
class FpDelivery(models.Model):
"""Extend delivery to auto-update portal job when delivered.
GAP 5: Delivery marked "delivered" → portal job → "shipped"
+ set actual_ship_date on the job.
"""
_inherit = 'fusion.plating.delivery'
def action_mark_delivered(self):
"""Override to cascade delivery completion to the portal job."""
res = super().action_mark_delivered()
PortalJob = self.env['fusion.plating.portal.job']
for delivery in self:
if not delivery.job_ref:
continue
# Find the portal job by name/reference
job = PortalJob.search(
[('name', '=', delivery.job_ref)], limit=1,
)
if not job:
continue
job.write({
'state': 'shipped',
'actual_ship_date': fields.Date.today(),
'tracking_ref': delivery.name,
})
job.message_post(body='Parts shipped — delivery %s marked delivered.' % delivery.name)
return res

View File

@@ -0,0 +1,17 @@
# -*- 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 fields, models
class FpPortalJob(models.Model):
"""Extend portal job with a link to its manufacturing order."""
_inherit = 'fusion.plating.portal.job'
x_fc_production_id = fields.Many2one(
'mrp.production',
string='Manufacturing Order',
help='The Odoo manufacturing order linked to this portal job.',
)

View File

@@ -0,0 +1,28 @@
# -*- 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 fields, models
class FpQualityHoldMrp(models.Model):
"""Add MRP references to the quality hold record.
These fields live here (not in fusion_plating_quality) so the QMS
module can install without an mrp dependency.
"""
_inherit = 'fusion.plating.quality.hold'
workorder_id = fields.Many2one(
'mrp.workorder',
string='Work Order',
)
production_id = fields.Many2one(
'mrp.production',
string='Manufacturing Order',
)
portal_job_id = fields.Many2one(
'fusion.plating.portal.job',
string='Portal Job',
)

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import fields, models
_logger = logging.getLogger(__name__)
class FpWorkCenter(models.Model):
"""Extend FP work centre with a link to its mirrored MRP work centre."""
_inherit = 'fusion.plating.work.center'
x_fc_mrp_workcenter_id = fields.Many2one(
'mrp.workcenter',
string='MRP Work Centre',
copy=False,
help='The Odoo MRP work centre that mirrors this FP work centre.',
)
def action_sync_to_mrp(self):
"""Create or update the mirrored mrp.workcenter record.
If the FP work centre already has an MRP work centre linked,
update its name and facility reference. Otherwise create a new
one and link it back.
"""
Workcenter = self.env['mrp.workcenter']
for rec in self:
vals = {
'name': rec.name,
'code': rec.code,
'x_fc_facility_id': rec.facility_id.id,
'x_fc_fp_work_center_id': rec.id,
'company_id': rec.facility_id.company_id.id,
}
if rec.x_fc_mrp_workcenter_id:
rec.x_fc_mrp_workcenter_id.write(vals)
_logger.info(
'Fusion Plating MRP bridge: updated mrp.workcenter %s '
'from FP work centre %s',
rec.x_fc_mrp_workcenter_id.id, rec.name,
)
else:
wc = Workcenter.create(vals)
rec.x_fc_mrp_workcenter_id = wc.id
_logger.info(
'Fusion Plating MRP bridge: created mrp.workcenter %s '
'from FP work centre %s',
wc.id, rec.name,
)
return {'type': 'ir.actions.client', 'tag': 'reload'}

View File

@@ -0,0 +1,93 @@
# -*- 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 MrpProduction(models.Model):
"""Extend manufacturing order with Fusion Plating references and
workflow automations that bridge MO lifecycle → portal job → delivery.
"""
_inherit = 'mrp.production'
x_fc_customer_spec_id = fields.Many2one(
'fusion.plating.customer.spec',
string='Customer Spec',
help='The customer specification governing this manufacturing order.',
)
x_fc_facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
help='The Fusion Plating facility where this order is produced.',
)
x_fc_portal_job_id = fields.Many2one(
'fusion.plating.portal.job',
string='Portal Job',
help='The portal job linked to this manufacturing order.',
)
# ------------------------------------------------------------------
# GAP 2: SO confirm → MO confirm → auto-create Portal Job
# ------------------------------------------------------------------
def action_confirm(self):
"""Override to auto-create a portal job when the MO is confirmed."""
res = super().action_confirm()
PortalJob = self.env['fusion.plating.portal.job']
for mo in self:
if mo.x_fc_portal_job_id:
# Already linked — just update state
mo.x_fc_portal_job_id.write({'state': 'in_progress'})
continue
# Resolve customer from sale order via origin
partner = False
if mo.origin:
so = self.env['sale.order'].search(
[('name', '=', mo.origin)], limit=1,
)
if so:
partner = so.partner_id
if not partner:
continue # No customer — skip portal job creation
job = PortalJob.create({
'name': mo.name,
'partner_id': partner.id,
'state': 'in_progress',
'received_date': fields.Date.today(),
'target_ship_date': (
mo.date_start.date() + __import__('datetime').timedelta(days=10)
if mo.date_start else False
),
'quantity': int(mo.product_qty),
'company_id': mo.company_id.id,
})
mo.x_fc_portal_job_id = job
return res
# ------------------------------------------------------------------
# GAP 3+4: MO done → update portal job + auto-create delivery
# ------------------------------------------------------------------
def button_mark_done(self):
"""Override to cascade MO completion to portal job and delivery."""
res = super().button_mark_done()
Delivery = self.env.get('fusion.plating.delivery')
for mo in self:
job = mo.x_fc_portal_job_id
if not job:
continue
# GAP 3: MO done → portal job ready_to_ship
job.write({'state': 'ready_to_ship'})
job.message_post(body='Manufacturing complete — ready to ship.')
# GAP 4: Auto-create delivery record
if Delivery is None:
continue
partner = job.partner_id
Delivery.create({
'partner_id': partner.id,
'job_ref': job.name,
'source_facility_id': mo.x_fc_facility_id.id if mo.x_fc_facility_id else False,
'state': 'draft',
})
return res

View File

@@ -0,0 +1,23 @@
# -*- 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 fields, models
class MrpWorkcenter(models.Model):
"""Extend MRP work centre with Fusion Plating facility and work centre."""
_inherit = 'mrp.workcenter'
x_fc_facility_id = fields.Many2one(
'fusion.plating.facility',
string='Plating Facility',
help='The Fusion Plating facility this MRP work centre belongs to.',
)
x_fc_fp_work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='FP Work Centre',
domain="[('facility_id', '=', x_fc_facility_id)]",
help='The Fusion Plating work centre mirrored by this MRP work centre.',
)

View File

@@ -0,0 +1,399 @@
# -*- 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 MrpWorkorder(models.Model):
"""Extend work order with plating fields, priority, chatter,
workflow step tracking, and smart-button computed fields.
"""
_name = 'mrp.workorder'
_inherit = ['mrp.workorder', 'mail.thread', 'mail.activity.mixin']
# ------------------------------------------------------------------
# Priority (Normal / Urgent / Hot)
# ------------------------------------------------------------------
x_fc_priority = fields.Selection(
[('0', 'Normal'), ('1', 'Urgent'), ('2', 'Hot')],
string='Priority',
default='0',
tracking=True,
)
# ------------------------------------------------------------------
# Plating-specific fields
# ------------------------------------------------------------------
x_fc_bath_id = fields.Many2one(
'fusion.plating.bath', string='Bath', tracking=True,
)
x_fc_tank_id = fields.Many2one(
'fusion.plating.tank', string='Tank',
)
x_fc_rack_ref = fields.Char(string='Rack / Fixture Ref')
x_fc_thickness_target = fields.Float(string='Target Thickness')
x_fc_thickness_uom = fields.Selection(
[('mils', 'mils'), ('microns', '\u00b5m')],
string='Thickness Unit', default='mils',
)
x_fc_dwell_time_minutes = fields.Float(string='Dwell Time (min)')
x_fc_facility_id = fields.Many2one(
'fusion.plating.facility', string='Facility',
related='workcenter_id.x_fc_facility_id', store=True, readonly=True,
)
x_fc_workcenter_cost_hour = fields.Float(
string='Station Rate ($/hr)',
related='workcenter_id.costs_hour', readonly=True,
)
# ------------------------------------------------------------------
# Workflow step tracking
# ------------------------------------------------------------------
x_fc_step_number = fields.Integer(
string='Step #', compute='_compute_step_info', store=True,
)
x_fc_total_steps = fields.Integer(
string='Total Steps', compute='_compute_step_info', store=True,
)
x_fc_step_display = fields.Char(
string='Current Step', compute='_compute_step_info', store=True,
)
@api.depends('production_id.workorder_ids', 'sequence')
def _compute_step_info(self):
for wo in self:
siblings = wo.production_id.workorder_ids.sorted('sequence')
total = len(siblings)
step = 0
for i, s in enumerate(siblings, 1):
if s.id == wo.id:
step = i
break
wo.x_fc_step_number = step
wo.x_fc_total_steps = total
wo.x_fc_step_display = f"Step {step} of {total}" if total else ""
# ------------------------------------------------------------------
# Smart button computes
# ------------------------------------------------------------------
x_fc_sale_order_id = fields.Many2one(
'sale.order', string='Sale Order',
compute='_compute_sale_order',
)
x_fc_portal_job_id = fields.Many2one(
'fusion.plating.portal.job', string='Portal Job',
compute='_compute_portal_job',
)
x_fc_customer_id = fields.Many2one(
'res.partner', string='Customer',
compute='_compute_customer', store=True,
)
x_fc_sale_order_name = fields.Char(
string='SO #', compute='_compute_sale_order', store=False,
)
x_fc_production_name = fields.Char(
string='MO #', related='production_id.name', store=False,
)
x_fc_quality_hold_count = fields.Integer(
string='Quality Holds', compute='_compute_quality_hold_count',
)
x_fc_delivery_count = fields.Integer(
string='Deliveries', compute='_compute_delivery_count',
)
@api.depends('production_id.origin')
def _compute_customer(self):
SO = self.env['sale.order']
for wo in self:
origin = wo.production_id.origin or ''
if origin:
so = SO.search([('name', '=', origin)], limit=1)
wo.x_fc_customer_id = so.partner_id if so else False
else:
wo.x_fc_customer_id = False
def _compute_sale_order(self):
SO = self.env['sale.order']
for wo in self:
origin = wo.production_id.origin or ''
if origin:
so = SO.search([('name', '=', origin)], limit=1)
wo.x_fc_sale_order_id = so
wo.x_fc_sale_order_name = so.name if so else ''
else:
wo.x_fc_sale_order_id = False
wo.x_fc_sale_order_name = ''
def _compute_portal_job(self):
for wo in self:
wo.x_fc_portal_job_id = (
wo.production_id.x_fc_portal_job_id
if wo.production_id else False
)
def _compute_quality_hold_count(self):
Hold = self.env.get('fusion.plating.quality.hold')
for wo in self:
if Hold and 'workorder_id' in Hold._fields:
wo.x_fc_quality_hold_count = Hold.search_count(
[('workorder_id', '=', wo.id)]
)
else:
wo.x_fc_quality_hold_count = 0
def _compute_delivery_count(self):
Delivery = self.env.get('fusion.plating.delivery')
for wo in self:
if Delivery and wo.production_id.x_fc_portal_job_id:
wo.x_fc_delivery_count = Delivery.search_count(
[('job_ref', '=', wo.production_id.x_fc_portal_job_id.name)]
)
else:
wo.x_fc_delivery_count = 0
# ------------------------------------------------------------------
# Smart button actions
# ------------------------------------------------------------------
def action_view_sale_order(self):
self.ensure_one()
so = self.x_fc_sale_order_id
if not so:
return
return {
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'res_id': so.id,
'view_mode': 'form',
'target': 'current',
}
def action_view_manufacturing_order(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp.production',
'res_id': self.production_id.id,
'view_mode': 'form',
'target': 'current',
}
def action_view_portal_job(self):
self.ensure_one()
job = self.x_fc_portal_job_id
if not job:
return
return {
'type': 'ir.actions.act_window',
'res_model': 'fusion.plating.portal.job',
'res_id': job.id,
'view_mode': 'form',
'target': 'current',
}
def action_view_quality_holds(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'fusion.plating.quality.hold',
'view_mode': 'list,form',
'domain': [('workorder_id', '=', self.id)],
'target': 'current',
}
def action_view_deliveries(self):
self.ensure_one()
job = self.x_fc_portal_job_id
if not job:
return
return {
'type': 'ir.actions.act_window',
'res_model': 'fusion.plating.delivery',
'view_mode': 'list,form',
'domain': [('job_ref', '=', job.name)],
'target': 'current',
}
# ------------------------------------------------------------------
# Process tree action (opens OWL client action)
# ------------------------------------------------------------------
def action_view_process_tree(self):
"""Open the OWL process tree view for this MO's routing."""
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'fp_process_tree',
'name': f'Process Tree — {self.production_id.name}',
'context': {'production_id': self.production_id.id},
}
# ------------------------------------------------------------------
# Process flow for horizontal pipeline bar
# ------------------------------------------------------------------
def get_process_flow(self):
"""Return process flow steps for the horizontal pipeline bar.
Returns a list of dicts, one per WO in this MO's routing:
[
{
'wo_id': 42,
'name': 'Incoming Inspection',
'workcenter': 'Incoming Inspection',
'sequence': 10,
'state': 'done',
'is_current': False,
'duration': 12.5,
'duration_expected': 15.0,
'duration_display': '12m',
},
...
]
"""
self.ensure_one()
siblings = self.production_id.workorder_ids.sorted('sequence')
result = []
for wo in siblings:
# Human-readable duration
dur = wo.duration or 0
if dur >= 60:
dur_display = f"{int(dur // 60)}h {int(dur % 60)}m"
elif dur > 0:
dur_display = f"{int(dur)}m"
else:
dur_display = ''
result.append({
'wo_id': wo.id,
'name': wo.name or wo.workcenter_id.name or '',
'workcenter': wo.workcenter_id.name or '',
'sequence': wo.sequence,
'state': wo.state,
'is_current': wo.id == self.id,
'duration': round(dur, 1),
'duration_expected': round(wo.duration_expected or 0, 1),
'duration_display': dur_display,
})
return result
# ------------------------------------------------------------------
# Cost summary for Time & Cost tab
# ------------------------------------------------------------------
def get_cost_summary(self):
"""Return cost breakdown for all WOs in this MO.
Returns:
{
'revenue': 450.00,
'station_costs': [
{'station': 'Alkaline Clean', 'rate': 30.0, 'duration': 34.5,
'labour_cost': 17.25, 'operation_cost': 5.75, 'total': 23.00},
...
],
'total_labour': 204.58,
'total_operation': 84.59,
'total_material': 76.50,
'total_cost': 365.67,
'gross_profit': 84.33,
'margin_pct': 19.0,
}
"""
self.ensure_one()
mo = self.production_id
# Revenue from sale order
revenue = 0
if mo.origin:
so = self.env['sale.order'].search([('name', '=', mo.origin)], limit=1)
if so:
revenue = sum(so.order_line.mapped('price_subtotal'))
# Station costs from all WOs
station_costs = []
total_labour = 0
total_operation = 0
for wo in mo.workorder_ids.sorted('sequence'):
rate = wo.costs_hour or wo.workcenter_id.costs_hour or 0
dur_hours = (wo.duration or 0) / 60.0
labour = dur_hours * rate
# Operation cost (dwell time based)
op_rate = rate * 0.5 # simplified: operation = 50% of labour rate
dwell = getattr(wo, 'x_fc_dwell_time_minutes', 0) or 0
op_cost = (dwell / 60.0) * op_rate
total = labour + op_cost
# Duration display
wo_dur = wo.duration or 0
if wo_dur >= 60:
wo_dur_display = f"{int(wo_dur // 60)}h {int(wo_dur % 60)}m"
else:
wo_dur_display = f"{int(wo_dur)}m"
station_costs.append({
'wo_id': wo.id,
'station': wo.workcenter_id.name or wo.name,
'rate': rate,
'duration_minutes': round(wo_dur, 1),
'duration_display': wo_dur_display,
'labour_cost': round(labour, 2),
'operation_cost': round(op_cost, 2),
'total': round(total, 2),
'state': wo.state,
})
total_labour += labour
total_operation += op_cost
# Material cost
total_material = sum(
m.product_id.standard_price * m.quantity
for m in mo.move_raw_ids
if m.state == 'done'
) if hasattr(mo, 'move_raw_ids') else 0
total_cost = total_labour + total_operation + total_material
gross_profit = revenue - total_cost
margin_pct = (gross_profit / revenue * 100) if revenue else 0
return {
'revenue': round(revenue, 2),
'station_costs': station_costs,
'total_labour': round(total_labour, 2),
'total_operation': round(total_operation, 2),
'total_material': round(total_material, 2),
'total_cost': round(total_cost, 2),
'gross_profit': round(gross_profit, 2),
'margin_pct': round(margin_pct, 1),
}
# ------------------------------------------------------------------
# Quality data (holds + NCRs)
# ------------------------------------------------------------------
def get_quality_data(self):
"""Return quality holds and linked NCRs for this WO."""
self.ensure_one()
holds = []
ncrs = []
Hold = self.env.get('fusion.plating.quality.hold')
if Hold and 'workorder_id' in Hold._fields:
for h in Hold.search([('workorder_id', '=', self.id)]):
holds.append({
'id': h.id,
'name': h.name,
'state': h.state,
'qty': h.qty_on_hold,
'reason': h.hold_reason,
'part_ref': h.part_ref or '',
})
NCR = self.env.get('fusion.plating.ncr')
if NCR:
bath_ids = self.production_id.workorder_ids.mapped('x_fc_bath_id').ids
if bath_ids:
for n in NCR.search([('bath_id', 'in', bath_ids)]):
ncrs.append({
'id': n.id,
'name': n.name,
'state': n.state,
'severity': n.severity,
'part_ref': n.part_ref or '',
})
return {'holds': holds, 'ncrs': ncrs}

View File

@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fp_bridge_mrp_workcenter_manager,fp.bridge.mrp.workcenter.manager,mrp.model_mrp_workcenter,fusion_plating.group_fusion_plating_manager,1,1,1,0
access_fp_bridge_mrp_workcenter_supervisor,fp.bridge.mrp.workcenter.supervisor,mrp.model_mrp_workcenter,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_bridge_mrp_workorder_manager,fp.bridge.mrp.workorder.manager,mrp_workorder.model_mrp_workorder,fusion_plating.group_fusion_plating_manager,1,1,1,0
access_fp_bridge_mrp_workorder_supervisor,fp.bridge.mrp.workorder.supervisor,mrp_workorder.model_mrp_workorder,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_bridge_mrp_production_manager,fp.bridge.mrp.production.manager,mrp.model_mrp_production,fusion_plating.group_fusion_plating_manager,1,1,1,0
access_fp_bridge_mrp_production_supervisor,fp.bridge.mrp.production.supervisor,mrp.model_mrp_production,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fp_bridge_mrp_workcenter_manager fp.bridge.mrp.workcenter.manager mrp.model_mrp_workcenter fusion_plating.group_fusion_plating_manager 1 1 1 0
3 access_fp_bridge_mrp_workcenter_supervisor fp.bridge.mrp.workcenter.supervisor mrp.model_mrp_workcenter fusion_plating.group_fusion_plating_supervisor 1 0 0 0
4 access_fp_bridge_mrp_workorder_manager fp.bridge.mrp.workorder.manager mrp_workorder.model_mrp_workorder fusion_plating.group_fusion_plating_manager 1 1 1 0
5 access_fp_bridge_mrp_workorder_supervisor fp.bridge.mrp.workorder.supervisor mrp_workorder.model_mrp_workorder fusion_plating.group_fusion_plating_supervisor 1 0 0 0
6 access_fp_bridge_mrp_production_manager fp.bridge.mrp.production.manager mrp.model_mrp_production fusion_plating.group_fusion_plating_manager 1 1 1 0
7 access_fp_bridge_mrp_production_supervisor fp.bridge.mrp.production.supervisor mrp.model_mrp_production fusion_plating.group_fusion_plating_supervisor 1 0 0 0

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Extend batch form with work order M2M when MRP bridge is installed.
-->
<odoo>
<record id="view_fp_batch_form_mrp_bridge" model="ir.ui.view">
<field name="name">fusion.plating.batch.form.mrp.bridge</field>
<field name="model">fusion.plating.batch</field>
<field name="inherit_id" ref="fusion_plating_batch.view_fp_batch_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='chemistry_ids']/.." position="after">
<page string="Work Orders" name="work_orders">
<field name="workorder_ids" widget="many2many_tags"/>
<p class="text-muted" invisible="workorder_ids">
Link MRP work orders processed in this batch for full traceability.
</p>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,28 @@
<?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.
Extend the quality hold form with MRP fields when the bridge is installed.
-->
<odoo>
<record id="view_fp_quality_hold_form_mrp_bridge" model="ir.ui.view">
<field name="name">fusion.plating.quality.hold.form.mrp.bridge</field>
<field name="model">fusion.plating.quality.hold</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_quality_hold_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='resolution']" position="before">
<group string="Source"
invisible="not workorder_id and not production_id and not portal_job_id">
<field name="workorder_id" readonly="1"/>
<field name="production_id" readonly="1"/>
<field name="portal_job_id" readonly="1"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,31 @@
<?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>
<!-- Extend mrp.production form: add Fusion Plating fields -->
<record id="view_mrp_production_form_fp_bridge" model="ir.ui.view">
<field name="name">mrp.production.form.fp.bridge</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<group string="Fusion Plating" name="fusion_plating">
<group>
<field name="x_fc_customer_spec_id"/>
<field name="x_fc_facility_id"/>
</group>
<group>
<field name="x_fc_portal_job_id"/>
</group>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,28 @@
<?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>
<!-- Extend mrp.workcenter form: add Fusion Plating group -->
<record id="view_mrp_workcenter_form_fp_bridge" model="ir.ui.view">
<field name="name">mrp.workcenter.form.fp.bridge</field>
<field name="model">mrp.workcenter</field>
<field name="inherit_id" ref="mrp.mrp_workcenter_view"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<group string="Fusion Plating" name="fusion_plating">
<group>
<field name="x_fc_facility_id"/>
<field name="x_fc_fp_work_center_id"/>
</group>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,200 @@
<?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.
Hub & Spoke layout for Work Order form:
- Smart buttons (SO, MO, Portal Job, Quality Holds, Deliveries)
- Process flow bar with step badge + tree action
- 5-tab notebook: Time & Cost | Plating Details | Quality | Components | Blocked By
- Chatter
-->
<odoo>
<record id="view_mrp_workorder_form_fp_bridge" model="ir.ui.view">
<field name="name">mrp.workorder.form.fp.bridge</field>
<field name="model">mrp.workorder</field>
<field name="inherit_id" ref="mrp.mrp_production_workorder_form_view_inherit"/>
<field name="arch" type="xml">
<!-- ============================================================
1. HIDDEN COMPUTED FIELDS (inside sheet, after company_id)
============================================================ -->
<xpath expr="//sheet//field[@name='company_id']" position="after">
<field name="x_fc_sale_order_id" invisible="1"/>
<field name="x_fc_portal_job_id" invisible="1"/>
<field name="x_fc_sale_order_name" invisible="1"/>
<field name="x_fc_production_name" invisible="1"/>
<field name="x_fc_step_number" invisible="1"/>
<field name="x_fc_total_steps" invisible="1"/>
</xpath>
<!-- ============================================================
2. SMART BUTTONS (inside button_box)
============================================================ -->
<xpath expr="//div[@name='button_box']" position="inside">
<!-- Sale Order -->
<button name="action_view_sale_order" type="object"
class="oe_stat_button" icon="fa-file-text-o"
invisible="not x_fc_sale_order_id">
<div class="o_stat_info">
<span class="o_stat_value">
<field name="x_fc_sale_order_name" nolabel="1"/>
</span>
<span class="o_stat_text">Sale Order</span>
</div>
</button>
<!-- Manufacturing Order -->
<button name="action_view_manufacturing_order" type="object"
class="oe_stat_button" icon="fa-industry">
<div class="o_stat_info">
<span class="o_stat_value">
<field name="x_fc_production_name" nolabel="1"/>
</span>
<span class="o_stat_text">Manufacturing</span>
</div>
</button>
<!-- Portal Job -->
<button name="action_view_portal_job" type="object"
class="oe_stat_button" icon="fa-globe"
invisible="not x_fc_portal_job_id">
<div class="o_stat_info">
<span class="o_stat_text">Portal Job</span>
</div>
</button>
<!-- Quality Holds -->
<button name="action_view_quality_holds" type="object"
class="oe_stat_button" icon="fa-exclamation-triangle">
<field name="x_fc_quality_hold_count" widget="statinfo"
string="Quality Holds"/>
</button>
<!-- Deliveries -->
<button name="action_view_deliveries" type="object"
class="oe_stat_button" icon="fa-truck">
<field name="x_fc_delivery_count" widget="statinfo"
string="Deliveries"/>
</button>
</xpath>
<!-- ============================================================
3. CUSTOMER FIELD — left column, after workcenter_id
============================================================ -->
<xpath expr="//sheet//field[@name='workcenter_id']" position="after">
<field name="x_fc_customer_id" readonly="1"
options="{'no_open': False}"/>
</xpath>
<!-- ============================================================
3b. STEP BADGE + PRIORITY — right column, after production_id
============================================================ -->
<xpath expr="//sheet//field[@name='production_id']" position="after">
<field name="x_fc_step_display" widget="badge" readonly="1"/>
<field name="x_fc_priority" widget="priority"/>
</xpath>
<!-- ============================================================
4. PROCESS FLOW BAR (before notebook)
============================================================ -->
<xpath expr="//notebook" position="before">
<div class="o_fp_process_flow mb-3">
<div class="d-flex align-items-center gap-2 px-3 py-2 bg-light rounded">
<i class="fa fa-random text-primary"/>
<span class="fw-bold">Process Flow:</span>
<field name="x_fc_step_display" widget="badge"
readonly="1" nolabel="1"/>
<span class="text-muted">|</span>
<span class="text-muted small">
Click "View Process Tree" for full routing
</span>
<button name="action_view_process_tree" type="object"
string="View Process Tree"
class="btn btn-sm btn-outline-primary ms-auto"
icon="fa-sitemap"/>
</div>
</div>
</xpath>
<!-- ============================================================
5. NOTEBOOK — restructured tabs
============================================================ -->
<!-- 5a. Rename "Time Tracking" → "Time &amp; Cost" and add cost summary -->
<xpath expr="//notebook/page[@name='time_tracking']" position="attributes">
<attribute name="string">Time &amp; Cost</attribute>
</xpath>
<xpath expr="//notebook/page[@name='time_tracking']" position="inside">
<group string="Cost Summary" name="cost_summary">
<group>
<field name="x_fc_workcenter_cost_hour"
string="Station Rate ($/hr)" readonly="1"/>
<field name="duration" widget="float_time"
string="Actual Duration" readonly="1"/>
</group>
<group>
<field name="duration_expected" widget="float_time"
string="Expected Duration" readonly="1"/>
</group>
</group>
</xpath>
<!-- 5b. Plating Details tab (insert AFTER Time & Cost) -->
<xpath expr="//notebook/page[@name='time_tracking']" position="after">
<page string="Plating Details" name="plating_details">
<group>
<group string="Bath &amp; Tank">
<field name="x_fc_facility_id"/>
<field name="x_fc_bath_id"/>
<field name="x_fc_tank_id"/>
<field name="x_fc_rack_ref"/>
</group>
<group string="Process Parameters">
<field name="x_fc_thickness_target"/>
<field name="x_fc_thickness_uom"/>
<field name="x_fc_dwell_time_minutes"/>
</group>
</group>
</page>
</xpath>
<!-- 5c. Quality tab (insert AFTER Plating Details) -->
<xpath expr="//notebook/page[@name='plating_details']" position="after">
<page string="Quality" name="quality">
<div class="mb-3">
<button name="action_view_quality_holds" type="object"
string="View Quality Holds"
class="btn btn-sm btn-outline-warning"
icon="fa-exclamation-triangle"/>
</div>
<p class="text-muted" invisible="x_fc_quality_hold_count > 0">
No quality holds on this work order.
</p>
<div invisible="x_fc_quality_hold_count == 0">
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle me-1"/>
<strong>
<field name="x_fc_quality_hold_count" nolabel="1"/>
quality hold(s)
</strong>
on this work order. Click the button above to review.
</div>
</div>
</page>
</xpath>
<!-- 5d. Components tab already exists at name="components" — no change needed -->
<!-- 5e. Blocked By tab — keep existing, just push it last visually
(it's already the last page in the base view, so no reorder needed) -->
<!-- ============================================================
6. CHATTER (after sheet)
============================================================ -->
<xpath expr="//sheet" position="after">
<chatter/>
</xpath>
</field>
</record>
</odoo>