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:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Configurator',
|
'name': 'Fusion Plating — Configurator',
|
||||||
'version': '19.0.7.1.0',
|
'version': '19.0.7.2.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -144,6 +144,78 @@ class SaleOrder(models.Model):
|
|||||||
])
|
])
|
||||||
rec.x_fc_wo_completion = '%d/%d' % (done, total) if total else '0/0'
|
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',
|
@api.depends('invoice_ids.amount_total', 'invoice_ids.state',
|
||||||
'invoice_ids.move_type')
|
'invoice_ids.move_type')
|
||||||
def _compute_invoiced_amount(self):
|
def _compute_invoiced_amount(self):
|
||||||
|
|||||||
@@ -177,6 +177,69 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</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', '<=', context_today()), ('state', 'in', ('draft', 'sent'))]"/>
|
||||||
|
<filter name="expired" string="Expired"
|
||||||
|
domain="[('validity_date', '<', 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 ===== -->
|
<!-- ===== Search view for Fusion Plating SO list ===== -->
|
||||||
<record id="view_sale_order_search_fp" model="ir.ui.view">
|
<record id="view_sale_order_search_fp" model="ir.ui.view">
|
||||||
<field name="name">sale.order.search.fp</field>
|
<field name="name">sale.order.search.fp</field>
|
||||||
@@ -222,7 +285,8 @@
|
|||||||
<field name="view_mode">list,form,kanban</field>
|
<field name="view_mode">list,form,kanban</field>
|
||||||
<field name="domain">[('state', 'in', ('draft', 'sent'))]</field>
|
<field name="domain">[('state', 'in', ('draft', 'sent'))]</field>
|
||||||
<field name="view_ids" eval="[(5, 0, 0),
|
<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="context">{'default_x_fc_delivery_method': 'shipping_partner'}</field>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
|||||||
Reference in New Issue
Block a user