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

@@ -14,7 +14,7 @@ class FpBakeOven(models.Model):
to a bake window record by serial number.
"""
_name = 'fusion.plating.bake.oven'
_description = 'Fusion Plating Bake Oven'
_description = 'Fusion Plating - Bake Oven'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, code'

View File

@@ -15,14 +15,14 @@ class FpBakeWindow(models.Model):
When a high-strength-steel part exits a plating tank, a clock starts.
The customer / specification defines a window (typically 1 to 4 hours)
inside which the relief bake MUST begin. Missing the window requires
scrap or rework there is no retroactive fix.
scrap or rework - there is no retroactive fix.
This model is the headline differentiator of the shop-floor module.
A cron job updates state every 5 minutes so the kanban board on the
tablet always reflects current jeopardy.
"""
_name = 'fusion.plating.bake.window'
_description = 'Fusion Plating Bake Window'
_description = 'Fusion Plating - Bake Window'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'bake_required_by, id desc'
_rec_name = 'name'
@@ -192,7 +192,7 @@ class FpBakeWindow(models.Model):
@api.depends('state', 'plate_exit_time', 'window_hours', 'bake_required_by',
'bake_start_time')
def _compute_status_color(self):
"""Kanban colour index neutral palette that works in light + dark.
"""Kanban colour index - neutral palette that works in light + dark.
0=no color, 1=red, 2=orange, 3=yellow, 4=green, 5=purple, 10=grey
"""
@@ -208,7 +208,7 @@ class FpBakeWindow(models.Model):
rec.status_color = 5 # purple
elif rec.state == 'awaiting_bake' and rec.bake_required_by:
if now >= rec.bake_required_by:
rec.status_color = 1 # red missed
rec.status_color = 1 # red - missed
elif rec.plate_exit_time and rec.window_hours:
elapsed = (now - rec.plate_exit_time).total_seconds()
total = rec.window_hours * 3600.0
@@ -229,7 +229,7 @@ class FpBakeWindow(models.Model):
now = fields.Datetime.now()
for rec in self:
if rec.state in ('baked', 'scrapped'):
rec.time_remaining_display = ''
rec.time_remaining_display = '-'
continue
if not rec.bake_required_by:
rec.time_remaining_display = ''
@@ -252,7 +252,7 @@ class FpBakeWindow(models.Model):
Hard guard: cannot start a bake on a missed_window record without
manager override (context `fp_skip_missed_window=True`). AS9100 /
Nadcap can't be retroactively documented starting a bake after
Nadcap can't be retroactively documented - starting a bake after
the window means the parts are likely scrap. The override exists
for the rare case the customer accepts a deviation in writing;
every override posts to chatter so the audit trail is intact.
@@ -267,13 +267,13 @@ class FpBakeWindow(models.Model):
raise UserError(_(
'Bake window %s has expired (required by %s). '
'A manager must override via the "Force Start "'
'(missed window)" action the override is '
'(missed window)" action - the override is '
'logged on chatter for audit. Otherwise the '
'parts must be scrapped.'
) % (rec.name, rec.bake_required_by))
rec.message_post(body=_(
'MANAGER OVERRIDE: bake started after missed window. '
'Window required by %s actual start %s. Customer '
'Window required by %s - actual start %s. Customer '
'deviation must be on file.'
) % (rec.bake_required_by, fields.Datetime.now()))
rec.write({

View File

@@ -14,7 +14,7 @@ class FpOperatorQueue(models.TransientModel):
table that would drift from reality.
"""
_name = 'fusion.plating.operator.queue'
_description = 'Fusion Plating Operator Next-Up Queue'
_description = 'Fusion Plating - Operator Next-Up Queue'
_order = 'priority desc, due_at, id'
operator_id = fields.Many2one(
@@ -71,7 +71,7 @@ class FpOperatorQueue(models.TransientModel):
# Show two buckets, in this order:
# 1) WOs explicitly assigned to this operator (their named tasks)
# 2) WOs with NO assignment (open for any operator to grab)
# Skip WOs assigned to OTHER operators strict per-aerospace
# Skip WOs assigned to OTHER operators - strict per-aerospace
# accountability (no one should "borrow" someone else's job).
MrpWO = self.env.get('mrp.workorder')
if MrpWO is not None:

View File

@@ -13,7 +13,7 @@ class FpShopfloorStation(models.Model):
so an operator can pair their device to a work centre with a single tap.
"""
_name = 'fusion.plating.shopfloor.station'
_description = 'Fusion Plating Shop Floor Station'
_description = 'Fusion Plating - Shop Floor Station'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, work_center_id, code'
@@ -73,7 +73,7 @@ class FpShopfloorStation(models.Model):
string='Notes',
)
# Phase 6 tablet PIN gate per-station roster + idle override.
# Phase 6 tablet PIN gate - per-station roster + idle override.
x_fc_authorised_user_ids = fields.Many2many(
'res.users',
relation='fp_shopfloor_station_authorised_user_rel',

View File

@@ -23,7 +23,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# Code TTL user-picked default per D4 (spec). Long enough for shift
# Code TTL - user-picked default per D4 (spec). Long enough for shift
# workers / weekend gaps; short enough that an old code in the inbox
# isn't a long-lived risk.
_CODE_TTL_HOURS = 72
@@ -77,7 +77,7 @@ class FpTabletPinReset(models.Model):
_sql_constraints = [
# At most ONE active (used_at IS NULL) row per user. Forces the
# "request new = invalidate old" behavior. Uses Postgres
# EXCLUDE partial unique index doesn't compose with the
# EXCLUDE - partial unique index doesn't compose with the
# other rows where used_at IS NOT NULL.
('one_active_per_user',
"EXCLUDE (user_id WITH =) WHERE (used_at IS NULL)",
@@ -152,7 +152,7 @@ class FpTabletPinReset(models.Model):
('user_id', '=', user.id),
('used_at', '=', False),
]).write({'used_at': fields.Datetime.now()})
# Generate the code 0000-9999, zero-padded.
# Generate the code - 0000-9999, zero-padded.
code = f"{secrets.randbelow(10000):04d}"
rec = self.sudo().create({
'user_id': user.id,
@@ -211,7 +211,7 @@ class FpTabletPinReset(models.Model):
)
if not secret:
raise UserError(_(
'Cannot sign reset token database.secret not set.'
'Cannot sign reset token - database.secret not set.'
))
payload = {
'user_id': int(user_id),
@@ -271,7 +271,7 @@ class FpTabletPinReset(models.Model):
@api.model
def _cron_purge_expired(self):
"""Daily cron delete used/expired rows > 7 days old.
"""Daily cron - delete used/expired rows > 7 days old.
Audit trail lives in fp.tablet.session.event, not here, so we
can purge aggressively without losing forensics."""
cutoff = fields.Datetime.now() - timedelta(days=7)

View File

@@ -29,7 +29,7 @@ class FpTabletSessionEvent(models.Model):
('ceiling_lock', '8-hour ceiling lock'),
('force_lock', 'Force lock (cron, stale session)'),
('admin_reset', 'Admin force-reset PIN'),
# Spec 2026-05-25 self-service PIN reset flow
# Spec 2026-05-25 - self-service PIN reset flow
('pin_reset_requested', 'PIN reset code requested (email sent)'),
('pin_reset_code_verified', 'PIN reset code verified'),
('pin_set_after_reset', 'New PIN set via email reset flow'),
@@ -152,7 +152,7 @@ class FpTabletSessionEvent(models.Model):
notes='Cron force-lock: session exceeded %d-hour ceiling' % ceiling_hours,
)
# Mark the original unlock event closed so it's not reprocessed
# next tick. write() is blocked by the model override use
# next tick. write() is blocked by the model override - use
# direct SQL bypass (this is the documented escape hatch for
# the retention/cron path).
self.env.cr.execute(

View File

@@ -5,7 +5,7 @@
"""Feature flags for fusion_plating_shopfloor.
Currently:
- x_fc_shopfloor_layout switches the Shop Floor client action
- x_fc_shopfloor_layout - switches the Shop Floor client action
between the legacy per-step kanban and the v2 plant-view kanban.
Backed by ir.config_parameter so the landing-action resolver can
read it cheaply on every action open without a recordset fetch.

View File

@@ -88,7 +88,7 @@ class ResUsers(models.Model):
"""Set or change this user's tablet PIN. Requires sudo OR self.
Caller is responsible for verifying the OLD pin separately if a
hash already exists this method just writes the new one.
hash already exists - this method just writes the new one.
"""
self.ensure_one()
if not pin or not pin.isdigit() or len(pin) != 4:
@@ -182,7 +182,7 @@ class ResUsers(models.Model):
so the standard auth chain returns a 401.
See docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
Section 2 Auth path.
Section 2 - Auth path.
"""
if isinstance(credential, dict) and credential.get('type') == 'fp_tablet_pin':
login = credential.get('login')
@@ -192,7 +192,7 @@ class ResUsers(models.Model):
user_sudo = self.sudo().search([('login', '=', login)], limit=1)
if not user_sudo or not user_sudo.active:
raise AccessDenied()
# Must hold a shop-branch role (transitively all_group_ids follows
# Must hold a shop-branch role (transitively - all_group_ids follows
# the implication chain so users who hold Owner directly still match
# the Technician/Manager checks below). Matches has_group() semantics
# and is futureproof against role-graph edits (CLAUDE.md rules 13l + 23).
@@ -223,12 +223,12 @@ class ResUsers(models.Model):
return super()._check_credentials(credential, env)
# ------------------------------------------------------------------
# _fp_resolve_from_header used by mail.template email_from / reply_to
# _fp_resolve_from_header - used by mail.template email_from / reply_to
# ------------------------------------------------------------------
# Picks the From address that matches the active outbound mail server's
# from_filter, so the message goes out perfectly aligned for SPF +
# DKIM + DMARC. Mismatched From triggers M365 greylisting (515 min
# delivery delay) on cross-provider mail the user feels this as
# DKIM + DMARC. Mismatched From triggers M365 greylisting (5-15 min
# delivery delay) on cross-provider mail - the user feels this as
# "the email takes a while." Mail-server lookups need sudo; the kiosk
# session calling the template has no read on ir.mail_server. Falls
# back to res.company.email if no usable mail server is configured.
@@ -239,12 +239,12 @@ class ResUsers(models.Model):
order='sequence asc, id asc', limit=1)
if srv and srv.from_filter and '@' in srv.from_filter:
# from_filter can be 'user@domain' OR a domain like '*@domain' /
# 'domain' only the exact-address form is safe to use as From.
# 'domain' - only the exact-address form is safe to use as From.
ff = srv.from_filter.strip()
if not ff.startswith('*') and ' ' not in ff:
return ff
if srv and srv.smtp_user and '@' in srv.smtp_user:
return srv.smtp_user
# Last-ditch fallback preserves the legacy behaviour for any
# Last-ditch fallback - preserves the legacy behaviour for any
# environment that has no mail server configured.
return self.company_id.email or self.email or ''