From 97c733b7c3830a4e30b8f1aa391cc2413a07cd82 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 21:23:41 -0400 Subject: [PATCH] =?UTF-8?q?feat(configurator):=20Phase=20F=20=E2=80=94=20q?= =?UTF-8?q?uotations=20list=20uplift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1 follow-up: x_fc_follow_up_date + x_fc_follow_up_user_id fields on sale.order, surfaced in the quotations list + a 'Needs Follow-Up' preset filter. F2 expires: native validity_date exposed as togglable column on the quotes list + an 'Expired' preset filter. F3 email status pills: x_fc_email_status computed (draft / sent / opened / won). 'Opened' detects via mail.notification.is_read on any email-type mail.message attached to this SO. F5 part numbers summary: x_fc_part_numbers_summary ("PN1, PN2 (+3 more)") across order_line parts, togglable column. F7 from-RFQ filter reuses existing x_fc_rfq_attachment_id. Views: - view_sale_order_list_fp_quotes (new list dedicated to quotes). - view_sale_order_search_fp_quotes with filters Draft / Sent / Won / From RFQ / Needs Follow-Up / Expired + group-bys. - action_fp_quotations rewired to both of the above. Bumped to 19.0.7.2.0. Closes all six phases originally planned. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__manifest__.py | 2 +- .../models/sale_order.py | 72 +++++++++++++++++++ .../views/sale_order_views.xml | 66 ++++++++++++++++- 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py index 9b1d7d28..3d374ed1 100644 --- a/fusion_plating/fusion_plating_configurator/__manifest__.py +++ b/fusion_plating/fusion_plating_configurator/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Configurator', - 'version': '19.0.7.1.0', + 'version': '19.0.7.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.', 'description': """ diff --git a/fusion_plating/fusion_plating_configurator/models/sale_order.py b/fusion_plating/fusion_plating_configurator/models/sale_order.py index 25b8a568..78523a69 100644 --- a/fusion_plating/fusion_plating_configurator/models/sale_order.py +++ b/fusion_plating/fusion_plating_configurator/models/sale_order.py @@ -144,6 +144,78 @@ class SaleOrder(models.Model): ]) rec.x_fc_wo_completion = '%d/%d' % (done, total) if total else '0/0' + # ---- Phase F: quotes list view polish ---- + x_fc_follow_up_date = fields.Date( + string='Follow-Up Date', + help='Date to chase the customer for a decision on this quote.', + tracking=True, + ) + x_fc_follow_up_user_id = fields.Many2one( + 'res.users', string='Follow-Up Owner', + help='Who should chase the customer on the follow-up date.', + ) + x_fc_email_status = fields.Selection( + [('draft', 'Draft'), + ('sent', 'Sent'), + ('opened', 'Opened'), + ('won', 'Order Received')], + string='Email Status', + compute='_compute_email_status', + store=True, + ) + x_fc_part_numbers_summary = fields.Char( + string='Part Numbers', + compute='_compute_part_numbers_summary', + ) + + @api.depends('state') + def _compute_email_status(self): + """Map state + mail tracking to a single visible pill. + + - draft SO with no tracked email sent => draft + - sent (Odoo state) => sent + - sent + mail opened => opened (detected via mail.message) + - state=sale/done => won + """ + for rec in self: + if rec.state in ('sale', 'done'): + rec.x_fc_email_status = 'won' + continue + if rec.state == 'draft': + rec.x_fc_email_status = 'draft' + continue + # state == 'sent' + opened = False + if rec.id: + msgs = self.env['mail.message'].sudo().search([ + ('model', '=', 'sale.order'), + ('res_id', '=', rec.id), + ('message_type', '=', 'email'), + ], limit=10) + # mail.notification tracks read timestamps + for m in msgs: + if m.notification_ids.filtered( + lambda n: n.is_read + ): + opened = True + break + rec.x_fc_email_status = 'opened' if opened else 'sent' + + @api.depends('order_line.x_fc_part_catalog_id.part_number') + def _compute_part_numbers_summary(self): + for rec in self: + parts = rec.order_line.mapped('x_fc_part_catalog_id.part_number') + parts = [p for p in parts if p] + if not parts: + rec.x_fc_part_numbers_summary = False + continue + if len(parts) <= 2: + rec.x_fc_part_numbers_summary = ', '.join(parts) + else: + rec.x_fc_part_numbers_summary = '%s, %s (+%d more)' % ( + parts[0], parts[1], len(parts) - 2, + ) + @api.depends('invoice_ids.amount_total', 'invoice_ids.state', 'invoice_ids.move_type') def _compute_invoiced_amount(self): 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 3c0d1438..fbbe8c35 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -177,6 +177,69 @@ + + + sale.order.list.fp.quotes + sale.order + + + + + + + + + + + + + + + + + + + + + + sale.order.search.fp.quotes + sale.order + + + + + + + + + + + + + + + + + + + + + + + sale.order.search.fp @@ -222,7 +285,8 @@ list,form,kanban [('state', 'in', ('draft', 'sent'))] + (0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_fp_quotes')})]"/> + {'default_x_fc_delivery_method': 'shipping_partner'}