changes
This commit is contained in:
@@ -155,6 +155,79 @@ class FpJob(models.Model):
|
||||
'name': self.sale_order_id.name,
|
||||
}
|
||||
|
||||
# All time logs across every step on this job — backs the Time Logs
|
||||
# tab on the form so the manager sees the full labour audit without
|
||||
# clicking into each step.
|
||||
time_log_ids = fields.One2many(
|
||||
'fp.job.step.timelog',
|
||||
'job_id',
|
||||
string='All Time Logs',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# 2026-04-28 — link to the auto-created Sub 8 racking inspection so
|
||||
# the job form can show a smart button + the manager can route into
|
||||
# the inspection without leaving the job screen.
|
||||
racking_inspection_ids = fields.One2many(
|
||||
'fp.racking.inspection',
|
||||
'x_fc_job_id',
|
||||
string='Racking Inspections',
|
||||
)
|
||||
racking_inspection_id = fields.Many2one(
|
||||
'fp.racking.inspection',
|
||||
string='Racking Inspection',
|
||||
compute='_compute_racking_inspection',
|
||||
store=False,
|
||||
help='The single racking inspection scoped to this job (Sub 8 '
|
||||
'enforces uniqueness). Smart button on the form routes here.',
|
||||
)
|
||||
# Computed alongside racking_inspection_id so views can render the
|
||||
# state badge without needing a related-on-non-stored field (which
|
||||
# the ORM rejects). Selection mirrors fp.racking.inspection.state.
|
||||
racking_inspection_state = fields.Selection(
|
||||
[('draft', 'Draft'),
|
||||
('inspecting', 'Inspecting'),
|
||||
('done', 'Done'),
|
||||
('discrepancy_flagged', 'Discrepancy Flagged')],
|
||||
string='Racking Inspection Status',
|
||||
compute='_compute_racking_inspection',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('racking_inspection_ids', 'racking_inspection_ids.state')
|
||||
def _compute_racking_inspection(self):
|
||||
for job in self:
|
||||
ri = job.racking_inspection_ids[:1]
|
||||
job.racking_inspection_id = ri
|
||||
job.racking_inspection_state = ri.state if ri else False
|
||||
|
||||
def action_view_racking_inspection(self):
|
||||
"""Open the racking inspection. Auto-create if missing (e.g. job
|
||||
was created before Sub 8 shipped, or auto-create silently failed
|
||||
at action_confirm time)."""
|
||||
self.ensure_one()
|
||||
if 'fp.racking.inspection' not in self.env:
|
||||
from odoo.exceptions import UserError
|
||||
raise UserError(_(
|
||||
'Sub 8 racking inspection module not installed. '
|
||||
'Install fusion_plating_receiving to enable.'
|
||||
))
|
||||
if not self.racking_inspection_id:
|
||||
self._fp_create_racking_inspection()
|
||||
self.invalidate_recordset(['racking_inspection_ids'])
|
||||
ri = self.racking_inspection_id or self.racking_inspection_ids[:1]
|
||||
if not ri:
|
||||
from odoo.exceptions import UserError
|
||||
raise UserError(_('Could not auto-create racking inspection.'))
|
||||
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.name,
|
||||
}
|
||||
|
||||
def action_view_steps(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
@@ -166,6 +239,53 @@ class FpJob(models.Model):
|
||||
'context': {'default_job_id': self.id},
|
||||
}
|
||||
|
||||
def action_open_move_wizard(self):
|
||||
"""Header button — opens the Move wizard pre-filled with the
|
||||
currently in-progress (or most recently in-progress) step as the
|
||||
from-step. Lets the manager move the job forward without first
|
||||
clicking into a specific step row.
|
||||
"""
|
||||
self.ensure_one()
|
||||
active_step = self.step_ids.filtered(
|
||||
lambda s: s.state == 'in_progress'
|
||||
)[:1]
|
||||
if not active_step:
|
||||
active_step = self.step_ids.filtered(
|
||||
lambda s: s.state in ('paused', 'ready')
|
||||
).sorted('sequence')[:1]
|
||||
if not active_step:
|
||||
raise UserError(_(
|
||||
'No in-progress, paused, or ready step found on this job. '
|
||||
'Either every step is done or the job is still in draft.'
|
||||
))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fp.job.step.move.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'name': _('Move Step — %s') % active_step.name,
|
||||
'context': {
|
||||
'default_from_step_id': active_step.id,
|
||||
'default_job_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_print_traveller(self):
|
||||
self.ensure_one()
|
||||
return self.env.ref(
|
||||
'fusion_plating_jobs.action_report_fp_job_traveller'
|
||||
).report_action(self)
|
||||
|
||||
def action_print_wo_detail(self):
|
||||
"""Print the Steelhead-style Work Order Detail PDF — chronological
|
||||
chain-of-custody + per-step inputs + Certified By page. Use this
|
||||
as the AS9100/Nadcap shippable audit document.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.env.ref(
|
||||
'fusion_plating_jobs.action_report_fp_job_wo_detail'
|
||||
).report_action(self)
|
||||
|
||||
def action_view_deliveries(self):
|
||||
self.ensure_one()
|
||||
if not self.delivery_id:
|
||||
@@ -497,6 +617,38 @@ class FpJob(models.Model):
|
||||
instructions.append(line)
|
||||
step_num += 1
|
||||
|
||||
# Map recipe_node.default_kind → step.kind so the
|
||||
# downstream gates (Sub 8 racking soft-gate, Policy B
|
||||
# contract-review gate) work even when the step gets
|
||||
# renamed by the customer (e.g. "Hang on Bar" instead
|
||||
# of "Racking"). Without this, gate detection falls
|
||||
# back to fragile name matching.
|
||||
_NODE_KIND_TO_STEP_KIND = {
|
||||
'cleaning': 'wet',
|
||||
'etch': 'wet',
|
||||
'rinse': 'wet',
|
||||
'plate': 'wet',
|
||||
'dry': 'wet',
|
||||
'wbf_test': 'wet',
|
||||
'bake': 'bake',
|
||||
'mask': 'mask',
|
||||
'demask': 'mask',
|
||||
'racking': 'rack',
|
||||
'derack': 'rack',
|
||||
'inspect': 'inspect',
|
||||
'final_inspect': 'inspect',
|
||||
'contract_review': 'other',
|
||||
'gating': 'other',
|
||||
'ship': 'other',
|
||||
}
|
||||
step_kind = 'other'
|
||||
node_kind = (
|
||||
node.default_kind
|
||||
if 'default_kind' in node._fields else None
|
||||
)
|
||||
if node_kind and node_kind in _NODE_KIND_TO_STEP_KIND:
|
||||
step_kind = _NODE_KIND_TO_STEP_KIND[node_kind]
|
||||
|
||||
vals = {
|
||||
'job_id': job.id,
|
||||
'name': node.name,
|
||||
@@ -504,6 +656,7 @@ class FpJob(models.Model):
|
||||
'duration_expected': node.estimated_duration or 0.0,
|
||||
'sequence': seq_counter[0],
|
||||
'recipe_node_id': node.id,
|
||||
'kind': step_kind,
|
||||
}
|
||||
if node.estimated_duration:
|
||||
vals['dwell_time_minutes'] = node.estimated_duration
|
||||
@@ -636,12 +789,79 @@ class FpJob(models.Model):
|
||||
)
|
||||
if pending_steps:
|
||||
pending_steps.write({'state': 'ready'})
|
||||
# 2026-04-28 — auto-populate facility_id + manager_id so the
|
||||
# job header surfaces them on the form. Page-1 audit found
|
||||
# both empty on confirmed jobs.
|
||||
job._fp_autofill_facility_and_manager()
|
||||
job._fp_create_portal_job()
|
||||
job._fp_create_qc_check_if_needed()
|
||||
job._fp_create_racking_inspection()
|
||||
job._fp_fire_notification('job_confirmed')
|
||||
return result
|
||||
|
||||
def _fp_autofill_facility_and_manager(self):
|
||||
"""Populate facility_id + manager_id on confirm if empty.
|
||||
|
||||
Resolution order:
|
||||
facility_id —
|
||||
1. Already set → leave alone.
|
||||
2. First step with a work_centre that has a facility → use it.
|
||||
3. Recipe's process_type → facility (if process_type carries one).
|
||||
4. Single-facility company → use that one.
|
||||
|
||||
manager_id —
|
||||
1. Already set → leave alone.
|
||||
2. Confirming user IS in the Plating Manager group → use them.
|
||||
3. Sale order user_id (the salesperson who confirmed the SO).
|
||||
4. The customer's account manager (partner.user_id).
|
||||
5. Leave blank — no sensible default.
|
||||
"""
|
||||
self.ensure_one()
|
||||
# ---- facility_id ----
|
||||
if not self.facility_id:
|
||||
facility = False
|
||||
for s in self.step_ids:
|
||||
if s.work_centre_id and 'facility_id' in s.work_centre_id._fields:
|
||||
facility = s.work_centre_id.facility_id
|
||||
if facility:
|
||||
break
|
||||
if not facility and self.recipe_id and 'process_type_id' in self.recipe_id._fields:
|
||||
pt = self.recipe_id.process_type_id
|
||||
if pt and 'facility_id' in pt._fields:
|
||||
facility = pt.facility_id
|
||||
if not facility:
|
||||
Facility = self.env.get('fusion.plating.facility')
|
||||
if Facility is not None:
|
||||
facilities = Facility.search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
])
|
||||
if len(facilities) == 1:
|
||||
facility = facilities
|
||||
if facility:
|
||||
self.facility_id = facility.id
|
||||
self.message_post(body=_(
|
||||
'Facility auto-set on confirm: %s'
|
||||
) % facility.display_name)
|
||||
|
||||
# ---- manager_id ----
|
||||
if not self.manager_id:
|
||||
mgr = False
|
||||
ManagerGroup = self.env.ref(
|
||||
'fusion_plating.group_fusion_plating_manager',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if ManagerGroup and self.env.user in ManagerGroup.user_ids:
|
||||
mgr = self.env.user
|
||||
elif self.sale_order_id and self.sale_order_id.user_id:
|
||||
mgr = self.sale_order_id.user_id
|
||||
elif self.partner_id and self.partner_id.user_id:
|
||||
mgr = self.partner_id.user_id
|
||||
if mgr:
|
||||
self.manager_id = mgr.id
|
||||
self.message_post(body=_(
|
||||
'Plating Manager auto-set on confirm: %s'
|
||||
) % mgr.name)
|
||||
|
||||
def _fp_create_racking_inspection(self):
|
||||
"""Auto-create a draft racking inspection on job confirm.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user