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