Split FSM into separate modules and switch customer to address book
This commit is contained in:
332
odoo/addons/dsrpt_address_book/models/dsrpt_contact.py
Normal file
332
odoo/addons/dsrpt_address_book/models/dsrpt_contact.py
Normal file
@@ -0,0 +1,332 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class Contact(models.Model):
|
||||
_name = 'dsrpt.contact'
|
||||
_description = 'Contact'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_rec_name = 'name'
|
||||
_order = 'create_date desc, name'
|
||||
|
||||
name = fields.Char(string='Name', required=True, tracking=True)
|
||||
display_name = fields.Char(string='Display Name') # Will be computed in realty_sales module
|
||||
active = fields.Boolean(string='Active', default=True, tracking=True)
|
||||
status = fields.Selection([
|
||||
('awaiting_qualification', 'Awaiting Qualification'),
|
||||
('not_customer', 'Not a Customer'),
|
||||
('supplier', 'Supplier'),
|
||||
('has_request', 'Has Request'),
|
||||
('has_contract', 'Has Contract'),
|
||||
('potential_investor', 'Potential Investor'),
|
||||
('cancelled', 'Cancelled')
|
||||
], string='Status', default='awaiting_qualification', tracking=True, help='Contact processing status for AI analysis')
|
||||
user_id = fields.Many2one('res.users', string='Responsible', tracking=True)
|
||||
source_id = fields.Many2one('contact.source', string='Source', tracking=True)
|
||||
qualification_date = fields.Datetime(string='Qualification Date', tracking=True, help='When contact was qualified')
|
||||
communication_ids = fields.One2many(
|
||||
'dsrpt.contact.communication',
|
||||
'contact_id',
|
||||
string='Communications'
|
||||
)
|
||||
note = fields.Text(string='Note')
|
||||
|
||||
# Relations
|
||||
event_ids = fields.One2many(
|
||||
'contact.event',
|
||||
'contact_id',
|
||||
string='Events'
|
||||
)
|
||||
# call_ids moved to dsrpt_calls module to avoid circular dependencies
|
||||
|
||||
# Computed fields
|
||||
event_count = fields.Integer(
|
||||
string='Events',
|
||||
compute='_compute_event_count'
|
||||
)
|
||||
phone_numbers = fields.Char(
|
||||
string='Phone Numbers',
|
||||
compute='_compute_phone_numbers',
|
||||
store=True
|
||||
)
|
||||
next_contact = fields.Datetime(
|
||||
string='Next Contact',
|
||||
tracking=True,
|
||||
help='Date of the nearest planned event'
|
||||
)
|
||||
# call_count moved to dsrpt_calls module
|
||||
|
||||
@api.depends('event_ids')
|
||||
def _compute_event_count(self):
|
||||
"""Counts the number of events"""
|
||||
for record in self:
|
||||
record.event_count = len(record.event_ids)
|
||||
|
||||
@api.depends('communication_ids.value', 'communication_ids.communication_type_id')
|
||||
def _compute_phone_numbers(self):
|
||||
"""Extracts all phone numbers for search functionality"""
|
||||
for record in self:
|
||||
phones = record.communication_ids.filtered(
|
||||
lambda c: c.communication_type_id.code == 'phone'
|
||||
).mapped('value')
|
||||
record.phone_numbers = ', '.join(phones) if phones else ''
|
||||
|
||||
def _update_next_contact(self, date_value=None):
|
||||
"""Update next_contact field with provided date"""
|
||||
for record in self:
|
||||
if date_value is not None:
|
||||
record.next_contact = date_value
|
||||
|
||||
# _compute_call_count moved to dsrpt_calls module
|
||||
|
||||
def action_view_events(self):
|
||||
"""Opens contact events"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'Events',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'contact.event',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('contact_id', '=', self.id)],
|
||||
'context': {'default_contact_id': self.id}
|
||||
}
|
||||
|
||||
def action_qualify(self):
|
||||
"""Action to trigger qualification for selected contacts"""
|
||||
qualified_count = 0
|
||||
no_calls_count = 0
|
||||
already_qualified_count = 0
|
||||
|
||||
for contact in self:
|
||||
# Skip if already qualified (status not 'awaiting_qualification')
|
||||
if contact.status != 'awaiting_qualification':
|
||||
already_qualified_count += 1
|
||||
continue
|
||||
|
||||
# Find last call with transcription for this contact
|
||||
last_call = self.env['dsrpt_calls.call'].search([
|
||||
('contact_id', '=', contact.id),
|
||||
('transcription', '!=', False)
|
||||
], order='date_start desc', limit=1)
|
||||
|
||||
if last_call:
|
||||
# Send on_need_qualification event directly to contact
|
||||
contact._event('on_need_qualification').notify(contact)
|
||||
qualified_count += 1
|
||||
else:
|
||||
no_calls_count += 1
|
||||
|
||||
# Build notification message
|
||||
message_parts = []
|
||||
if qualified_count:
|
||||
message_parts.append(f"{qualified_count} contacts queued for qualification")
|
||||
if no_calls_count:
|
||||
message_parts.append(f"{no_calls_count} contacts have no calls with transcription")
|
||||
if already_qualified_count:
|
||||
message_parts.append(f"{already_qualified_count} contacts already qualified")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Qualification Process',
|
||||
'message': '. '.join(message_parts) if message_parts else 'No contacts to qualify',
|
||||
'type': 'success' if qualified_count else 'info',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
def action_view_calls(self):
|
||||
"""Opens contact calls"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'Calls',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'dsrpt_calls.call',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('contact_id', '=', self.id)],
|
||||
'context': {'default_contact_id': self.id}
|
||||
}
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
# Найти предпочитаемый способ связи
|
||||
preferred = record.communication_ids.filtered('is_preferred')
|
||||
if preferred:
|
||||
name = f"{record.name} ({preferred[0].value})"
|
||||
else:
|
||||
name = record.name
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
def action_view_telegram_card(self):
|
||||
"""Open telegram card editing form"""
|
||||
self.ensure_one()
|
||||
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
|
||||
card_url = f"{base_url}/telegram/contact-card/{self.id}"
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': card_url,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def _process_name_suggestion_from_call(self, call_record):
|
||||
"""Queue job method to process name suggestion from call transcription"""
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
if not call_record.transcription:
|
||||
_logger.warning(f"No transcription available for call {call_record.id}")
|
||||
return
|
||||
|
||||
# Get AI collection and component
|
||||
collection = self.env['dsrpt.ai.collection'].search([], limit=1)
|
||||
if not collection:
|
||||
_logger.error("No AI collection found")
|
||||
return
|
||||
|
||||
work = collection.work_on(model_name='field.change.suggestion')
|
||||
ai_component = work.component(usage='field.change.suggestion')
|
||||
|
||||
# Get suggested name from AI
|
||||
suggested_name = ai_component.suggest_name_from_transcription(call_record.transcription)
|
||||
|
||||
# Only create suggestion if AI returned a valid name
|
||||
if suggested_name and suggested_name not in ['Не определено', 'Undefined', '', False, 'Unknown']:
|
||||
# Find telegram user for notification
|
||||
telegram_user = self._find_telegram_user_for_suggestions()
|
||||
|
||||
# Create field suggestion with AI result
|
||||
suggestion_vals = {
|
||||
'res_model': 'dsrpt.contact',
|
||||
'res_id': self.id,
|
||||
'field_name': 'name',
|
||||
'current_value': self.name,
|
||||
'suggested_value': suggested_name,
|
||||
'telegram_user_id': telegram_user.id if telegram_user else None
|
||||
}
|
||||
|
||||
suggestion = self.env['field.change.suggestion'].create(suggestion_vals)
|
||||
_logger.info(f"Created name suggestion {suggestion.id} with value '{suggested_name}' for contact {self.id}")
|
||||
|
||||
else:
|
||||
_logger.info(f"AI did not return valid name suggestion for contact {self.id}, skipping")
|
||||
|
||||
def _find_telegram_user_for_suggestions(self):
|
||||
"""Find telegram user for notifications"""
|
||||
telegram_user = None
|
||||
|
||||
# Priority: contact owner > first admin user
|
||||
if hasattr(self, 'user_id') and self.user_id:
|
||||
telegram_user = self.env['telegram.user'].search([
|
||||
('odoo_user_id', '=', self.user_id.id),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not telegram_user:
|
||||
# Find first active telegram admin user
|
||||
telegram_user = self.env['telegram.user'].search([
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
return telegram_user
|
||||
|
||||
def _process_note_suggestion_from_context(self, context_record):
|
||||
"""Queue job method to process note suggestion from contact history"""
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Get AI collection and component
|
||||
collection = self.env['dsrpt.ai.collection'].search([], limit=1)
|
||||
if not collection:
|
||||
_logger.error("No AI collection found")
|
||||
return
|
||||
|
||||
work = collection.work_on(model_name='field.change.suggestion')
|
||||
ai_component = work.component(usage='field.change.suggestion')
|
||||
|
||||
# Gather contact history for AI
|
||||
history = self._gather_contact_history_for_ai()
|
||||
|
||||
# Get suggested summary from AI
|
||||
suggested_summary = ai_component.suggest_summary_from_history(history)
|
||||
|
||||
# Only create suggestion if AI returned a valid summary
|
||||
if suggested_summary and suggested_summary not in ['Не определено', 'Undefined', '', False]:
|
||||
# Find telegram user for notification
|
||||
telegram_user = self._find_telegram_user_for_suggestions()
|
||||
|
||||
# Create field suggestion with AI result
|
||||
suggestion_vals = {
|
||||
'res_model': 'dsrpt.contact',
|
||||
'res_id': self.id,
|
||||
'field_name': 'note',
|
||||
'current_value': self.note or '',
|
||||
'suggested_value': suggested_summary,
|
||||
'telegram_user_id': telegram_user.id if telegram_user else None
|
||||
}
|
||||
|
||||
suggestion = self.env['field.change.suggestion'].create(suggestion_vals)
|
||||
_logger.info(f"Created note suggestion {suggestion.id} with summary for contact {self.id}")
|
||||
|
||||
else:
|
||||
_logger.info(f"AI did not return valid note suggestion for contact {self.id}, skipping")
|
||||
|
||||
def _gather_contact_history_for_ai(self):
|
||||
"""Gather contact history for summary generation"""
|
||||
history_parts = []
|
||||
|
||||
# Add contact basic info
|
||||
history_parts.append(f"Contact: {self.name}")
|
||||
|
||||
# Add communications
|
||||
for comm in self.communication_ids:
|
||||
history_parts.append(f"{comm.communication_type_id.name}: {comm.value}")
|
||||
|
||||
# Add recent calls if module is installed
|
||||
if 'dsrpt_calls.call' in self.env:
|
||||
calls = self.env['dsrpt_calls.call'].search([
|
||||
('contact_id', '=', self.id)
|
||||
], limit=5, order='date_start desc')
|
||||
for call in calls:
|
||||
history_parts.append(f"Call {call.date_start}: {call.duration} sec")
|
||||
if call.transcription:
|
||||
history_parts.append(f"Summary: {call.transcription[:200]}...")
|
||||
|
||||
return "\n".join(history_parts)
|
||||
|
||||
def recompute_next_contact_all(self):
|
||||
"""Clear next_contact for all contacts (simplified logic)"""
|
||||
all_contacts = self.env['dsrpt.contact'].search([])
|
||||
all_contacts._update_next_contact(None)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Reset Completed',
|
||||
'message': f'"Next Contact" cleared for {len(all_contacts)} contacts',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write to automatically set qualification_date when status changes"""
|
||||
# Check if status is being changed
|
||||
if 'status' in vals:
|
||||
for record in self:
|
||||
old_status = record.status
|
||||
new_status = vals['status']
|
||||
|
||||
# If changing FROM awaiting_qualification TO any other status
|
||||
if (old_status == 'awaiting_qualification' and
|
||||
new_status != 'awaiting_qualification' and
|
||||
not record.qualification_date):
|
||||
# Set qualification date automatically
|
||||
vals['qualification_date'] = fields.Datetime.now()
|
||||
|
||||
return super().write(vals)
|
||||
Reference in New Issue
Block a user