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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user