This commit is contained in:
gsinghpal
2026-05-22 18:01:31 -04:00
parent d127e19b45
commit f661724c72
34 changed files with 1011 additions and 59 deletions

View File

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

View File

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

View File

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

View 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

View File

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