diff --git a/membership_variable_period/README.rst b/membership_variable_period/README.rst
index 57733312..d291b421 100644
--- a/membership_variable_period/README.rst
+++ b/membership_variable_period/README.rst
@@ -40,6 +40,10 @@ and adapting them for this purpose. As now the quota is not attached to a fixed
period, you can also invoice more than one quantity for being a member for
the corresponding number of periods.
+A member's membership is renewed when a new variable period membership product
+of the same category is purchased. The new period will begin the day after the
+end of the last period.
+
Finally, a cron has been included that triggers the recalculation of the
membership state, allowing to have "old members", which doesn't work well
on standard.
diff --git a/membership_variable_period/models/__init__.py b/membership_variable_period/models/__init__.py
index fb031c59..d4531c08 100644
--- a/membership_variable_period/models/__init__.py
+++ b/membership_variable_period/models/__init__.py
@@ -1,3 +1,4 @@
# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
from . import product_template
from . import account_move_line
+from . import res_partner
diff --git a/membership_variable_period/models/account_move_line.py b/membership_variable_period/models/account_move_line.py
index eb69d462..930109d1 100644
--- a/membership_variable_period/models/account_move_line.py
+++ b/membership_variable_period/models/account_move_line.py
@@ -13,19 +13,29 @@ class AccountMoveLine(models.Model):
_inherit = "account.move.line"
def _prepare_membership_line(self, move, product, price_unit, line_id, qty=1.0):
+ line = self.browse(line_id)
qty = int(math.ceil(qty))
- date_from = move.invoice_date or fields.Date.today()
+ partner = (
+ line._get_partner_for_membership()
+ if hasattr(line, "_get_partner_for_membership")
+ else move.partner_id
+ )
+ date_from = (
+ partner.get_membership_renewal_date(product)
+ or move.invoice_date
+ or fields.Date.today()
+ )
date_to = product.product_tmpl_id._get_next_date(date_from, qty=qty)
date_to = date_to and (date_to - timedelta(days=1)) or False
return {
- "partner": move.partner_id.id,
+ "partner": partner.id,
"membership_id": product.id,
"member_price": price_unit,
"date": move.invoice_date or fields.Date.today(),
"date_from": date_from,
"date_to": date_to,
"state": "waiting",
- "account_invoice_line": line_id,
+ "account_invoice_line": line.id,
}
@api.model
diff --git a/membership_variable_period/models/res_partner.py b/membership_variable_period/models/res_partner.py
new file mode 100644
index 00000000..700ded35
--- /dev/null
+++ b/membership_variable_period/models/res_partner.py
@@ -0,0 +1,41 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from datetime import timedelta
+
+from odoo import api, fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ @api.model
+ def _membership_renewable_member_states(self):
+ """Inherit this method to define renewable membership states.
+
+ :return tuple: list of renewable membership states
+ """
+ return self._membership_member_states()
+
+ def get_membership_renewal_date(self, product):
+ """Retrieve the renewal date of a member for a given membership product.
+
+ :param product (ModelVariable period for memberships
and adapting them for this purpose. As now the quota is not attached to a fixed
period, you can also invoice more than one quantity for being a member for
the corresponding number of periods.
A member’s membership is renewed when a new variable period membership product +of the same category is purchased. The new period will begin the day after the +end of the last period.
Finally, a cron has been included that triggers the recalculation of the membership state, allowing to have “old members”, which doesn’t work well on standard.
diff --git a/membership_variable_period/tests/test_membership_variable_period.py b/membership_variable_period/tests/test_membership_variable_period.py index 39ba2725..e0f4443c 100644 --- a/membership_variable_period/tests/test_membership_variable_period.py +++ b/membership_variable_period/tests/test_membership_variable_period.py @@ -5,6 +5,8 @@ # License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 from datetime import date +from freezegun import freeze_time + from odoo import fields from odoo.tests import common, tagged @@ -23,6 +25,10 @@ def setUpClass(cls): [("visible", "=", True)], limit=1 ) coa.try_loading(company=cls.env.company, install_demo=False) + cls.category_gold = cls.env.ref("membership_extension.membership_category_gold") + cls.category_silver = cls.env.ref( + "membership_extension.membership_category_silver" + ) cls.product = cls.env["product.product"].create( { "name": "Membership product with variable period", @@ -32,17 +38,28 @@ def setUpClass(cls): "membership_interval_unit": "weeks", } ) + cls.product_silver = cls.env["product.product"].create( + { + "name": "Silver membership product with variable period", + "membership": True, + "membership_type": "variable", + "membership_interval_qty": 1, + "membership_interval_unit": "weeks", + "membership_category_id": cls.category_silver.id, + } + ) cls.partner = cls.env["res.partner"].create({"name": "Test"}) - def create_invoice(self, invoice_date, quantity=1.0): + def create_invoice(self, invoice_date, quantity=1.0, product=None): + product = product or self.product invoice_form = common.Form( self.env["account.move"].with_context(default_move_type="out_invoice") ) invoice_form.invoice_date = invoice_date invoice_form.partner_id = self.partner with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.product_id = self.product - invoice_line_form.price_unit = self.product.list_price + invoice_line_form.product_id = product + invoice_line_form.price_unit = product.list_price invoice_line_form.quantity = quantity return invoice_form.save() @@ -235,3 +252,55 @@ def test_create_invoice_line_with_no_product(self): self.assertFalse(invoice.invoice_line_ids[0].product_id) self.assertFalse(invoice.invoice_line_ids[0].membership_lines) + + @freeze_time("2025-01-01") + def test_create_membership_renewal(self): + self.product.membership_category_id = self.category_gold + for _ in range(2): + invoice = self.create_invoice("2025-01-01") + membership_line = invoice.invoice_line_ids[0].membership_lines[0] + membership_line.write({"state": "invoiced"}) + self.assertEqual( + membership_line.date_from, fields.Date.from_string("2025-01-08") + ) + self.assertEqual(membership_line.date_to, fields.Date.from_string("2025-01-14")) + self.assertEqual( + self.partner.membership_start, fields.Date.from_string("2025-01-01") + ) + self.assertEqual( + self.partner.membership_stop, fields.Date.from_string("2025-01-14") + ) + + @freeze_time("2025-01-01") + def test_check_membership_non_renewal(self): + self.product.membership_category_id = self.category_gold + invoice = self.create_invoice("2024-12-01") + membership_line = invoice.invoice_line_ids[0].membership_lines[0] + membership_line.write({"state": "invoiced"}) + self.assertEqual(membership_line.date_to, fields.Date.from_string("2024-12-07")) + + # The previous membership has expired + invoice = self.create_invoice("2025-01-01") + membership_line = invoice.invoice_line_ids[0].membership_lines[0] + self.assertEqual( + membership_line.date_from, fields.Date.from_string("2025-01-01") + ) + self.assertEqual(membership_line.state, "waiting") + + # The state of the previous membership is still waiting + invoice = self.create_invoice("2025-01-01") + membership_line = invoice.invoice_line_ids[0].membership_lines[0] + membership_line.write({"state": "invoiced"}) + self.assertEqual( + membership_line.date_from, fields.Date.from_string("2025-01-01") + ) + self.assertEqual(self.partner.membership_category_ids, self.category_gold) + + # The previous membership has a different category + invoice = self.create_invoice("2025-01-01", product=self.product_silver) + membership_line = invoice.invoice_line_ids[0].membership_lines[0] + membership_line.write({"state": "invoiced"}) + self.assertEqual( + membership_line.date_from, fields.Date.from_string("2025-01-01") + ) + self.assertTrue(self.category_silver in self.partner.membership_category_ids)