odex25_standard/odex25_base/tour_genius/models/reminder.py

279 lines
10 KiB
Python

# -*- coding: utf-8 -*-
"""
Reminder Model.
Automated reminders for incomplete training.
"""
from odoo import models, fields, api, _
from datetime import timedelta
import logging
_logger = logging.getLogger(__name__)
class GeniusReminder(models.Model):
_name = 'genius.reminder'
_description = 'Training Reminder'
_order = 'scheduled_date'
_inherit = ['mail.thread']
name = fields.Char(
string='Subject',
required=True,
default=_("Training Reminder")
)
user_id = fields.Many2one(
'res.users',
string='User',
required=True,
ondelete='cascade',
index=True
)
# What to remind about
reminder_type = fields.Selection([
('incomplete_topic', 'Incomplete Topic'),
('incomplete_plan', 'Incomplete Plan'),
('new_topic', 'New Topic Available'),
('quiz_due', 'Quiz Due'),
('streak', 'Keep Your Streak'),
('custom', 'Custom Message'),
], string='Type', required=True, default='incomplete_topic')
topic_id = fields.Many2one(
'genius.topic',
string='Topic',
help='Related topic (if applicable)'
)
plan_id = fields.Many2one(
'genius.plan',
string='Plan',
help='Related plan (if applicable)'
)
# Scheduling
scheduled_date = fields.Datetime(
string='Scheduled For',
required=True,
default=lambda self: fields.Datetime.now() + timedelta(days=1)
)
# Status
state = fields.Selection([
('pending', 'Pending'),
('sent', 'Sent'),
('cancelled', 'Cancelled'),
('failed', 'Failed'),
], string='Status', default='pending', required=True, index=True)
sent_date = fields.Datetime(string='Sent At')
# Message Content
message_body = fields.Html(
string='Message',
compute='_compute_message_body',
store=True
)
custom_message = fields.Html(
string='Custom Message',
help='Custom message for custom reminder type'
)
# Repeat Settings
is_recurring = fields.Boolean(
string='Recurring',
default=False
)
recurrence_interval = fields.Integer(
string='Repeat Every (days)',
default=7
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company
)
# -------------------------------------------------------------------------
# Computed Fields
# -------------------------------------------------------------------------
@api.depends('reminder_type', 'topic_id', 'plan_id', 'user_id', 'custom_message')
def _compute_message_body(self):
for rec in self:
if rec.reminder_type == 'custom' and rec.custom_message:
rec.message_body = rec.custom_message
elif rec.reminder_type == 'incomplete_topic' and rec.topic_id:
rec.message_body = _("""
<p>Hi %(user)s,</p>
<p>You haven't completed the training topic: <strong>%(topic)s</strong></p>
<p>Continue your learning journey today!</p>
""") % {
'user': rec.user_id.name,
'topic': rec.topic_id.name,
}
elif rec.reminder_type == 'incomplete_plan' and rec.plan_id:
rec.message_body = _("""
<p>Hi %(user)s,</p>
<p>You have an incomplete training plan: <strong>%(plan)s</strong></p>
<p>Keep up your progress!</p>
""") % {
'user': rec.user_id.name,
'plan': rec.plan_id.name,
}
elif rec.reminder_type == 'streak':
rec.message_body = _("""
<p>Hi %(user)s,</p>
<p>Don't break your learning streak!</p>
<p>Complete a quick topic today to keep your momentum.</p>
""") % {'user': rec.user_id.name}
elif rec.reminder_type == 'new_topic' and rec.topic_id:
rec.message_body = _("""
<p>Hi %(user)s,</p>
<p>A new training topic is available: <strong>%(topic)s</strong></p>
<p>Check it out!</p>
""") % {
'user': rec.user_id.name,
'topic': rec.topic_id.name,
}
elif rec.reminder_type == 'quiz_due' and rec.topic_id:
rec.message_body = _("""
<p>Hi %(user)s,</p>
<p>You have completed the tour <strong>%(topic)s</strong>, but haven't passed the quiz yet.</p>
<p>Take the quiz now to get Certified!</p>
""") % {
'user': rec.user_id.name,
'topic': rec.topic_id.name,
}
else:
rec.message_body = _("<p>You have a training reminder.</p>")
# -------------------------------------------------------------------------
# Actions
# -------------------------------------------------------------------------
def action_send(self):
"""Send the reminder now"""
for rec in self:
rec._send_reminder()
def action_cancel(self):
"""Cancel the reminder"""
self.write({'state': 'cancelled'})
def _send_reminder(self):
"""Actually send the reminder email"""
self.ensure_one()
# Validate email before attempting to send
if not self.user_id.email:
_logger.warning("Cannot send reminder %s: user %s has no email",
self.id, self.user_id.name)
self.write({'state': 'failed'})
return False
try:
template = self.env.ref('tour_genius.email_template_training_reminder', raise_if_not_found=False)
if template:
template.send_mail(self.id, force_send=True)
else:
# Fallback: send simple email
self.env['mail.mail'].create({
'subject': self.name,
'body_html': self.message_body,
'email_to': self.user_id.email,
'auto_delete': True,
}).send()
self.write({
'state': 'sent',
'sent_date': fields.Datetime.now(),
})
# Handle recurrence
if self.is_recurring:
self.copy({
'scheduled_date': fields.Datetime.now() + timedelta(days=self.recurrence_interval),
'state': 'pending',
'sent_date': False,
})
except Exception as e:
self.write({'state': 'failed'})
raise
# -------------------------------------------------------------------------
# Scheduled Actions
# -------------------------------------------------------------------------
@api.model
def _cron_send_reminders(self):
"""Cron job to send pending reminders"""
pending = self.search([
('state', '=', 'pending'),
('scheduled_date', '<=', fields.Datetime.now()),
])
for reminder in pending:
try:
reminder._send_reminder()
except Exception as e:
_logger.exception("Failed to send reminder %s: %s", reminder.id, str(e))
# State already marked as 'failed' in _send_reminder
@api.model
def _cron_create_auto_reminders(self):
"""Create automatic reminders for users with incomplete training or pending quizzes"""
# 1. Stale Progress (Incomplete > 3 days)
three_days_ago = fields.Datetime.now() - timedelta(days=3)
stale_progress = self.env['genius.progress'].search([
('state', '=', 'in_progress'),
('date_started', '<=', three_days_ago),
])
for progress in stale_progress:
# Check if reminder already exists
existing = self.search([
('user_id', '=', progress.user_id.id),
('topic_id', '=', progress.topic_id.id),
('reminder_type', '=', 'incomplete_topic'),
('state', '=', 'pending'),
], limit=1)
if not existing:
self.create({
'name': _("Continue your training: %s") % progress.topic_id.name,
'user_id': progress.user_id.id,
'reminder_type': 'incomplete_topic',
'topic_id': progress.topic_id.id,
'plan_id': progress.plan_id.id if progress.plan_id else False,
'scheduled_date': fields.Datetime.now() + timedelta(hours=24),
})
# 2. Quiz Reminders (Completed > 1 day but Not Verified, and Quiz exists)
one_day_ago = fields.Datetime.now() - timedelta(days=1)
unverified_done = self.env['genius.progress'].search([
('state', '=', 'done'),
('date_completed', '<=', one_day_ago),
])
for progress in unverified_done:
# Check if Topic has a Quiz
if progress.topic_id.quiz_id:
# Check if already reminded
existing = self.search([
('user_id', '=', progress.user_id.id),
('topic_id', '=', progress.topic_id.id),
('reminder_type', '=', 'quiz_due'),
('state', 'in', ['pending', 'sent']), # Don't spam if sent
], limit=1)
if not existing:
self.create({
'name': _("Get Certified: %s") % progress.topic_id.name,
'user_id': progress.user_id.id,
'reminder_type': 'quiz_due',
'topic_id': progress.topic_id.id,
'scheduled_date': fields.Datetime.now() + timedelta(hours=24),
})