195 lines
8.0 KiB
Python
195 lines
8.0 KiB
Python
# -*- 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()
|