feat(odoo): add WhatsApp conversation model
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
139
odoo_whatsapp_hub/models/whatsapp_conversation.py
Normal file
139
odoo_whatsapp_hub/models/whatsapp_conversation.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WhatsAppConversation(models.Model):
|
||||||
|
_name = 'whatsapp.conversation'
|
||||||
|
_description = 'WhatsApp Conversation'
|
||||||
|
_order = 'last_message_at desc'
|
||||||
|
_rec_name = 'display_name'
|
||||||
|
|
||||||
|
external_id = fields.Char(string='ID Externo', index=True)
|
||||||
|
account_id = fields.Many2one(
|
||||||
|
'whatsapp.account',
|
||||||
|
string='Cuenta WhatsApp',
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
partner_id = fields.Many2one(
|
||||||
|
'res.partner',
|
||||||
|
string='Contacto',
|
||||||
|
ondelete='set null',
|
||||||
|
)
|
||||||
|
phone_number = fields.Char(string='Teléfono', required=True, index=True)
|
||||||
|
contact_name = fields.Char(string='Nombre del Contacto')
|
||||||
|
status = fields.Selection([
|
||||||
|
('bot', 'Bot'),
|
||||||
|
('waiting', 'En Espera'),
|
||||||
|
('active', 'Activa'),
|
||||||
|
('resolved', 'Resuelta'),
|
||||||
|
], string='Estado', default='bot')
|
||||||
|
assigned_user_id = fields.Many2one(
|
||||||
|
'res.users',
|
||||||
|
string='Agente Asignado',
|
||||||
|
)
|
||||||
|
last_message_at = fields.Datetime(string='Último Mensaje')
|
||||||
|
last_message_preview = fields.Char(
|
||||||
|
string='Último Mensaje',
|
||||||
|
compute='_compute_last_message',
|
||||||
|
)
|
||||||
|
message_ids = fields.One2many(
|
||||||
|
'whatsapp.message',
|
||||||
|
'conversation_id',
|
||||||
|
string='Mensajes',
|
||||||
|
)
|
||||||
|
message_count = fields.Integer(
|
||||||
|
string='Mensajes',
|
||||||
|
compute='_compute_message_count',
|
||||||
|
)
|
||||||
|
display_name = fields.Char(
|
||||||
|
string='Nombre',
|
||||||
|
compute='_compute_display_name',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
unread_count = fields.Integer(
|
||||||
|
string='No Leídos',
|
||||||
|
compute='_compute_unread_count',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('partner_id', 'contact_name', 'phone_number')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for conv in self:
|
||||||
|
if conv.partner_id:
|
||||||
|
conv.display_name = conv.partner_id.name
|
||||||
|
elif conv.contact_name:
|
||||||
|
conv.display_name = conv.contact_name
|
||||||
|
else:
|
||||||
|
conv.display_name = conv.phone_number
|
||||||
|
|
||||||
|
@api.depends('message_ids')
|
||||||
|
def _compute_message_count(self):
|
||||||
|
for conv in self:
|
||||||
|
conv.message_count = len(conv.message_ids)
|
||||||
|
|
||||||
|
@api.depends('message_ids.content')
|
||||||
|
def _compute_last_message(self):
|
||||||
|
for conv in self:
|
||||||
|
last_msg = conv.message_ids[:1]
|
||||||
|
if last_msg:
|
||||||
|
content = last_msg.content or ''
|
||||||
|
conv.last_message_preview = content[:50] + '...' if len(content) > 50 else content
|
||||||
|
else:
|
||||||
|
conv.last_message_preview = ''
|
||||||
|
|
||||||
|
@api.depends('message_ids.is_read')
|
||||||
|
def _compute_unread_count(self):
|
||||||
|
for conv in self:
|
||||||
|
conv.unread_count = len(conv.message_ids.filtered(
|
||||||
|
lambda m: m.direction == 'inbound' and not m.is_read
|
||||||
|
))
|
||||||
|
|
||||||
|
def action_open_chat(self):
|
||||||
|
"""Open chat view for this conversation"""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': self.display_name,
|
||||||
|
'res_model': 'whatsapp.conversation',
|
||||||
|
'res_id': self.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_mark_resolved(self):
|
||||||
|
"""Mark conversation as resolved"""
|
||||||
|
self.write({'status': 'resolved'})
|
||||||
|
|
||||||
|
def action_assign_to_me(self):
|
||||||
|
"""Assign conversation to current user"""
|
||||||
|
self.write({
|
||||||
|
'assigned_user_id': self.env.user.id,
|
||||||
|
'status': 'active',
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def find_or_create_by_phone(self, phone, account_id, contact_name=None):
|
||||||
|
"""Find or create conversation by phone number"""
|
||||||
|
conversation = self.search([
|
||||||
|
('phone_number', '=', phone),
|
||||||
|
('account_id', '=', account_id),
|
||||||
|
('status', '!=', 'resolved'),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not conversation:
|
||||||
|
partner = self.env['res.partner'].search([
|
||||||
|
'|',
|
||||||
|
('phone', 'ilike', phone[-10:]),
|
||||||
|
('mobile', 'ilike', phone[-10:]),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
conversation = self.create({
|
||||||
|
'phone_number': phone,
|
||||||
|
'account_id': account_id,
|
||||||
|
'contact_name': contact_name,
|
||||||
|
'partner_id': partner.id if partner else False,
|
||||||
|
})
|
||||||
|
|
||||||
|
return conversation
|
||||||
95
odoo_whatsapp_hub/wizards/mass_whatsapp.py
Normal file
95
odoo_whatsapp_hub/wizards/mass_whatsapp.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class WhatsAppMassWizard(models.TransientModel):
|
||||||
|
_name = 'whatsapp.mass.wizard'
|
||||||
|
_description = 'Send Mass WhatsApp Message'
|
||||||
|
|
||||||
|
account_id = fields.Many2one(
|
||||||
|
'whatsapp.account',
|
||||||
|
string='Cuenta WhatsApp',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env['whatsapp.account'].get_default_account(),
|
||||||
|
)
|
||||||
|
partner_ids = fields.Many2many(
|
||||||
|
'res.partner',
|
||||||
|
string='Contactos',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
content = fields.Text(string='Mensaje', required=True)
|
||||||
|
use_template = fields.Boolean(string='Usar Variables')
|
||||||
|
total_count = fields.Integer(
|
||||||
|
string='Total Contactos',
|
||||||
|
compute='_compute_stats',
|
||||||
|
)
|
||||||
|
valid_count = fields.Integer(
|
||||||
|
string='Con Teléfono',
|
||||||
|
compute='_compute_stats',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('partner_ids')
|
||||||
|
def _compute_stats(self):
|
||||||
|
for wizard in self:
|
||||||
|
wizard.total_count = len(wizard.partner_ids)
|
||||||
|
wizard.valid_count = len(wizard.partner_ids.filtered(
|
||||||
|
lambda p: p.mobile or p.phone
|
||||||
|
))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
res = super().default_get(fields_list)
|
||||||
|
active_ids = self.env.context.get('active_ids', [])
|
||||||
|
active_model = self.env.context.get('active_model')
|
||||||
|
|
||||||
|
if active_model == 'res.partner' and active_ids:
|
||||||
|
res['partner_ids'] = [(6, 0, active_ids)]
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def action_send(self):
|
||||||
|
"""Send WhatsApp to all selected partners"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
if not self.account_id:
|
||||||
|
raise UserError('Seleccione una cuenta de WhatsApp')
|
||||||
|
|
||||||
|
sent_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
for partner in self.partner_ids:
|
||||||
|
phone = partner.mobile or partner.phone
|
||||||
|
if not phone:
|
||||||
|
failed_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = self.content
|
||||||
|
if self.use_template:
|
||||||
|
content = content.replace('{{name}}', partner.name or '')
|
||||||
|
content = content.replace('{{email}}', partner.email or '')
|
||||||
|
|
||||||
|
conversation = self.env['whatsapp.conversation'].find_or_create_by_phone(
|
||||||
|
phone=phone,
|
||||||
|
account_id=self.account_id.id,
|
||||||
|
contact_name=partner.name,
|
||||||
|
)
|
||||||
|
conversation.partner_id = partner
|
||||||
|
|
||||||
|
self.env['whatsapp.message'].send_message(
|
||||||
|
conversation_id=conversation.id,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
sent_count += 1
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
failed_count += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'message': f'Enviados: {sent_count}, Fallidos: {failed_count}',
|
||||||
|
'type': 'success' if failed_count == 0 else 'warning',
|
||||||
|
}
|
||||||
|
}
|
||||||
78
odoo_whatsapp_hub/wizards/send_whatsapp.py
Normal file
78
odoo_whatsapp_hub/wizards/send_whatsapp.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class WhatsAppSendWizard(models.TransientModel):
|
||||||
|
_name = 'whatsapp.send.wizard'
|
||||||
|
_description = 'Send WhatsApp Message'
|
||||||
|
|
||||||
|
partner_id = fields.Many2one('res.partner', string='Contacto')
|
||||||
|
phone = fields.Char(string='Teléfono', required=True)
|
||||||
|
account_id = fields.Many2one(
|
||||||
|
'whatsapp.account',
|
||||||
|
string='Cuenta WhatsApp',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env['whatsapp.account'].get_default_account(),
|
||||||
|
)
|
||||||
|
message_type = fields.Selection([
|
||||||
|
('text', 'Texto'),
|
||||||
|
('image', 'Imagen'),
|
||||||
|
('document', 'Documento'),
|
||||||
|
], string='Tipo', default='text', required=True)
|
||||||
|
content = fields.Text(string='Mensaje', required=True)
|
||||||
|
media_url = fields.Char(string='URL del Archivo')
|
||||||
|
attachment_id = fields.Many2one('ir.attachment', string='Adjunto')
|
||||||
|
res_model = fields.Char(string='Modelo Origen')
|
||||||
|
res_id = fields.Integer(string='ID Origen')
|
||||||
|
|
||||||
|
@api.onchange('attachment_id')
|
||||||
|
def _onchange_attachment_id(self):
|
||||||
|
if self.attachment_id:
|
||||||
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||||
|
self.media_url = f"{base_url}/web/content/{self.attachment_id.id}"
|
||||||
|
|
||||||
|
def action_send(self):
|
||||||
|
"""Send the WhatsApp message"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
if not self.account_id:
|
||||||
|
raise UserError('Seleccione una cuenta de WhatsApp')
|
||||||
|
|
||||||
|
conversation = self.env['whatsapp.conversation'].find_or_create_by_phone(
|
||||||
|
phone=self.phone,
|
||||||
|
account_id=self.account_id.id,
|
||||||
|
contact_name=self.partner_id.name if self.partner_id else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.partner_id:
|
||||||
|
conversation.partner_id = self.partner_id
|
||||||
|
|
||||||
|
self.env['whatsapp.message'].send_message(
|
||||||
|
conversation_id=conversation.id,
|
||||||
|
content=self.content,
|
||||||
|
message_type=self.message_type,
|
||||||
|
media_url=self.media_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'message': 'Mensaje enviado correctamente',
|
||||||
|
'type': 'success',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_send_and_open(self):
|
||||||
|
"""Send message and open conversation"""
|
||||||
|
self.action_send()
|
||||||
|
|
||||||
|
conversation = self.env['whatsapp.conversation'].search([
|
||||||
|
('phone_number', '=', self.phone),
|
||||||
|
('account_id', '=', self.account_id.id),
|
||||||
|
], limit=1, order='id desc')
|
||||||
|
|
||||||
|
if conversation:
|
||||||
|
return conversation.action_open_chat()
|
||||||
|
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
Reference in New Issue
Block a user