changes
This commit is contained in:
@@ -11,9 +11,9 @@ from . import fp_job_step
|
||||
from . import fp_job_node_override
|
||||
from . import fp_portal_job
|
||||
from . import account_move
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import res_users
|
||||
|
||||
# Phase 3 — parallel job/step links on dependent modules' models.
|
||||
from . import fp_batch
|
||||
|
||||
@@ -367,6 +367,16 @@ class FpJobStep(models.Model):
|
||||
if cr_action:
|
||||
return cr_action
|
||||
|
||||
# Racking step routing — same idea as Contract Review. If the
|
||||
# operator clicks Finish on a Racking step but the linked
|
||||
# racking inspection isn't done yet, route them straight to
|
||||
# the inspection form instead of throwing a "find the smart
|
||||
# button" error message. They complete the line check-off,
|
||||
# mark Done, and re-click Finish & Next to advance.
|
||||
ri_action = self._fp_racking_inspection_redirect()
|
||||
if ri_action:
|
||||
return ri_action
|
||||
|
||||
# Prompt-first behaviour: show the Record Inputs dialog when the
|
||||
# recipe step has authored prompts and nothing has been captured
|
||||
# in this run. Bypass when context flag is set (i.e. we're being
|
||||
@@ -631,15 +641,34 @@ class FpJobStep(models.Model):
|
||||
def _fp_open_contract_review(self):
|
||||
"""Auto-create the QA-005 form for this step's part if missing,
|
||||
return the act_window pointing at it. Called from button_start
|
||||
on Contract Review steps."""
|
||||
on Contract Review steps.
|
||||
|
||||
Returns None when the review is already satisfied (state
|
||||
'complete' or 'dismissed') — letting button_start fall through
|
||||
to the standard path so the step starts directly, without an
|
||||
unnecessary detour through an already-signed form. This mirrors
|
||||
the Finish & Next redirect behaviour: once contract review is
|
||||
cleared for a part, neither Start nor Finish stops to ask
|
||||
about it again.
|
||||
|
||||
Also short-circuits when the customer doesn't require contract
|
||||
review and via the manager-bypass context flag, to keep entry
|
||||
and finish gates in lockstep.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.env.context.get('fp_skip_contract_review_gate'):
|
||||
return None
|
||||
part = self._fp_resolve_contract_review_part()
|
||||
if not part:
|
||||
return None
|
||||
if not part.partner_id.x_fc_contract_review_required:
|
||||
return None
|
||||
Review = self.env.get('fp.contract.review')
|
||||
if Review is None:
|
||||
return None # quality module not installed — skip
|
||||
review = part.x_fc_contract_review_id
|
||||
if review and review.state in ('complete', 'dismissed'):
|
||||
return None # already satisfied — fall through to normal start
|
||||
if not review:
|
||||
review = Review.sudo().create({
|
||||
'part_id': part.id,
|
||||
@@ -767,6 +796,46 @@ class FpJobStep(models.Model):
|
||||
'name': _('Racking Inspection — %s') % self.job_id.name,
|
||||
}
|
||||
|
||||
def _fp_racking_inspection_redirect(self):
|
||||
"""Return an act_window opening the linked racking inspection
|
||||
form, or False to indicate "no redirect needed".
|
||||
|
||||
Mirrors ``_fp_contract_review_redirect``. Triggers when:
|
||||
* this step is a Racking step (matched by ``_fp_is_racking_step``)
|
||||
* the linked ``fp.racking.inspection`` exists and is NOT yet in
|
||||
a terminal state (``done`` / ``discrepancy_flagged``)
|
||||
|
||||
When the inspection is already terminal — or doesn't exist at
|
||||
all — returns False so action_finish_and_advance falls through
|
||||
to the normal finish path. The hard gate
|
||||
(``_fp_check_racking_inspection_complete``) still fires from
|
||||
``button_finish`` for any caller that bypasses the redirect.
|
||||
|
||||
Manager bypass via ``fp_skip_racking_inspection_gate=True``.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.env.context.get('fp_skip_racking_inspection_gate'):
|
||||
return False
|
||||
if not self._fp_is_racking_step():
|
||||
return False
|
||||
if 'fp.racking.inspection' not in self.env:
|
||||
return False
|
||||
ri = self.job_id.racking_inspection_id
|
||||
if not ri:
|
||||
# No inspection record at all — let the soft gate handle
|
||||
# this with a chatter warning, don't redirect.
|
||||
return False
|
||||
if ri.state in ('done', 'discrepancy_flagged'):
|
||||
return False
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fp.racking.inspection',
|
||||
'res_id': ri.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
'name': _('Racking Inspection — %s') % self.job_id.name,
|
||||
}
|
||||
|
||||
def _fp_check_racking_inspection_complete(self):
|
||||
"""Soft gate — block button_finish on a Racking step until the
|
||||
linked inspection is in a terminal state. discrepancy_flagged
|
||||
@@ -939,32 +1008,51 @@ class FpJobStep(models.Model):
|
||||
"""Return an ir.actions.act_window opening the part's QA-005
|
||||
Contract Review form, or False to indicate "no redirect needed".
|
||||
|
||||
Triggers when:
|
||||
* the recipe node is flagged default_kind='contract_review', AND
|
||||
* the linked part has no review yet OR the review is still in
|
||||
a non-terminal state (draft / assistant_review / manager_review).
|
||||
Triggers when ALL of these are true:
|
||||
* the step is a Contract Review step (matched via
|
||||
``_fp_is_contract_review_step`` — name OR template kind OR
|
||||
node kind, same as the finish-time gate),
|
||||
* the customer requires contract review
|
||||
(``partner.x_fc_contract_review_required = True``), AND
|
||||
* the linked part either has no review yet OR the review is
|
||||
still in a non-terminal state (draft / assistant_review /
|
||||
manager_review).
|
||||
|
||||
Once the review reaches state 'complete' or 'dismissed' the step
|
||||
is allowed to finish through the normal path, which is how the
|
||||
operator clears the contract-review gate after signing QA-005.
|
||||
Once the review reaches state 'complete' or 'dismissed' the
|
||||
step is allowed to finish through the normal path. This is how
|
||||
Finish & Next moves on to the next step automatically once the
|
||||
contract review is already satisfied for that part — including
|
||||
when the review was completed on a previous order.
|
||||
|
||||
Soft-fail: if the job has no part_catalog_id we cannot route to
|
||||
a per-part review, so we fall through to the standard wizard
|
||||
rather than blocking the operator.
|
||||
Resolution mirrors ``_fp_check_contract_review_complete`` so a
|
||||
single source of truth governs both ENTRY (this redirect) and
|
||||
FINISH (the gate) — they always agree on whether a step is a
|
||||
contract review and which part it's bound to.
|
||||
|
||||
Soft-fail: if no part can be resolved we fall through to the
|
||||
standard wizard rather than blocking the operator.
|
||||
"""
|
||||
self.ensure_one()
|
||||
node = self.recipe_node_id
|
||||
if not node or node.default_kind != 'contract_review':
|
||||
# Manager bypass — same context flag the gate honours.
|
||||
if self.env.context.get('fp_skip_contract_review_gate'):
|
||||
return False
|
||||
part = self.job_id.part_catalog_id
|
||||
if not self._fp_is_contract_review_step():
|
||||
return False
|
||||
part = self._fp_resolve_contract_review_part() \
|
||||
or self.job_id.part_catalog_id
|
||||
if not part:
|
||||
_logger.warning(
|
||||
"Contract-review step '%s' on job %s has no part_catalog_id "
|
||||
"— cannot redirect to QA-005 form, falling through to "
|
||||
"Contract-review step '%s' on job %s has no part — "
|
||||
"cannot redirect to QA-005 form, falling through to "
|
||||
"standard wizard.",
|
||||
self.name, self.job_id.name,
|
||||
)
|
||||
return False
|
||||
# Customer flag check — when the customer doesn't require
|
||||
# contract review, the redirect doesn't fire and the step
|
||||
# finishes through the normal path. Matches the gate's policy.
|
||||
if not part.partner_id.x_fc_contract_review_required:
|
||||
return False
|
||||
review = part.x_fc_contract_review_id
|
||||
if review and review.state in ('complete', 'dismissed'):
|
||||
return False
|
||||
@@ -1022,6 +1110,28 @@ class FpJobStep(models.Model):
|
||||
related='recipe_node_id.collect_measurements',
|
||||
readonly=True,
|
||||
)
|
||||
# Job context related fields — used by the quick-look modal so the
|
||||
# operator can see which job / customer / part / qty this step
|
||||
# belongs to without opening the parent job form. Related (not
|
||||
# stored) so they always reflect the live job record.
|
||||
quick_look_partner_id = fields.Many2one(
|
||||
'res.partner', string='Customer',
|
||||
related='job_id.partner_id', readonly=True,
|
||||
)
|
||||
quick_look_part_catalog_id = fields.Many2one(
|
||||
'fp.part.catalog', string='Part',
|
||||
related='job_id.part_catalog_id', readonly=True,
|
||||
)
|
||||
quick_look_qty = fields.Float(
|
||||
string='Order Qty',
|
||||
related='job_id.qty', readonly=True,
|
||||
)
|
||||
quick_look_instruction_attachment_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
string='Instruction Images',
|
||||
related='recipe_node_id.instruction_attachment_ids',
|
||||
readonly=True,
|
||||
)
|
||||
quick_look_prompt_ids = fields.Many2many(
|
||||
'fusion.plating.process.node.input',
|
||||
string='Prompts',
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# x_fc_use_native_jobs — company-level setting that controls whether
|
||||
# SO confirmation creates a native fp.job record (this module) or
|
||||
# the legacy mrp.production / mrp.workorder records (bridge_mrp).
|
||||
#
|
||||
# Default: False (legacy MO flow). Phase 9 cutover flips this to True
|
||||
# on entech.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
x_fc_use_native_jobs = fields.Boolean(
|
||||
string='Use Native Plating Jobs',
|
||||
config_parameter='fusion_plating_jobs.use_native_jobs',
|
||||
help='When enabled, SO confirmation creates fp.job records '
|
||||
'instead of mrp.production. Phase-2 migration toggle.',
|
||||
)
|
||||
40
fusion_plating/fusion_plating_jobs/models/res_users.py
Normal file
40
fusion_plating/fusion_plating_jobs/models/res_users.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- 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 ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
x_fc_initials = fields.Char(
|
||||
string='Plating Initials',
|
||||
help='Operator / inspector initials used to pre-fill signature '
|
||||
'and "Reviewer Initials" style prompts in the Record Inputs '
|
||||
'dialog. Editable in the dialog itself — when the user types '
|
||||
'a different value and saves, it persists here for every '
|
||||
'future job and step.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _fp_default_initials(self):
|
||||
"""Best-effort initials derived from the user's display name.
|
||||
|
||||
Used as a fallback when ``x_fc_initials`` is empty so the
|
||||
operator still gets a sensible pre-fill on their first run.
|
||||
E.g. "John Doe" -> "JD", "Mary Anne Smith" -> "MAS".
|
||||
"""
|
||||
name = (self.name or '').strip()
|
||||
if not name:
|
||||
return ''
|
||||
return ''.join(
|
||||
piece[0] for piece in name.split() if piece
|
||||
).upper()[:6]
|
||||
|
||||
def fp_get_initials(self):
|
||||
"""Resolve the user's initials for the dialog: stored override
|
||||
first, fall back to the auto-derived value from their name."""
|
||||
self.ensure_one()
|
||||
return self.x_fc_initials or self._fp_default_initials()
|
||||
@@ -2,12 +2,10 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# sale.order.action_confirm hook — creates fp.job records when the
|
||||
# x_fc_use_native_jobs setting is True. Mirrors bridge_mrp's
|
||||
# _fp_auto_create_mo but creates fp.job instead of mrp.production.
|
||||
#
|
||||
# When the setting is False (default), this hook is a no-op and
|
||||
# bridge_mrp's MO-creation hook handles the flow.
|
||||
# sale.order.action_confirm hook — creates fp.job records on confirm.
|
||||
# Sub 11 (2026-04-26) removed MRP entirely; fp.job is the only fulfilment
|
||||
# path. The former x_fc_use_native_jobs migration toggle was dropped in
|
||||
# 19.0.8.19.0 once the legacy bridge_mrp fallback became unreachable.
|
||||
|
||||
import logging
|
||||
|
||||
@@ -82,18 +80,7 @@ class SaleOrder(models.Model):
|
||||
)
|
||||
|
||||
def _compute_workflow_stage(self):
|
||||
"""Native-jobs override — walks fp.job state instead of mrp.production.
|
||||
|
||||
When `use_native_jobs` is on, the SO is fulfilled by `fp.job`
|
||||
records, not MRP MOs. The bridge_mrp compute reads `mrp.production`
|
||||
and would falsely stall the banner. We branch at the top: native
|
||||
mode → fp.job walker; legacy mode → super() (bridge_mrp).
|
||||
"""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
native = ICP.get_param('fusion_plating_jobs.use_native_jobs') == 'True'
|
||||
if not native:
|
||||
return super()._compute_workflow_stage()
|
||||
|
||||
"""Walk fp.job state to derive the SO workflow banner."""
|
||||
Job = self.env['fp.job']
|
||||
Delivery = self.env.get('fusion.plating.delivery')
|
||||
for so in self:
|
||||
@@ -201,27 +188,24 @@ class SaleOrder(models.Model):
|
||||
|
||||
def action_confirm(self):
|
||||
result = super().action_confirm()
|
||||
# Only run when the native flag is on
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
if ICP.get_param('fusion_plating_jobs.use_native_jobs') == 'True':
|
||||
for so in self:
|
||||
so._fp_auto_create_job()
|
||||
# Auto-confirm any draft jobs we just created so steps
|
||||
# generate immediately (no manager click required).
|
||||
# Best-effort: an exception in side-effects shouldn't
|
||||
# block the SO confirm itself.
|
||||
draft_jobs = self.env['fp.job'].sudo().search([
|
||||
('sale_order_id', '=', so.id),
|
||||
('state', '=', 'draft'),
|
||||
])
|
||||
for job in draft_jobs:
|
||||
try:
|
||||
job.action_confirm()
|
||||
except Exception as exc:
|
||||
so.message_post(body=_(
|
||||
'Auto-confirm of fp.job %(job)s failed: %(err)s. '
|
||||
'Confirm manually from the job form.'
|
||||
) % {'job': job.name, 'err': exc})
|
||||
for so in self:
|
||||
so._fp_auto_create_job()
|
||||
# Auto-confirm any draft jobs we just created so steps
|
||||
# generate immediately (no manager click required).
|
||||
# Best-effort: an exception in side-effects shouldn't
|
||||
# block the SO confirm itself.
|
||||
draft_jobs = self.env['fp.job'].sudo().search([
|
||||
('sale_order_id', '=', so.id),
|
||||
('state', '=', 'draft'),
|
||||
])
|
||||
for job in draft_jobs:
|
||||
try:
|
||||
job.action_confirm()
|
||||
except Exception as exc:
|
||||
so.message_post(body=_(
|
||||
'Auto-confirm of fp.job %(job)s failed: %(err)s. '
|
||||
'Confirm manually from the job form.'
|
||||
) % {'job': job.name, 'err': exc})
|
||||
return result
|
||||
|
||||
def _fp_auto_create_job(self):
|
||||
|
||||
Reference in New Issue
Block a user