# -*- coding: utf-8 -*- import logging import re from odoo import models, fields, api, _ _logger = logging.getLogger(__name__) class SocialWhatsAppMessage(models.Model): _name = 'social.whatsapp.message' _description = 'Customer WhatsApp Message & Communication History' _order = 'timestamp desc, id desc' meta_message_id = fields.Char(string='WhatsApp Message ID (wamid)', index=True, copy=False) direction = fields.Selection([ ('inbound', 'Inbound (Customer to Business)'), ('outbound', 'Outbound (Business to Customer)'), ], string='Direction', default='inbound', required=True, index=True) from_phone = fields.Char(string='Sender Phone', required=True, index=True) to_phone = fields.Char(string='Recipient Phone') partner_id = fields.Many2one('res.partner', string='Customer / Partner', index=True) sale_order_id = fields.Many2one('sale.order', string='Identified Order', index=True) body = fields.Text(string='Message Text') message_type = fields.Char(string='Message Type', default='text') timestamp = fields.Datetime(string='Timestamp', default=fields.Datetime.now, required=True) raw_payload = fields.Text(string='Raw Payload', groups='dine360_meta_social.group_social_manager') status = fields.Selection([ ('received', 'Received'), ('processed', 'Processed'), ('replied', 'Replied'), ], string='Status', default='received', index=True) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) _sql_constraints = [ ('meta_msg_id_unique', 'unique(meta_message_id)', 'WhatsApp message ID must be unique to prevent duplicate processing.') ] @api.model def process_incoming_message(self, message_data, company=None): """ Processes an inbound message from WhatsApp Cloud API webhook: - Deduplicates using meta_message_id - Associates phone number with res.partner - Identifies existing order via text analysis or recent orders - Stores in communication history """ company = company or self.env.company msg_id = message_data.get('id') from_phone = message_data.get('from', '') text_body = message_data.get('text', {}).get('body', '') if message_data.get('type') == 'text' else '' msg_type = message_data.get('type', 'text') if not msg_id or not from_phone: _logger.warning("Invalid incoming WhatsApp message format: %s", message_data) return False # 1. Deduplication check existing = self.search([('meta_message_id', '=', msg_id)], limit=1) if existing: _logger.info("WhatsApp message %s already processed. Skipping duplicate.", msg_id) return existing # 2. Partner association (match by mobile or phone) wa_api = self.env['social.whatsapp.api'] cleaned_phone = wa_api.format_phone(from_phone) partner = self._find_partner_by_phone(cleaned_phone) # 3. Order identification (look for SO\d+ or check most recent order) order = self._identify_order(text_body, partner) record = self.create({ 'meta_message_id': msg_id, 'direction': 'inbound', 'from_phone': from_phone, 'to_phone': company.whatsapp_business_phone_number or '', 'partner_id': partner.id if partner else False, 'sale_order_id': order.id if order else False, 'body': text_body, 'message_type': msg_type, 'timestamp': fields.Datetime.now(), 'company_id': company.id, 'status': 'received', }) _logger.info("Processed inbound WhatsApp message from %s (Partner: %s, Order: %s)", from_phone, partner.name if partner else "Unknown", order.name if order else "None") return record @api.model def _find_partner_by_phone(self, phone): if not phone: return False digits = re.sub(r'\D', '', phone) if not digits: return False # 1. Search by phone_sanitized partner = self.env['res.partner'].search([ '|', '|', ('phone_sanitized', '=', f"+{digits}"), ('phone_sanitized', '=', digits), ('phone_sanitized', 'like', digits[-10:] if len(digits) >= 10 else digits) ], limit=1) if partner: return partner # 2. Search partners with phone/mobile and compare normalized digits potential_partners = self.env['res.partner'].search([ '|', ('mobile', '!=', False), ('phone', '!=', False) ], limit=200) for p in potential_partners: p_mob = re.sub(r'\D', '', p.mobile or '') p_pho = re.sub(r'\D', '', p.phone or '') target = digits[-8:] if len(digits) >= 8 else digits if (p_mob and target in p_mob) or (p_pho and target in p_pho): return p return False @api.model def _identify_order(self, text, partner): if not text: return False # Try matching order reference like SO1234 or S01234 or order #1234 match = re.search(r'\b(SO\d+|S\d{4,})\b', text, re.IGNORECASE) if match: order_name = match.group(1).upper() order = self.env['sale.order'].search([('name', '=ilike', order_name)], limit=1) if order: return order # If customer asks about order status and has a recent order order_keywords = ['where is my order', 'order status', 'my order', 'track order', 'delivery status'] if partner and any(kw in text.lower() for kw in order_keywords): recent_order = self.env['sale.order'].search([ ('partner_id', '=', partner.id) ], order='date_order desc', limit=1) if recent_order: return recent_order return False