From 2476961f50b97e7bea65dd2a03426fac32657244 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 21:11:18 -0400 Subject: [PATCH] =?UTF-8?q?feat(configurator):=20Phase=20D=20batch=201=20?= =?UTF-8?q?=E2=80=94=20countdown,=20notes=20split,=20margin,=20contact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../models/sale_order.py | 84 +++++++++++++++++++ .../views/sale_order_views.xml | 23 +++++ 2 files changed, 107 insertions(+) diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order.py b/fusion_plating/fusion_plating_configurator/models/sale_order.py index d4428a1a..c1079122 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order.py @@ -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.""" diff --git a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml index 59d01e26..268bf5fd 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -84,15 +84,37 @@ + + + + + + + + + + + + + + + + + @@ -122,6 +144,7 @@ +