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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user