From 9376d03d9dea16c5e630c2dd70d89e4a6ad0bb52 Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 4 May 2026 12:51:13 +0200 Subject: [PATCH] add membership_expiry_reminder --- membership_expiry_reminder/__init__.py | 3 + membership_expiry_reminder/__manifest__.py | 17 ++++ membership_expiry_reminder/data/ir_cron.xml | 17 ++++ .../data/mail_template.xml | 81 +++++++++++++++++ membership_expiry_reminder/models/__init__.py | 3 + .../models/res_config_settings.py | 15 ++++ .../models/res_partner.py | 88 +++++++++++++++++++ .../views/res_config_settings_views.xml | 32 +++++++ 8 files changed, 256 insertions(+) create mode 100644 membership_expiry_reminder/__init__.py create mode 100644 membership_expiry_reminder/__manifest__.py create mode 100644 membership_expiry_reminder/data/ir_cron.xml create mode 100644 membership_expiry_reminder/data/mail_template.xml create mode 100644 membership_expiry_reminder/models/__init__.py create mode 100644 membership_expiry_reminder/models/res_config_settings.py create mode 100644 membership_expiry_reminder/models/res_partner.py create mode 100644 membership_expiry_reminder/views/res_config_settings_views.xml diff --git a/membership_expiry_reminder/__init__.py b/membership_expiry_reminder/__init__.py new file mode 100644 index 0000000..69f7bab --- /dev/null +++ b/membership_expiry_reminder/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/membership_expiry_reminder/__manifest__.py b/membership_expiry_reminder/__manifest__.py new file mode 100644 index 0000000..6b9a673 --- /dev/null +++ b/membership_expiry_reminder/__manifest__.py @@ -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, +} diff --git a/membership_expiry_reminder/data/ir_cron.xml b/membership_expiry_reminder/data/ir_cron.xml new file mode 100644 index 0000000..3300ece --- /dev/null +++ b/membership_expiry_reminder/data/ir_cron.xml @@ -0,0 +1,17 @@ + + + + + + Membership: Send Expiry Reminder Emails + + code + model._cron_send_membership_expiry_reminders() + 1 + days + True + + + + + diff --git a/membership_expiry_reminder/data/mail_template.xml b/membership_expiry_reminder/data/mail_template.xml new file mode 100644 index 0000000..979d30d --- /dev/null +++ b/membership_expiry_reminder/data/mail_template.xml @@ -0,0 +1,81 @@ + + + + + + Membership: Renewal Reminder + + Reminder: your membership expires soon + {{ (object.company_id.email_formatted or user.email_formatted) }} + {{ object.id }} + {{ object.lang }} + + +
+ + + + + + + + + + +
+

+ Dear Member Name, +

+
+

+ This is a friendly reminder that your membership with + Your Organisation + is due to expire on + expiry date. +

+

+ To continue enjoying your membership benefits without interruption, + please renew your membership before that date. +

+ + + + +

+ + Renew my membership + +

+

+ Or copy this link into your browser:
+ + + +

+
+ +

+ If you have any questions, please do not hesitate to contact us. +

+

+ Kind regards,
+ Your Organisation +

+
+ Your Organisation + + • + + + • + +
+
+
+
+ +
+
diff --git a/membership_expiry_reminder/models/__init__.py b/membership_expiry_reminder/models/__init__.py new file mode 100644 index 0000000..f1d9ae6 --- /dev/null +++ b/membership_expiry_reminder/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import res_config_settings, res_partner diff --git a/membership_expiry_reminder/models/res_config_settings.py b/membership_expiry_reminder/models/res_config_settings.py new file mode 100644 index 0000000..31bacff --- /dev/null +++ b/membership_expiry_reminder/models/res_config_settings.py @@ -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.", + ) diff --git a/membership_expiry_reminder/models/res_partner.py b/membership_expiry_reminder/models/res_partner.py new file mode 100644 index 0000000..3c59d9b --- /dev/null +++ b/membership_expiry_reminder/models/res_partner.py @@ -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 diff --git a/membership_expiry_reminder/views/res_config_settings_views.xml b/membership_expiry_reminder/views/res_config_settings_views.xml new file mode 100644 index 0000000..4742bb9 --- /dev/null +++ b/membership_expiry_reminder/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + + res.config.settings.view.form.inherit.membership.expiry.reminder + res.config.settings + + + + + + + +
+ Send reminder + + days before expiry +
+
+
+
+
+
+
+
+