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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user