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 Invoicing',
'name': 'Fusion Plating - Invoicing',
'version': '19.0.3.8.0',
'category': 'Manufacturing/Plating',
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
'description': """
Fusion Plating Invoicing
Fusion Plating - Invoicing
===========================
Part of the Fusion Plating product family by Nexa Systems Inc.

View File

@@ -10,7 +10,7 @@ The legacy `fp.invoice.strategy.default` model is left in place; the
new sale.order onchange falls back to it for any partner whose record
hasn't been migrated, so downstream code keeps working mid-rollout.
property_payment_term_id is intentionally skipped it lives in
property_payment_term_id is intentionally skipped - it lives in
ir_property rather than as a plain column, and the legacy onchange
fallback already reads payment_term from the strategy default record
when the partner doesn't have one set directly.

View File

@@ -26,11 +26,11 @@ class AccountMove(models.Model):
Two defensive defaults so newly-created invoices come out
compliant out of the box:
1. **invoice_payment_term_id** pulled from the customer's
1. **invoice_payment_term_id** - pulled from the customer's
property_payment_term_id (Net-30, COD, etc.). Without this
the due date silently becomes "immediate", wrong for B2B.
2. **ref** (customer reference / PO#) pulled from the source
2. **ref** (customer reference / PO#) - pulled from the source
sale order's client_order_ref or x_fc_po_number. Customer
AP teams reject invoices that don't quote their PO# back.
We already populate this on the SO confirm path, but a
@@ -60,7 +60,7 @@ class AccountMove(models.Model):
"""Block post when:
• customer is on account hold (existing rule), or
• the invoice has no payment term (auto-fill missed it AND
partner had no default accountant must pick one).
partner had no default - accountant must pick one).
"""
for move in self:
if move.move_type in ('out_invoice', 'out_refund') and move.partner_id:
@@ -69,14 +69,14 @@ class AccountMove(models.Model):
is_manager = self.env['res.partner']._fp_user_can_override_account_hold()
if not is_manager:
raise UserError(_(
'Cannot post invoice customer "%s" is on account hold.\n'
'Cannot post invoice - customer "%s" is on account hold.\n'
'Reason: %s\n\n'
'Contact a manager to override.'
) % (hold_partner.name,
hold_partner.x_fc_account_hold_reason or 'No reason specified'))
if not move.invoice_payment_term_id:
raise UserError(_(
'Cannot post invoice "%s" no payment terms set.\n\n'
'Cannot post invoice "%s" - no payment terms set.\n\n'
'Pick payment terms (Net-30, COD, etc.) on the invoice, '
'or set a default on the customer "%s" so future '
'invoices inherit it automatically.'

View File

@@ -19,7 +19,7 @@ class FpDelivery(models.Model):
def action_mark_delivered(self):
res = super().action_mark_delivered()
SaleOrder = self.env['sale.order']
# Sub 11 MRP gone; resolve via delivery.job_ref → fp.job.name → fp.job.origin.
# Sub 11 - MRP gone; resolve via delivery.job_ref → fp.job.name → fp.job.origin.
Job = self.env['fp.job'] if 'fp.job' in self.env else None
for delivery in self:
so = False

View File

@@ -13,7 +13,7 @@ class FpInvoiceStrategyDefault(models.Model):
strategy and deposit percentage auto-fill from this record.
"""
_name = 'fp.invoice.strategy.default'
_description = 'Fusion Plating Invoice Strategy Default'
_description = 'Fusion Plating - Invoice Strategy Default'
_order = 'partner_id'
partner_id = fields.Many2one(
@@ -42,7 +42,7 @@ class FpInvoiceStrategyDefault(models.Model):
bits.append(rec.partner_id.display_name)
if rec.default_strategy:
bits.append(labels.get(rec.default_strategy, rec.default_strategy))
rec.display_name = ' '.join(bits) or 'Invoice Strategy'
rec.display_name = ' - '.join(bits) or 'Invoice Strategy'
_sql_constraints = [
('fp_invoice_strategy_partner_uniq', 'unique(partner_id)',

View File

@@ -22,14 +22,14 @@ class ResPartner(models.Model):
Plating Manager OR Plating Administrator qualifies. Administrator
is checked explicitly (in addition to the implied chain) because
Odoo's ``implied_ids`` cascade does NOT reliably propagate to
existing users on module upgrade admin (uid 1) typically lands
existing users on module upgrade - admin (uid 1) typically lands
in Administrator only, with no Manager membership. Without this
defensive check, the highest-privileged user can't bypass holds.
See CLAUDE.md "Implied group cascade" rule.
"""
user = self.env.user
# Phase G: fixed audit-finding-11 old code referenced
# Phase G: fixed audit-finding-11 - old code referenced
# 'fusion_plating.group_fusion_plating_administrator', an xmlid
# that never existed, so the gate always returned False. Replaced
# with group_fp_manager which transitively implies Owner via
@@ -48,7 +48,7 @@ class ResPartner(models.Model):
# invoice strategy, delivery method, and deadlines on every new SO so
# repeat customers don't need re-typing the same values each order.
# Tax type lives on `property_account_position_id` (Odoo native fiscal
# position) and payment terms on `property_payment_term_id` both are
# position) and payment terms on `property_payment_term_id` - both are
# surfaced on the same Plating Defaults tab in the partner form.
x_fc_default_invoice_strategy = fields.Selection(

View File

@@ -15,7 +15,7 @@ _logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
# Explicit related field dotted refs like `partner_id.x_fc_account_hold`
# Explicit related field - dotted refs like `partner_id.x_fc_account_hold`
# in `invisible=` modifiers are fragile in Odoo 19 (the related field
# has to be in the record cache for the evaluator). Surfacing it as a
# plain field on sale.order makes the banner condition deterministic.
@@ -67,7 +67,7 @@ class SaleOrder(models.Model):
@api.onchange('x_fc_planned_start_date')
def _onchange_planned_start_date_deadlines(self):
"""Recompute deadlines when planned start changes without it
"""Recompute deadlines when planned start changes - without it
the partner offsets would only fire on partner_id change."""
self._fp_recompute_default_deadlines()
@@ -104,14 +104,14 @@ class SaleOrder(models.Model):
# whole downstream chain (CoC, BoL, invoice) from going
# out unreferenced. The PO# is on `client_order_ref`
# (Odoo standard) AND mirrored to `x_fc_po_number`
# (FP-specific) accept either as filled.
# (FP-specific) - accept either as filled.
#
# Two escape hatches for real-world cases:
# * x_fc_po_pending estimator flag: "PO coming later".
# * x_fc_po_pending - estimator flag: "PO coming later".
# Confirms the order without a PO number and schedules
# a chase activity for x_fc_po_expected_date so sales
# doesn't forget to chase the paperwork.
# * x_fc_po_override manager waiver: "proceed without
# * x_fc_po_override - manager waiver: "proceed without
# a formal PO (handshake deal)". Permanent.
po_set = bool(order.client_order_ref) or bool(
getattr(order, 'x_fc_po_number', False)
@@ -120,11 +120,11 @@ class SaleOrder(models.Model):
po_override = bool(getattr(order, 'x_fc_po_override', False))
if not po_set and not po_pending and not po_override:
raise UserError(_(
'Cannot confirm SO "%(so)s" Customer PO# is required.\n\n'
'Cannot confirm SO "%(so)s" - Customer PO# is required.\n\n'
'Set the customer\'s purchase order number in the '
'"Customer Reference" field (or x_fc_po_number) before '
'confirming. If the customer will provide the PO '
'later, tick "PO Pending" and set "PO Expected By" '
'later, tick "PO Pending" and set "PO Expected By" - '
'the order will confirm with a follow-up activity to '
'chase the paperwork. A Plating Manager can also set '
'"PO Override" for handshake deals.'
@@ -133,14 +133,14 @@ class SaleOrder(models.Model):
# --- Account hold check ---
# Hold lives on the commercial_partner (the company). Resolve
# through that so a hold on the parent applies to every child
# contact too typical case is "all of Acme is on hold", not
# contact too - typical case is "all of Acme is on hold", not
# "specifically the AP clerk's contact card".
hold_partner = order.partner_id.commercial_partner_id
if hold_partner.x_fc_account_hold:
is_manager = self.env['res.partner']._fp_user_can_override_account_hold()
if not is_manager:
raise UserError(_(
'Cannot confirm customer "%s" is on account hold.\n'
'Cannot confirm - customer "%s" is on account hold.\n'
'Reason: %s\n\n'
'Contact a manager to override.'
) % (hold_partner.name,
@@ -164,7 +164,7 @@ class SaleOrder(models.Model):
if not getattr(order, 'x_fc_po_pending', False):
continue
if getattr(order, 'x_fc_po_number', False) or order.client_order_ref:
continue # PO already set nothing to chase
continue # PO already set - nothing to chase
from datetime import timedelta
expected = (
order.x_fc_po_expected_date
@@ -197,7 +197,7 @@ class SaleOrder(models.Model):
order._create_full_invoice()
elif strategy == 'progress' and order.x_fc_progress_initial_percent:
order._create_progress_initial_invoice()
# 'net_terms' no action on confirm; invoiced when delivery is marked delivered
# 'net_terms' - no action on confirm; invoiced when delivery is marked delivered
return res
@@ -212,7 +212,7 @@ class SaleOrder(models.Model):
return
try:
# The wizard's sale_order_ids default reads active_ids AT CREATE
# time context must be set on .with_context(), not on the
# time - context must be set on .with_context(), not on the
# subsequent create_invoices() call.
wizard = self.env['sale.advance.payment.inv'].with_context(
active_ids=self.ids,
@@ -224,7 +224,7 @@ class SaleOrder(models.Model):
})
wizard.create_invoices()
self.message_post(
body=_('Deposit invoice (%.0f%%) created strategy: Deposit.') % percent,
body=_('Deposit invoice (%.0f%%) created - strategy: Deposit.') % percent,
)
except Exception as e:
_logger.warning('Failed to create deposit invoice for SO %s: %s', self.name, e)
@@ -239,7 +239,7 @@ class SaleOrder(models.Model):
invoices = self._create_invoices()
if invoices:
self.message_post(
body=_('Full invoice created strategy: COD / Prepay.'),
body=_('Full invoice created - strategy: COD / Prepay.'),
)
except Exception as e:
_logger.warning('Failed to create COD invoice for SO %s: %s', self.name, e)
@@ -248,7 +248,7 @@ class SaleOrder(models.Model):
)
def _create_progress_initial_invoice(self):
"""Progress Billing first invoice at SO confirm.
"""Progress Billing - first invoice at SO confirm.
Uses Odoo's down-payment mechanism to bill the initial percentage.
The remainder is billed on delivery via `_create_final_balance_invoice`.
@@ -269,7 +269,7 @@ class SaleOrder(models.Model):
wizard.create_invoices()
self.message_post(
body=_(
'Progress invoice initial %.0f%% created strategy: Progress Billing. '
'Progress invoice - initial %.0f%% created - strategy: Progress Billing. '
'Final balance will be invoiced on delivery.'
) % percent,
)
@@ -288,7 +288,7 @@ class SaleOrder(models.Model):
"""
self.ensure_one()
if self.x_fc_final_invoice_id:
return self.x_fc_final_invoice_id # Already invoiced don't double
return self.x_fc_final_invoice_id # Already invoiced - don't double
if self.invoice_status == 'invoiced':
return False # Nothing more to bill
try:
@@ -300,7 +300,7 @@ class SaleOrder(models.Model):
).get(self.x_fc_invoice_strategy, self.x_fc_invoice_strategy)
self.message_post(
body=_(
'Final invoice created on delivery strategy: %s.'
'Final invoice created on delivery - strategy: %s.'
) % strategy_label,
)
return invoices

View File

@@ -17,7 +17,7 @@
<!-- Backward-compat: new Manager role implies old Accounting group.
2026-05-29: Manager (+ QM + Owner via implication) also gets Odoo's
Billing group so they can create customer invoices from a Sale
Order. Billing only not Accountant. The SO-origin workflow gate
Order. Billing only - not Accountant. The SO-origin workflow gate
in fusion_plating_jobs is unchanged (off-SO invoices still blocked).
Spec: docs/superpowers/specs/2026-05-29-manager-invoice-permission-design.md -->
<record id="fusion_plating.group_fp_manager" model="res.groups">

View File

@@ -30,7 +30,7 @@ class TestManagerInvoicePermission(TransactionCase):
self.assertTrue(owner.has_group('account.group_account_invoice'))
def test_shop_manager_does_not_get_billing(self):
# Shop Manager is BELOW Manager must NOT inherit Billing
# Shop Manager is BELOW Manager - must NOT inherit Billing
# ("managers and ABOVE" scope).
shop = self._user_with('fusion_plating.group_fp_shop_manager_v2')
self.assertFalse(shop.has_group('account.group_account_invoice'))

View File

@@ -14,7 +14,7 @@
<xpath expr="//sheet" position="before">
<div class="alert alert-danger mb-0" role="alert"
invisible="not x_fc_account_hold">
<strong>Account Hold Active</strong>
<strong>Account Hold Active</strong> -
<field name="x_fc_account_hold_reason" readonly="1" nolabel="1"/>
<br/>
<small>
@@ -24,7 +24,7 @@
</div>
</xpath>
<!-- Single "Plating Defaults" tab invoice strategy, delivery,
<!-- Single "Plating Defaults" tab - invoice strategy, delivery,
deadlines, tax type, payment terms. Set once here, cascades
onto every new SO for this customer. -->
<xpath expr="//notebook" position="inside">
@@ -57,7 +57,7 @@
</group>
</page>
<!-- Phase D5 Account Hold management (the override gate per
<!-- Phase D5 - Account Hold management (the override gate per
spec section 2.E Layer 3). Was previously gated on the
fold-in group_fp_accounting; consolidated to group_fp_manager
and resolves audit-finding-11 _administrator typo by removing

View File

@@ -18,7 +18,7 @@
role="alert"
invisible="not x_fc_partner_account_hold">
<i class="fa fa-ban me-1" title="Account hold"/>
<strong>Account Hold</strong> SO confirmation, invoicing
<strong>Account Hold</strong> - SO confirmation, invoicing
and shipping are blocked for non-managers.
</div>
</xpath>