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