add membership_expiry_reminder
This commit is contained in:
parent
de0520d5a8
commit
9376d03d9d
8 changed files with 256 additions and 0 deletions
3
membership_expiry_reminder/__init__.py
Normal file
3
membership_expiry_reminder/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
17
membership_expiry_reminder/__manifest__.py
Normal file
17
membership_expiry_reminder/__manifest__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Membership Expiry Reminder",
|
||||
"summary": "Sends an automated email reminder to members approaching their "
|
||||
"membership expiry date, with a link to renew online.",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "AGPL-3",
|
||||
"author": "Custom",
|
||||
"depends": ["membership", "website_sale"],
|
||||
"data": [
|
||||
"data/mail_template.xml",
|
||||
"data/ir_cron.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
17
membership_expiry_reminder/data/ir_cron.xml
Normal file
17
membership_expiry_reminder/data/ir_cron.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="ir_cron_membership_expiry_reminder" model="ir.cron">
|
||||
<field name="name">Membership: Send Expiry Reminder Emails</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_send_membership_expiry_reminders()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
81
membership_expiry_reminder/data/mail_template.xml
Normal file
81
membership_expiry_reminder/data/mail_template.xml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="email_template_membership_expiry" model="mail.template">
|
||||
<field name="name">Membership: Renewal Reminder</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="subject">Reminder: your membership expires soon</field>
|
||||
<field name="email_from">{{ (object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.id }}</field>
|
||||
<field name="lang">{{ object.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin:0px;padding:0px;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table style="width:100%;max-width:600px;margin:auto;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:24px 24px 8px;background:#f8f8f8;border-bottom:2px solid #e0e0e0;">
|
||||
<h2 style="margin:0;color:#333;">
|
||||
Dear <t t-out="object.name or ''">Member Name</t>,
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:24px;">
|
||||
<p style="color:#555;font-size:15px;line-height:1.6;">
|
||||
This is a friendly reminder that your membership with
|
||||
<strong t-out="object.company_id.name or ''">Your Organisation</strong>
|
||||
is due to expire on
|
||||
<strong t-out="format_date(object.membership_stop) or ''">expiry date</strong>.
|
||||
</p>
|
||||
<p style="color:#555;font-size:15px;line-height:1.6;">
|
||||
To continue enjoying your membership benefits without interruption,
|
||||
please renew your membership before that date.
|
||||
</p>
|
||||
|
||||
<t t-set="last_line" t-value="object.member_lines and object.member_lines[0] or False"/>
|
||||
<t t-if="last_line and last_line.membership_id and last_line.membership_id.product_tmpl_id.website_url">
|
||||
<t t-set="product_url" t-value="object.get_base_url() + last_line.membership_id.product_tmpl_id.website_url"/>
|
||||
<p style="text-align:center;margin:32px 0;">
|
||||
<a t-att-href="product_url"
|
||||
style="background:#875A7B;color:#fff;padding:14px 32px;border-radius:4px;
|
||||
text-decoration:none;font-size:16px;font-weight:bold;display:inline-block;">
|
||||
Renew my membership
|
||||
</a>
|
||||
</p>
|
||||
<p style="color:#888;font-size:13px;text-align:center;">
|
||||
Or copy this link into your browser:<br/>
|
||||
<a t-att-href="product_url" style="color:#875A7B;">
|
||||
<t t-out="product_url"/>
|
||||
</a>
|
||||
</p>
|
||||
</t>
|
||||
|
||||
<p style="color:#555;font-size:15px;line-height:1.6;margin-top:24px;">
|
||||
If you have any questions, please do not hesitate to contact us.
|
||||
</p>
|
||||
<p style="color:#555;font-size:15px;">
|
||||
Kind regards,<br/>
|
||||
<strong t-out="object.company_id.name or ''">Your Organisation</strong>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:16px 24px;background:#f8f8f8;border-top:1px solid #e0e0e0;
|
||||
color:#aaa;font-size:12px;text-align:center;">
|
||||
<t t-out="object.company_id.name or ''">Your Organisation</t>
|
||||
<t t-if="object.company_id.street">
|
||||
• <t t-out="object.company_id.street"/>
|
||||
</t>
|
||||
<t t-if="object.company_id.email">
|
||||
• <t t-out="object.company_id.email"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
3
membership_expiry_reminder/models/__init__.py
Normal file
3
membership_expiry_reminder/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import res_config_settings, res_partner
|
||||
15
membership_expiry_reminder/models/res_config_settings.py
Normal file
15
membership_expiry_reminder/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
membership_expiry_reminder_days = fields.Integer(
|
||||
string="Days before expiry to send reminder",
|
||||
default=30,
|
||||
config_parameter="membership_expiry_reminder.days_before_expiry",
|
||||
help="Number of days before the membership end date to send the renewal "
|
||||
"reminder email. Set to 0 to disable automatic reminders.",
|
||||
)
|
||||
88
membership_expiry_reminder/models/res_partner.py
Normal file
88
membership_expiry_reminder/models/res_partner.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
membership_expiry_reminder_sent = fields.Boolean(
|
||||
string="Expiry Reminder Sent",
|
||||
default=False,
|
||||
copy=False,
|
||||
help="Checked when the membership renewal reminder email has been sent "
|
||||
"for the current membership period. Reset automatically when the "
|
||||
"membership is renewed.",
|
||||
)
|
||||
|
||||
def _cron_send_membership_expiry_reminders(self):
|
||||
"""Daily cron: find members whose membership expires within the next N days
|
||||
and who have not yet received a reminder. This catches up on any members
|
||||
that were missed on previous runs."""
|
||||
days = int(
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("membership_expiry_reminder.days_before_expiry", 30)
|
||||
)
|
||||
if not days:
|
||||
_logger.info(
|
||||
"membership_expiry_reminder: disabled (days_before_expiry = 0)"
|
||||
)
|
||||
return
|
||||
|
||||
template = self.env.ref(
|
||||
"membership_expiry_reminder.email_template_membership_expiry",
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not template:
|
||||
_logger.warning(
|
||||
"membership_expiry_reminder: email template not found, skipping."
|
||||
)
|
||||
return
|
||||
|
||||
today = fields.Date.today()
|
||||
deadline = today + timedelta(days=days)
|
||||
|
||||
partners = self.search(
|
||||
[
|
||||
("membership_state", "=", "paid"),
|
||||
("membership_stop", ">=", today),
|
||||
("membership_stop", "<=", deadline),
|
||||
("membership_expiry_reminder_sent", "=", False),
|
||||
("email", "!=", False),
|
||||
]
|
||||
)
|
||||
|
||||
_logger.info(
|
||||
"membership_expiry_reminder: sending reminders to %d partner(s) "
|
||||
"(expiry between today and %s, i.e. within %d days)",
|
||||
len(partners),
|
||||
deadline,
|
||||
days,
|
||||
)
|
||||
|
||||
for partner in partners:
|
||||
try:
|
||||
template.send_mail(partner.id, force_send=True)
|
||||
partner.membership_expiry_reminder_sent = True
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"membership_expiry_reminder: failed to send email to partner %d (%s)",
|
||||
partner.id,
|
||||
partner.email,
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
# Reset the reminder flag when the membership is renewed
|
||||
# (i.e., when member_lines changes, which updates membership_stop)
|
||||
result = super().write(vals)
|
||||
if "membership_stop" in vals:
|
||||
self.filtered("membership_expiry_reminder_sent").write(
|
||||
{"membership_expiry_reminder_sent": False}
|
||||
)
|
||||
return result
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="res_config_settings_view_form_membership_expiry" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.membership.expiry.reminder</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="90"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Members" id="membership_expiry_reminder"
|
||||
string="Members" name="membership">
|
||||
<block title="Membership Renewal Reminders"
|
||||
id="membership_expiry_reminder_block">
|
||||
<setting
|
||||
string="Expiry reminder"
|
||||
help="An email is sent automatically to active members before their membership expires. Set to 0 to disable.">
|
||||
<div class="text-muted content-group mt16">
|
||||
<span>Send reminder </span>
|
||||
<field name="membership_expiry_reminder_days"
|
||||
class="text-center"
|
||||
style="width: 10%; min-width: 4rem;"/>
|
||||
<span> days before expiry</span>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue