diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py
index 7c350fc8..5e6f5730 100644
--- a/fusion_plating/fusion_plating_jobs/models/fp_job.py
+++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py
@@ -599,17 +599,33 @@ class FpJob(models.Model):
job.next_milestone_action = False
job.next_milestone_label = ''
continue
- if job.state != 'done':
+ # New state machine (spec 2026-05-25). The auto-advance
+ # helper normally fires button_finish post-super, so we
+ # rarely see state='in_progress' here. When we do (e.g.
+ # historical jobs caught mid-migration, or jobs whose
+ # cert/delivery infra failed mid-transition), surface
+ # mark_done as a manual fallback.
+ if job.state == 'in_progress':
job.next_milestone_action = 'mark_done'
- elif job._fp_has_draft_required_certs():
+ elif job.state == 'awaiting_cert':
job.next_milestone_action = 'issue_certs'
- elif (not job.delivery_id
- or job.delivery_id.state == 'draft'):
- job.next_milestone_action = 'schedule_delivery'
- elif job.delivery_id.state in ('scheduled', 'in_transit'):
+ elif job.state == 'awaiting_ship':
job.next_milestone_action = 'mark_shipped'
+ elif job.state == 'done':
+ # Legacy path — historical jobs that closed before the
+ # new state machine landed. Preserve the old cascade
+ # so their milestone buttons keep working.
+ if job._fp_has_draft_required_certs():
+ job.next_milestone_action = 'issue_certs'
+ elif (not job.delivery_id
+ or job.delivery_id.state == 'draft'):
+ job.next_milestone_action = 'schedule_delivery'
+ elif job.delivery_id.state in ('scheduled', 'in_transit'):
+ job.next_milestone_action = 'mark_shipped'
+ else:
+ job.next_milestone_action = 'closed'
else:
- job.next_milestone_action = 'closed'
+ job.next_milestone_action = False
job.next_milestone_label = labels.get(
job.next_milestone_action, ''
)
@@ -646,7 +662,10 @@ class FpJob(models.Model):
'mark_done': self.button_mark_done,
'issue_certs': self._action_open_draft_certs,
'schedule_delivery': self._action_open_draft_delivery,
- 'mark_shipped': self._action_mark_active_delivery_delivered,
+ # Spec 2026-05-25: dispatch between the new state-machine
+ # path (state=awaiting_ship → button_mark_shipped) and the
+ # legacy delivery path (state=done + scheduled delivery).
+ 'mark_shipped': self._action_mark_shipped_dispatch,
}
fn = action_map.get(self.next_milestone_action)
if not fn:
@@ -734,6 +753,18 @@ class FpJob(models.Model):
) % self.delivery_id.name)
return True
+ def _action_mark_shipped_dispatch(self):
+ """Dispatch the milestone-cascade 'Mark Shipped' button to the
+ right handler based on job state. Spec 2026-05-25:
+ - awaiting_ship → button_mark_shipped (new state machine)
+ - done + active delivery → _action_mark_active_delivery_delivered
+ (legacy historical path)
+ """
+ self.ensure_one()
+ if self.state == 'awaiting_ship':
+ return self.button_mark_shipped()
+ return self._action_mark_active_delivery_delivered()
+
@api.depends(
'sale_order_id', 'delivery_id', 'portal_job_id', 'step_ids',
'step_ids.time_log_ids', 'origin', 'partner_id',
@@ -1997,6 +2028,32 @@ class FpJob(models.Model):
job._fp_fire_notification('job_complete')
return True
+ def button_mark_shipped(self):
+ """Manual transition awaiting_ship → done. Operator-facing
+ button on the job form; restricted to Manager / Owner via
+ groups= on the view button.
+
+ Does NOT re-run the bake/qty/QC gates — those passed when the
+ job first transitioned out of in_progress. This is just the
+ "yes, shipped" stamp.
+
+ Future hook: delivery.action_mark_delivered will call this
+ automatically — out of scope for this iteration (spec 2026-05-25).
+ """
+ for job in self:
+ if job.state != 'awaiting_ship':
+ raise UserError(_(
+ 'Job %s cannot be marked Shipped — state is "%s" '
+ '(expected "awaiting_ship").'
+ ) % (job.name, job.state))
+ job.state = 'done'
+ job.date_finished = fields.Datetime.now()
+ job._fp_fire_notification('job_shipped')
+ job.message_post(body=_(
+ 'Marked shipped by %s.'
+ ) % self.env.user.name)
+ return True
+
# ------------------------------------------------------------------
# Notifications dispatch (Phase 4)
#
diff --git a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml
index 63946950..c3f06464 100644
--- a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml
+++ b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml
@@ -68,7 +68,8 @@
string="Mark Shipped"
class="btn-success"
icon="fa-paper-plane"
- invisible="next_milestone_action != 'mark_shipped'"/>
+ invisible="next_milestone_action != 'mark_shipped'"
+ groups="fusion_plating.group_fp_manager,fusion_plating.group_fp_owner"/>