chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,13 @@
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Customer Portal',
|
||||
'name': 'Fusion Plating - Customer Portal',
|
||||
'version': '19.0.4.5.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Customer-facing portal for plating shops: online RFQ, job status, '
|
||||
'CoC downloads, invoice access.',
|
||||
'description': """
|
||||
Fusion Plating — Customer Portal
|
||||
Fusion Plating - Customer Portal
|
||||
================================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
@@ -60,7 +60,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'views/fp_quote_request_views.xml',
|
||||
'views/fp_portal_dashboard.xml',
|
||||
'views/fp_portal_templates.xml',
|
||||
'views/fp_portal_account_summary.xml', # NEW — Task 10
|
||||
'views/fp_portal_account_summary.xml', # NEW - Task 10
|
||||
'views/fp_portal_configurator_templates.xml',
|
||||
'views/fp_portal_breadcrumbs.xml',
|
||||
'views/fp_sale_order_portal.xml',
|
||||
@@ -70,20 +70,20 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'web.assets_frontend': [
|
||||
# Tokens MUST be first so every later file sees the variables.
|
||||
'fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss',
|
||||
# Phase 1 — button system
|
||||
# Phase 1 - button system
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_buttons.scss',
|
||||
# Phase 2 — badges, cards, stepper, dashboard layout
|
||||
# Phase 2 - badges, cards, stepper, dashboard layout
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_badges.scss',
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_cards.scss',
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_stepper.scss',
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss',
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_timeline.scss',
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss', # NEW — Task 5
|
||||
'fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss', # NEW - Task 5
|
||||
# Catch-all legacy rules (last)
|
||||
'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss',
|
||||
'fusion_plating_portal/static/src/js/fp_rfq_form.js',
|
||||
'fusion_plating_portal/static/src/js/fp_portal_sidebar.js', # NEW — Task 5
|
||||
'fusion_plating_portal/static/src/js/fp_portal_account_summary.js', # NEW — Task 10 fix
|
||||
'fusion_plating_portal/static/src/js/fp_portal_sidebar.js', # NEW - Task 5
|
||||
'fusion_plating_portal/static/src/js/fp_portal_account_summary.js', # NEW - Task 10 fix
|
||||
'fusion_plating_portal/static/src/js/fp_portal_list_search.js', # list search + sort
|
||||
],
|
||||
},
|
||||
|
||||
@@ -116,7 +116,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
return partner.commercial_partner_id
|
||||
|
||||
# ==========================================================================
|
||||
# Sidebar — items + active-state resolution
|
||||
# Sidebar - items + active-state resolution
|
||||
# ==========================================================================
|
||||
# Sidebar item structure: list of dicts with `type` = 'item' | 'section_label'.
|
||||
# Items have label / url / icon / key. Key matches either a page_name set by
|
||||
@@ -150,7 +150,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
'fp_account_summary': 'fp_account_summary',
|
||||
}
|
||||
_FP_URL_PREFIX_TO_SIDEBAR_KEY = [
|
||||
# Order matters — first match wins, so list longer prefixes first.
|
||||
# Order matters - first match wins, so list longer prefixes first.
|
||||
('/my/orders', 'odoo_orders'),
|
||||
('/my/quotes', 'odoo_orders'), # /my/quotes is also sale_portal
|
||||
('/my/invoices', 'fp_account_summary'),
|
||||
@@ -194,7 +194,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
partner = request.env.user.partner_id
|
||||
commercial = partner.commercial_partner_id
|
||||
values['fp_partner_display_name'] = commercial.name or partner.name
|
||||
# Internal staff (share=False) get the clean employee experience — no
|
||||
# Internal staff (share=False) get the clean employee experience - no
|
||||
# customer sidebar. Customers (share=True / portal users) keep it.
|
||||
values['fp_show_customer_sidebar'] = bool(request.env.user.share)
|
||||
return values
|
||||
@@ -204,7 +204,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
# funnel through this helper. It sets up chatter/pager but doesn't
|
||||
# touch _prepare_portal_layout_values, so our sidebar context wouldn't
|
||||
# otherwise reach those templates. Inject our keys conservatively via
|
||||
# setdefault — never overwrite anything the page already set.
|
||||
# setdefault - never overwrite anything the page already set.
|
||||
values = super()._get_page_view_values(
|
||||
document, access_token, values, session_history, no_breadcrumbs, **kwargs,
|
||||
)
|
||||
@@ -219,7 +219,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
# ==========================================================================
|
||||
# 5 customer-facing stages aligned with the dashboard stepper.
|
||||
# Each entry: (label, timestamp_field_name_on_fp_portal_job).
|
||||
# Inspected and Plating share `in_progress_started_at` — when state moves
|
||||
# Inspected and Plating share `in_progress_started_at` - when state moves
|
||||
# away from 'received' it means inspection finished and plating started.
|
||||
_FP_STAGES = [
|
||||
('Received', 'received_at'),
|
||||
@@ -255,7 +255,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
Records created post-hook never hit the interpolation branch.
|
||||
"""
|
||||
state_idx = self._FP_STATE_TO_STEP_IDX.get(job.state, 0)
|
||||
# Baseline datetime for interpolation — prefer the precise received_at
|
||||
# Baseline datetime for interpolation - prefer the precise received_at
|
||||
# but fall through to received_date (Date) converted to midnight.
|
||||
baseline = job.received_at
|
||||
if not baseline and job.received_date:
|
||||
@@ -321,7 +321,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
of {label, sub, url, icon_class, icon, pending}.
|
||||
"""
|
||||
# 5 fixed groups in display order. Indices used in the appends below
|
||||
# — if you reorder, update every groups[N] reference.
|
||||
# - if you reorder, update every groups[N] reference.
|
||||
# 0 = from_you, 1 = specs, 2 = work_order, 3 = quality, 4 = shipping
|
||||
groups = [
|
||||
{'key': 'from_you', 'label': 'From You', 'docs': []},
|
||||
@@ -331,7 +331,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
{'key': 'shipping', 'label': 'Shipping', 'docs': []},
|
||||
]
|
||||
|
||||
# FROM YOU — surface the Sales Order Confirmation via the fp.job
|
||||
# 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.
|
||||
@@ -381,7 +381,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
'icon': '📄',
|
||||
})
|
||||
|
||||
# SPECIFICATIONS (idx 1) — V1: placeholder (V2 will pull customer spec)
|
||||
# SPECIFICATIONS (idx 1) - V1: placeholder (V2 will pull customer spec)
|
||||
groups[1]['docs'].append({
|
||||
'label': 'Customer Specification',
|
||||
'sub': 'Will appear when EN Plating links the spec',
|
||||
@@ -389,7 +389,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
'icon': '📋',
|
||||
})
|
||||
|
||||
# WORK ORDER (idx 2) — EN Plating WO Detail PDF via
|
||||
# WORK ORDER (idx 2) - EN Plating WO Detail PDF via
|
||||
# fusion_plating_jobs.report_fp_job_wo_detail_template. Requires a
|
||||
# linked backend fp.job; placeholder otherwise.
|
||||
if backend_job:
|
||||
@@ -408,7 +408,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
'icon': '🛠',
|
||||
})
|
||||
|
||||
# QUALITY (idx 3) — CoC from coc_attachment_id (the legacy direct field)
|
||||
# QUALITY (idx 3) - CoC from coc_attachment_id (the legacy direct field)
|
||||
if job.coc_attachment_id:
|
||||
groups[3]['docs'].append({
|
||||
'label': 'Certificate of Conformance',
|
||||
@@ -428,7 +428,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
'icon': '📑',
|
||||
})
|
||||
|
||||
# SHIPPING (idx 4) — packing list + tracking. Two separate
|
||||
# SHIPPING (idx 4) - packing list + tracking. Two separate
|
||||
# docs so each can be pending/ready independently. Previously
|
||||
# combined into one entry; that broke when tracking_ref landed
|
||||
# before the packing slip (KeyError 'url').
|
||||
@@ -480,7 +480,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
return '%.1f MB' % (size / (1024 * 1024))
|
||||
|
||||
# ==========================================================================
|
||||
# Account Summary (Sub-A IA) — invoices + credits + statements
|
||||
# Account Summary (Sub-A IA) - invoices + credits + statements
|
||||
# ==========================================================================
|
||||
_FP_ACCOUNT_SUMMARY_TABS = [
|
||||
('invoices', 'Invoices', 'out_invoice'),
|
||||
@@ -516,18 +516,18 @@ class FpCustomerPortal(CustomerPortal):
|
||||
search, sort, page):
|
||||
"""Return {records, total, pager_offset} for one tab+filter combination.
|
||||
|
||||
tab — 'invoices' | 'credit_memos' | 'statements'
|
||||
filter_state — 'open' | 'closed' | 'all'
|
||||
search — substring matched against name OR ref (case-insensitive)
|
||||
sort — key from _FP_ACCOUNT_SUMMARY_SORTS
|
||||
page — 1-indexed
|
||||
tab - 'invoices' | 'credit_memos' | 'statements'
|
||||
filter_state - 'open' | 'closed' | 'all'
|
||||
search - substring matched against name OR ref (case-insensitive)
|
||||
sort - key from _FP_ACCOUNT_SUMMARY_SORTS
|
||||
page - 1-indexed
|
||||
|
||||
Uses commercial_partner.env so this helper works both in HTTP
|
||||
context and in unit tests without requiring request to be active.
|
||||
"""
|
||||
env = commercial_partner.env
|
||||
if tab == 'statements':
|
||||
# V1 placeholder — Statements is a 'coming soon' tab.
|
||||
# V1 placeholder - Statements is a 'coming soon' tab.
|
||||
return {'records': env['account.move'].browse(), 'total': 0,
|
||||
'offset': 0}
|
||||
|
||||
@@ -621,7 +621,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
)
|
||||
def home(self, **kw):
|
||||
# Internal staff don't belong on the customer dashboard. Send them to
|
||||
# the employee clock portal — but only when fusion_clock is installed
|
||||
# the employee clock portal - but only when fusion_clock is installed
|
||||
# (x_fclk_enable_clock proves it) AND the user actually has an employee
|
||||
# record, otherwise /my/clock -> /my would bounce into a redirect loop.
|
||||
user = request.env.user
|
||||
@@ -851,7 +851,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
methods=['GET'],
|
||||
)
|
||||
def portal_new_quote_request(self, **kw):
|
||||
"""GET — legacy entry point, redirected to the configurator wizard."""
|
||||
"""GET - legacy entry point, redirected to the configurator wizard."""
|
||||
return request.redirect('/my/configurator/new')
|
||||
|
||||
# ==========================================================================
|
||||
@@ -1285,7 +1285,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
type='http', auth='user', website=True,
|
||||
)
|
||||
def portal_my_purchase_orders(self, **kw):
|
||||
"""Legacy URL — redirected to Odoo default /my/orders (Sub-A IA)."""
|
||||
"""Legacy URL - redirected to Odoo default /my/orders (Sub-A IA)."""
|
||||
return request.redirect('/my/orders')
|
||||
|
||||
# ==========================================================================
|
||||
@@ -1296,7 +1296,7 @@ class FpCustomerPortal(CustomerPortal):
|
||||
type='http', auth='user', website=True,
|
||||
)
|
||||
def portal_my_fp_invoices(self, **kw):
|
||||
"""Legacy URL — redirected to /my/account_summary (Sub-A IA)."""
|
||||
"""Legacy URL - redirected to /my/account_summary (Sub-A IA)."""
|
||||
return request.redirect('/my/account_summary')
|
||||
|
||||
# ==========================================================================
|
||||
|
||||
@@ -23,7 +23,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
"""
|
||||
|
||||
# ======================================================================
|
||||
# Landing — start new or view past requests
|
||||
# Landing - start new or view past requests
|
||||
# ======================================================================
|
||||
@http.route('/my/configurator', type='http', auth='user', website=True)
|
||||
def portal_configurator_landing(self, **kw):
|
||||
@@ -41,7 +41,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
return request.render('fusion_plating_portal.portal_configurator_landing', values)
|
||||
|
||||
# ======================================================================
|
||||
# Step 1 — Upload part or enter manual measurements
|
||||
# Step 1 - Upload part or enter manual measurements
|
||||
# ======================================================================
|
||||
@http.route(
|
||||
'/my/configurator/new', type='http', auth='user', website=True,
|
||||
@@ -57,7 +57,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
'substrate_material': kw.get('substrate_material', 'steel'),
|
||||
'geometry_source': kw.get('geometry_source', 'upload'),
|
||||
# Manual measurements are HIDDEN in the template per customer
|
||||
# feedback 2026-05-17 — kept as hidden 0s so the rest of the
|
||||
# feedback 2026-05-17 - kept as hidden 0s so the rest of the
|
||||
# flow doesn't error on missing keys. Backend computes them
|
||||
# if/when needed.
|
||||
'surface_area': float(kw.get('surface_area', 0) or 0),
|
||||
@@ -88,7 +88,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
session_data['attachment_names'].append(
|
||||
'%s (%s)' % (file_upload.filename, label),
|
||||
)
|
||||
# STL surface-area auto-calc (silent — backend value only,
|
||||
# STL surface-area auto-calc (silent - backend value only,
|
||||
# not surfaced in UI per the hidden-measurements decision).
|
||||
fname = file_upload.filename.lower()
|
||||
if fname.endswith('.stl'):
|
||||
@@ -128,7 +128,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
return request.render('fusion_plating_portal.portal_configurator_step1', values)
|
||||
|
||||
# ======================================================================
|
||||
# Step 2 — Select coating configuration
|
||||
# Step 2 - Select coating configuration
|
||||
# ======================================================================
|
||||
@http.route(
|
||||
'/my/configurator/coating', type='http', auth='user', website=True,
|
||||
@@ -149,7 +149,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
return request.redirect('/my/configurator/estimate')
|
||||
|
||||
# fp.coating.config retired post-Sub-11. Use process.type as the
|
||||
# customer-facing coating picker — its records (Hard Chrome, EN
|
||||
# customer-facing coating picker - its records (Hard Chrome, EN
|
||||
# Low-Phos, etc.) are exactly what a customer would select from.
|
||||
coatings = request.env['fusion.plating.process.type'].sudo().search(
|
||||
[('active', '=', True)], order='sequence, name',
|
||||
@@ -163,7 +163,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
return request.render('fusion_plating_portal.portal_configurator_step2', values)
|
||||
|
||||
# ======================================================================
|
||||
# Step 3 — Estimate & submit
|
||||
# Step 3 - Estimate & submit
|
||||
# ======================================================================
|
||||
@http.route('/my/configurator/estimate', type='http', auth='user', website=True)
|
||||
def portal_configurator_step3(self, **kw):
|
||||
@@ -184,7 +184,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
try:
|
||||
estimated_price = self._estimate_price(session_data, coating)
|
||||
except Exception:
|
||||
_logger.info('Skipping price estimate — pricing helper unavailable.', exc_info=True)
|
||||
_logger.info('Skipping price estimate - pricing helper unavailable.', exc_info=True)
|
||||
estimated_price = {'min': 0, 'max': 0, 'available': False}
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
@@ -197,7 +197,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
return request.render('fusion_plating_portal.portal_configurator_step3', values)
|
||||
|
||||
# ======================================================================
|
||||
# Submit — create quote request
|
||||
# Submit - create quote request
|
||||
# ======================================================================
|
||||
@http.route(
|
||||
'/my/configurator/submit', type='http', auth='user', website=True,
|
||||
@@ -254,7 +254,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
quote = request.env['fusion.plating.quote.request'].sudo().create(vals)
|
||||
|
||||
# Re-key uploaded attachments onto the new quote request so they
|
||||
# appear on its chatter. Multi-upload — customer may have sent
|
||||
# appear on its chatter. Multi-upload - customer may have sent
|
||||
# both a drawing and a 3D model (or more).
|
||||
att_ids = session_data.get('attachment_ids') or []
|
||||
if not att_ids and session_data.get('attachment_id'):
|
||||
@@ -289,7 +289,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
Post-coating-retire (Sub-11) the rule schema still references
|
||||
fp.coating.config; ``coating`` here is now a process.type record.
|
||||
That means rules with a ``coating_config_id`` set won't match
|
||||
and we silently fall through to {'available': False} — which
|
||||
and we silently fall through to {'available': False} - which
|
||||
the template renders as 'Quote will be priced by EN Plating'.
|
||||
Estimate is non-essential; final price comes from EN's review.
|
||||
"""
|
||||
@@ -308,7 +308,7 @@ class FpPortalConfigurator(CustomerPortal):
|
||||
best_score = -1
|
||||
for rule in rules:
|
||||
score = 0
|
||||
# Skip any rule keyed to coating_config — model is gone.
|
||||
# Skip any rule keyed to coating_config - model is gone.
|
||||
if 'coating_config_id' in rule._fields and rule.coating_config_id:
|
||||
continue
|
||||
if getattr(rule, 'substrate_material', None):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc. — DEMO DATA (temporary)
|
||||
Copyright 2026 Nexa Systems Inc. - DEMO DATA (temporary)
|
||||
Remove this file and its manifest entry before production release.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
@@ -16,7 +16,7 @@
|
||||
<field name="quantity">250</field>
|
||||
<field name="target_delivery" eval="(datetime.datetime.today() + timedelta(days=30)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">new</field>
|
||||
<field name="part_description" type="html"><p>Electroless nickel plating (mid-phosphorus) on aluminium 6061-T6 brackets per AMS 2404. Thickness 0.0005" +/- 0.0001". Parts are 4" x 2" x 0.25" — drawings attached. Require CoC with each shipment.</p></field>
|
||||
<field name="part_description" type="html"><p>Electroless nickel plating (mid-phosphorus) on aluminium 6061-T6 brackets per AMS 2404. Thickness 0.0005" +/- 0.0001". Parts are 4" x 2" x 0.25" - drawings attached. Require CoC with each shipment.</p></field>
|
||||
<field name="special_instructions" type="html"><p>Customer requires lot traceability and material certificates. Parts must be individually bagged after plating.</p></field>
|
||||
</record>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<field name="received_date" eval="(datetime.datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="target_ship_date" eval="(datetime.datetime.today() + timedelta(days=10)).strftime('%Y-%m-%d')"/>
|
||||
<field name="quantity">50</field>
|
||||
<field name="notes" type="html"><p>EN mid-phos plating on aluminium brackets. Parts received and inspected — ready for processing.</p></field>
|
||||
<field name="notes" type="html"><p>EN mid-phos plating on aluminium brackets. Parts received and inspected - ready for processing.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="demo_portal_job_002" model="fusion.plating.portal.job">
|
||||
@@ -64,7 +64,7 @@
|
||||
<field name="received_date" eval="(datetime.datetime.today() - timedelta(days=5)).strftime('%Y-%m-%d')"/>
|
||||
<field name="target_ship_date" eval="(datetime.datetime.today() + timedelta(days=5)).strftime('%Y-%m-%d')"/>
|
||||
<field name="quantity">100</field>
|
||||
<field name="notes" type="html"><p>Hard chrome plating on hydraulic rods. Parts currently in the chrome tank — expected completion tomorrow.</p></field>
|
||||
<field name="notes" type="html"><p>Hard chrome plating on hydraulic rods. Parts currently in the chrome tank - expected completion tomorrow.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="demo_portal_job_003" model="fusion.plating.portal.job">
|
||||
@@ -74,7 +74,7 @@
|
||||
<field name="received_date" eval="(datetime.datetime.today() - timedelta(days=8)).strftime('%Y-%m-%d')"/>
|
||||
<field name="target_ship_date" eval="(datetime.datetime.today() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="quantity">200</field>
|
||||
<field name="notes" type="html"><p>Type II anodize on fasteners. Plating complete — in final QC thickness and adhesion checks.</p></field>
|
||||
<field name="notes" type="html"><p>Type II anodize on fasteners. Plating complete - in final QC thickness and adhesion checks.</p></field>
|
||||
</record>
|
||||
|
||||
<record id="demo_portal_job_004" model="fusion.plating.portal.job">
|
||||
|
||||
@@ -9,7 +9,7 @@ from odoo import _, api, fields, models
|
||||
class FpPortalJob(models.Model):
|
||||
"""Lightweight portal-facing view of a production job.
|
||||
|
||||
This is intentionally a simple, decoupled model — it does NOT replace any
|
||||
This is intentionally a simple, decoupled model - it does NOT replace any
|
||||
real job/MO model from process packs (e.g. fusion_plating_process_en).
|
||||
Instead, the shop populates this once per job (manually or via a small
|
||||
sync rule from the real job) so the customer sees a clean, sanitised
|
||||
@@ -19,7 +19,7 @@ class FpPortalJob(models.Model):
|
||||
optional CoC + packing list attachments, and a tracking reference.
|
||||
"""
|
||||
_name = 'fusion.plating.portal.job'
|
||||
_description = 'Fusion Plating — Portal Job'
|
||||
_description = 'Fusion Plating - Portal Job'
|
||||
_inherit = ['portal.mixin', 'mail.thread']
|
||||
_order = 'received_date desc, id desc'
|
||||
|
||||
@@ -249,7 +249,7 @@ class FpPortalJob(models.Model):
|
||||
def walk(node, depth):
|
||||
for child in node.child_ids.sorted('sequence'):
|
||||
if not child.customer_visible:
|
||||
# Hidden node — and its sub-tree is also hidden,
|
||||
# Hidden node - and its sub-tree is also hidden,
|
||||
# because if you're skipping the parent the kids
|
||||
# never make sense in isolation.
|
||||
continue
|
||||
@@ -264,14 +264,14 @@ class FpPortalJob(models.Model):
|
||||
return result
|
||||
|
||||
# ==========================================================================
|
||||
# State recompute — single source of truth derived from upstream models
|
||||
# State recompute - single source of truth derived from upstream models
|
||||
# ==========================================================================
|
||||
# The portal state should ALWAYS reflect the real shop-floor state of the
|
||||
# linked fp.job(s), the outbound shipment(s), and the customer invoice.
|
||||
# Earlier paths wrote state directly from each event hook (tracking number
|
||||
# arrived → 'shipped'; invoice posted → 'complete') which drifted out of
|
||||
# sync the moment any of those events fired before the job was actually
|
||||
# done — e.g. a FedEx label booked early would promote portal state to
|
||||
# done - e.g. a FedEx label booked early would promote portal state to
|
||||
# 'shipped' even though the WO was still in 'confirmed'. The helper below
|
||||
# is the new single source of truth; the hooks now delegate to it.
|
||||
def _fp_recompute_portal_state(self):
|
||||
@@ -284,7 +284,7 @@ class FpPortalJob(models.Model):
|
||||
for portal in self:
|
||||
jobs = Job.sudo().search([('portal_job_id', '=', portal.id)])
|
||||
if not jobs:
|
||||
# No linked job — leave manual edits alone.
|
||||
# No linked job - leave manual edits alone.
|
||||
continue
|
||||
|
||||
all_done = all(j.state == 'done' for j in jobs)
|
||||
@@ -312,7 +312,7 @@ class FpPortalJob(models.Model):
|
||||
elif ship.status == 'shipped':
|
||||
ship_in_transit = True
|
||||
|
||||
# Invoice signal — any posted customer invoice on the SO.
|
||||
# Invoice signal - any posted customer invoice on the SO.
|
||||
invoiced = False
|
||||
for j in jobs:
|
||||
so = j.sale_order_id
|
||||
|
||||
@@ -13,14 +13,14 @@ class FpQuoteRequest(models.Model):
|
||||
|
||||
The RFQ is the entry point for new business through the customer portal.
|
||||
A customer fills out the public form (logged in), uploads any drawings,
|
||||
and submits — the record lands in the shop's backend in state ``new``.
|
||||
and submits - the record lands in the shop's backend in state ``new``.
|
||||
|
||||
The shop reviews, prices, and either quotes (``quoted``), declines, or
|
||||
lets the request expire. The portal mixin gives each request a stable
|
||||
access token URL so quote PDFs can be linked from chatter.
|
||||
"""
|
||||
_name = 'fusion.plating.quote.request'
|
||||
_description = 'Fusion Plating — Quote Request'
|
||||
_description = 'Fusion Plating - Quote Request'
|
||||
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
|
||||
_order = 'create_date desc, id desc'
|
||||
|
||||
@@ -139,7 +139,7 @@ class FpQuoteRequest(models.Model):
|
||||
)
|
||||
notes_internal = fields.Html(
|
||||
string='Internal Notes',
|
||||
help='Visible to shop users only — never shown on the customer portal.',
|
||||
help='Visible to shop users only - never shown on the customer portal.',
|
||||
)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
|
||||
@@ -13,7 +13,7 @@ class FpQuoteRequestLine(models.Model):
|
||||
part number, quantity, description, and file attachments.
|
||||
"""
|
||||
_name = 'fusion.plating.quote.request.line'
|
||||
_description = 'Fusion Plating — Quote Request Line'
|
||||
_description = 'Fusion Plating - Quote Request Line'
|
||||
_order = 'sequence, id'
|
||||
|
||||
request_id = fields.Many2one(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<!-- Quote Request: portal users see only their own -->
|
||||
<record id="fp_quote_request_portal_rule" model="ir.rule">
|
||||
<field name="name">Plating Quote Request: portal — own company</field>
|
||||
<field name="name">Plating Quote Request: portal - own company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_quote_request"/>
|
||||
<field name="domain_force">[('partner_id','child_of', user.commercial_partner_id.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
@@ -24,7 +24,7 @@
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Quote Request: internal shop users — all -->
|
||||
<!-- Quote Request: internal shop users - all -->
|
||||
<record id="fp_quote_request_internal_rule" model="ir.rule">
|
||||
<field name="name">Plating Quote Request: internal shop users</field>
|
||||
<field name="model_id" ref="model_fusion_plating_quote_request"/>
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<!-- Quote Request Line: portal users see only their own (via parent) -->
|
||||
<record id="fp_quote_request_line_portal_rule" model="ir.rule">
|
||||
<field name="name">Plating Quote Request Line: portal — own company</field>
|
||||
<field name="name">Plating Quote Request Line: portal - own company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_quote_request_line"/>
|
||||
<field name="domain_force">[('request_id.partner_id','child_of', user.commercial_partner_id.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
@@ -44,7 +44,7 @@
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Quote Request Line: internal shop users — all -->
|
||||
<!-- Quote Request Line: internal shop users - all -->
|
||||
<record id="fp_quote_request_line_internal_rule" model="ir.rule">
|
||||
<field name="name">Plating Quote Request Line: internal shop users</field>
|
||||
<field name="model_id" ref="model_fusion_plating_quote_request_line"/>
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<!-- Portal Job: portal users see only their own -->
|
||||
<record id="fp_portal_job_portal_rule" model="ir.rule">
|
||||
<field name="name">Plating Portal Job: portal — own company</field>
|
||||
<field name="name">Plating Portal Job: portal - own company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_portal_job"/>
|
||||
<field name="domain_force">[('partner_id','child_of', user.commercial_partner_id.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
@@ -64,7 +64,7 @@
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Portal Job: internal shop users — all -->
|
||||
<!-- Portal Job: internal shop users - all -->
|
||||
<record id="fp_portal_job_internal_rule" model="ir.rule">
|
||||
<field name="name">Plating Portal Job: internal shop users</field>
|
||||
<field name="model_id" ref="model_fusion_plating_portal_job"/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Fusion Plating — Portal Account Summary
|
||||
* Fusion Plating - Portal Account Summary
|
||||
* Wires the sort dropdown change event to navigate to the option's value
|
||||
* (which is a fully-formed /my/account_summary URL). Replaces an inline
|
||||
* `onchange` attribute on the <select> so the template stays CSP-clean.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Fusion Plating — portal list search + filter UI helper.
|
||||
* Fusion Plating - portal list search + filter UI helper.
|
||||
*
|
||||
* Provides client-side, real-time multi-keyword filtering for any portal
|
||||
* list page that opts in via the markup contract below. Pure vanilla JS,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Fusion Plating — Portal sidebar hamburger toggle.
|
||||
* Vanilla JS — no OWL / no jQuery. Loaded on every /my/* page.
|
||||
* Fusion Plating - Portal sidebar hamburger toggle.
|
||||
* Vanilla JS - no OWL / no jQuery. Loaded on every /my/* page.
|
||||
* Below 768px the sidebar is translateX(-100%); toggling
|
||||
* .o_fp_open on both sidebar + backdrop shows/hides it.
|
||||
*/
|
||||
@@ -43,7 +43,7 @@
|
||||
// breakpoint prevents the backdrop's display:block from leaking onto
|
||||
// the desktop layout. SCSS scopes the sidebar's drawer transform to
|
||||
// @media (max-width: 768px), but the backdrop's display rule isn't
|
||||
// media-scoped (intentionally — the JS owns that lifecycle).
|
||||
// media-scoped (intentionally - the JS owns that lifecycle).
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.innerWidth > 768 && sidebar.classList.contains("o_fp_open")) {
|
||||
toggleOpen(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Customer Portal · Design Tokens
|
||||
// Fusion Plating - Customer Portal · Design Tokens
|
||||
// Brand palette pulled from enplating.com live CSS (2026-05-17).
|
||||
// Loaded first in web.assets_frontend so every later SCSS file sees these.
|
||||
// Per Odoo 19 SCSS rules (CLAUDE.md rule 8/9): no @import; tokens are SCSS
|
||||
@@ -77,7 +77,7 @@ $fp-space-6: 1.5rem;
|
||||
$fp-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
$fp-font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, monospace;
|
||||
|
||||
// Dark-mode placeholder — DEFERRED per spec.
|
||||
// Dark-mode placeholder - DEFERRED per spec.
|
||||
// When implementing, branch on $o-webclient-color-scheme per CLAUDE.md rule 9.
|
||||
// Example pattern (do NOT enable now):
|
||||
// @if $o-webclient-color-scheme == dark {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Status badges
|
||||
// Fusion Plating - Portal · Status badges
|
||||
// Pill with coloured dot + soft glow halo. Maps directly to fp.portal.job.state
|
||||
// (and similar enum fields on quote / invoice / delivery).
|
||||
// ============================================================================
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// State mapping — extend with `class="o_fp_badge o_fp_badge_<state>"`.
|
||||
// State mapping - extend with `class="o_fp_badge o_fp_badge_<state>"`.
|
||||
.o_fp_badge_received,
|
||||
.o_fp_badge_new {
|
||||
background: $fp-section-bg;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Button system
|
||||
// Fusion Plating - Portal · Button system
|
||||
// Gradient primary CTA, soft secondary, ghost tertiary, gradient danger.
|
||||
// All states use class hooks under .o_fp_btn_* so they don't fight Bootstrap.
|
||||
// ============================================================================
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// PRIMARY — gradient teal CTA
|
||||
// PRIMARY - gradient teal CTA
|
||||
.o_fp_btn_primary {
|
||||
@extend .o_fp_btn;
|
||||
background: $fp-gradient-primary;
|
||||
@@ -45,7 +45,7 @@
|
||||
&:hover { box-shadow: $fp-shadow-button-hover; color: #fff; }
|
||||
}
|
||||
|
||||
// SECONDARY — outlined, very subtle gradient
|
||||
// SECONDARY - outlined, very subtle gradient
|
||||
.o_fp_btn_secondary {
|
||||
@extend .o_fp_btn;
|
||||
background: $fp-gradient-secondary;
|
||||
@@ -55,7 +55,7 @@
|
||||
&:hover { background: $fp-section-bg; color: $fp-teal-dark; }
|
||||
}
|
||||
|
||||
// GHOST — text-only with subtle hover
|
||||
// GHOST - text-only with subtle hover
|
||||
.o_fp_btn_ghost {
|
||||
@extend .o_fp_btn;
|
||||
background: transparent;
|
||||
@@ -64,7 +64,7 @@
|
||||
&:hover { background: rgba(46, 175, 147, .08); color: $fp-teal-dark; }
|
||||
}
|
||||
|
||||
// DANGER — gradient red
|
||||
// DANGER - gradient red
|
||||
.o_fp_btn_danger {
|
||||
@extend .o_fp_btn;
|
||||
background: $fp-gradient-danger;
|
||||
@@ -73,7 +73,7 @@
|
||||
&:hover { color: #fff; }
|
||||
}
|
||||
|
||||
// MINT-PILL — soft branded "view all" affordance
|
||||
// MINT-PILL - soft branded "view all" affordance
|
||||
.o_fp_btn_mint {
|
||||
@extend .o_fp_btn;
|
||||
background: $fp-gradient-mint;
|
||||
@@ -83,7 +83,7 @@
|
||||
&:hover { color: $fp-teal-dark; }
|
||||
}
|
||||
|
||||
// Size modifiers — match Bootstrap btn-sm / btn-lg sizing
|
||||
// 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; }
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Card shells + KPI tiles + doc chips
|
||||
// Fusion Plating - Portal · Card shells + KPI tiles + doc chips
|
||||
// ============================================================================
|
||||
|
||||
// Generic card shell
|
||||
@@ -132,7 +132,7 @@
|
||||
padding: .25rem .5rem;
|
||||
}
|
||||
|
||||
// Icon color variants — tint per doc category
|
||||
// Icon color variants - tint per doc category
|
||||
.o_fp_doc_icon_input { background: #eff6ff; color: #1e40af; }
|
||||
.o_fp_doc_icon_drawing { background: $fp-success-bg; color: $fp-success-text; }
|
||||
.o_fp_doc_icon_spec { background: $fp-amber-bg; color: $fp-amber-text; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Dashboard layout
|
||||
// Fusion Plating - Portal · Dashboard layout
|
||||
// Jobs-forward grid: welcome strip → KPI tile row → hero jobs section →
|
||||
// secondary panel strip.
|
||||
// ============================================================================
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
// Job card: outer wrap is a plain div so we can place an interactive
|
||||
// actions footer (Repeat Order form, doc download links) as a SIBLING
|
||||
// of the main anchor — forms inside anchors are invalid HTML and
|
||||
// of the main anchor - forms inside anchors are invalid HTML and
|
||||
// browser-buggy. Hover/lift effect lives on the wrap; click target is
|
||||
// the inner .o_fp_job_card_main anchor only.
|
||||
.o_fp_job_card {
|
||||
@@ -161,7 +161,7 @@
|
||||
.o_fp_job_ship_icon { color: $fp-muted-light; margin-right: .3rem; }
|
||||
}
|
||||
|
||||
// Actions footer — siblings of the main anchor. Doc download chips on
|
||||
// Actions footer - siblings of the main anchor. Doc download chips on
|
||||
// the left, Repeat Order on the right. Border-top visually separates.
|
||||
.o_fp_job_card_actions {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Sidebar shell
|
||||
// Fusion Plating - Portal · Sidebar shell
|
||||
// Sticky 240px left rail wrapping every /my/* page. Grouped sections
|
||||
// (Dashboard / ACTIVITY / DOCUMENTS / ACCOUNT). Active page = mint
|
||||
// gradient fill + brand teal left bar. Below 768px collapses to a
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Internal staff (employee portal) — no customer sidebar. Collapse the grid
|
||||
// Internal staff (employee portal) - no customer sidebar. Collapse the grid
|
||||
// to a single column so page content isn't pushed right by the now-empty
|
||||
// 240px sidebar track.
|
||||
.o_fp_portal_shell--no-sidebar {
|
||||
@@ -157,7 +157,7 @@
|
||||
.o_fp_portal_main {
|
||||
// Stretches with the grid row so the right column matches the
|
||||
// sidebar's height on short pages (empty list states, statements
|
||||
// tab, etc.) — uniform visual rhythm.
|
||||
// tab, etc.) - uniform visual rhythm.
|
||||
min-height: 100%;
|
||||
// Bootstrap tables can grow wider than the grid track without this;
|
||||
// min-width: 0 lets the flex/grid child shrink and lets overflow-x
|
||||
@@ -167,7 +167,7 @@
|
||||
// Neutralise Odoo's container pt-3 + templates' mt-3 on the first
|
||||
// child so the right column's top aligns flush with the sidebar's
|
||||
// top edge. !important is required because Bootstrap 5 spacing
|
||||
// utilities (.pt-3, .mt-3) ship with !important by default — without
|
||||
// utilities (.pt-3, .mt-3) ship with !important by default - without
|
||||
// matching specificity Bootstrap wins and the right column sits
|
||||
// ~32px (pt-3 + mt-3) lower than the sidebar.
|
||||
#wrap > .container {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Numbered stepper
|
||||
// Fusion Plating - Portal · Numbered stepper
|
||||
// Horizontal circle+line stepper for job progress on dashboard cards.
|
||||
// 5 steps fixed (Received / Inspected / Plating / QC / Ship) by default;
|
||||
// macro accepts variable step count.
|
||||
@@ -128,5 +128,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy .o_fp_step_labels container removed — labels are now nested
|
||||
// Legacy .o_fp_step_labels container removed - labels are now nested
|
||||
// inside each .o_fp_step_unit (see above) so they centre on their circle.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// Fusion Plating — Portal · Vertical timeline (job detail page)
|
||||
// Fusion Plating - Portal · Vertical timeline (job detail page)
|
||||
// ============================================================================
|
||||
|
||||
.o_fp_timeline {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Customer Portal · Legacy catch-all
|
||||
// Fusion Plating - Customer Portal · Legacy catch-all
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Generic portal card surface — used by empty-state messages on the secondary
|
||||
// Generic portal card surface - used by empty-state messages on the secondary
|
||||
// portal pages (quote requests, POs, invoices, certifications) and by the
|
||||
// configurator coating cards. Replaced by .o_fp_card on the redesigned
|
||||
// surfaces (/my/home, /my/jobs, /my/jobs/<id>).
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// RFQ Form — Part row card (dynamically inserted by fp_rfq_form.js)
|
||||
// RFQ Form - Part row card (dynamically inserted by fp_rfq_form.js)
|
||||
// -----------------------------------------------------------------------------
|
||||
.o_fp_part_row {
|
||||
background-color: var(--bs-body-bg);
|
||||
|
||||
@@ -44,7 +44,7 @@ class TestEmployeePortalGating(HttpCase):
|
||||
if 'hr.employee' not in self.env:
|
||||
self.skipTest('hr not installed')
|
||||
if 'x_fclk_enable_clock' not in self.env['hr.employee']._fields:
|
||||
self.skipTest('fusion_clock not installed — redirect intentionally inert')
|
||||
self.skipTest('fusion_clock not installed - redirect intentionally inert')
|
||||
internal = self.env['res.users'].create({
|
||||
'name': 'Shop Hand',
|
||||
'login': 'gating_shop_hand',
|
||||
@@ -54,7 +54,7 @@ class TestEmployeePortalGating(HttpCase):
|
||||
self.assertFalse(internal.share)
|
||||
self.env['hr.employee'].create({'name': 'Shop Hand', 'user_id': internal.id})
|
||||
self.authenticate('gating_shop_hand', 'gating_shop_hand')
|
||||
# Don't follow the redirect — just assert we're bounced toward /my/clock.
|
||||
# Don't follow the redirect - just assert we're bounced toward /my/clock.
|
||||
r = self.url_open('/my/home', allow_redirects=False)
|
||||
self.assertIn(r.status_code, (301, 302, 303, 307, 308))
|
||||
self.assertIn('/my/clock', r.headers.get('Location', ''))
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
t-if="pager and pager.get('page_count', 0) > 1">
|
||||
<div class="text-muted small">
|
||||
Showing
|
||||
<t t-out="pager['offset'] + 1"/>–<t t-out="min(pager['offset'] + 10, total)"/>
|
||||
<t t-out="pager['offset'] + 1"/>-<t t-out="min(pager['offset'] + 10, total)"/>
|
||||
of <t t-out="total"/>
|
||||
</div>
|
||||
<ul class="pagination mb-0">
|
||||
|
||||
@@ -108,9 +108,9 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- STEP 1 — Upload Part / Manual Measurements -->
|
||||
<!-- STEP 1 - Upload Part / Manual Measurements -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_configurator_step1" name="Configurator Step 1 — Upload Part">
|
||||
<template id="portal_configurator_step1" name="Configurator Step 1 - Upload Part">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="o_fp_portal_form mt-3" style="max-width: 720px; margin: 0 auto;">
|
||||
|
||||
@@ -156,10 +156,10 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- File Uploads — separate drawing + 3D model.
|
||||
<!-- File Uploads - separate drawing + 3D model.
|
||||
Customer can upload either or both. STL gets
|
||||
trimesh surface-area auto-calc server-side
|
||||
(not shown to customer — backend uses it for
|
||||
(not shown to customer - backend uses it for
|
||||
future pricing). -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
@@ -181,7 +181,7 @@
|
||||
<i class="fa fa-cube"/>
|
||||
<p class="mb-1 fw-semibold">STL / STP / STEP / IGES</p>
|
||||
<p class="small text-muted mb-2">
|
||||
Optional — speeds up estimation
|
||||
Optional - speeds up estimation
|
||||
</p>
|
||||
<input type="file" name="part_3d_model" id="part_3d_model"
|
||||
class="form-control"
|
||||
@@ -191,7 +191,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Manual measurements hidden per customer-feedback 2026-05-17:
|
||||
backend computes these (or doesn't) — not the
|
||||
backend computes these (or doesn't) - not the
|
||||
customer's job. Fields kept as hidden inputs at 0
|
||||
so the controller doesn't error on missing keys. -->
|
||||
<input type="hidden" name="geometry_source" value="upload"/>
|
||||
@@ -219,9 +219,9 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- STEP 2 — Select Coating Configuration -->
|
||||
<!-- STEP 2 - Select Coating Configuration -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_configurator_step2" name="Configurator Step 2 — Select Coating">
|
||||
<template id="portal_configurator_step2" name="Configurator Step 2 - Select Coating">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="o_fp_portal_form mt-3" style="max-width: 900px; margin: 0 auto;">
|
||||
|
||||
@@ -319,9 +319,9 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- STEP 3 — Estimate & Submit -->
|
||||
<!-- STEP 3 - Estimate & Submit -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_configurator_step3" name="Configurator Step 3 — Estimate & Submit">
|
||||
<template id="portal_configurator_step3" name="Configurator Step 3 - Estimate & Submit">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="o_fp_portal_form mt-3" style="max-width: 720px; margin: 0 auto;">
|
||||
|
||||
@@ -455,7 +455,7 @@
|
||||
<!-- ================================================================== -->
|
||||
<!-- SUCCESS PAGE -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_configurator_success" name="Configurator — Quote Submitted">
|
||||
<template id="portal_configurator_success" name="Configurator - Quote Submitted">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="o_fp_portal_form mt-3" style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="card text-center py-5">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Portal Home Dashboard — jobs-forward layout (v19.0.3.1.0 redesign) -->
|
||||
<!-- Portal Home Dashboard - jobs-forward layout (v19.0.3.1.0 redesign) -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="fp_portal_home_dashboard" name="Plating Portal Dashboard">
|
||||
<t t-call="portal.portal_layout">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Status badge — pass state (string) and label (string) -->
|
||||
<!-- Status badge - pass state (string) and label (string) -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="fp_portal_status_badge" name="Portal: Status Badge">
|
||||
<span t-attf-class="o_fp_badge o_fp_badge_#{state}">
|
||||
@@ -19,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Numbered horizontal stepper — pass `steps` list of dicts: -->
|
||||
<!-- Numbered horizontal stepper - pass `steps` list of dicts: -->
|
||||
<!-- {label, status: 'done'|'active'|'pending', time_label} -->
|
||||
<!-- active_state: 'normal' (teal) or 'warn' (amber) -->
|
||||
<!-- ================================================================== -->
|
||||
@@ -30,7 +30,7 @@
|
||||
<!-- Unit = circle + its label stacked. Label is absolutely
|
||||
positioned below the circle (in SCSS) so its horizontal
|
||||
centre lines up with the circle no matter how wide the
|
||||
text is — fixes the column-vs-edge distribution
|
||||
text is - fixes the column-vs-edge distribution
|
||||
mismatch we had with a separate labels row. -->
|
||||
<div class="o_fp_step_unit">
|
||||
<div t-attf-class="o_fp_step_circle #{
|
||||
@@ -61,7 +61,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Doc chip (compact) — pass doc dict {icon, label, url, pending} -->
|
||||
<!-- Doc chip (compact) - pass doc dict {icon, label, url, pending} -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="fp_portal_doc_chip" name="Portal: Doc Chip">
|
||||
<t t-if="doc.get('pending')">
|
||||
@@ -79,7 +79,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Job card — shared between /my/home dashboard and /my/jobs list. -->
|
||||
<!-- Job card - shared between /my/home dashboard and /my/jobs list. -->
|
||||
<!-- Pass `job` (fusion.plating.portal.job). Renders a wrap div with -->
|
||||
<!-- inner anchor (whole card click target = detail page) and a sibling -->
|
||||
<!-- actions footer (doc download chips + Repeat Order form). Forms -->
|
||||
@@ -228,7 +228,7 @@
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Search input — real-time client-side filtering, no submit -->
|
||||
<!-- Search input - real-time client-side filtering, no submit -->
|
||||
<div class="ms-auto d-flex align-items-center gap-2"
|
||||
t-att-style="'flex: 1 1 auto; max-width: 360px;' + ('' if filters else 'margin-left: 0 !important')">
|
||||
<input type="search"
|
||||
@@ -239,7 +239,7 @@
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
|
||||
<!-- Sort dropdown — navigates on change (wired by fp_portal_list_search.js) -->
|
||||
<!-- Sort dropdown - navigates on change (wired by fp_portal_list_search.js) -->
|
||||
<select class="form-select form-select-sm o_fp_sort_select" style="max-width: 180px" t-if="sorts">
|
||||
<t t-foreach="sorts" t-as="s">
|
||||
<option t-att-value="url + '?filter_state=' + (active_filter or 'all') + '&sortby=' + s[0] + (extra_qs or '')"
|
||||
@@ -250,14 +250,14 @@
|
||||
</div>
|
||||
<!-- Clip notice: shown server-side when >500 records were found -->
|
||||
<div class="o_fp_list_search_meta small text-muted mb-2" t-if="clipped">
|
||||
Showing latest 500 of <span t-out="result_total"/> — refine your filter to narrow further.
|
||||
Showing latest 500 of <span t-out="result_total"/> - refine your filter to narrow further.
|
||||
</div>
|
||||
<!-- Live count: shown by JS while the user is typing in the search box -->
|
||||
<div class="o_fp_list_search_count small text-muted mb-2 d-none"/>
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Doc group (detail page) — pass label + docs list of dicts: -->
|
||||
<!-- Doc group (detail page) - pass label + docs list of dicts: -->
|
||||
<!-- {label, sub, url, icon_class, pending} -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="fp_portal_doc_group" name="Portal: Doc Group">
|
||||
@@ -271,7 +271,7 @@
|
||||
<div class="o_fp_doc_name" t-out="doc['label']"/>
|
||||
<div class="o_fp_doc_sub" t-out="doc.get('sub') or ''"/>
|
||||
</div>
|
||||
<span style="color: #cbd5e1; font-size: .72rem">—</span>
|
||||
<span style="color: #cbd5e1; font-size: .72rem">-</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<!-- Inherit portal.portal_layout to wrap content in sidebar shell -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="fp_portal_shell"
|
||||
name="FP Portal Shell — Sidebar Wrap"
|
||||
name="FP Portal Shell - Sidebar Wrap"
|
||||
inherit_id="portal.portal_layout"
|
||||
priority="50">
|
||||
<!-- Force Odoo's outer breadcrumb container to render even when a page
|
||||
@@ -72,14 +72,14 @@
|
||||
<!-- Sidebar navigation component -->
|
||||
<t t-call="fusion_plating_portal.fp_portal_sidebar"/>
|
||||
</t>
|
||||
<!-- Main content area — original #wrap re-emitted here via $0 -->
|
||||
<!-- Main content area - original #wrap re-emitted here via $0 -->
|
||||
<main class="o_fp_portal_main">$0</main>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Sidebar template — rendered by fp_portal_shell -->
|
||||
<!-- Sidebar template - rendered by fp_portal_shell -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="fp_portal_sidebar" name="FP Portal Sidebar">
|
||||
<aside class="o_fp_portal_sidebar">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- QUOTE REQUESTS — list with filter pills + real-time search -->
|
||||
<!-- QUOTE REQUESTS - list with filter pills + real-time search -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_quote_requests" name="My Quote Requests">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -94,7 +94,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- QUOTE REQUEST — detail -->
|
||||
<!-- QUOTE REQUEST - detail -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_quote_request" name="My Quote Request">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -250,7 +250,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- QUOTE REQUEST — new form (enhanced with multi-part, addresses) -->
|
||||
<!-- QUOTE REQUEST - new form (enhanced with multi-part, addresses) -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_new_quote_request_form" name="New Quote Request">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -431,7 +431,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- JOBS — list with filter pills + real-time search (cards layout) -->
|
||||
<!-- JOBS - list with filter pills + real-time search (cards layout) -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_jobs" name="My Work Orders">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -493,7 +493,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- JOB — detail -->
|
||||
<!-- JOB - detail -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_job" name="My Work Order">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -677,7 +677,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- DELIVERIES / PACKING SLIPS — list with search + sort -->
|
||||
<!-- DELIVERIES / PACKING SLIPS - list with search + sort -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_deliveries" name="My Deliveries">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -685,7 +685,7 @@
|
||||
<t t-set="title">Packing Slips / Deliveries</t>
|
||||
</t>
|
||||
|
||||
<!-- Search + sort strip (no filter pills — all rows are delivered) -->
|
||||
<!-- Search + sort strip (no filter pills - all rows are delivered) -->
|
||||
<t t-call="fusion_plating_portal.fp_portal_list_controls">
|
||||
<t t-set="filters" t-value="filters"/>
|
||||
<t t-set="active_filter" t-value="filter_state"/>
|
||||
@@ -743,7 +743,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CERTIFICATIONS — list with search + sort -->
|
||||
<!-- CERTIFICATIONS - list with search + sort -->
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_certifications" name="My Certifications">
|
||||
<t t-call="portal.portal_layout">
|
||||
@@ -751,7 +751,7 @@
|
||||
<t t-set="title">Certifications & Quality</t>
|
||||
</t>
|
||||
|
||||
<!-- Search + sort strip (no filter pills — all certs are terminal) -->
|
||||
<!-- Search + sort strip (no filter pills - all certs are terminal) -->
|
||||
<t t-call="fusion_plating_portal.fp_portal_list_controls">
|
||||
<t t-set="filters" t-value="filters"/>
|
||||
<t t-set="active_filter" t-value="filter_state"/>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Quote Request — list -->
|
||||
<!-- Quote Request - list -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_fp_quote_request_list" model="ir.ui.view">
|
||||
<field name="name">fp.quote.request.list</field>
|
||||
@@ -39,7 +39,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Quote Request — form -->
|
||||
<!-- Quote Request - form -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_fp_quote_request_form" model="ir.ui.view">
|
||||
<field name="name">fp.quote.request.form</field>
|
||||
@@ -136,7 +136,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Quote Request — search -->
|
||||
<!-- Quote Request - search -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_fp_quote_request_search" model="ir.ui.view">
|
||||
<field name="name">fp.quote.request.search</field>
|
||||
@@ -162,7 +162,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Quote Request — action -->
|
||||
<!-- Quote Request - action -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_fp_quote_request" model="ir.actions.act_window">
|
||||
<field name="name">Quote Requests</field>
|
||||
@@ -182,7 +182,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Portal Job — list -->
|
||||
<!-- Portal Job - list -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_fp_portal_job_list" model="ir.ui.view">
|
||||
<field name="name">fp.portal.job.list</field>
|
||||
@@ -207,7 +207,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Portal Job — form -->
|
||||
<!-- Portal Job - form -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_fp_portal_job_form" model="ir.ui.view">
|
||||
<field name="name">fp.portal.job.form</field>
|
||||
@@ -256,7 +256,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Portal Job — search -->
|
||||
<!-- Portal Job - search -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_fp_portal_job_search" model="ir.ui.view">
|
||||
<field name="name">fp.portal.job.search</field>
|
||||
@@ -283,7 +283,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- Portal Job — action -->
|
||||
<!-- Portal Job - action -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_fp_portal_job" model="ir.actions.act_window">
|
||||
<field name="name">Work Orders</field>
|
||||
@@ -293,7 +293,7 @@
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- res.partner — extend form to surface portal flag + counts -->
|
||||
<!-- res.partner - extend form to surface portal flag + counts -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_partner_form_fp_portal" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.fp.portal</field>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Adds a leading "Part #" column to the customer portal's Sales Order
|
||||
products table. Reads sale.order.line.x_fc_part_catalog_id.part_number
|
||||
(defined in fusion_plating_configurator). The existing second column
|
||||
keeps line.name — the customer-facing description.
|
||||
keeps line.name - the customer-facing description.
|
||||
|
||||
Layout after this inherit:
|
||||
| Part # | Description | Quantity | Unit Price | [Disc] | [Taxes] | Amount |
|
||||
@@ -15,7 +15,7 @@
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- /my/orders — inject filter strip + data-fp-filterable before the -->
|
||||
<!-- /my/orders - inject filter strip + data-fp-filterable before the -->
|
||||
<!-- Odoo portal table so real-time search works client-side. -->
|
||||
<!-- Sort dropdown reuses Odoo's existing sortby param; filter pills -->
|
||||
<!-- link to URL params that Odoo's stock route honours natively. -->
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Inject the controls strip right after the portal_searchbar call
|
||||
and before the "no orders" alert / portal_table. The anchor is
|
||||
the <div t-if="not orders"> alert — we inject before it. -->
|
||||
the <div t-if="not orders"> alert - we inject before it. -->
|
||||
<xpath expr="//div[@t-if='not orders']" position="before">
|
||||
<t t-call="fusion_plating_portal.fp_portal_list_controls">
|
||||
<t t-set="filters" t-value="False"/>
|
||||
|
||||
Reference in New Issue
Block a user