141 lines
5.9 KiB
Python
141 lines
5.9 KiB
Python
# -*- 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
|