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:
@@ -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'
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (5–15 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 ''
|
||||
|
||||
Reference in New Issue
Block a user