# -*- coding: utf-8 -*- import logging from odoo import models, fields, api, _ _logger = logging.getLogger(__name__) class SocialWhatsAppNotification(models.Model): _name = 'social.whatsapp.notification' _description = 'WhatsApp Order Notification Log' _order = 'create_date desc, id desc' sale_order_id = fields.Many2one('sale.order', string='Sale Order', ondelete='cascade', index=True) picking_id = fields.Many2one('stock.picking', string='Stock Delivery', ondelete='set null') partner_id = fields.Many2one('res.partner', string='Customer', related='sale_order_id.partner_id', store=True) phone_number = fields.Char(string='Recipient Phone', required=True) event_type = fields.Selection([ ('order_confirmed', 'Order Confirmed'), ('order_processing', 'Order Processing'), ('order_shipped', 'Order Shipped'), ('order_delivered', 'Order Delivered'), ('customer_enquiry', 'Customer Enquiry'), ], string='Event', required=True, index=True) template_id = fields.Many2one('social.whatsapp.template', string='Template Mapping') template_name = fields.Char(string='Meta Template Name') parameters_sent = fields.Text(string='Parameters Sent') state = fields.Selection([ ('pending', 'Pending Send'), ('sent', 'Sent'), ('delivered', 'Delivered'), ('read', 'Read'), ('failed', 'Failed'), ], string='Status', default='pending', index=True, required=True) meta_message_id = fields.Char(string='WhatsApp Message ID (wamid)', index=True, copy=False) error_message = fields.Text(string='Error Message') retry_count = fields.Integer(string='Retry Count', default=0) max_retries = fields.Integer(string='Max Retries', default=3) last_attempt = fields.Datetime(string='Last Attempt') company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) @api.model def send_order_notification(self, order, event_type, picking=None): """ Safe, non-blocking notification dispatcher for sale order events. Enforces deduplication, template verification, phone validation, and asynchronous retry. """ if not order or not order.exists(): return False company = order.company_id or self.env.company if not company.whatsapp_enabled: return False # 1. Deduplication check: prevent sending the exact same event notification twice for this order duplicate = self.search([ ('sale_order_id', '=', order.id), ('event_type', '=', event_type), ('state', 'in', ('pending', 'sent', 'delivered', 'read')) ], limit=1) if duplicate: _logger.info("WhatsApp notification for %s event '%s' already sent (ID: %s). Skipping duplicate.", order.name, event_type, duplicate.id) return duplicate # 2. Template verification: only send if approved template is configured and active template = self.env['social.whatsapp.template'].search([ ('event_type', '=', event_type), ('company_id', '=', company.id), ('is_active', '=', True) ], limit=1) if not template: _logger.warning("No active WhatsApp template configured for event '%s' in company %s. Notification skipped.", event_type, company.name) return False # 3. Recipient phone number extraction & validation raw_phone = order.partner_id.mobile or order.partner_id.phone if not raw_phone: _logger.warning("No phone number found on customer %s for order %s.", order.partner_id.name, order.name) self.create({ 'sale_order_id': order.id, 'picking_id': picking.id if picking else False, 'partner_id': order.partner_id.id, 'phone_number': 'MISSING', 'event_type': event_type, 'template_id': template.id, 'template_name': template.template_name, 'state': 'failed', 'error_message': _("Customer has no phone or mobile number."), 'company_id': company.id, 'last_attempt': fields.Datetime.now(), }) return False wa_api = self.env['social.whatsapp.api'] formatted_phone = wa_api.format_phone(raw_phone, partner=order.partner_id) parameters = template.build_parameters(order, picking=picking) # 4. Create pending notification record notification = self.create({ 'sale_order_id': order.id, 'picking_id': picking.id if picking else False, 'phone_number': formatted_phone, 'event_type': event_type, 'template_id': template.id, 'template_name': template.template_name, 'parameters_sent': ", ".join(parameters), 'state': 'pending', 'company_id': company.id, 'last_attempt': fields.Datetime.now(), }) # 5. Dispatch via WhatsApp Cloud API try: result = wa_api.send_template_message( company=company, to_phone=formatted_phone, template_name=template.template_name, language_code=template.language_code, parameters=parameters ) now = fields.Datetime.now() if result.get('success'): notification.write({ 'state': 'sent', 'meta_message_id': result.get('message_id'), 'error_message': False, 'last_attempt': now, }) else: notification.write({ 'state': 'failed', 'error_message': result.get('error'), 'retry_count': 1, 'last_attempt': now, }) except Exception as e: _logger.exception("Unexpected error sending WhatsApp notification: %s", e) notification.write({ 'state': 'failed', 'error_message': str(e), 'retry_count': 1, 'last_attempt': fields.Datetime.now(), }) return notification def action_retry(self): """Action for administrators to retry failed notifications.""" wa_api = self.env['social.whatsapp.api'] for rec in self: if rec.state != 'failed': continue if not rec.template_id: rec.error_message = _("Template mapping no longer exists.") continue order = rec.sale_order_id params = rec.template_id.build_parameters(order, picking=rec.picking_id) result = wa_api.send_template_message( company=rec.company_id, to_phone=rec.phone_number, template_name=rec.template_id.template_name, language_code=rec.template_id.language_code, parameters=params ) now = fields.Datetime.now() if result.get('success'): rec.write({ 'state': 'sent', 'meta_message_id': result.get('message_id'), 'error_message': False, 'last_attempt': now, }) else: rec.write({ 'retry_count': rec.retry_count + 1, 'error_message': result.get('error'), 'last_attempt': now, }) return True @api.model def retry_failed_notifications_cron(self, limit=20): """Cron job to automatically retry failed notifications.""" records = self.search([ ('state', '=', 'failed'), ('retry_count', '<', 3), ], limit=limit) if records: _logger.info("Retrying %d failed WhatsApp notifications...", len(records)) records.action_retry()