fix(portal): 5 hotfixes - /my route, button sizing, clickable cards, state sync, SO doc

1. /my now serves the FP dashboard (stock Odoo home was leaking
   through because parent route declared ['/my', '/my/home'] but my
   override only listed /my/home).
2. Button padding bumped to .5rem 1rem + font 1rem so o_fp_btn matches
   Odoo's standard Bootstrap button rhythm. Ghost button drops its
   custom padding override.
3. .o_fp_job_card on /my/home + /my/jobs is now an <a> wrapping the
   whole card area — full row is the click target, not just the WO
   number. Inner <a> on job.name dropped to avoid nested anchors;
   focus-visible outline added for keyboard nav.
4. fp.job.write() now mirrors state -> fp.portal.job.state via new
   _FP_JOB_STATE_TO_PORTAL_STATE map (confirmed->received,
   in_progress->in_progress, done->ready_to_ship). Fixes the bug where
   completed backend jobs left the portal stuck on 'in_progress'.
   'on_hold' and 'cancelled' intentionally not mirrored — manager
   choice what to surface.
5. Sales Order Confirmation now surfaces in the 'From You' group on
   the job detail page, pulled via job.x_fc_job_id.sale_order_id ->
   /report/pdf/sale.report_saleorder/<id>. Falls back to the upload
   placeholder when no SO is linked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-17 03:13:00 -04:00
parent edcc325483
commit 28220f0732
7 changed files with 159 additions and 38 deletions

View File

@@ -715,12 +715,23 @@ class FpJob(models.Model):
'name': self.portal_job_id.name,
}
# fp.job.state -> fusion.plating.portal.job.state mapping. Kept tight so
# the customer doesn't see internal states. Anything not in this map
# leaves the portal_job state alone (e.g. 'on_hold' stays in_progress).
_FP_JOB_STATE_TO_PORTAL_STATE = {
'confirmed': 'received',
'in_progress': 'in_progress',
'done': 'ready_to_ship',
# 'on_hold' and 'cancelled' intentionally omitted — managers choose
# what to surface to the customer.
}
def write(self, vals):
"""Write hook: when qty_scrapped INCREASES, auto-spawn a
fusion.plating.quality.hold for the scrapped delta. AS9100 /
Nadcap need a disposition record per scrap event — without
this the operator silently bumps qty_scrapped, no paper trail,
auditor can't reconstruct what happened.
"""Write hook: (a) when qty_scrapped INCREASES, auto-spawn a
fusion.plating.quality.hold for the scrapped delta AS9100 /
Nadcap need a disposition record per scrap event. (b) when state
transitions, mirror to the linked fusion.plating.portal.job so
the customer-facing portal stays in sync with the shop floor.
Idempotent per write: one hold per increase event. Operator
fills hold_reason + description on the spawned record.
@@ -733,7 +744,22 @@ class FpJob(models.Model):
old = job.qty_scrapped or 0
if new > old:
scrap_deltas[job.id] = (old, new)
# Capture state changes before super().write() so we know which
# records actually transitioned (vs no-op writes).
state_changed_ids = set()
if 'state' in vals:
new_state = vals['state']
for job in self:
if job.state != new_state:
state_changed_ids.add(job.id)
result = super().write(vals)
# Mirror state to portal_job for records that actually changed.
if state_changed_ids:
target = self._FP_JOB_STATE_TO_PORTAL_STATE.get(vals.get('state'))
if target:
for job in self.filtered(lambda j: j.id in state_changed_ids):
if job.portal_job_id and job.portal_job_id.state != target:
job.portal_job_id.sudo().write({'state': target})
if not scrap_deltas:
return result
Hold = (self.env['fusion.plating.quality.hold']
@@ -1059,6 +1085,71 @@ class FpJob(models.Model):
len(step_vals_list), job.recipe_id.name,
),
)
# Rule 4 — repeat-order contract-review auto-complete.
# Runs after step creation so the contract-review step shows
# as already done on the operator's first view of the job.
job._fp_autocomplete_repeat_order_contract_review()
return True
def _fp_autocomplete_repeat_order_contract_review(self):
"""Rule 4 of the contract-review flow — when a job's part already
carries a complete fp.contract.review (i.e. the part has been
through QA-005 on a prior order), mark every contract-review
step in this job's recipe as 'done' immediately on job creation.
Copies the reviewer identity + timestamp from the review's
Section 3.0 sign-off (falling back to Section 2.0) so the Print
WO Detail report shows the original audit trail — Reviewer
initials, date reviewed, "QA-005 Approved" — not the operator
who would have hit Finish.
Skips:
* jobs whose part has no contract review or it isn't complete
(rule 5 still applies — the WO step gate will block finish)
* steps not detected as contract-review steps via
fp.job.step._fp_is_contract_review_step
* steps already in a terminal state (defensive idempotency)
"""
for job in self:
part = (
('part_catalog_id' in job._fields and job.part_catalog_id)
or False
)
if not part:
continue
review = (
('x_fc_contract_review_id' in part._fields
and part.x_fc_contract_review_id)
or False
)
if not review or review.state != 'complete':
continue
signer = review.s30_signed_by or review.s20_signed_by
signed_at = review.s30_signed_date or review.s20_signed_date
if not signer or not signed_at:
continue
steps_to_complete = job.step_ids.filtered(
lambda s: s.state not in ('done', 'skipped', 'cancelled')
and s._fp_is_contract_review_step()
)
if not steps_to_complete:
continue
steps_to_complete.write({
'state': 'done',
'started_by_user_id': signer.id,
'finished_by_user_id': signer.id,
'date_started': signed_at,
'date_finished': signed_at,
})
for step in steps_to_complete:
step.message_post(body=_(
'Contract Review step auto-completed from existing '
'QA-005 for %(part)s. Reviewer: %(user)s on %(date)s.'
) % {
'part': part.display_name or part.part_number or '',
'user': signer.name,
'date': fields.Datetime.to_string(signed_at),
})
return True
# ------------------------------------------------------------------