fix(contract-review): WO step routes to QA-005 + auto-stage on part create

Two bugs fixed in one drop, both targeting the contract review (QA-005)
enforcement gap reported on entech.

## Bug 1 — WO step routed to wrong wizard

Symptom: clicking Finish & Next or Record on a Contract Review step in
WH/JOB/00339 opened the generic measurement wizard with three fake
prompts (Reviewer Initials / Date Reviewed / QA-005 Approved). No path
to the actual QA-005 form from the work order.

Root cause: action_finish_and_advance + action_open_input_wizard had no
branch for recipe_node.default_kind == 'contract_review'. The step.kind
mapping collapses contract_review -> 'other' so kind-based detection
wouldn't have worked either; gate has to live at the recipe-node layer.

Fix in fusion_plating_jobs/models/fp_job_step.py (v19.0.8.14.6):
- action_finish_and_advance:329 calls _fp_contract_review_redirect
  before the input-wizard branch
- action_open_input_wizard:844 same gate, keeps Record button consistent
- _fp_contract_review_redirect:866 (new) returns the part's
  action_start_contract_review() unless review.state in
  (complete, dismissed) — gate clears so the step can finish after
  the operator signs QA-005.

## Bug 2 — Part create did not enforce contract review

Symptom: spec called for a banner-only UX. User wanted true automatic
enforcement on first part creation under an enforced customer.

Fix in fusion_plating_quality/models/fp_part_catalog.py (v19.0.4.10.0):
- @api.model_create_multi def create() override
- _fp_enforce_contract_review_on_create() helper auto-stages the
  fp.contract.review record AND surfaces three prominent reminders:
    1. Sticky bus.bus warning toast (top-right, doesn't auto-dismiss)
    2. mail.activity (To Do) on the part for the current user
    3. Smart button on the part form lights up (review now exists)
- Idempotent: skips parts that already carry a review id
- Soft-fails: bus or activity outage doesn't block part creation
- create()-only — write/update flows never re-trigger

Sub 4's existing info banner stays as a fourth surface.

## Tests

- fusion_plating_jobs/tests/test_fp_job_extensions.py:
  +TestContractReviewStepRouting (5 tests covering both routing methods,
  the complete/dismissed gate-clear, and non-CR step regression)
- fusion_plating_quality/tests/test_part_catalog_contract_review_enforcement.py
  (NEW): 9 tests covering auto-create, batch create, idempotency,
  activity surface, bus surface, write-must-not-retrigger, soft-fail.
- docs/superpowers/tests/2026-04-22-sub4-smoke.py: flipped the
  "no review yet" assertion to "review auto-created" to match new
  behavior. Sign-flow assertions unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-03 20:02:52 -04:00
parent 1da27ed6bf
commit ee80673579
8 changed files with 584 additions and 9 deletions

View File

@@ -3,9 +3,13 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class FpPartCatalog(models.Model):
_inherit = 'fp.part.catalog'
@@ -86,6 +90,134 @@ class FpPartCatalog(models.Model):
and not completed and not in_production
)
# ---- Create override — auto-stage + alert -------------------------------
@api.model_create_multi
def create(self, vals_list):
"""Auto-stage the contract review and alert the user when a new
part is added under a customer that has contract-review
enforcement enabled (res.partner.x_fc_contract_review_required).
Fires only on .create() — write/update flows never re-trigger
the alert. Existing parts in the system are unaffected.
Three surfaces, in increasing persistence:
1. Sticky warning toast via bus.bus (instant, visible top-right).
2. mail.activity scheduled on the part for the current user
(lives in their Activities inbox until the review is complete).
3. Smart button on the part form lights up because the review
record was auto-created (always visible while the part is open).
The original info banner from Sub 4's spec also still renders,
which makes the part form itself self-explanatory.
"""
parts = super().create(vals_list)
# Defer side-effects so the create transaction commits cleanly
# even if the bus/activity layer is unavailable (e.g. during
# tests with a stripped-down environment).
parts._fp_enforce_contract_review_on_create()
return parts
def _fp_enforce_contract_review_on_create(self):
"""Per-record handler called from the create() override.
Idempotent: skips parts that already have a review record (e.g.
duplicated parts that copied the link, or parts created via an
import wizard that pre-staged them). Soft-fails on each side
effect so a bus or activity outage never blocks part creation.
"""
Review = self.env['fp.contract.review']
Activity = self.env['mail.activity']
Bus = self.env['bus.bus']
activity_type = self.env.ref(
'mail.mail_activity_data_todo', raise_if_not_found=False,
)
# Resolve the model id once for all parts in the batch.
try:
model_id = self.env['ir.model']._get('fp.part.catalog').id
except Exception:
model_id = False
for part in self:
if not part.partner_id or not part.partner_id.x_fc_contract_review_required:
continue
if part.x_fc_contract_review_id:
# Already linked to a review (carried over from a copy
# or pre-set in vals). Don't replace; just notify.
review = part.x_fc_contract_review_id
else:
# Lazy-create the review record so the smart button on
# the part form lights up immediately.
review = Review.create({
'part_id': part.id,
'state': 'assistant_review',
})
part.x_fc_contract_review_id = review.id
part_label = (
part.part_number
or part.name
or _('this part')
)
customer_label = part.partner_id.display_name
# 1) Persistent activity — sits in the user's Activities
# inbox + shows on the part record's chatter clock until
# the user marks it done.
if activity_type and model_id:
try:
Activity.create({
'res_model_id': model_id,
'res_id': part.id,
'activity_type_id': activity_type.id,
'summary': _(
'Complete Contract Review (QA-005) for %s'
) % part_label,
'note': _(
'Customer <b>%(c)s</b> requires a Contract '
'Review on new parts. Open the QA-005 form '
'using the <b>Contract Review</b> smart '
'button at the top of this part, then sign '
'Sections 2.0 and 3.0 to complete the '
'review.'
) % {'c': customer_label},
'user_id': self.env.user.id,
})
except Exception:
_logger.warning(
'fp.part.catalog %s: could not schedule '
'contract-review activity',
part.id, exc_info=True,
)
# 2) Sticky warning toast — visible immediately to the
# user who created the part. Doesn't auto-dismiss.
try:
Bus._sendone(
self.env.user.partner_id,
'simple_notification',
{
'title': _('Contract Review Required — %s')
% part_label,
'message': _(
'Customer %(c)s requires a Contract Review '
'(QA-005) on new parts. The review record '
'has been pre-created — open it using the '
'Contract Review smart button at the top '
'of the part form, or from your Activities '
'inbox.'
) % {'c': customer_label},
'type': 'warning',
'sticky': True,
},
)
except Exception:
_logger.warning(
'fp.part.catalog %s: could not push contract-'
'review notification', part.id, exc_info=True,
)
# ---- Actions -------------------------------------------------------------
def action_start_contract_review(self):