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:
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user