From 4930a89970e51d1abd3af1275847b83027e918a1 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 25 May 2026 09:26:03 -0400 Subject: [PATCH] feat(fp.job): button_mark_shipped + milestone cascade integration (Task 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../fusion_plating_jobs/models/fp_job.py | 73 +++++++++++++++++-- .../views/fp_job_form_inherit.xml | 3 +- 2 files changed, 67 insertions(+), 9 deletions(-) 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"/>