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

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