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

@@ -6,7 +6,7 @@ One `fp.box` per physical box received against a `fp.receiving`. Auto-created
when the receiver enters `box_count_in` and marks the receiving Counted
(see `fp.receiving._fp_sync_boxes`). Each box carries a sequence number
(n of N), a status that advances through the shop, and a scannable identity
(`/fp/box/<id>`) printed on the External Job Sticker one label per box.
(`/fp/box/<id>`) printed on the External Job Sticker - one label per box.
Box-level tracking (not box CONTENTS): we track WHICH box and WHERE it is,
not the per-box part breakdown. The same boxes go back to the customer
@@ -19,7 +19,7 @@ STATE_ORDER = ['received', 'racked', 'in_process', 'packed', 'shipped']
class FpBox(models.Model):
_name = 'fp.box'
_description = 'Fusion Plating Tracked Box'
_description = 'Fusion Plating - Tracked Box'
_inherit = ['mail.thread']
_order = 'receiving_id, box_number'
@@ -51,7 +51,7 @@ class FpBox(models.Model):
], string='Status', default='received', required=True, tracking=True, index=True)
location_note = fields.Char(string='Location / Note', tracking=True,
help='Free text where is this box now (rack, bay, shelf).')
help='Free text - where is this box now (rack, bay, shelf).')
scan_url = fields.Char(string='Scan URL', compute='_compute_scan_url')
_box_uniq = models.Constraint(

View File

@@ -18,7 +18,7 @@ from odoo import fields, models
class FpOutboundPackage(models.Model):
_name = 'fp.outbound.package'
_description = 'Fusion Plating Outbound Package (per-box detail)'
_description = 'Fusion Plating - Outbound Package (per-box detail)'
_order = 'sequence, id'
receiving_id = fields.Many2one(

View File

@@ -3,9 +3,9 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 8 Racking-time inspection record. Captures the per-part
# Sub 8 - Racking-time inspection record. Captures the per-part
# inspection the racking crew performs when they open the customer's
# boxes (which is DIFFERENT from receiving receiving is box count
# boxes (which is DIFFERENT from receiving - receiving is box count
# only). One record per MO.
from dateutil.relativedelta import relativedelta
@@ -21,7 +21,7 @@ class FpRackingInspection(models.Model):
_order = 'create_date desc, id desc'
name = fields.Char(compute='_compute_name', store=True)
# Phase 6 (Sub 11) production_id retired (MRP module gone).
# Phase 6 (Sub 11) - production_id retired (MRP module gone).
# x_fc_job_id is the canonical link. Declared here so this module's
# views can reference it at view-load time; fusion_plating_jobs adds
# the constraints + compute overrides via inheritance.
@@ -83,7 +83,7 @@ class FpRackingInspection(models.Model):
flagged_count = fields.Integer(compute='_compute_line_stats')
has_variance = fields.Boolean(compute='_compute_line_stats')
# Phase 6 (Sub 11) production_id retired (MRP module gone). The
# Phase 6 (Sub 11) - production_id retired (MRP module gone). The
# uniqueness constraint that used to ride on production_id is now
# enforced via @api.constrains on x_fc_job_id (added by
# fusion_plating_jobs).
@@ -153,7 +153,7 @@ class FpRackingInspection(models.Model):
'inspection_completed': fields.Datetime.now(),
})
if new_state == 'discrepancy_flagged':
# 2026-04-28 Activity must land on a real user.
# 2026-04-28 - Activity must land on a real user.
# Resolve the assignee in priority order:
# 1. The job's plating manager (if set on fp.job)
# 2. The inspector who just flagged it
@@ -180,18 +180,18 @@ class FpRackingInspection(models.Model):
rec.name or ''
),
note=_(
'%(n)d line(s) flagged review before starting '
'%(n)d line(s) flagged - review before starting '
'the first plating WO.'
) % {'n': rec.flagged_count},
user_id=assignee,
date_deadline=deadline,
)
rec.message_post(body=_(
'Inspection completed %(ok)d ok / %(flag)d flagged.'
'Inspection completed - %(ok)d ok / %(flag)d flagged.'
) % {'ok': rec.ok_count, 'flag': rec.flagged_count})
def action_reopen(self):
"""Manager only reopen a done inspection."""
"""Manager only - reopen a done inspection."""
if not self.env.user.has_group(
'fusion_plating.group_fusion_plating_manager'):
raise UserError(_('Only a Plating Manager can reopen a completed '
@@ -239,7 +239,7 @@ class FpRackingInspectionLine(models.Model):
)
notes = fields.Char(string='Notes')
# 2026-04-28 photos on a line (compliance need: damage evidence,
# 2026-04-28 - photos on a line (compliance need: damage evidence,
# box-by-box condition record). Many2many to ir.attachment so an
# operator can shoot multiple angles per box from the floor without
# leaving the form. Cascade-deleted with the line.
@@ -267,7 +267,7 @@ class FpRackingInspectionLine(models.Model):
def create(self, vals_list):
# Auto-populate part_catalog_id from the parent inspection's job
# when the operator added a line without picking a part. The
# job's SO carries the customer's part pre-fill the line so
# job's SO carries the customer's part - pre-fill the line so
# the audit trail captures it without requiring extra clicks.
for vals in vals_list:
if not vals.get('part_catalog_id') and vals.get('inspection_id'):

View File

@@ -15,7 +15,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# labelary.com free ZPL→PDF rasterization service. 8dpmm = 203dpi
# labelary.com - free ZPL→PDF rasterization service. 8dpmm = 203dpi
# (ZD450 default), label size 4x6 in. No API key required for the
# typical low-volume use case (limit: ~5 req/s anonymous). PDF output
# is requested via the Accept header.
@@ -31,7 +31,7 @@ class FpReceiving(models.Model):
for customer parts arriving at the shop.
"""
_name = 'fp.receiving'
_description = 'Fusion Plating Receiving'
_description = 'Fusion Plating - Receiving'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
_order = 'received_date desc, id desc'
@@ -55,7 +55,7 @@ class FpReceiving(models.Model):
received_date = fields.Datetime(
string='Received Date', default=fields.Datetime.now, tracking=True,
)
# Sub 8 simplified state machine. Receiving = box count only. The
# Sub 8 - simplified state machine. Receiving = box count only. The
# part-level inspection that used to happen here now lives on
# fp.racking.inspection (racking crew does it when they open the
# boxes). Legacy state values are kept in the Selection so existing
@@ -65,11 +65,11 @@ class FpReceiving(models.Model):
('draft', 'Awaiting Parts'),
('counted', 'Counted'),
('closed', 'Closed'),
# Legacy values kept readable, never written by new code.
# Legacy values - kept readable, never written by new code.
# 2026-05-20: `staged` collapsed away. The state had zero
# downstream effect (same SO mapping as counted, no field
# captured, action_mark_staged just flipped a flag) and
# median dwell was 11 sec pure ceremony. Pre-migrate
# median dwell was 11 sec - pure ceremony. Pre-migrate
# advances any existing 'staged' record to 'closed'.
('staged', 'Staged (legacy)'),
('inspecting', 'Inspecting (legacy)'),
@@ -83,7 +83,7 @@ class FpReceiving(models.Model):
string='Boxes Received',
tracking=True,
help='Number of boxes the receiver counted when the truck '
'dropped off. Receiving is box count only parts are '
'dropped off. Receiving is box count only - parts are '
'inspected by the racking crew when boxes are opened.',
)
box_ids = fields.One2many(
@@ -107,7 +107,7 @@ class FpReceiving(models.Model):
)
carrier_tracking = fields.Char(string='Inbound Tracking #')
# ---- Phase A outbound carrier + shipment link ----------------------
# ---- Phase A - outbound carrier + shipment link ----------------------
# The receiver picks the OUTBOUND (return) carrier here; clicking
# "Create Outbound Shipment" creates a draft fusion.shipment which
# owns weight, dimensions, label PDF, tracking. The shop's workflow
@@ -132,7 +132,7 @@ class FpReceiving(models.Model):
# Curated FedEx services for a Canadian B2B plating shop. The
# carrier-level selection (~38 options) is overwhelming and mostly
# noise frieght tiers want 150+ lb, regional services don't apply
# noise - frieght tiers want 150+ lb, regional services don't apply
# to CA-origin shipments, distribution-program services need extra
# account config. Sweep against the live sandbox (see
# scripts/fp_fedex_service_matrix.py) confirmed these 12 are the
@@ -162,7 +162,7 @@ class FpReceiving(models.Model):
Pulls labels from the carrier's full selection (so they match
whatever fusion_shipping ships) but filters to the codes in
_FP_USABLE_FEDEX_SERVICES. Order in the dropdown follows the
tuple cheapest CA-domestic first, premium international last.
tuple - cheapest CA-domestic first, premium international last.
Empty list when fusion_shipping isn't installed.
"""
@@ -219,7 +219,7 @@ class FpReceiving(models.Model):
ship and ship.x_fc_label_zpl_attachment_id
)
# ---- Phase C Outbound packaging fields -----------------------------
# ---- Phase C - Outbound packaging fields -----------------------------
# Operator enters these at receiving time so the shipping label can be
# generated immediately. Pushed to the linked fusion.shipment when
# action_generate_outbound_label fires.
@@ -276,7 +276,7 @@ class FpReceiving(models.Model):
"""Propagate carrier change to a linked DRAFT shipment.
Once a shipment is confirmed / shipped / delivered, we leave it
alone changing the carrier on a non-draft shipment is a
alone - changing the carrier on a non-draft shipment is a
destructive operation that needs explicit user intent (cancel +
re-create), not a side-effect of editing the receiving form.
"""
@@ -324,7 +324,7 @@ class FpReceiving(models.Model):
self.ensure_one()
vals = {}
carrier = self.x_fc_carrier_id
# carrier_type Selection on fusion.shipment ('canada_post',
# carrier_type - Selection on fusion.shipment ('canada_post',
# 'ups_rest', 'fedex_rest', etc.). Map from delivery_type by
# stripping the 'fusion_' prefix (e.g. 'fusion_fedex_rest' →
# 'fedex_rest'). Selection on the model may not include every
@@ -339,7 +339,7 @@ class FpReceiving(models.Model):
)
if ct in valid_types:
vals['carrier_type'] = ct
# service_type carrier-specific. FedEx REST stores it on
# service_type - carrier-specific. FedEx REST stores it on
# carrier.fedex_rest_service_type; UPS REST has its own field.
# Read whichever attribute exists.
if carrier:
@@ -384,14 +384,14 @@ class FpReceiving(models.Model):
'target': 'current',
}
# ---- Phase C Generate Outbound Label -------------------------------
# ---- Phase C - Generate Outbound Label -------------------------------
def action_generate_outbound_label(self):
"""Open the confirmation wizard before the actual API call.
Two guards live here so the user can't accidentally bill
themselves for duplicate shipments:
1. If a label is already attached to the linked shipment,
refuse to regenerate operator must void the shipment
refuse to regenerate - operator must void the shipment
first.
2. Otherwise pop fp.label.generate.wizard so the operator
confirms carrier + service tier + weight before any API
@@ -405,7 +405,7 @@ class FpReceiving(models.Model):
raise UserError(_(
'A shipping label already exists for this receiving '
'(shipment %s). Void that shipment first if you need '
'to regenerate otherwise every click would create a '
'to regenerate - otherwise every click would create a '
'new billable FedEx shipment with its own tracking '
'number.'
) % self.x_fc_outbound_shipment_id.name)
@@ -420,7 +420,7 @@ class FpReceiving(models.Model):
wiz = Wizard.create(Wizard._fp_default_from_receiving(self.env, self))
return {
'type': 'ir.actions.act_window',
'name': _('Generate Label %s') % self.name,
'name': _('Generate Label - %s') % self.name,
'res_model': 'fp.label.generate.wizard',
'res_id': wiz.id,
'view_mode': 'form',
@@ -491,7 +491,7 @@ class FpReceiving(models.Model):
))
if not self.sale_order_id:
raise UserError(_(
'Receiving "%s" is not linked to a sale order '
'Receiving "%s" is not linked to a sale order - '
'cannot generate a shipping label.'
) % self.name)
if not self.sale_order_id.partner_shipping_id \
@@ -519,7 +519,7 @@ class FpReceiving(models.Model):
})
return {
'type': 'ir.actions.act_window',
'name': _('Enter Label Manually %s') % self.name,
'name': _('Enter Label Manually - %s') % self.name,
'res_model': Wizard._name,
'res_id': wiz.id,
'view_mode': 'form',
@@ -636,7 +636,7 @@ class FpReceiving(models.Model):
Package = self.env.get('stock.package')
if Package is not None and picking.move_line_ids:
default_pkg_type = self._fp_resolve_carrier_default_package_type()
# Build the list of (weight, dimensions) tuples one per
# Build the list of (weight, dimensions) tuples - one per
# outbound package. Multi-piece shipments use the per-row
# data from x_fc_outbound_package_ids; single-piece falls
# back to the receiving's top-level weight/dim fields.
@@ -690,7 +690,7 @@ class FpReceiving(models.Model):
"""Return the stock.package.type to use for the synthetic
outbound package. Reads the carrier's per-provider default
(e.g. fedex_rest_default_package_type_id). Returns False when
no default is configured the API call will then fail with a
no default is configured - the API call will then fail with a
clear PACKAGINGTYPE error pointing the admin at the setup.
"""
self.ensure_one()
@@ -713,7 +713,7 @@ class FpReceiving(models.Model):
"""Copy tracking + label(s) from the picking back to the linked
fusion.shipment AND to the per-package rows for multi-piece
shipments. shipping_data is the list returned by
carrier.send_shipping `[{exact_price, tracking_number}, ...]`,
carrier.send_shipping - `[{exact_price, tracking_number}, ...]`,
one dict per package, in submission order.
Multi-piece (MPS): walks shipping_data alongside the picking's
@@ -760,7 +760,7 @@ class FpReceiving(models.Model):
continue
cleaned = raw.replace(b'^POI', b'')
if cleaned == raw:
# No ^POI present keep using the original attachment.
# No ^POI present - keep using the original attachment.
cleaned_zpl_atts |= zpl
continue
cleaned_name = (zpl.name or 'label.zpl').rsplit('.', 1)
@@ -800,7 +800,7 @@ class FpReceiving(models.Model):
# Primary slot keeps backward-compat: prefer PDF for the main
# button, fall back to whatever the carrier returned otherwise.
primary_atts = pdf_atts or label_atts
# Per-package shipping_data list one entry per package.
# Per-package shipping_data list - one entry per package.
sd_list = shipping_data if isinstance(shipping_data, list) else [
shipping_data
]
@@ -812,7 +812,7 @@ class FpReceiving(models.Model):
)
# Walk both lists in parallel; carrier returns one tracking +
# label per package in submission order. Some carriers return
# one combined tracking_ref split by '+' handle both.
# one combined tracking_ref split by '+' - handle both.
primary_tracking = ''
per_pkg_trackings = []
for sd in sd_list:
@@ -826,7 +826,7 @@ class FpReceiving(models.Model):
per_pkg_trackings.append(part)
primary_tracking = per_pkg_trackings[0] if per_pkg_trackings else ''
# Write per-row labels + tracking. Attachments are paired by
# index N labels and N rows. Excess on either side is ignored.
# index - N labels and N rows. Excess on either side is ignored.
# Use primary_atts (PDF-preferred) so the per-row "Label" link
# opens a printable PDF, not raw ZPL.
for idx, row in enumerate(rows):
@@ -865,7 +865,7 @@ class FpReceiving(models.Model):
)) % (primary_tracking or '(see attached PDF)'))
# Validate the synthetic picking so it lands in 'done' state
# instead of sitting at 'ready'. The shipping label is the proof
# of dispatch keeping the picking open misleads anyone looking
# of dispatch - keeping the picking open misleads anyone looking
# at the warehouse view. Wrapped in try/except so any quirk in
# the validation flow (e.g. zero on-hand stock) doesn't block
# the label generation success path.
@@ -883,7 +883,7 @@ class FpReceiving(models.Model):
).button_validate()
# If button_validate still returned an action (a wizard
# popped up despite the context flags), log and move on
# the label is already saved; manual validation later
# - the label is already saved; manual validation later
# is fine.
if isinstance(result, dict) and result.get('res_model'):
_logger.info(
@@ -920,7 +920,7 @@ class FpReceiving(models.Model):
so = self.sale_order_id
if not so:
raise UserError(_(
'No sale order linked cannot resolve sender / '
'No sale order linked - cannot resolve sender / '
'recipient addresses for the quote.'
))
if carrier.delivery_type != 'fusion_fedex_rest':
@@ -954,7 +954,7 @@ class FpReceiving(models.Model):
FedexRestRequest._get_shipping_price (price, service_name,
delivery_timestamp, etc.).
"""
# Lazy import fusion_plating_receiving depends on
# Lazy import - fusion_plating_receiving depends on
# fusion_shipping but importing at module load order can race
# with the registry. Inside-method keeps everything sane.
from odoo.addons.fusion_shipping.api.fedex_rest.request import (
@@ -1028,7 +1028,7 @@ class FpReceiving(models.Model):
'%(transit)s'
'<div class="fp_shipping_quote_footnote" '
'style="font-size: 11px; opacity: 0.65; margin-top: 10px;">'
'Quote is an estimate from FedEx final charges may differ.'
'Quote is an estimate from FedEx - final charges may differ.'
'</div>'
'</div>'
) % {
@@ -1076,7 +1076,7 @@ class FpReceiving(models.Model):
def _fp_zpl_to_pdf_via_labelary(self, zpl_bytes):
"""POST raw ZPL to labelary and return the rendered PDF bytes.
Returns None on any failure caller treats labelary as a
Returns None on any failure - caller treats labelary as a
best-effort enhancement, never a blocker for label generation.
See CLAUDE.md "labelary.com dependency" for privacy + ratelimit
notes.
@@ -1101,7 +1101,7 @@ class FpReceiving(models.Model):
return None
if not res.ok:
_logger.warning(
'Receiving %s: labelary returned %s %s',
'Receiving %s: labelary returned %s - %s',
self.name, res.status_code, res.text[:200],
)
return None
@@ -1111,7 +1111,7 @@ class FpReceiving(models.Model):
"""Open the ZPL/ZPLII label for direct-to-thermal-printer use.
Visibility on the form is gated by x_fc_has_label_zpl so this
only appears when a ZPL attachment is actually present i.e.
only appears when a ZPL attachment is actually present - i.e.
the carrier returned ZPL on Generate, or a ZPL fetch was added
later. When no ZPL exists, the operator should use the PDF
button instead (PDF prints on any printer).
@@ -1188,7 +1188,7 @@ class FpReceiving(models.Model):
return records
# -------------------------------------------------------------------------
# Sub 8 box-count-only actions (new primary flow)
# Sub 8 - box-count-only actions (new primary flow)
# -------------------------------------------------------------------------
@api.depends('box_ids')
def _compute_box_count_tracked(self):
@@ -1258,7 +1258,7 @@ class FpReceiving(models.Model):
rec._fp_sync_boxes()
def action_mark_staged(self):
"""Deprecated 2026-05-20 `staged` state was dead ceremony
"""Deprecated 2026-05-20 - `staged` state was dead ceremony
(median dwell 11 sec, no captured data, no downstream effect).
Kept as a thin shim so any legacy button binding still works:
it advances counted records straight to closed.
@@ -1272,7 +1272,7 @@ class FpReceiving(models.Model):
rec.action_close()
def action_close(self):
"""Close the receiving all boxes opened, inspection complete.
"""Close the receiving - all boxes opened, inspection complete.
2026-05-20: now reachable directly from `counted` (the `staged`
intermediate was dropped). Legacy values 'staged' / 'accepted'
@@ -1293,7 +1293,7 @@ class FpReceiving(models.Model):
"""Reset a Closed receiving back to Counted.
Recovery escape hatch for when receiving was closed prematurely.
Blocked once downstream work has begun operator must cancel
Blocked once downstream work has begun - operator must cancel
every fp.job spawned from this SO and avoid touching any step
before the rewind is allowed. Without the gate it's trivial to
rewind a receiving while jobs are mid-flight, which silently
@@ -1315,14 +1315,14 @@ class FpReceiving(models.Model):
)
if started:
raise UserError(_(
'Cannot reset %d step(s) on this order have '
'Cannot reset - %d step(s) on this order have '
'been started. Reset is only allowed before '
'work begins.'
) % len(started))
active = jobs.filtered(lambda j: j.state != 'cancelled')
if active:
raise UserError(_(
'Cannot reset %d work order(s) on this sale '
'Cannot reset - %d work order(s) on this sale '
'order are not cancelled. Cancel them first, '
'then retry.'
) % len(active))
@@ -1331,7 +1331,7 @@ class FpReceiving(models.Model):
rec.message_post(body=_('Receiving reset to Counted.'))
# -------------------------------------------------------------------------
# Legacy state actions kept for backward compatibility.
# Legacy state actions - kept for backward compatibility.
# Deprecated: Sub 8 moves part-level inspection to fp.racking.inspection.
# Retained so existing UI bindings don't blow up.
# -------------------------------------------------------------------------
@@ -1345,7 +1345,7 @@ class FpReceiving(models.Model):
rec.received_date = fields.Datetime.now()
def action_accept(self):
"""Accept the receiving parts match and condition is OK.
"""Accept the receiving - parts match and condition is OK.
Quantity-mismatch policy: if expected_qty != received_qty,
operators must use action_flag_discrepancy() instead. Managers
@@ -1358,12 +1358,12 @@ class FpReceiving(models.Model):
if rec.state not in ('inspecting', 'resolved'):
raise UserError(_('Can only accept from Inspecting or Resolved state.'))
if rec.unresolved_damage_count > 0:
raise UserError(_('Cannot accept there are %d unresolved damage entries.') % rec.unresolved_damage_count)
raise UserError(_('Cannot accept - there are %d unresolved damage entries.') % rec.unresolved_damage_count)
qty_match = rec.expected_qty > 0 and rec.received_qty == rec.expected_qty
if not qty_match:
if not is_manager:
raise UserError(_(
'Cannot accept quantity mismatch (expected %(exp)d, '
'Cannot accept - quantity mismatch (expected %(exp)d, '
'received %(rcv)d).\n\nUse "Flag Discrepancy" instead, '
'or have a manager override.'
) % {'exp': rec.expected_qty, 'rcv': rec.received_qty})
@@ -1373,10 +1373,10 @@ class FpReceiving(models.Model):
) % {'exp': rec.expected_qty, 'rcv': rec.received_qty})
rec.state = 'accepted'
rec._update_so_receiving_status()
rec.message_post(body=_('Parts accepted quantity: %d, all checks passed.') % rec.received_qty)
rec.message_post(body=_('Parts accepted - quantity: %d, all checks passed.') % rec.received_qty)
def action_flag_discrepancy(self):
"""Flag a discrepancy qty mismatch or damage found."""
"""Flag a discrepancy - qty mismatch or damage found."""
for rec in self:
if rec.state != 'inspecting':
raise UserError(_('Can only flag discrepancy from Inspecting state.'))
@@ -1385,11 +1385,11 @@ class FpReceiving(models.Model):
# Create follow-up activity for the sales team
rec.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Receiving discrepancy %s') % rec.name,
summary=_('Receiving discrepancy - %s') % rec.name,
note=_('Qty expected: %d, received: %d. Check damage log for details.') % (
rec.expected_qty, rec.received_qty),
)
rec.message_post(body=_('Discrepancy flagged follow-up required.'))
rec.message_post(body=_('Discrepancy flagged - follow-up required.'))
def action_resolve(self):
"""Resolve a discrepancy after customer follow-up."""
@@ -1417,7 +1417,7 @@ class FpReceiving(models.Model):
for rec in self:
if not rec.sale_order_id:
continue
# Internal denormalized status field elevate the write so a
# Internal denormalized status field - elevate the write so a
# non-privileged technician (tablet receiving) isn't blocked by
# sale.order ACL inside action_mark_counted / action_close.
so = rec.sale_order_id.sudo()
@@ -1441,7 +1441,7 @@ class FpReceiving(models.Model):
The 2026-05-18 cert-creation gate (fp.job.button_mark_done)
blocks completion until ``job.qty_received`` is non-zero, but
nothing was writing the field receiving and job were two
nothing was writing the field - receiving and job were two
disconnected records on the same SO. Operators completed
receiving, then hit "Quantity Received is blank" with no
obvious next step.
@@ -1453,14 +1453,14 @@ class FpReceiving(models.Model):
matches the only job.
Best-effort: if no job matches (e.g. receiving without a
spawned job, or part-catalog mismatch), skip silently the
spawned job, or part-catalog mismatch), skip silently - the
receiving record itself still has the qty for audit.
"""
Job = self.env.get('fp.job')
if Job is None:
return # fusion_plating_jobs not installed
# Match criteria depend on fields owned by fusion_plating_jobs.
# Bail out cleanly if the registry doesn't have them the same
# Bail out cleanly if the registry doesn't have them - the same
# hook then becomes a no-op in any install topology that
# doesn't ship the jobs module (and in test scope where the
# field may not be materialised on fp.job yet).

View File

@@ -13,7 +13,7 @@ class FpReceivingDamage(models.Model):
severity, photos, required action, and customer follow-up.
"""
_name = 'fp.receiving.damage'
_description = 'Fusion Plating Receiving Damage'
_description = 'Fusion Plating - Receiving Damage'
_order = 'id'
receiving_id = fields.Many2one(

View File

@@ -13,7 +13,7 @@ class FpReceivingLine(models.Model):
distinct part number in the receiving record.
"""
_name = 'fp.receiving.line'
_description = 'Fusion Plating Receiving Line'
_description = 'Fusion Plating - Receiving Line'
_order = 'id'
receiving_id = fields.Many2one(

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""Phase C extend fusion.shipment with dimension fields.
"""Phase C - extend fusion.shipment with dimension fields.
fusion_shipping's native model has `weight` but no length/width/height.
The plating workflow needs all four captured at receiving time so the
@@ -60,7 +60,7 @@ class FusionShipment(models.Model):
def action_view_label_zpl(self):
"""Download the ZPL label for direct-to-thermal-printer use.
ZPL is text/plain the PDF preview dialog can't render it, so
ZPL is text/plain - the PDF preview dialog can't render it, so
this stays on the legacy download path (no preview, just a file
the operator sends to their Zebra). Mirrors fp.receiving's
action_print_label_zpl so the button exists on both forms.
@@ -78,7 +78,7 @@ class FusionShipment(models.Model):
record_ids=self.id,
)
# Phase C resolved carrier tracking URL with the tracking number
# Phase C - resolved carrier tracking URL with the tracking number
# substituted into the carrier.tracking_url template. Used by the
# shipment_labeled email template and any other place that needs a
# working clickable tracking link. Single source of truth so both
@@ -108,7 +108,7 @@ class FusionShipment(models.Model):
def write(self, vals):
"""Sync the carrier tracking number + label to the customer
portal job whenever they land on the shipment. The portal_job
currently shows `delivery.name` as 'tracking' wrong; the
currently shows `delivery.name` as 'tracking' - wrong; the
customer wants the carrier's actual tracking number so the
clickable link goes to FedEx/UPS/etc."""
res = super().write(vals)
@@ -162,7 +162,7 @@ class FusionShipment(models.Model):
)
if vals:
portal.sudo().write(vals)
# State is now derived centrally see
# State is now derived centrally - see
# fusion.plating.portal.job._fp_recompute_portal_state. It
# only promotes to 'shipped' when every linked WO is done
# AND the shipment.status is 'shipped' or 'delivered'. A

View File

@@ -42,7 +42,7 @@ class SaleOrder(models.Model):
"""Override to auto-create receiving record on SO confirmation.
Per-line metadata (part catalog, part number) is sourced from
``sale.order.line.x_fc_part_catalog_id`` NOT from the SO header.
``sale.order.line.x_fc_part_catalog_id`` - NOT from the SO header.
The header field exists too but is rarely populated; the line
carries the authoritative part link in the configurator flow.
@@ -76,7 +76,7 @@ class SaleOrder(models.Model):
})
# Seamless flow: after a single interactive confirm, jump straight
# to the Receive Parts screen so the dock counts parts in right away
# (idiot-proof no hunting for the smart button). Guarded to a
# (idiot-proof - no hunting for the smart button). Guarded to a
# single order + an opt-out context flag so batch / programmatic
# confirms (and tests) keep the native return value.
if (len(self) == 1