fix(plating): UAT-caught UX annoyances + lurking bugs

Five fixes from the end-to-end UAT debrief:

1. Menu discoverability (HIGH)
   Added a prominent "+ New Direct Order" button in the Sale Orders
   list header toolbar (class=btn-primary, display=always). The
   existing menuitem at Plating > Sales > New Direct Order was
   buried in a submenu that didn't always expand; the toolbar
   button is a guaranteed entry point from the most common screen.

2. Escape/X destroys wizard state (HIGH)
   Added a prominent info banner at the top of the wizard form:
   "Changes are not saved until you click Create & Confirm Order.
   Closing this window (Esc or X) discards your entries." The
   Cancel button now has confirm="Discard this order? All header
   data and line items will be lost." so the intentional-cancel
   path also prompts.

3. Shell/cron crash in _fp_auto_create_mo (MEDIUM)
   bridge_mrp/models/sale_order.py:232-264 used _() inside list
   comprehensions to format the internal chatter summary of newly
   created / adopted MOs. _() resolves language from env.context,
   which is empty in odoo-shell and cron contexts — triggering a
   translate.get_text_alias crash AFTER the MOs had been created.
   These strings are internal audit log text, not user-facing UI;
   dropped the _() wrappers so the message builds safely from any
   context. Same for the per-group error-message on savepoint
   rollback.

4. Misleading "100%" margin (MEDIUM)
   x_fc_margin_percent displayed 100% on every SO because the cost
   rollup from fp.coating.config.unit_cost isn't populated yet.
   Added x_fc_margin_available Boolean (True only when at least
   one line's coating has a non-zero unit_cost). The SO Plating
   tab now hides the margin numbers when margin_available=False
   and shows an inline muted note: "Margin n/a — coating cost
   rollup not yet populated on any line's treatment."

5. Account Hold banner too loud (LOW)
   fusion_plating_invoicing was injecting a full-height danger
   alert above every SO header. Slimmed it to a one-line compact
   alert with icon: "Account Hold — SO confirmation, invoicing
   and shipping are blocked for non-managers." Half the vertical
   footprint, less visual competition with the Plating chip bar.

Verified via UAT on S00071.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-20 01:03:26 -04:00
parent 068a654c2b
commit bee5ba4d3f
5 changed files with 73 additions and 25 deletions

View File

@@ -229,35 +229,38 @@ class SaleOrder(models.Model):
self.env.cr.execute('RELEASE SAVEPOINT %s' % savepoint_name)
except Exception as exc:
self.env.cr.execute('ROLLBACK TO SAVEPOINT %s' % savepoint_name)
self.message_post(body=_(
self.message_post(body=(
'Auto-MO group %s failed: %s'
) % (tag or 'single-line', exc))
continue
if created or adopted:
# _() needs a lang in env.context; in shell/cron this may be
# unset. Compose the message with plain format strings — this
# text is an internal chatter log, not user-facing UI.
msg_parts = []
if created:
lines_html = '<br/>'.join([
_('MO <a href="/odoo/manufacturing/%s">%s</a> '
'(%s, %d source line%s)') % (
mo.id, mo.name, tag or 'untagged',
n, 's' if n != 1 else ''
)
'MO <a href="/odoo/manufacturing/%s">%s</a> '
'(%s, %d source line%s)' % (
mo.id, mo.name, tag or 'untagged',
n, 's' if n != 1 else ''
)
for mo, tag, n in created
])
msg_parts.append(
_('%d draft MO(s) auto-created:<br/>%s') % (
'%d draft MO(s) auto-created:<br/>%s' % (
len(created), lines_html,
)
)
if adopted:
adopted_html = '<br/>'.join([
_('MO <a href="/odoo/manufacturing/%s">%s</a> '
'(legacy, now line-linked)') % (mo.id, mo.name)
'MO <a href="/odoo/manufacturing/%s">%s</a> '
'(legacy, now line-linked)' % (mo.id, mo.name)
for mo in adopted
])
msg_parts.append(
_('%d legacy MO(s) adopted:<br/>%s') % (
'%d legacy MO(s) adopted:<br/>%s' % (
len(adopted), adopted_html,
)
)

View File

@@ -113,6 +113,12 @@ class SaleOrder(models.Model):
string='Margin %',
compute='_compute_margin',
)
x_fc_margin_available = fields.Boolean(
string='Margin Available',
compute='_compute_margin',
help='False when no order line has a costed coating — the '
'margin fields should render "n/a" in the UI.',
)
x_fc_workorder_count = fields.Integer(
string='Active WOs',
@@ -486,24 +492,34 @@ class SaleOrder(models.Model):
@api.depends('order_line.price_subtotal', 'amount_untaxed')
def _compute_margin(self):
"""Simple margin: untaxed total minus rolled-up cost from coating configs.
"""Margin = untaxed total rolled-up cost from coating configs.
x_fc_margin_percent is stored as a fraction (0.0 - 1.0) so the
widget='percentage' formats it correctly (a 100% margin reads
as 100%, not 10000%).
widget='percentage' formats 100% as 100%, not 10000%.
x_fc_margin_available is False when NO line has a costed coating
(i.e. fp.coating.config.unit_cost isn't populated anywhere). The
UI should render margin fields as "n/a" in that case rather than
showing a misleading 100%.
"""
for rec in self:
has_cost_data = False
cost = 0.0
for line in rec.order_line:
if line.x_fc_coating_config_id:
cost_per_unit = getattr(
line.x_fc_coating_config_id, 'unit_cost', 0.0,
) or 0.0
cost += cost_per_unit * (line.product_uom_qty or 0)
cc = line.x_fc_coating_config_id
if not cc:
continue
if 'unit_cost' not in cc._fields:
continue
if cc.unit_cost:
has_cost_data = True
cost_per_unit = cc.unit_cost or 0.0
cost += cost_per_unit * (line.product_uom_qty or 0)
rec.x_fc_margin_available = has_cost_data
rec.x_fc_margin_amount = (rec.amount_untaxed or 0) - cost
rec.x_fc_margin_percent = (
(rec.x_fc_margin_amount / rec.amount_untaxed)
if rec.amount_untaxed else 0.0
if (rec.amount_untaxed and has_cost_data) else 0.0
)
@api.onchange('upload_rfq_file')

View File

@@ -148,11 +148,21 @@
</group>
<group>
<group string="Margin">
<div colspan="2"
invisible="x_fc_margin_available"
class="text-muted">
<i class="fa fa-info-circle me-1"/>
Margin n/a — coating cost rollup not yet
populated on any line's treatment.
</div>
<field name="x_fc_margin_amount"
widget="monetary"
options="{'currency_field': 'currency_id'}"/>
options="{'currency_field': 'currency_id'}"
invisible="not x_fc_margin_available"/>
<field name="x_fc_margin_percent"
widget="percentage"/>
widget="percentage"
invisible="not x_fc_margin_available"/>
<field name="x_fc_margin_available" invisible="1"/>
</group>
</group>
<group>
@@ -188,6 +198,13 @@
<field name="arch" type="xml">
<list string="Sale Orders" decoration-info="state == 'draft'"
decoration-muted="state == 'cancel'">
<header>
<button name="%(action_fp_direct_order_wizard)d"
type="action"
string="+ New Direct Order"
class="btn-primary"
display="always"/>
</header>
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_po_number"/>

View File

@@ -6,6 +6,13 @@
<field name="model">fp.direct.order.wizard</field>
<field name="arch" type="xml">
<form string="Direct Order Entry">
<div class="alert alert-info py-2 mb-0 small"
role="alert">
<i class="fa fa-info-circle me-1"/>
Changes are not saved until you click
<strong>Create &amp; Confirm Order</strong>. Closing this
window (Esc or X) discards your entries.
</div>
<div class="alert alert-warning mb-0"
role="alert"
invisible="not missing_info_msg">
@@ -194,7 +201,10 @@
type="object"
string="Create &amp; Confirm Order"
class="btn-primary"/>
<button string="Cancel" special="cancel" class="btn-secondary"/>
<button string="Cancel"
special="cancel"
class="btn-secondary"
confirm="Discard this order? All header data and line items will be lost."/>
</footer>
</form>
</field>

View File

@@ -13,10 +13,12 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//form/header" position="before">
<div class="alert alert-danger" role="alert"
<div class="alert alert-danger py-1 px-2 mb-0 small"
role="alert"
invisible="not partner_id or not partner_id.x_fc_account_hold">
<strong>Account Hold</strong> — This customer is on account hold.
SO confirmation, invoicing, and shipping are blocked for non-managers.
<i class="fa fa-ban me-1"/>
<strong>Account Hold</strong> — SO confirmation, invoicing
and shipping are blocked for non-managers.
</div>
</xpath>
</field>