Split FSM into separate modules and switch customer to address book

This commit is contained in:
Ruslan Bakiev
2026-02-13 15:27:48 +07:00
parent 98a92286ce
commit dc58e1ffe4
46 changed files with 3125 additions and 152 deletions

View 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)