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

@@ -10,7 +10,7 @@ from odoo.exceptions import UserError
class FpDirectOrderLine(models.Model):
"""Sub 9 persistent so the parent draft survives navigation."""
"""Sub 9 - persistent so the parent draft survives navigation."""
_name = 'fp.direct.order.line'
_description = 'Fusion Plating - Direct Order Line'
_order = 'sequence, id'
@@ -54,7 +54,7 @@ class FpDirectOrderLine(models.Model):
# Specification picker (customer_spec_id) added by
# fusion_plating_quality. Legacy coating_config_id +
# treatment_ids removed.
# Sub 9 (polished 2026-04-28) process variant per line. The picker
# Sub 9 (polished 2026-04-28) - process variant per line. The picker
# now lets the estimator pick ANY root recipe in the system: the
# part's own variants, another customer's variants, or a template
# marked is_template. Cross-part picks auto-clone onto this part on
@@ -64,7 +64,7 @@ class FpDirectOrderLine(models.Model):
string='Process Variant',
domain="[('id', 'in', recipe_choice_ids)]",
ondelete='set null',
help='Pick any recipe the part\'s own variant, another part\'s '
help='Pick any recipe - the part\'s own variant, another part\'s '
'recipe, or a template from the library. Cross-part picks '
'are cloned onto this part on save so per-line edits stay '
'scoped. Use the Customize button to open the Process '
@@ -88,7 +88,7 @@ class FpDirectOrderLine(models.Model):
SOL = self.env['sale.order.line']
for rec in self:
ids = set()
# 1) Templates the "parent recipes" the operator sees first.
# 1) Templates - the "parent recipes" the operator sees first.
templates = Node.search([
('parent_id', '=', False),
('node_type', '=', 'recipe'),
@@ -108,7 +108,7 @@ class FpDirectOrderLine(models.Model):
], order='create_date desc', limit=500
).mapped('x_fc_process_variant_id')
ids.update(used.ids)
# 4) The wizard's order-level Material/Process recipe must be
# 4) The wizard's order-level Material/Process recipe - must be
# selectable on the line so the G3 propagation can write it
# without the domain rejecting (2026-05-27 fix).
if rec.wizard_id and rec.wizard_id.material_process:
@@ -117,7 +117,7 @@ class FpDirectOrderLine(models.Model):
save_as_default_process = fields.Boolean(
string='Set as Part Default',
help='When ticked, the chosen process variant becomes this part\'s '
'default on order submit future orders for the same part '
'default on order submit - future orders for the same part '
'pre-fill with this variant.',
)
# Read-only preview of the process tree that WILL drive WO generation
@@ -160,7 +160,7 @@ class FpDirectOrderLine(models.Model):
"""Pre-fill the line from the part's saved defaults when the part
changes.
2026-04-28 polish: variant is no longer cleared instead it
2026-04-28 polish: variant is no longer cleared - instead it
pre-fills from the part's `default_process_id` so the estimator
gets a sensible starting point. (Domain is system-wide now, so
a stale value would still load fine; we just upgrade the UX.)
@@ -168,7 +168,7 @@ class FpDirectOrderLine(models.Model):
Pre-fill coating + treatments from the part's saved defaults so
the estimator doesn't re-pick the same coating every repeat
customer. Defaults only apply when the line currently has no
coating set editing an existing line with a chosen coating
coating set - editing an existing line with a chosen coating
doesn't get clobbered.
For BRAND-NEW parts (no defaults saved yet) auto-tick
@@ -176,13 +176,13 @@ class FpDirectOrderLine(models.Model):
back to the part. Without this, the estimator has to remember
to tick the toggle and the second order doesn't pre-fill.
(The explanatory popup was removed 2026-05-29 at the client's
request the ticked "Save as Default" checkbox is the cue now.)
request - the ticked "Save as Default" checkbox is the cue now.)
"""
warning = None
for rec in self:
# Pre-fill variant from the part's default (was: blanket clear).
if rec.part_catalog_id and rec.part_catalog_id.default_process_id:
# Only overwrite when blank or pointing at a different part
# Only overwrite when blank or pointing at a different part -
# don't clobber a deliberate cross-part pick the estimator
# made before changing the part.
if (not rec.process_variant_id
@@ -201,7 +201,7 @@ class FpDirectOrderLine(models.Model):
# falls back to the most recent SO line for (part, customer)
# when the part default is empty. If that lookup will find
# a hit, this is NOT a first-time use from the operator's
# perspective the spec will silently pre-fill from history.
# perspective - the spec will silently pre-fill from history.
# Suppress the warning in that case so we don't pop a
# misleading "no saved specification" alert right when the
# spec actually does auto-fill.
@@ -220,7 +220,7 @@ class FpDirectOrderLine(models.Model):
# of the part. Auto-tick the push_to_defaults toggle so
# whatever the estimator picks becomes the saved default.
# The explanatory popup was removed 2026-05-29 at the
# client's request the ticked "Save as Default" checkbox on
# client's request - the ticked "Save as Default" checkbox on
# the line is the visible cue now (no nag dialog).
# `is_one_off` always wins (operator opted out of catalog
# persistence), so don't auto-tick in that case.
@@ -255,7 +255,7 @@ class FpDirectOrderLine(models.Model):
currency_field='currency_id',
compute='_compute_line_subtotal',
)
# Sub 9 taxes per line. Defaults from the FP-SERVICE product's
# Sub 9 - taxes per line. Defaults from the FP-SERVICE product's
# sale taxes; fiscal-position-mapped from the customer when the
# wizard creates the SO line. Overridable per row.
tax_ids = fields.Many2many(
@@ -311,7 +311,7 @@ class FpDirectOrderLine(models.Model):
string='Start at Node',
domain="[('id', 'child_of', process_variant_id and process_variant_id.id or 0)]",
help='For re-work jobs: pick the recipe step where this job should '
'begin. Pick a recipe first nodes are scoped to it. Skips '
'begin. Pick a recipe first - nodes are scoped to it. Skips '
'earlier steps in the generated WO but keeps later siblings '
'and sub-processes.',
)
@@ -342,12 +342,12 @@ class FpDirectOrderLine(models.Model):
string='Line Description',
help='Customer-facing text. Becomes the SO line description and '
'prints on the acknowledgement, invoice, and packing slip. '
'Edit freely your changes override the template.',
'Edit freely - your changes override the template.',
)
internal_description = fields.Text(
string='Internal Description',
help='Shop-floor instructions. Prints on WO / traveler. Never on '
'customer docs. Edit freely your changes override the template.',
'customer docs. Edit freely - your changes override the template.',
)
# ---- Missing info per line ----
@@ -356,12 +356,12 @@ class FpDirectOrderLine(models.Model):
compute='_compute_is_missing_info',
)
# ---- Sub 5 / Phase 1 Serials / Job# / Thickness --------------------
# ---- Sub 5 / Phase 1 - Serials / Job# / Thickness --------------------
# These mirror the SO-line fields and are carried over when the wizard
# creates the sale order. Serial stays optional; Job# is left blank
# here and gets auto-assigned by action_confirm on the SO.
#
# 2026-04-28 Phase 1 multi-serial. M2M is the source of truth;
# 2026-04-28 Phase 1 - multi-serial. M2M is the source of truth;
# serial_id stays as a computed alias so existing flows that read
# the singular continue to work.
serial_ids = fields.Many2many(
@@ -383,7 +383,7 @@ class FpDirectOrderLine(models.Model):
compute='_compute_primary_serial',
inverse='_inverse_primary_serial',
store=False,
help='First of the line\'s serials back-compat alias.',
help='First of the line\'s serials - back-compat alias.',
)
@api.depends('serial_ids')
@@ -503,7 +503,7 @@ class FpDirectOrderLine(models.Model):
compute='_compute_serials_text',
inverse='_inverse_serials_text',
store=False,
help='Comma-separated list of serial numbers typing here parses, '
help='Comma-separated list of serial numbers - typing here parses, '
'creates new fp.serial records as needed, and updates the M2M.',
)
@@ -549,7 +549,7 @@ class FpDirectOrderLine(models.Model):
new = Serial.sudo().create({'name': name})
ids.append(new.id)
rec.serial_ids = [(6, 0, ids)]
# Anchor field for the FpExpressActionBtns widget renders the
# Anchor field for the FpExpressActionBtns widget - renders the
# stacked DWG / OPEN buttons in one list column. The widget reads
# part_catalog_id from the line; this field's value is unused.
action_btns_anchor = fields.Many2one(
@@ -599,7 +599,7 @@ class FpDirectOrderLine(models.Model):
@api.onchange('part_catalog_id')
def _onchange_part_default_thickness(self):
"""Auto-fill thickness range + Express defaults same chain as the SO line.
"""Auto-fill thickness range + Express defaults - same chain as the SO line.
For each cell, the chain is:
1. Operator already typed → keep
@@ -683,7 +683,7 @@ class FpDirectOrderLine(models.Model):
def _fp_sync_to_part(self):
"""Push tracked line fields back to the linked part's defaults.
Called from create + write. Last-write-wins semantics if two
Called from create + write. Last-write-wins semantics - if two
orders simultaneously edit the same part, the later one's values
become the part's defaults. Acceptable per dev-stage policy;
the part chatter records the change either way.
@@ -777,7 +777,7 @@ class FpDirectOrderLine(models.Model):
def action_upload_masking_ref(self):
"""Attach a masking reference (image/PDF) to this line.
Called by the Express 'MASK REF' button once per file (multi-select
Called by the Express 'MASK REF' button - once per file (multi-select
loops in JS), via context keys fp_masking_file + fp_masking_filename.
Stored on the line's masking_attachment_ids; carried to the SO line
and the job's masking step at order confirm.
@@ -800,7 +800,7 @@ class FpDirectOrderLine(models.Model):
def action_upload_drawing(self):
"""Attach a file (via context) to the line's part as a drawing.
Mirrors sale.order.line.action_upload_drawing same behaviour,
Mirrors sale.order.line.action_upload_drawing - same behaviour,
same context keys (fp_drawing_file + fp_drawing_filename).
"""
self.ensure_one()
@@ -833,7 +833,7 @@ class FpDirectOrderLine(models.Model):
def action_generate_serial(self):
"""Generate one auto-sequenced fp.serial and append to the M2M.
Phase 1: appends instead of replacing repeated clicks add more.
Phase 1: appends instead of replacing - repeated clicks add more.
"""
self.ensure_one()
seq = self.env['ir.sequence'].next_by_code('fp.serial') or 'FP-SN-0000'
@@ -897,8 +897,8 @@ class FpDirectOrderLine(models.Model):
Order of precedence for "remember last entered" fields
(process_variant_id, unit_price, tax_ids):
1. What the operator already typed on this line never clobber
2. Most recent SO line for (part_catalog_id, partner) the
1. What the operator already typed on this line - never clobber
2. Most recent SO line for (part_catalog_id, partner) - the
"last entered" carry-over so repeat orders feel sticky
3. Fall back to product / part defaults
@@ -934,17 +934,17 @@ class FpDirectOrderLine(models.Model):
], order='create_date desc', limit=1)
if not recent:
return
# Process variant only if the line doesn't already have a pick.
# Process variant - only if the line doesn't already have a pick.
# The part's default still applies as a fallback in
# _onchange_part_clears_variant above; this beats it when the
# customer's last SO had a specific variant.
if not self.process_variant_id and recent.x_fc_process_variant_id:
self.process_variant_id = recent.x_fc_process_variant_id
# Unit price only when blank/zero. Avoids overwriting a
# Unit price - only when blank/zero. Avoids overwriting a
# quote-driven or hand-typed price.
if not self.unit_price and recent.price_unit:
self.unit_price = recent.price_unit
# Taxes only when blank. The downstream
# Taxes - only when blank. The downstream
# _seed_default_taxes() fallback handles the no-prior-line case.
# NB: SO line field is `tax_ids` (Odoo 19 renamed from tax_id).
if not self.tax_ids and recent.tax_ids:
@@ -966,7 +966,7 @@ class FpDirectOrderLine(models.Model):
if taxes:
self.tax_ids = [(6, 0, taxes.ids)]
# Auto-fill unit_price from a customer price list extended in
# Auto-fill unit_price from a customer price list - extended in
# fusion_plating_quality (the spec field lives there). The base
# configurator wizard no longer triggers price lookup since
# coating_config_id is gone.
@@ -989,12 +989,12 @@ class FpDirectOrderLine(models.Model):
@api.onchange('part_catalog_id')
def _onchange_suggest_template(self):
"""Offer a sensible default template part-specific wins.
"""Offer a sensible default template - part-specific wins.
Priority (first non-empty result wins):
1. This part's lowest-sequence active template
2. This customer's templates (no part)
3. Don't auto-pick user has to choose
3. Don't auto-pick - user has to choose
"""
if self.description_template_id or self.line_description:
return
@@ -1033,7 +1033,7 @@ class FpDirectOrderLine(models.Model):
"""Seed a Direct Order line from a `fp.quote.configurator` row.
Single source of truth for both the per-quote "Promote" action and
the bulk "Add From Quotes" sub-wizard keeps the field mapping
the bulk "Add From Quotes" sub-wizard - keeps the field mapping
in one place so the two flows can never drift.
"""
if not quote.part_catalog_id:
@@ -1087,13 +1087,13 @@ class FpDirectOrderLine(models.Model):
return new_rev
# ==================================================================
# 2026-04-28 polish recipe handling shared with sale.order.line
# 2026-04-28 polish - recipe handling shared with sale.order.line
# ==================================================================
def _fp_clone_recipe_to_part(self):
"""Deep-copy the picked recipe onto this line's part if it isn't
already scoped there. Returns the cloned (or unchanged) variant.
Mirrors `sale.order.line._fp_clone_recipe_to_part` same
Mirrors `sale.order.line._fp_clone_recipe_to_part` - same
contract, same edge cases. The wizard runs this on every save
path (create/write) plus when Customize is clicked, so a
cross-part pick never leaks edits to the source recipe.
@@ -1107,7 +1107,7 @@ class FpDirectOrderLine(models.Model):
return recipe
clone_name = recipe.name or _('Untitled Recipe')
if part.part_number and part.part_number.lower() not in clone_name.lower():
clone_name = '%s %s' % (clone_name, part.part_number or part.display_name)
clone_name = '%s - %s' % (clone_name, part.part_number or part.display_name)
clone = recipe.copy({
'name': clone_name,
'part_catalog_id': part.id,
@@ -1148,12 +1148,12 @@ class FpDirectOrderLine(models.Model):
line.part_catalog_id.action_set_default_variant(recipe.id)
def action_customize_process(self):
"""Open the Process Composer for this line's variant auto-clones
"""Open the Process Composer for this line's variant - auto-clones
first if the variant isn't yet scoped to this part."""
self.ensure_one()
if not self.part_catalog_id:
raise UserError(_(
'Pick a part on this line before customizing the process '
'Pick a part on this line before customizing the process - '
'the recipe needs a part to scope the variant.'
))
if not self.process_variant_id:
@@ -1167,7 +1167,7 @@ class FpDirectOrderLine(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_part_process_composer',
'name': _('Customize Process %s') % (
'name': _('Customize Process - %s') % (
self.part_catalog_id.display_name
or self.part_catalog_id.part_number
or '?'

View File

@@ -12,7 +12,7 @@ from odoo.exceptions import UserError
class FpDirectOrderWizard(models.Model):
"""Direct order entry for repeat customers.
Sub 9 converted from TransientModel to persistent Model so an
Sub 9 - converted from TransientModel to persistent Model so an
estimator can save a draft, navigate elsewhere (part form, Process
Composer, customer record), and come back. Entries persist across
sessions; finished drafts move to state='confirmed' and link to the
@@ -48,7 +48,7 @@ class FpDirectOrderWizard(models.Model):
'sale.order',
string='Sale Order',
readonly=True, copy=False, tracking=True,
help='Set when the draft is confirmed points to the SO created.',
help='Set when the draft is confirmed - points to the SO created.',
)
user_id = fields.Many2one(
'res.users', string='Estimator',
@@ -98,7 +98,7 @@ class FpDirectOrderWizard(models.Model):
)
internal_deadline = fields.Date(string='Internal Deadline')
customer_deadline = fields.Date(string='Customer Deadline', tracking=True)
# Lead Time promised production window. Mirrors directly to
# Lead Time - promised production window. Mirrors directly to
# x_fc_lead_time_min_days / x_fc_lead_time_max_days on the SO via
# _prepare_order_vals. Leaving both 0 = Standard (no commitment).
lead_time_min_days = fields.Integer(string='Lead Time Min (days)')
@@ -115,7 +115,7 @@ class FpDirectOrderWizard(models.Model):
)
# ---- PO ----
# Originally required at wizard time that's what makes this a
# Originally required at wizard time - that's what makes this a
# "direct" order vs. a quote. Relaxed 2026-04-23: some customers
# don't send their PO until after the order is in progress. The
# wizard now accepts a PO Pending flag in lieu of a PO#/doc; the
@@ -172,7 +172,7 @@ class FpDirectOrderWizard(models.Model):
@api.model
def _fp_default_terms_and_conditions(self):
"""Seed Terms & Conditions from the Accounting default same source
"""Seed Terms & Conditions from the Accounting default - same source
as the standard sale.order.note field.
Respects the `account.use_invoice_terms` system parameter (toggled
@@ -191,7 +191,7 @@ class FpDirectOrderWizard(models.Model):
)
if not raw:
return False
# Defensive HTML strip works whether the source was clean plain
# Defensive HTML strip - works whether the source was clean plain
# text, a real html field, or a "plain" field polluted by the
# rich-text editor (entech case 2026-05-27).
if '<' in raw and '>' in raw:
@@ -199,7 +199,7 @@ class FpDirectOrderWizard(models.Model):
from lxml import html as lxml_html
raw = lxml_html.fromstring(raw).text_content().strip()
except Exception:
# Last-ditch regex fallback no lxml, malformed html, etc.
# Last-ditch regex fallback - no lxml, malformed html, etc.
import re
raw = re.sub(r'<[^>]+>', '', raw).strip()
return raw or False
@@ -212,7 +212,7 @@ class FpDirectOrderWizard(models.Model):
set the recipe line-by-line.
Lines that have explicitly overridden their recipe AFTER this
onchange last fired won't be clobbered we only update lines
onchange last fired won't be clobbered - we only update lines
whose current process_variant_id is empty OR matches the PREVIOUS
material_process value (i.e. they inherited from the header and
haven't been customised).
@@ -290,7 +290,7 @@ class FpDirectOrderWizard(models.Model):
progress_initial_percent = fields.Float(
string='Progress - Initial %', default=50.0,
)
# Sub 9 payment terms surfaced on the wizard so the resulting SO
# Sub 9 - payment terms surfaced on the wizard so the resulting SO
# picks them up. Auto-seeded from the customer's invoice-strategy
# default (or the partner's property_payment_term_id), then nudged
# again when the strategy changes (COD/Prepay → Immediate Payment).
@@ -313,7 +313,7 @@ class FpDirectOrderWizard(models.Model):
terms_and_conditions = fields.Text(
string='Terms & Conditions',
default=lambda self: self._fp_default_terms_and_conditions(),
help='Customer-facing terms prints on quote / SO / invoice. '
help='Customer-facing terms - prints on quote / SO / invoice. '
'Seeded from the Accounting default terms '
'(Settings → Invoicing → Default Terms & Conditions).',
)
@@ -326,18 +326,18 @@ class FpDirectOrderWizard(models.Model):
# Material/Process Tag IS the recipe: when set on the order header,
# every line auto-uses this recipe (unless the line explicitly
# overrides via its own process_variant_id). Was a Char tag until
# 19.0.22.1.0 converted to Many2One per customer feedback.
# 19.0.22.1.0 - converted to Many2One per customer feedback.
material_process = fields.Many2one(
'fusion.plating.process.node',
string='Material / Process Tag',
domain="[('node_type', '=', 'recipe')]",
help='Pick a recipe applies automatically to every line on this '
help='Pick a recipe - applies automatically to every line on this '
'order. Individual lines can still override via their own '
'Process / Recipe column.',
)
validity_date = fields.Date(
string='Quote Validity',
help='Mirrors sale.order.validity_date when the quote/SO expires.',
help='Mirrors sale.order.validity_date - when the quote/SO expires.',
)
view_source = fields.Selection(
[('express', 'Express Orders View'),
@@ -506,7 +506,7 @@ class FpDirectOrderWizard(models.Model):
for rec in self:
has_missing = False
for line in rec.line_ids:
# coating_config_id intentionally NOT in the gate
# coating_config_id intentionally NOT in the gate -
# it's optional now (rework / inspection-only / masking
# work doesn't need a primary treatment).
if (not line.part_catalog_id
@@ -535,7 +535,7 @@ class FpDirectOrderWizard(models.Model):
self._apply_strategy_payment_term()
return
# Partner-level plating defaults primary cascade. Customers
# Partner-level plating defaults - primary cascade. Customers
# migrated to the new partner fields skip the legacy lookup below.
partner = self.partner_id
if partner.x_fc_default_invoice_strategy:
@@ -544,7 +544,7 @@ class FpDirectOrderWizard(models.Model):
self.deposit_percent = partner.x_fc_default_deposit_percent
if partner.x_fc_default_delivery_method:
self.delivery_method = partner.x_fc_default_delivery_method
# Lead-time default band set once per customer in their
# Lead-time default band - set once per customer in their
# Plating profile, auto-copies onto every new Express Order.
# Only fills when the operator hasn't already typed a value.
if (partner.x_fc_default_lead_time_min_days
@@ -554,7 +554,7 @@ class FpDirectOrderWizard(models.Model):
and not self.lead_time_max_days):
self.lead_time_max_days = partner.x_fc_default_lead_time_max_days
# Deadline auto-fill anchored to planned_start_date with today
# Deadline auto-fill - anchored to planned_start_date with today
# as fallback. Honours explicit deadlines the user already typed.
anchor = self.planned_start_date or fields.Date.context_today(self)
if (partner.x_fc_default_internal_deadline_days
@@ -619,7 +619,7 @@ class FpDirectOrderWizard(models.Model):
"""Recompute deadlines from partner offsets when start moves.
Runs only if the partner has offsets configured AND deadlines
are still blank typing a manual deadline locks it.
are still blank - typing a manual deadline locks it.
"""
if not self.partner_id or not self.planned_start_date:
return
@@ -645,7 +645,7 @@ class FpDirectOrderWizard(models.Model):
blocks invoice posting, which silently strands SOs at the
invoicing step. Better to default to net-30 and let the
estimator override if the customer's terms are different.
Never overwrites an explicit user choice only fills the gap.
Never overwrites an explicit user choice - only fills the gap.
"""
Pt = self.env['account.payment.term']
for rec in self:
@@ -678,7 +678,7 @@ class FpDirectOrderWizard(models.Model):
Wired to the "New Direct Order" menu / button. Creating the
record up front means the draft is auto-persisted from the
first keystroke the estimator can navigate away (to the
first keystroke - the estimator can navigate away (to the
part form, the Process Composer, etc.) without losing work.
"""
draft = self.create({})
@@ -752,7 +752,7 @@ class FpDirectOrderWizard(models.Model):
Returns an action that opens the newly-created SO in form view so
the user can review, adjust, and manually confirm / send. The
wizard deliberately does not auto-confirm or auto-email see
wizard deliberately does not auto-confirm or auto-email - see
Sub 1 in the Fine-Tuning Initiative roadmap (CLAUDE.md).
"""
self.ensure_one()
@@ -760,14 +760,14 @@ class FpDirectOrderWizard(models.Model):
raise UserError(_('Pick a customer before confirming.'))
if not self.line_ids:
raise UserError(_('Add at least one part line before confirming.'))
# Account-hold hard block same policy as sale.order.action_confirm
# Account-hold hard block - same policy as sale.order.action_confirm
# but enforced earlier so the wizard doesn't waste Sarah's time.
# Manager override allowed via context key fp_skip_account_hold=True.
# Resolved through commercial_partner so a hold on the company
# blocks every child-contact entry too.
commercial = self.partner_id.commercial_partner_id
# Bypass: Plating Manager (or anything above Quality Manager,
# Owner via the Phase A implied_ids diamond). Phase G fix:
# Bypass: Plating Manager (or anything above - Quality Manager,
# Owner - via the Phase A implied_ids diamond). Phase G fix:
# old code also checked 'group_fusion_plating_administrator',
# an xmlid that never existed and always returned False
# (audit-finding-11). The Manager check alone is now correct
@@ -872,7 +872,7 @@ class FpDirectOrderWizard(models.Model):
'note': self.terms_and_conditions or False,
# Express Orders header (2026-05-26)
'x_fc_internal_notes': self.internal_notes or False,
# material_process is a Many2One since 19.0.22.1.0 pass .id
# material_process is a Many2One since 19.0.22.1.0 - pass .id
'x_fc_material_process': self.material_process.id if self.material_process else False,
'x_fc_tooling_charge': self.charge_amount or self.tooling_charge or 0.0,
'pricelist_id': self.pricelist_id.id if self.pricelist_id else False,
@@ -888,7 +888,7 @@ class FpDirectOrderWizard(models.Model):
part = line._get_or_bump_revision()
resolved_parts[line.id] = part
# Build the line header. Specification is optional; when
# 2026-05-27 drop the legacy "spec - PART Rev (xN)" header
# 2026-05-27 - drop the legacy "spec - PART Rev (xN)" header
# entirely from the customer-facing line name. Per user
# request, customer-facing reports (SO confirmation, invoice,
# CoC, packing slip, BoL) should show ONLY:
@@ -898,12 +898,12 @@ class FpDirectOrderWizard(models.Model):
# Part Number column. The header was duplicating that info
# in the Description column.
extended = (line.line_description or '').strip()
line_desc = extended or (part.part_number or '')
line_desc = extended or (part.part_number or '-')
# G3 robustness (2026-05-27): make sure the line's process
# variant carries the order-level recipe if no per-line
# override exists. Belt-and-braces with the onchange/create
# propagation catches the multi-part case where a new
# propagation - catches the multi-part case where a new
# part's line never received the order recipe.
if not line.process_variant_id and self.material_process:
line.process_variant_id = self.material_process.id
@@ -924,7 +924,7 @@ class FpDirectOrderWizard(models.Model):
'x_fc_internal_description': line.internal_description or False,
# x_fc_customer_spec_id is set on the resulting SO line
# by an extension in fusion_plating_quality (post-create
# patch see fp_direct_order_line_inherit.py).
# patch - see fp_direct_order_line_inherit.py).
'x_fc_part_deadline': line.part_deadline,
'x_fc_part_deadline_offset_days': line.part_deadline_offset_days,
'x_fc_rush_order': line.rush_order,
@@ -933,7 +933,7 @@ class FpDirectOrderWizard(models.Model):
'x_fc_start_at_node_id': line.start_at_node_id.id or False,
'x_fc_is_one_off': line.is_one_off,
'x_fc_quote_id': line.quote_id.id or False,
# Recipe propagation (G3) line's process_variant_id, falling
# Recipe propagation (G3) - line's process_variant_id, falling
# back to the order-level material_process recipe so multi-part
# orders still get the header recipe on every line.
'x_fc_process_variant_id': (
@@ -941,7 +941,7 @@ class FpDirectOrderWizard(models.Model):
or (self.material_process.id if self.material_process else False)
),
'x_fc_save_as_default_process': line.save_as_default_process,
# Sub 5 / Phase 1 carry serial M2M to the SO line.
# Sub 5 / Phase 1 - carry serial M2M to the SO line.
# x_fc_serial_id is back-compat alias and auto-resolves
# from x_fc_serial_ids on SO-line read; passing both is
# safe (the alias setter just appends to the M2M).
@@ -950,14 +950,14 @@ class FpDirectOrderWizard(models.Model):
'x_fc_serial_id': line.serial_id.id or False,
'x_fc_job_number': line.job_number or False,
'x_fc_thickness_range': line.thickness_range or False,
# Express Orders per-line flags (2026-05-26) carry to
# Express Orders per-line flags (2026-05-26) - carry to
# the SO line so the override-application helper can read
# them at job creation time.
'x_fc_customer_line_ref': line.customer_line_ref or False,
'x_fc_masking_enabled': line.masking_enabled,
'x_fc_bake_instructions': line.bake_instructions or False,
'x_fc_masking_attachment_ids': [(6, 0, line.masking_attachment_ids.ids)],
# Sub 9 explicit tax override from the wizard line.
# Sub 9 - explicit tax override from the wizard line.
# When blank, Odoo will compute taxes from the product
# defaults at SO-line save time (the standard behaviour).
# NB. Odoo 19 renamed the SO line field to tax_ids.
@@ -967,7 +967,7 @@ class FpDirectOrderWizard(models.Model):
'x_fc_lot_total': line.lot_total or 0.0,
}))
# 4b. Additional charge one typed line, taxed by the order-level
# 4b. Additional charge - one typed line, taxed by the order-level
# tax. The charge type's name labels the line; charge_amount with
# legacy tooling_charge fallback for in-flight drafts.
charge_amt = self.charge_amount or self.tooling_charge or 0.0
@@ -986,7 +986,7 @@ class FpDirectOrderWizard(models.Model):
if self.tax_id else False),
}))
# 5. Create stays in draft / quotation. Sub 1: user reviews
# 5. Create - stays in draft / quotation. Sub 1: user reviews
# and manually clicks Confirm / Send. No auto-confirm, no
# auto-email to the client.
so = self.env['sale.order'].create(so_vals)
@@ -994,7 +994,7 @@ class FpDirectOrderWizard(models.Model):
# Mark this draft as confirmed and link the SO.
self.write({'state': 'confirmed', 'sale_order_id': so.id})
# Sub 10 flip every linked quote to "won" now that an SO exists.
# Sub 10 - flip every linked quote to "won" now that an SO exists.
# We deliberately wait until SO creation rather than at promote
# time, because "won" should mean "the deal closed", not "we put
# it on a draft." A draft can still be cancelled.
@@ -1009,10 +1009,10 @@ class FpDirectOrderWizard(models.Model):
})
for q in linked_quotes:
q.message_post(body=_(
'Quote won promoted onto Direct Order %(doo)s, SO %(so)s.'
'Quote won - promoted onto Direct Order %(doo)s, SO %(so)s.'
) % {'doo': self.name, 'so': so.name})
# 6. Push-to-defaults Specification carry-over to the part's
# 6. Push-to-defaults - Specification carry-over to the part's
# x_fc_default_customer_spec_id is handled by an inherit in
# fusion_plating_quality (the field lives there).
# Thickness range: lives in configurator, push here.
@@ -1029,7 +1029,7 @@ class FpDirectOrderWizard(models.Model):
# Always-on (no push_to_defaults check): the spec says type-once,
# saves to part. Empty cells DON'T clobber existing defaults
# (otherwise an empty bake cell would erase a part's bake default
# bad UX). Masking is a Boolean so always written.
# - bad UX). Masking is a Boolean so always written.
for line in self.line_ids:
if line.is_one_off:
continue

View File

@@ -35,7 +35,7 @@
<i class="fa fa-exclamation-triangle me-1"/>
<strong>Legacy view.</strong> This form is being retired in favour of the new
<a href="#" class="alert-link"><strong>Express Orders</strong></a> view, which
is faster for batch entry every column inline, type-once-and-remember per-part
is faster for batch entry - every column inline, type-once-and-remember per-part
defaults, masking + bake toggles per line. Click <em>Switch to Express ➜</em>
above to flip this draft into the new view.
</div>
@@ -69,7 +69,7 @@
<field name="user_id" readonly="state != 'draft'"
options="{'no_create': True}"/>
<p class="text-muted" invisible="state != 'draft'">
Skip the quotation stage create a confirmed order
Skip the quotation stage - create a confirmed order
when the customer has already sent a PO. Drafts auto-save.
</p>
</div>
@@ -113,7 +113,7 @@
<field name="planned_start_date"/>
<field name="internal_deadline"/>
<!-- Labelled "Delivery Date" here to match
the SO header field of the same name
the SO header field of the same name -
same field, same value, just consistent
wording end-to-end. Backing field is
still `customer_deadline` (wizard) →
@@ -361,7 +361,7 @@
</group>
<group string="Terms &amp; Conditions (prints on customer docs)">
<field name="terms_and_conditions" nolabel="1"
placeholder="Customer-facing terms seeded from company default."/>
placeholder="Customer-facing terms - seeded from company default."/>
</group>
</group>
</page>
@@ -373,7 +373,7 @@
</field>
</record>
<!-- Form action keeps the same external ID as before so existing
<!-- Form action - keeps the same external ID as before so existing
button references survive (act_window cannot be replaced by a
server action with the same xmlid). target='current' lets the
estimator breadcrumb between the wizard and the part form / Composer.
@@ -471,7 +471,7 @@
<field name="context">{'search_default_filter_draft': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No drafts yet start one!
No drafts yet - start one!
</p>
<p>
Drafts persist across sessions. Save your progress, switch to a

View File

@@ -15,7 +15,7 @@ _logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------
# CSV column spec order matters for the downloadable template
# CSV column spec - order matters for the downloadable template
# ---------------------------------------------------------------------
CSV_COLUMNS = [
'part_number', # required
@@ -99,7 +99,7 @@ class FpPartCatalogImportWizard(models.TransientModel):
Import button. User can fix and re-upload, or commit.
"""
_name = 'fp.part.catalog.import.wizard'
_description = 'Fusion Plating Part Catalog CSV Import'
_description = 'Fusion Plating - Part Catalog CSV Import'
state = fields.Selection(
[('draft', 'Draft'), ('preview', 'Preview'), ('done', 'Done')],
@@ -152,7 +152,7 @@ class FpPartCatalogImportWizard(models.TransientModel):
'PN-12345', 'Widget A', 'Acme Corp', 'Rev A', '1',
'steel', '12.5', 'sq_in', 'moderate', '0.4',
'50', '30', '20', '2', 'Mask threaded holes',
'no', 'no', 'yes', 'Example row delete before import',
'no', 'no', 'yes', 'Example row - delete before import',
])
data = buf.getvalue().encode('utf-8')
att = self.env['ir.attachment'].create({
@@ -256,7 +256,7 @@ class FpPartCatalogImportWizard(models.TransientModel):
duplicates.append({'row': i, 'customer': partner.name, 'part_number': part_number})
continue
# Build the prepared vals (no partner id yet may need creating)
# Build the prepared vals (no partner id yet - may need creating)
valid_rows.append({
'row': i,
'customer_raw': customer_raw,
@@ -313,7 +313,7 @@ class FpPartCatalogImportWizard(models.TransientModel):
try:
valid_rows = json.loads(self.parsed_rows_json or '[]')
except ValueError:
raise UserError(_('Preview data lost please Preview again.'))
raise UserError(_('Preview data lost - please Preview again.'))
Partner = self.env['res.partner']
Part = self.env['fp.part.catalog']

View File

@@ -15,7 +15,7 @@
<h1>Import Parts from CSV</h1>
<p class="text-muted">
Bulk-load part catalog entries. The wizard validates
every row before writing nothing is imported until
every row before writing - nothing is imported until
you approve the preview.
</p>
</div>
@@ -40,7 +40,7 @@
<code>surface_area</code>, <code>surface_area_uom</code>,
<code>complexity</code>, <code>weight</code>,
dimensions, masking, flags. The importer accepts
readable values e.g. "Stainless Steel" maps to
readable values - e.g. "Stainless Steel" maps to
<code>stainless</code>, "sq in" to <code>sq_in</code>.
</div>

View File

@@ -130,7 +130,7 @@ class FpPartRevisionBumpWizard(models.TransientModel):
'model_attachment_id': part.model_attachment_id.id,
})
# Optional new PDF drawing appended to the drawing list.
# Optional new PDF drawing - appended to the drawing list.
if self.new_drawing_file:
drawing_att = self.env['ir.attachment'].create({
'name': self.new_drawing_filename or 'drawing.pdf',
@@ -140,7 +140,7 @@ class FpPartRevisionBumpWizard(models.TransientModel):
})
new_part.drawing_attachment_ids = [(4, drawing_att.id)]
# Optional new 3D model replaces the model attachment.
# Optional new 3D model - replaces the model attachment.
if self.new_model_file:
model_att = self.env['ir.attachment'].create({
'name': self.new_model_filename or 'model.step',

View File

@@ -17,7 +17,7 @@
<p class="text-muted">
Bump the revision label for
<strong><field name="part_id" readonly="1" nolabel="1" options="{'no_open': True}"/></strong>.
The pre-filled label is a best-effort guess
The pre-filled label is a best-effort guess -
adjust it to match the customer's actual scheme.
</p>
</div>

View File

@@ -10,7 +10,7 @@ from odoo.exceptions import UserError
class FpQuotePromoteWizard(models.TransientModel):
"""Chooser dialog: promote a won quote into a Direct Order draft.
Sub 10 quote→direct-order handoff. The estimator picks either an
Sub 10 - quote→direct-order handoff. The estimator picks either an
existing open draft for this customer (lets multiple quotes
consolidate onto a single PO) or creates a fresh draft.
"""
@@ -62,7 +62,7 @@ class FpQuotePromoteWizard(models.TransientModel):
self.ensure_one()
q = self.quote_id
# Re-check the not-already-promoted invariant a separate user
# Re-check the not-already-promoted invariant - a separate user
# could have added this quote to a draft between the action open
# and the click, so we re-verify before mutating.
existing_line = self.env['fp.direct.order.line'].search([
@@ -91,7 +91,7 @@ class FpQuotePromoteWizard(models.TransientModel):
'currency_id': q.currency_id.id,
})
# Currency must match Direct Order doesn't convert.
# Currency must match - Direct Order doesn't convert.
if target.currency_id != q.currency_id:
raise UserError(_(
'Quote currency (%s) does not match Direct Order '

View File

@@ -32,7 +32,7 @@
role="alert"
invisible="open_drafts_count != 0 or target_mode != 'new'">
<i class="fa fa-info-circle me-1"/>
No open drafts for this customer a fresh Direct
No open drafts for this customer - a fresh Direct
Order will be created.
</div>
</sheet>

View File

@@ -5,11 +5,11 @@
"""Bulk-add serial numbers to a sale.order.line or fp.direct.order.line.
Three input modes operator picks one:
Three input modes - operator picks one:
1. **Paste a list** one per line, comma- or whitespace-separated.
2. **Range fill** prefix + start..end (e.g. SN- + 1..30 → SN-001..SN-030).
3. **Scan barcodes** repeated input (kept simple for Phase 1: the same
1. **Paste a list** - one per line, comma- or whitespace-separated.
2. **Range fill** - prefix + start..end (e.g. SN- + 1..30 → SN-001..SN-030).
3. **Scan barcodes** - repeated input (kept simple for Phase 1: the same
paste textarea works for a barcode reader that types-and-Enters).
Existing serials with the same `name` are reused (the company-uniqueness
@@ -29,7 +29,7 @@ from odoo.exceptions import UserError, ValidationError
class FpSerialBulkAddWizard(models.TransientModel):
_name = 'fp.serial.bulk.add.wizard'
_description = 'Fusion Plating Bulk Add Serials'
_description = 'Fusion Plating - Bulk Add Serials'
target_model = fields.Selection(
[
@@ -58,7 +58,7 @@ class FpSerialBulkAddWizard(models.TransientModel):
string='Serial List',
help='One serial per line, or comma-separated. Whitespace and '
'blank lines are ignored. Barcode scanners that emit one '
'serial + Enter at a time also work just leave the cursor '
'serial + Enter at a time also work - just leave the cursor '
'in this box and scan.',
)
@@ -127,7 +127,7 @@ class FpSerialBulkAddWizard(models.TransientModel):
count = self.end_number - self.start_number + 1
if count > 1000:
raise ValidationError(_(
'Range covers %s entries too many. Cap at 1000 per call.'
'Range covers %s entries - too many. Cap at 1000 per call.'
) % count)
names = []
prefix = self.prefix or ''
@@ -188,7 +188,7 @@ class FpSerialBulkAddWizard(models.TransientModel):
else:
raise UserError(_('Unsupported mode: %s') % self.mode)
# 2. Quantity sanity check block if we'd exceed the line qty.
# 2. Quantity sanity check - block if we'd exceed the line qty.
target_field = (
'x_fc_serial_ids' if self.target_model == 'sale.order.line'
else 'serial_ids'
@@ -228,7 +228,7 @@ class FpSerialBulkAddWizard(models.TransientModel):
all_serials = existing + created
# Order-preserving: rebuild from the input order so paste/range
# ordering is preserved on the M2M (matters for paste_text the
# ordering is preserved on the M2M (matters for paste_text - the
# operator typed them in physical-rack order).
serial_by_name = {s.name: s for s in all_serials}
ordered_ids = [serial_by_name[n].id for n in names if n in serial_by_name]

View File

@@ -18,7 +18,7 @@
<group invisible="mode != 'paste'">
<separator string="Paste a List"/>
<field name="paste_text" nolabel="1"
placeholder="One per line, or comma-separated.&#10;Example:&#10;SN-001&#10;SN-002&#10;CUST-12345&#10;or scan barcodes one per Enter."/>
placeholder="One per line, or comma-separated.&#10;Example:&#10;SN-001&#10;SN-002&#10;CUST-12345&#10;or scan barcodes - one per Enter."/>
</group>
<!-- Range mode -->