diff --git a/odoo_whatsapp_hub/models/whatsapp_conversation.py b/odoo_whatsapp_hub/models/whatsapp_conversation.py new file mode 100644 index 0000000..093058d --- /dev/null +++ b/odoo_whatsapp_hub/models/whatsapp_conversation.py @@ -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 diff --git a/odoo_whatsapp_hub/wizards/mass_whatsapp.py b/odoo_whatsapp_hub/wizards/mass_whatsapp.py new file mode 100644 index 0000000..42f9448 --- /dev/null +++ b/odoo_whatsapp_hub/wizards/mass_whatsapp.py @@ -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', + } + } diff --git a/odoo_whatsapp_hub/wizards/send_whatsapp.py b/odoo_whatsapp_hub/wizards/send_whatsapp.py new file mode 100644 index 0000000..68635fa --- /dev/null +++ b/odoo_whatsapp_hub/wizards/send_whatsapp.py @@ -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'}