feat(configurator): Phase F — quotations list uplift

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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 21:23:41 -04:00
parent 94eb7ef415
commit 97c733b7c3
3 changed files with 138 additions and 2 deletions

View File

@@ -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': """

View File

@@ -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):

View File

@@ -177,6 +177,69 @@
</field>
</record>
<!-- ===== Quotes list view (state in draft/sent) ===== -->
<record id="view_sale_order_list_fp_quotes" model="ir.ui.view">
<field name="name">sale.order.list.fp.quotes</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<list string="Quotations" decoration-muted="state == 'cancel'">
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_part_numbers_summary" optional="show"/>
<field name="x_fc_po_number" optional="hide"/>
<field name="x_fc_customer_job_number" optional="hide"/>
<field name="create_date" string="Created" optional="show"/>
<field name="validity_date" string="Expires" optional="show"/>
<field name="x_fc_follow_up_date" optional="show"/>
<field name="x_fc_follow_up_user_id" optional="show"/>
<field name="amount_total" sum="Total"/>
<field name="x_fc_email_status" widget="badge"
decoration-info="x_fc_email_status == 'sent'"
decoration-warning="x_fc_email_status == 'opened'"
decoration-success="x_fc_email_status == 'won'"/>
<field name="currency_id" column_invisible="1"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<!-- ===== Quotes search view ===== -->
<record id="view_sale_order_search_fp_quotes" model="ir.ui.view">
<field name="name">sale.order.search.fp.quotes</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<search string="Quotations">
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_part_numbers_summary" string="Part Number"/>
<filter name="my_quotes" string="My Quotes"
domain="[('user_id', '=', uid)]"/>
<separator/>
<filter name="draft" string="Draft"
domain="[('state', '=', 'draft')]"/>
<filter name="sent" string="Sent"
domain="[('state', '=', 'sent')]"/>
<filter name="won" string="Won"
domain="[('state', 'in', ('sale', 'done'))]"/>
<separator/>
<filter name="from_rfq" string="From RFQ"
domain="[('x_fc_rfq_attachment_id', '!=', False)]"/>
<filter name="needs_followup" string="Needs Follow-Up"
domain="[('x_fc_follow_up_date', '&lt;=', context_today()), ('state', 'in', ('draft', 'sent'))]"/>
<filter name="expired" string="Expired"
domain="[('validity_date', '&lt;', context_today()), ('state', 'in', ('draft', 'sent'))]"/>
<group>
<filter string="Customer" name="group_partner"
context="{'group_by': 'partner_id'}"/>
<filter string="Status" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Follow-Up Owner" name="group_followup"
context="{'group_by': 'x_fc_follow_up_user_id'}"/>
</group>
</search>
</field>
</record>
<!-- ===== Search view for Fusion Plating SO list ===== -->
<record id="view_sale_order_search_fp" model="ir.ui.view">
<field name="name">sale.order.search.fp</field>
@@ -222,7 +285,8 @@
<field name="view_mode">list,form,kanban</field>
<field name="domain">[('state', 'in', ('draft', 'sent'))]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_fp')})]"/>
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_fp_quotes')})]"/>
<field name="search_view_id" ref="view_sale_order_search_fp_quotes"/>
<field name="context">{'default_x_fc_delivery_method': 'shipping_partner'}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">