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.16.9',
|
||||
'version': '19.0.10.18.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
@@ -57,11 +57,11 @@ full design rationale and §6.2 of the implementation plan for task list.
|
||||
# so the statusbar's m2o has its targets available at view-render time).
|
||||
'data/fp_workflow_state_data.xml',
|
||||
'views/fp_workflow_state_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/fp_job_step_quick_look_views.xml',
|
||||
'views/fp_job_form_inherit.xml',
|
||||
'views/fp_job_quality_buttons.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/fp_receiving_views.xml',
|
||||
'views/fp_certificate_views.xml',
|
||||
'views/fp_job_consumption_views.xml',
|
||||
'views/fp_step_priority_views.xml',
|
||||
|
||||
@@ -22,6 +22,7 @@ from . import fp_certificate
|
||||
from . import fp_thickness_reading
|
||||
from . import fp_delivery
|
||||
from . import fp_racking_inspection
|
||||
from . import fp_receiving
|
||||
|
||||
# Phase 4 — light refactors batch B (notifications, KPI source tag).
|
||||
from . import fp_notification_trigger
|
||||
|
||||
@@ -137,10 +137,13 @@ class AccountMove(models.Model):
|
||||
if not job or not job.portal_job_id:
|
||||
return
|
||||
portal = job.portal_job_id
|
||||
if 'state' in portal._fields:
|
||||
portal.state = 'complete'
|
||||
if 'invoice_ref' in portal._fields:
|
||||
portal.invoice_ref = self.name
|
||||
# Recompute state via the central helper — it'll only land on
|
||||
# 'complete' if the WO is actually done AND the shipment is
|
||||
# delivered. Posting an invoice early no longer skips the floor.
|
||||
if hasattr(portal, '_fp_recompute_portal_state'):
|
||||
portal._fp_recompute_portal_state()
|
||||
_logger.info(
|
||||
'Invoice %s linked to fp.job %s portal %s',
|
||||
self.name, job.name, portal.name,
|
||||
|
||||
@@ -745,16 +745,10 @@ class FpJob(models.Model):
|
||||
'name': self.portal_job_id.name,
|
||||
}
|
||||
|
||||
# fp.job.state -> fusion.plating.portal.job.state mapping. Kept tight so
|
||||
# the customer doesn't see internal states. Anything not in this map
|
||||
# leaves the portal_job state alone (e.g. 'on_hold' stays in_progress).
|
||||
_FP_JOB_STATE_TO_PORTAL_STATE = {
|
||||
'confirmed': 'received',
|
||||
'in_progress': 'in_progress',
|
||||
'done': 'ready_to_ship',
|
||||
# 'on_hold' and 'cancelled' intentionally omitted — managers choose
|
||||
# what to surface to the customer.
|
||||
}
|
||||
# Sub-portal state sync — see fusion_plating_portal/.../fp_portal_job.py
|
||||
# `_fp_recompute_portal_state` for the rules. The mapping table that
|
||||
# used to live here was replaced by the helper so shipment / invoice
|
||||
# signals can't drift away from the WO state any more.
|
||||
|
||||
def write(self, vals):
|
||||
"""Write hook: (a) when qty_scrapped INCREASES, auto-spawn a
|
||||
@@ -783,13 +777,13 @@ class FpJob(models.Model):
|
||||
if job.state != new_state:
|
||||
state_changed_ids.add(job.id)
|
||||
result = super().write(vals)
|
||||
# Mirror state to portal_job for records that actually changed.
|
||||
# Mirror state to portal_job via the central recompute helper, so
|
||||
# the portal state always derives from the WO + shipment + invoice
|
||||
# together rather than the most-recent event flag.
|
||||
if state_changed_ids:
|
||||
target = self._FP_JOB_STATE_TO_PORTAL_STATE.get(vals.get('state'))
|
||||
if target:
|
||||
for job in self.filtered(lambda j: j.id in state_changed_ids):
|
||||
if job.portal_job_id and job.portal_job_id.state != target:
|
||||
job.portal_job_id.sudo().write({'state': target})
|
||||
for job in self.filtered(lambda j: j.id in state_changed_ids):
|
||||
if job.portal_job_id:
|
||||
job.portal_job_id._fp_recompute_portal_state()
|
||||
if not scrap_deltas:
|
||||
return result
|
||||
Hold = (self.env['fusion.plating.quality.hold']
|
||||
|
||||
67
fusion_plating/fusion_plating_jobs/models/fp_receiving.py
Normal file
67
fusion_plating/fusion_plating_jobs/models/fp_receiving.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Adds the Work Order smart button + header action to fp.receiving so
|
||||
# the receiving form mirrors the SO's WO entry point. Button appears
|
||||
# once the receiving is closed and stays until every linked fp.job
|
||||
# reaches state='done'.
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class FpReceiving(models.Model):
|
||||
_inherit = 'fp.receiving'
|
||||
|
||||
x_fc_fp_job_count = fields.Integer(
|
||||
string='Work Orders',
|
||||
compute='_compute_fp_job_count',
|
||||
)
|
||||
x_fc_show_work_order_btn = fields.Boolean(
|
||||
string='Show Work Order Button',
|
||||
compute='_compute_show_work_order_btn',
|
||||
help='True once this receiving is closed and at least one linked '
|
||||
'work order is still open (state != done). Hidden again '
|
||||
'when every job is done.',
|
||||
)
|
||||
|
||||
def _compute_fp_job_count(self):
|
||||
Job = self.env['fp.job'].sudo()
|
||||
for rec in self:
|
||||
if rec.sale_order_id:
|
||||
rec.x_fc_fp_job_count = Job.search_count(
|
||||
[('sale_order_id', '=', rec.sale_order_id.id)]
|
||||
)
|
||||
else:
|
||||
rec.x_fc_fp_job_count = 0
|
||||
|
||||
def _compute_show_work_order_btn(self):
|
||||
Job = self.env['fp.job'].sudo()
|
||||
for rec in self:
|
||||
if rec.state != 'closed' or not rec.sale_order_id:
|
||||
rec.x_fc_show_work_order_btn = False
|
||||
continue
|
||||
jobs = Job.search([('sale_order_id', '=', rec.sale_order_id.id)])
|
||||
rec.x_fc_show_work_order_btn = bool(jobs) and any(
|
||||
j.state != 'done' for j in jobs
|
||||
)
|
||||
|
||||
def action_view_fp_jobs(self):
|
||||
"""Open the work order(s) linked to this receiving's sale order."""
|
||||
self.ensure_one()
|
||||
if not self.sale_order_id:
|
||||
return False
|
||||
jobs = self.env['fp.job'].search([
|
||||
('sale_order_id', '=', self.sale_order_id.id),
|
||||
])
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Work Orders'),
|
||||
'res_model': 'fp.job',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('sale_order_id', '=', self.sale_order_id.id)],
|
||||
'context': {'default_sale_order_id': self.sale_order_id.id},
|
||||
}
|
||||
if len(jobs) == 1:
|
||||
action.update({'view_mode': 'form', 'res_id': jobs.id})
|
||||
return action
|
||||
@@ -24,6 +24,13 @@ class SaleOrder(models.Model):
|
||||
string='Work Orders',
|
||||
compute='_compute_fp_job_count',
|
||||
)
|
||||
x_fc_show_work_order_btn = fields.Boolean(
|
||||
string='Show Work Order Header Button',
|
||||
compute='_compute_show_work_order_btn',
|
||||
help='True once any receiving record on this SO has closed and '
|
||||
'at least one work order is still open (state != done). '
|
||||
'Hidden again when every WO is done.',
|
||||
)
|
||||
x_fc_fp_certificate_count = fields.Integer(
|
||||
string='Certificates',
|
||||
compute='_compute_fp_certificate_count',
|
||||
@@ -114,6 +121,25 @@ class SaleOrder(models.Model):
|
||||
[('sale_order_id', '=', so.id)]
|
||||
)
|
||||
|
||||
def _compute_show_work_order_btn(self):
|
||||
Job = self.env['fp.job'].sudo()
|
||||
Recv = self.env.get('fp.receiving')
|
||||
for so in self:
|
||||
if Recv is None:
|
||||
so.x_fc_show_work_order_btn = False
|
||||
continue
|
||||
has_closed_recv = bool(Recv.sudo().search_count([
|
||||
('sale_order_id', '=', so.id),
|
||||
('state', '=', 'closed'),
|
||||
]))
|
||||
if not has_closed_recv:
|
||||
so.x_fc_show_work_order_btn = False
|
||||
continue
|
||||
jobs = Job.search([('sale_order_id', '=', so.id)])
|
||||
so.x_fc_show_work_order_btn = bool(jobs) and any(
|
||||
j.state != 'done' for j in jobs
|
||||
)
|
||||
|
||||
def _compute_fp_certificate_count(self):
|
||||
Cert = self.env['fp.certificate'].sudo()
|
||||
for so in self:
|
||||
|
||||
@@ -63,15 +63,10 @@
|
||||
<field name="all_steps_terminal" invisible="1"/>
|
||||
<field name="next_milestone_action" invisible="1"/>
|
||||
<button name="action_print_sticker" type="object"
|
||||
string="Print Sticker"
|
||||
class="btn-secondary"
|
||||
icon="fa-tag"
|
||||
invisible="state == 'draft'"/>
|
||||
<button name="action_print_wo_detail" type="object"
|
||||
string="Print WO Detail"
|
||||
class="btn-secondary"
|
||||
icon="fa-file-text-o"
|
||||
invisible="state in ('draft', 'cancelled')"/>
|
||||
icon="fa-qrcode"
|
||||
invisible="state == 'draft'"
|
||||
help="Print Sticker"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Sub 14 — Replace the generic Draft/Confirmed/In Progress/Done
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
Adds the Work Order smart button + header button to fp.receiving so
|
||||
the receiving form mirrors the SO's WO entry point. Header button
|
||||
appears once state == 'closed' and at least one linked fp.job is
|
||||
still open. Smart button is always visible when WOs exist.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_receiving_form_fp_jobs" model="ir.ui.view">
|
||||
<field name="name">fp.receiving.form.fp.jobs</field>
|
||||
<field name="model">fp.receiving</field>
|
||||
<field name="inherit_id" ref="fusion_plating_receiving.view_fp_receiving_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Work Order header button — only after receiving is
|
||||
closed and while at least one job is still open. -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<field name="x_fc_show_work_order_btn" invisible="1"/>
|
||||
<button name="action_view_fp_jobs"
|
||||
string="Work Order" type="object"
|
||||
class="btn-primary" icon="fa-cogs"
|
||||
invisible="not x_fc_show_work_order_btn"
|
||||
help="Open the Work Order(s) for this receiving. Hidden automatically once every linked WO is marked Done."/>
|
||||
</xpath>
|
||||
|
||||
<!-- Work Order smart button on the button_box (mirrors the
|
||||
one on the SO form). Always visible when count > 0. -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_fp_jobs" type="object"
|
||||
class="oe_stat_button" icon="fa-cogs"
|
||||
invisible="x_fc_fp_job_count == 0">
|
||||
<field name="x_fc_fp_job_count" widget="statinfo" string="WO"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -33,6 +33,19 @@
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Work Order header action — appears once any linked
|
||||
receiving is closed AND at least one WO is still open.
|
||||
Reuses the existing action_view_fp_jobs smart-button
|
||||
target so single-job SOs land on the form directly. -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<field name="x_fc_show_work_order_btn" invisible="1"/>
|
||||
<button name="action_view_fp_jobs"
|
||||
string="Work Order" type="object"
|
||||
class="btn-primary" icon="fa-cogs"
|
||||
invisible="not x_fc_show_work_order_btn"
|
||||
help="Open the Work Order(s) for this order. Hidden automatically once every linked WO is marked Done."/>
|
||||
</xpath>
|
||||
|
||||
<!-- Quote ref: small grey "Originally quoted as Q202605-200"
|
||||
line under the SO name (the big SO-30000 heading). Only
|
||||
renders once the SO has been confirmed (quote_ref is set
|
||||
|
||||
Reference in New Issue
Block a user