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:
gsinghpal
2026-06-05 00:16:19 -04:00
parent c9eb61ee0c
commit 8c76a16366
789 changed files with 4692 additions and 4692 deletions

View File

@@ -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
],
},

View File

@@ -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')
# ==========================================================================

View File

@@ -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):

View File

@@ -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">

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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"/>

View File

@@ -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.

View File

@@ -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,

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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.

View File

@@ -1,5 +1,5 @@
// ============================================================================
// Fusion Plating Portal · Vertical timeline (job detail page)
// Fusion Plating - Portal · Vertical timeline (job detail page)
// ============================================================================
.o_fp_timeline {

View File

@@ -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);

View File

@@ -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', ''))

View File

@@ -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">

View File

@@ -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 &amp; Submit">
<template id="portal_configurator_step3" name="Configurator Step 3 - Estimate &amp; 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">

View File

@@ -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">

View File

@@ -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') + '&amp;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="">

View File

@@ -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">

View File

@@ -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 &amp; 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"/>

View File

@@ -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>

View File

@@ -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"/>