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

@@ -4,12 +4,12 @@
# Part of the Fusion Plating product family.
{
'name': 'Fusion Plating Receiving & Inspection',
'name': 'Fusion Plating - Receiving & Inspection',
'version': '19.0.3.29.1',
'category': 'Manufacturing/Plating',
'summary': 'Parts receiving, inspection, damage logging, and manufacturing gate.',
'description': """
Fusion Plating Receiving & Inspection
Fusion Plating - Receiving & Inspection
=========================================
Part of the Fusion Plating product family by Nexa Systems Inc.
@@ -52,7 +52,7 @@ Provides:
'assets': {
# Theme-aware shipping-quote callout. Registered in BOTH
# bundles so the dark-mode compile picks up the @if branch
# (see CLAUDE.md "Dark Mode" no runtime DOM toggle in
# (see CLAUDE.md "Dark Mode" - no runtime DOM toggle in
# Odoo 19).
'web.assets_backend': [
'fusion_plating_receiving/static/src/scss/fp_shipping_quote.scss',

View File

@@ -9,7 +9,7 @@
delivery_type='fixed'; Phase D will flip Purolator (and any others
we add integrations for) to their real delivery_type.
noupdate=1 these records are upserted once on install. Hand-edits
noupdate=1 - these records are upserted once on install. Hand-edits
on the carrier records (e.g. renaming "FedEx" to "FedEx Express")
are preserved across module upgrades.
-->

View File

@@ -8,7 +8,7 @@ entech at write time) have free-text values like "FedEx", "Purolator"
in carrier_name. This migration walks them and populates the new M2O
when a unique case-insensitive name match exists.
delivery.carrier.name is jsonb (translatable) in Odoo 19 match
delivery.carrier.name is jsonb (translatable) in Odoo 19 - match
strips to the en_US translation. Ambiguous values stay as text in
carrier_name for the operator to pick manually.
"""
@@ -19,7 +19,7 @@ _logger = logging.getLogger(__name__)
def migrate(cr, version):
# Skip if the field doesn't exist yet (defensive the column is
# Skip if the field doesn't exist yet (defensive - the column is
# added by the registry update that runs before post-migrate).
cr.execute("""
SELECT 1
@@ -28,7 +28,7 @@ def migrate(cr, version):
AND column_name = 'x_fc_carrier_id'
""")
if not cr.fetchone():
_logger.warning('x_fc_carrier_id column not present skip.')
_logger.warning('x_fc_carrier_id column not present - skip.')
return
cr.execute("""

View File

@@ -4,11 +4,11 @@
#
# Backfill fp.job.qty_received from closed fp.receiving lines.
#
# Triggering issue (2026-05-20): WO-30043 and any other job created
# before the new _update_job_qty_received hook shipped has
# Triggering issue (2026-05-20): WO-30043 - and any other job created
# before the new _update_job_qty_received hook shipped - has
# qty_received=0 even though its receiving is closed. The
# button_mark_done gate then blocks the operator with no obvious next
# step ("Quantity Received is blank close the receiving record...").
# step ("Quantity Received is blank - close the receiving record...").
# Receiving IS closed. The propagation was missing.
#
# This migration walks every (closed / accepted / resolved) receiving,
@@ -39,6 +39,6 @@ def migrate(cr, version):
if updated:
_logger.info(
'fp.job.qty_received backfilled from receiving lines on '
'%d job(s) fixes WO-30043 and any sibling stuck jobs.',
'%d job(s) - fixes WO-30043 and any sibling stuck jobs.',
updated,
)

View File

@@ -2,17 +2,17 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# 2026-05-20 `staged` state retirement.
# 2026-05-20 - `staged` state retirement.
#
# Drop `staged` from the active receiving state machine. The state had
# zero downstream effect (same SO mapping as counted), no captured
# data, and median dwell of 11 sec pure ceremony between Counted
# data, and median dwell of 11 sec - pure ceremony between Counted
# and Closed. Any existing records currently sitting in `staged` get
# promoted to `closed` (they're already past the box-count step;
# closed is the next logical resting place).
#
# `staged` stays in the Selection as a (legacy) value so historical
# records that ever held it can still be read we just don't write
# records that ever held it can still be read - we just don't write
# to it anymore. The view's statusbar_visible drops it.
import logging

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Phase 6 (Sub 11) drop legacy MRP column from fp_racking_inspection.
# Phase 6 (Sub 11) - drop legacy MRP column from fp_racking_inspection.
import logging
_logger = logging.getLogger(__name__)

View File

@@ -11,7 +11,7 @@ field), leaving every auto-generated receiving line with an empty
to prefill ``received_qty``.
This migration walks existing receiving records and rebuilds the line
metadata from the linked SO's order lines via position-based zip only
metadata from the linked SO's order lines via position-based zip - only
when the receiving line count matches the SO line count (otherwise the
mapping isn't safe and we leave the record alone for manual review).
"""
@@ -52,7 +52,7 @@ def migrate(cr, version):
""", (so_id,))
so_lines = cr.fetchall()
if len(so_lines) != len(recv_line_ids):
# Mismatch don't risk corrupting a non-trivial mapping.
# Mismatch - don't risk corrupting a non-trivial mapping.
skipped += 1
continue
# Receiving lines come ordered by id ascending (the create call

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

View File

@@ -1,12 +1,12 @@
// =============================================================================
// Fusion Plating Shipping-Quote Callout Panel
// Fusion Plating - Shipping-Quote Callout Panel
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Yellow-tinted info panel rendered on the right side of the receiving
// form. Branches on Odoo's compile-time $o-webclient-color-scheme so it
// produces readable contrast in BOTH light (warm cornsilk) and dark
// (deep amber) bundles. See CLAUDE.md "Dark Mode" for why we branch at
// compile time instead of using a runtime class selector Odoo 19
// compile time instead of using a runtime class selector - Odoo 19
// serves two pre-compiled bundles, no .o_dark_mode toggle fires.
// =============================================================================
@@ -63,7 +63,7 @@ $fp-quote-muted : var(--fp-quote-muted, $_fp-quote-muted-hex);
}
}
// Quote body the HTML field rendered by _fp_format_shipping_quote.
// Quote body - the HTML field rendered by _fp_format_shipping_quote.
// Inherits the body colour from the wrapper; the .text-muted-style
// small-print uses our muted token.
.o_field_html,
@@ -78,7 +78,7 @@ $fp-quote-muted : var(--fp-quote-muted, $_fp-quote-muted-hex);
}
// =============================================================================
// Receive Parts header button dark yellow / goldenrod
// Receive Parts header button - dark yellow / goldenrod
// Applied to the SO-form header action that opens the receiving record(s).
// Uses !important to defeat the .o_form_statusbar .btn cascades in both
// the light and dark Odoo bundles.

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""Phase A carrier field + outbound shipment link tests on fp.receiving.
"""Phase A - carrier field + outbound shipment link tests on fp.receiving.
See docs/superpowers/specs/2026-05-18-phase-a-shipping-carrier-foundation-design.md.
"""
@@ -76,7 +76,7 @@ class TestCarrierFields(TransactionCase):
recv.action_create_outbound_shipment()
ship = recv.x_fc_outbound_shipment_id
self.assertEqual(ship.carrier_id, self.carrier_ups)
# Onchange triggers via the Form helper we simulate by calling
# Onchange triggers via the Form helper - we simulate by calling
# the handler directly after writing.
recv.x_fc_carrier_id = self.carrier_fedex.id
recv._onchange_x_fc_carrier_id()
@@ -86,7 +86,7 @@ class TestCarrierFields(TransactionCase):
recv = self._make_receiving(x_fc_carrier_id=self.carrier_ups.id)
recv.action_create_outbound_shipment()
ship = recv.x_fc_outbound_shipment_id
# Confirm the shipment propagation must stop.
# Confirm the shipment - propagation must stop.
ship.status = 'confirmed'
recv.x_fc_carrier_id = self.carrier_fedex.id
recv._onchange_x_fc_carrier_id()

View File

@@ -26,7 +26,7 @@ class TestTechnicianReceivingAcl(TransactionCase):
cls.tech = cls.env['res.users'].create({
'name': 'Tech ACL',
'login': 'tech_acl_recv',
# Odoo 19: group_ids (NOT groups_id) CLAUDE.md rule 13c.
# Odoo 19: group_ids (NOT groups_id) - CLAUDE.md rule 13c.
'group_ids': [(6, 0, [
cls.env.ref('fusion_plating.group_fp_technician').id,
])],
@@ -47,7 +47,7 @@ class TestTechnicianReceivingAcl(TransactionCase):
rec_as_tech.action_close()
self.assertEqual(rec.state, 'closed')
# The SO status write inside _update_so_receiving_status must have
# gone through (it is sudo'd) proves no AccessError on sale.order.
# gone through (it is sudo'd) - proves no AccessError on sale.order.
self.assertEqual(self.so.x_fc_receiving_status, 'received')
def test_technician_can_create_damage(self):

View File

@@ -2,7 +2,7 @@
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Box-level tracking fp.box list / form / search / kanban + menu.
Box-level tracking - fp.box list / form / search / kanban + menu.
-->
<odoo>

View File

@@ -2,7 +2,7 @@
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Sub 8 Racking-time inspection views.
Sub 8 - Racking-time inspection views.
-->
<odoo>
@@ -188,7 +188,7 @@
<p>
Racking inspections are auto-created when an MO is confirmed.
The racking crew opens the customer's boxes, counts parts,
and logs condition findings the per-part quality check
and logs condition findings - the per-part quality check
that used to live on receiving.
</p>
</field>

View File

@@ -21,9 +21,9 @@
<field name="domain">[('state', '=', 'discrepancy')]</field>
</record>
<!-- ===== SHIPPING & RECEIVING combined menu (v) ===== -->
<!-- ===== SHIPPING & RECEIVING - combined menu (v) ===== -->
<!-- Renamed from "Receiving & Inspection" so the same dock workflow -->
<!-- parts coming in AND parts going out lives in one place. -->
<!-- - parts coming in AND parts going out - lives in one place. -->
<!-- Logistics module reparents its 5 menu items under this root. -->
<!-- 2026-06-02: opened to Technician (was Shop Manager+) so technicians
can browse + edit receiving in the backend, not just the tablet card.
@@ -35,7 +35,7 @@
sequence="15"
groups="fusion_plating.group_fp_technician"/>
<!-- Inbound (sequences 1030) -->
<!-- Inbound (sequences 10-30) -->
<menuitem id="menu_fp_receiving_all"
name="All Receiving"
parent="menu_fp_receiving_root"
@@ -53,7 +53,7 @@
parent="menu_fp_receiving_root"
action="action_fp_receiving_discrepancy"
sequence="30"/>
<!-- Outbound items (4080) added by fusion_plating_logistics -->
<!-- Outbound items (40-80) added by fusion_plating_logistics -->
</odoo>

View File

@@ -140,7 +140,7 @@
<strong>Receiving = box count only.</strong>
Count the boxes the truck dropped off, set the number
below, and stage them for racking. The racking crew
opens the boxes and inspects each part click
opens the boxes and inspects each part - click
<strong>Racking Inspections</strong> above to jump
straight to the open inspection for this SO.
</div>
@@ -185,7 +185,7 @@
</group>
<!-- Shipping-quote preview. The .fp_shipping_quote_callout
class in fp_shipping_quote.scss handles
colour for both light + dark bundles
colour for both light + dark bundles -
yellow tint that flips to deep amber on
dark theme. Structure-only styling stays
inline; semantic colour lives in SCSS. -->

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Phase C MPS extends fusion_shipping's shipment form with the
Phase C MPS - extends fusion_shipping's shipment form with the
All Labels list (x_fc_label_attachment_ids, one entry per package
on a multi-piece shipment).
-->

View File

@@ -22,7 +22,7 @@
</button>
</xpath>
<!-- Receive Parts header action appears after SO confirmation
<!-- Receive Parts header action - appears after SO confirmation
while at least one receiving record is still open, and
disappears once every receiving record is closed. Reuses
the existing action_view_receiving method so a single

View File

@@ -5,7 +5,7 @@
Opens when fp.receiving.action_generate_outbound_label fires (assuming
a label hasn't already been generated). Forces the operator to look at
carrier + service tier + weight before the API call is made cheaper
carrier + service tier + weight before the API call is made - cheaper
shipping decisions, no surprise charges, and a hard gate against the
accidental-double-click bug where every Generate click leaked a fresh
FedEx shipment + tracking number.
@@ -19,7 +19,7 @@ from odoo.exceptions import UserError
class FpLabelGenerateWizard(models.TransientModel):
_name = 'fp.label.generate.wizard'
_description = 'Fusion Plating Confirm Label Generation'
_description = 'Fusion Plating - Confirm Label Generation'
receiving_id = fields.Many2one(
'fp.receiving', required=True, readonly=True, ondelete='cascade',
@@ -52,7 +52,7 @@ class FpLabelGenerateWizard(models.TransientModel):
)
def _fp_get_service_type_selection(self):
# Single source of truth pulls the curated list from
# Single source of truth - pulls the curated list from
# fp.receiving so both the form dropdown and the wizard stay
# in sync. See fp.receiving._FP_USABLE_FEDEX_SERVICES.
Receiving = self.env.get('fp.receiving')

View File

@@ -17,7 +17,7 @@
<form string="Generate Outbound Label">
<sheet>
<div class="oe_title">
<h2>Generate Label
<h2>Generate Label -
<field name="receiving_name"
readonly="1" nolabel="1" class="oe_inline"/>
</h2>

View File

@@ -21,7 +21,7 @@ from odoo.exceptions import UserError
class FpLabelManualWizard(models.TransientModel):
_name = 'fp.label.manual.wizard'
_description = 'Fusion Plating Manual Outbound Label Entry'
_description = 'Fusion Plating - Manual Outbound Label Entry'
receiving_id = fields.Many2one(
'fp.receiving', required=True, readonly=True, ondelete='cascade',
@@ -35,7 +35,7 @@ class FpLabelManualWizard(models.TransientModel):
)
note = fields.Text(
string='Why Manual?', readonly=True,
help='Explanatory message set by the caller (no API, API '
help='Explanatory message - set by the caller (no API, API '
'failure, etc.).',
)
label_pdf = fields.Binary(string='Shipping Label PDF')
@@ -55,7 +55,7 @@ class FpLabelManualWizard(models.TransientModel):
ship = self.shipment_id
if not ship:
raise UserError(_(
'No outbound shipment linked to this receiving '
'No outbound shipment linked to this receiving - '
'cannot save manual label.'
))
# Create the attachment, then write the shipment.
@@ -73,7 +73,7 @@ class FpLabelManualWizard(models.TransientModel):
'status': 'confirmed',
})
ship.message_post(body=Markup(_(
'Manual label saved tracking <b>%s</b>.'
'Manual label saved - tracking <b>%s</b>.'
)) % self.tracking_number)
self.receiving_id.message_post(body=Markup(_(
'Outbound label entered manually. Tracking: <b>%s</b>'

View File

@@ -12,7 +12,7 @@
<form string="Manual Outbound Label Entry">
<sheet>
<div class="oe_title">
<h2>Enter Label Manually
<h2>Enter Label Manually -
<field name="receiving_name"
readonly="1" nolabel="1" class="oe_inline"/>
</h2>