feat(configurator): Phase D batch 1 — countdown, notes split, margin, contact
Phase D first landing covers the quick-win Steelhead-parity fields on
the SO form / list:
- D1: x_fc_deadline_countdown ("in 2d 3h", "overdue 1d 4h") computed
from commitment_date. Surfaced in SO form scheduling group and as
togglable column on the SO list.
- D4: x_fc_internal_note + x_fc_external_note split (html). Existing
'note' field is left untouched for back-compat. External note is
intended for the SO acknowledgement + portal; internal note is
shop-floor only.
- D8: x_fc_margin_amount + x_fc_margin_percent, currently computed
against fp.coating.config.unit_cost if defined (else 0 -> 100%
margin). When cost rollup lands on fp.coating.config, margin will
reflect reality automatically.
- D12: x_fc_contact_phone related to partner.phone (readonly) on SO
header.
- D13: x_fc_ship_via Char on SO header (carrier name).
Smoke: S00066 shows 'in 9d 22h' countdown + \$3025 margin; S00069
shows 'in 24d 22h' + \$750. Contact phone pulls from partner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,90 @@ class SaleOrder(models.Model):
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# ---- Phase D: SO detail view polish ----
|
||||
x_fc_external_note = fields.Html(
|
||||
string='External Notes',
|
||||
help='Customer-visible notes. Appear on the SO acknowledgement '
|
||||
'and customer portal.',
|
||||
)
|
||||
x_fc_internal_note = fields.Html(
|
||||
string='Internal Notes',
|
||||
help='Internal-only notes for the estimator / planner / shop floor.',
|
||||
)
|
||||
x_fc_ship_via = fields.Char(
|
||||
string='Ship Via',
|
||||
help='Carrier or delivery method name (UPS, FedEx, customer pickup, etc.).',
|
||||
tracking=True,
|
||||
)
|
||||
x_fc_contact_phone = fields.Char(
|
||||
related='partner_id.phone', string='Contact Phone', readonly=True,
|
||||
)
|
||||
x_fc_deadline_countdown = fields.Char(
|
||||
string='Deadline',
|
||||
compute='_compute_deadline_countdown',
|
||||
)
|
||||
x_fc_margin_amount = fields.Monetary(
|
||||
string='Margin',
|
||||
compute='_compute_margin', currency_field='currency_id',
|
||||
)
|
||||
x_fc_margin_percent = fields.Float(
|
||||
string='Margin %',
|
||||
compute='_compute_margin',
|
||||
)
|
||||
|
||||
@api.depends('commitment_date')
|
||||
def _compute_deadline_countdown(self):
|
||||
from datetime import datetime
|
||||
now = fields.Datetime.now()
|
||||
for rec in self:
|
||||
if not rec.commitment_date:
|
||||
rec.x_fc_deadline_countdown = False
|
||||
continue
|
||||
target = rec.commitment_date
|
||||
if isinstance(target, datetime):
|
||||
delta = target - now
|
||||
else:
|
||||
from datetime import datetime as _dt
|
||||
delta = _dt.combine(target, _dt.min.time()) - now
|
||||
secs = int(delta.total_seconds())
|
||||
if secs == 0:
|
||||
rec.x_fc_deadline_countdown = 'due now'
|
||||
continue
|
||||
past = secs < 0
|
||||
secs = abs(secs)
|
||||
days = secs // 86400
|
||||
hours = (secs % 86400) // 3600
|
||||
mins = (secs % 3600) // 60
|
||||
bits = []
|
||||
if days:
|
||||
bits.append('%dd' % days)
|
||||
if hours:
|
||||
bits.append('%dh' % hours)
|
||||
if mins and not days:
|
||||
bits.append('%dm' % mins)
|
||||
phrase = ' '.join(bits) or '<1m'
|
||||
rec.x_fc_deadline_countdown = (
|
||||
'overdue %s' % phrase if past else 'in %s' % phrase
|
||||
)
|
||||
|
||||
@api.depends('order_line.price_subtotal', 'amount_untaxed')
|
||||
def _compute_margin(self):
|
||||
"""Simple margin: untaxed total minus rolled-up cost from coating configs."""
|
||||
for rec in self:
|
||||
cost = 0.0
|
||||
for line in rec.order_line:
|
||||
if line.x_fc_coating_config_id:
|
||||
# If coating_config has a cost field, use it; otherwise 0.
|
||||
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)
|
||||
rec.x_fc_margin_amount = (rec.amount_untaxed or 0) - cost
|
||||
rec.x_fc_margin_percent = (
|
||||
(rec.x_fc_margin_amount / rec.amount_untaxed * 100.0)
|
||||
if rec.amount_untaxed else 0.0
|
||||
)
|
||||
|
||||
@api.onchange('upload_rfq_file')
|
||||
def _onchange_upload_rfq_file(self):
|
||||
"""Create attachment from uploaded binary and link it."""
|
||||
|
||||
@@ -84,15 +84,37 @@
|
||||
<group>
|
||||
<group string="Customer Reference">
|
||||
<field name="x_fc_customer_job_number"/>
|
||||
<field name="x_fc_contact_phone"/>
|
||||
<field name="x_fc_ship_via"/>
|
||||
</group>
|
||||
<group string="Scheduling">
|
||||
<field name="x_fc_planned_start_date"/>
|
||||
<field name="x_fc_internal_deadline"/>
|
||||
<field name="commitment_date" string="Customer Deadline"/>
|
||||
<field name="x_fc_deadline_countdown" readonly="1"/>
|
||||
<field name="x_fc_is_blanket_order"/>
|
||||
<field name="x_fc_block_partial_shipments"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Margin">
|
||||
<field name="x_fc_margin_amount"
|
||||
widget="monetary"
|
||||
options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="x_fc_margin_percent"
|
||||
widget="percentage"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Internal Notes">
|
||||
<field name="x_fc_internal_note" nolabel="1"
|
||||
placeholder="Internal notes for estimator / planner / shop floor..."/>
|
||||
</group>
|
||||
<group string="External Notes (customer-visible)">
|
||||
<field name="x_fc_external_note" nolabel="1"
|
||||
placeholder="Notes that appear on the acknowledgement and portal..."/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']/list/field[@name='product_uom_qty']" position="before">
|
||||
@@ -122,6 +144,7 @@
|
||||
<field name="x_fc_customer_job_number" optional="show"/>
|
||||
<field name="x_fc_internal_deadline" optional="show"/>
|
||||
<field name="commitment_date" string="Customer Deadline" optional="show"/>
|
||||
<field name="x_fc_deadline_countdown" optional="show"/>
|
||||
<field name="x_fc_planned_start_date" optional="hide"/>
|
||||
<field name="x_fc_part_catalog_id" optional="hide"/>
|
||||
<field name="x_fc_coating_config_id" optional="hide"/>
|
||||
|
||||
Reference in New Issue
Block a user