feat(fp.job): button_mark_shipped + milestone cascade integration (Task 5)
button_mark_shipped: manual transition awaiting_ship → done. Does
not re-run the bake/qty/QC gates — those passed at the in_progress
→ awaiting_cert/ship transition. Just the 'yes, shipped' stamp.
Milestone cascade (_compute_next_milestone_action) extended to
recognize the two new states:
- awaiting_cert → 'issue_certs' button
- awaiting_ship → 'mark_shipped' button
Legacy state='done' branch preserved for historical jobs.
action_advance_next_milestone now dispatches 'mark_shipped' via
_action_mark_shipped_dispatch which routes:
awaiting_ship → button_mark_shipped (new path)
done + active delivery → _action_mark_active_delivery_delivered
(legacy, unchanged)
View: 'Mark Shipped' milestone button gated on Manager/Owner groups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
#
|
||||
|
||||
@@ -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"/>
|
||||
<field name="all_steps_terminal" invisible="1"/>
|
||||
<field name="next_milestone_action" invisible="1"/>
|
||||
<button name="action_print_sticker" type="object"
|
||||
|
||||
Reference in New Issue
Block a user