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:
gsinghpal
2026-04-19 21:11:18 -04:00
parent 6b4b0c9eb7
commit 2476961f50
2 changed files with 107 additions and 0 deletions

View File

@@ -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."""

View File

@@ -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"/>