diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index 149a2f96..3560b710 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -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 # ------------------------------------------------------------------ diff --git a/fusion_plating/fusion_plating_portal/__manifest__.py b/fusion_plating/fusion_plating_portal/__manifest__.py index 3349b5c0..8b23a936 100644 --- a/fusion_plating/fusion_plating_portal/__manifest__.py +++ b/fusion_plating/fusion_plating_portal/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Customer Portal', - 'version': '19.0.3.3.0', + 'version': '19.0.3.4.0', 'category': 'Manufacturing/Plating', 'summary': 'Customer-facing portal for plating shops: online RFQ, job status, ' 'CoC downloads, invoice access.', diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index 52c31b89..d6b9f4c0 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -188,13 +188,31 @@ class FpCustomerPortal(CustomerPortal): {'key': 'shipping', 'label': 'Shipping', 'docs': []}, ] - # FROM YOU — V1: placeholder (V2 will pull PO + drawings via SO link) - groups[0]['docs'].append({ - 'label': 'Customer documents', - 'sub': 'Upload your PO and drawings via your sales contact for now', - 'pending': True, - 'icon': '📄', - }) + # FROM YOU — surface the Sales Order Confirmation via the fp.job + # link added by fusion_plating_jobs (job.x_fc_job_id.sale_order_id). + # When no SO is linked, fall back to the placeholder so customers + # know where to upload their PO + drawings. + so = None + backend_job = job.x_fc_job_id if 'x_fc_job_id' in job._fields else None + if backend_job and 'sale_order_id' in backend_job._fields: + so = backend_job.sale_order_id + if so: + groups[0]['docs'].append({ + 'label': 'Sales Order Confirmation · %s' % so.name, + 'sub': 'EN Plating · %s' % ( + so.date_order and so.date_order.strftime('%b %d, %Y') or '' + ), + 'url': '/report/pdf/sale.report_saleorder/%s' % so.id, + 'icon_class': 'o_fp_doc_icon_input', + 'icon': '📄', + }) + else: + groups[0]['docs'].append({ + 'label': 'Customer documents', + 'sub': 'Upload your PO and drawings via your sales contact for now', + 'pending': True, + 'icon': '📄', + }) # SPECIFICATIONS — V1: placeholder (V2 will pull customer spec) groups[1]['docs'].append({ @@ -261,7 +279,7 @@ class FpCustomerPortal(CustomerPortal): # DASHBOARD # ========================================================================== @http.route( - ['/my/home'], + ['/my', '/my/home'], type='http', auth='user', website=True, diff --git a/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss b/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss index c892c4af..19761ba4 100644 --- a/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss +++ b/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss @@ -9,12 +9,13 @@ align-items: center; justify-content: center; gap: $fp-space-2; - padding: .55rem 1.1rem; + // Match Odoo's standard Bootstrap button rhythm (38px tall). + padding: .5rem 1rem; border-radius: $fp-radius-button; font-family: $fp-font; - font-size: .85rem; - font-weight: 600; - line-height: 1.1; + font-size: 1rem; + font-weight: 500; + line-height: 1.4; border: none; cursor: pointer; text-decoration: none; @@ -60,7 +61,6 @@ background: transparent; color: $fp-teal; font-weight: 500; - padding: .45rem .85rem; &:hover { background: rgba(46, 175, 147, .08); color: $fp-teal-dark; } } @@ -83,6 +83,6 @@ &:hover { color: $fp-teal-dark; } } -// Size modifiers -.o_fp_btn_sm { padding: .35rem .75rem; font-size: .76rem; } -.o_fp_btn_lg { padding: .75rem 1.4rem; font-size: .95rem; } +// Size modifiers — match Bootstrap btn-sm / btn-lg sizing +.o_fp_btn_sm { padding: .25rem .5rem; font-size: .875rem; } +.o_fp_btn_lg { padding: .5rem 1rem; font-size: 1.25rem; } diff --git a/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss b/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss index eae0163b..c1f80f00 100644 --- a/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss +++ b/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss @@ -98,6 +98,26 @@ margin-bottom: $fp-space-3; box-shadow: $fp-shadow-card; + // Works for both
and wrappers. When rendered as an anchor + // the whole card becomes a click target (jobs list + dashboard). + display: block; + color: inherit; + text-decoration: none; + transition: box-shadow .15s ease, transform .08s ease, border-color .15s ease; + + &:hover, + &:focus-visible { + color: inherit; + text-decoration: none; + box-shadow: $fp-shadow-card-hover; + border-color: $fp-aqua; + transform: translateY(-1px); + } + &:focus-visible { + outline: 2px solid $fp-teal; + outline-offset: 2px; + } + .o_fp_job_header { display: flex; justify-content: space-between; diff --git a/fusion_plating/fusion_plating_portal/views/fp_portal_dashboard.xml b/fusion_plating/fusion_plating_portal/views/fp_portal_dashboard.xml index 8a91f192..4e585517 100644 --- a/fusion_plating/fusion_plating_portal/views/fp_portal_dashboard.xml +++ b/fusion_plating/fusion_plating_portal/views/fp_portal_dashboard.xml @@ -67,12 +67,10 @@ -
+
- + units · ETA @@ -102,20 +100,16 @@
- - - + 📑 CoC - - - + 📑 CoC · pending 📦
-
+ diff --git a/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml b/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml index 69f0471d..80d02500 100644 --- a/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml +++ b/fusion_plating/fusion_plating_portal/views/fp_portal_templates.xml @@ -436,12 +436,10 @@