diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py index e44e9da11f..1120979d29 100644 --- a/subscription_oca/models/sale_subscription.py +++ b/subscription_oca/models/sale_subscription.py @@ -215,21 +215,46 @@ def _onchange_template_id(self): else: self.calculate_recurring_next_date(today) + def _get_recurrence_delta(self): + self.ensure_one() + type_interval = self.template_id.recurring_rule_type + interval = int(self.template_id.recurring_interval or 0) or 1 + return relativedelta(**{type_interval: interval}) + + def _get_first_invoice_date(self): + self.ensure_one() + return self.date_start or fields.Date.today() + + def _get_next_invoice_date(self, previous_date): + self.ensure_one() + if isinstance(previous_date, datetime): + previous_date = previous_date.date() + elif not isinstance(previous_date, date): + previous_date = fields.Date.to_date(previous_date) + return previous_date + self._get_recurrence_delta() + + def _set_next_invoice_date_after_invoice(self, invoice_date=None): + self.ensure_one() + previous_date = invoice_date or self.recurring_next_date + self.recurring_next_date = self._get_next_invoice_date(previous_date) + + def _get_contract_end_date(self): + self.ensure_one() + if self.template_id.recurring_rule_boundary == "unlimited": + return False + return self.template_id._get_date(self._get_first_invoice_date()) + def calculate_recurring_next_date(self, start_date): if self.account_invoice_ids_count == 0: if not start_date: - start_date = self.date_start or date.today() + start_date = self._get_first_invoice_date() if isinstance(start_date, datetime): start_date = start_date.date() elif not isinstance(start_date, date): start_date = fields.Date.to_date(start_date) self.recurring_next_date = start_date else: - type_interval = self.template_id.recurring_rule_type - interval = int(self.template_id.recurring_interval) - self.recurring_next_date = start_date + relativedelta( - **{type_interval: interval} - ) + self.recurring_next_date = self._get_next_invoice_date(start_date) @api.onchange("partner_id") def onchange_partner_id(self): @@ -371,12 +396,12 @@ def generate_invoice(self): if not invoice_number: invoice_number = self.env._("To validate") message_body = f"{msg_static} {invoice_number}" - self.calculate_recurring_next_date(self.recurring_next_date) + self._set_next_invoice_date_after_invoice() self.message_post(body=Markup(message_body)) def manual_invoice(self): invoice_id = self.create_invoice() - self.calculate_recurring_next_date(self.recurring_next_date) + self._set_next_invoice_date_after_invoice() context = dict(self.env.context) context["form_view_initial_mode"] = "edit" return { diff --git a/subscription_oca/tests/__init__.py b/subscription_oca/tests/__init__.py index f445239d7f..df980baa5e 100644 --- a/subscription_oca/tests/__init__.py +++ b/subscription_oca/tests/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import test_subscription_oca +from . import test_subscription_recurrence_dates diff --git a/subscription_oca/tests/test_subscription_recurrence_dates.py b/subscription_oca/tests/test_subscription_recurrence_dates.py new file mode 100644 index 0000000000..501d76517f --- /dev/null +++ b/subscription_oca/tests/test_subscription_recurrence_dates.py @@ -0,0 +1,94 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta + +from odoo import fields + +from odoo.addons.base.tests.common import BaseCommon + + +class TestSubscriptionRecurrenceDates(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.partner = cls.env["res.partner"].create({"name": "Recurrence partner"}) + cls.pricelist = cls.env["product.pricelist"].create( + {"name": "Recurrence pricelist"} + ) + + def _make_template(self, rule_type, boundary="unlimited", count=1, interval=1): + return self.env["sale.subscription.template"].create( + { + "name": f"Tmpl {rule_type} {boundary}", + "code": f"REC-{rule_type}-{boundary}", + "recurring_rule_type": rule_type, + "recurring_rule_boundary": boundary, + "recurring_rule_count": count, + "recurring_interval": interval, + } + ) + + def _make_subscription(self, template, date_start=None): + return self.env["sale.subscription"].create( + { + "partner_id": self.partner.id, + "pricelist_id": self.pricelist.id, + "template_id": template.id, + "date_start": date_start or fields.Date.today(), + } + ) + + def test_recurrence_delta_daily(self): + sub = self._make_subscription(self._make_template("days")) + self.assertEqual(sub._get_recurrence_delta(), relativedelta(days=1)) + + def test_recurrence_delta_weekly(self): + sub = self._make_subscription(self._make_template("weeks")) + self.assertEqual(sub._get_recurrence_delta(), relativedelta(weeks=1)) + + def test_recurrence_delta_monthly(self): + sub = self._make_subscription(self._make_template("months")) + self.assertEqual(sub._get_recurrence_delta(), relativedelta(months=1)) + + def test_recurrence_delta_yearly(self): + sub = self._make_subscription(self._make_template("years")) + self.assertEqual(sub._get_recurrence_delta(), relativedelta(years=1)) + + def test_recurrence_delta_respects_interval(self): + sub = self._make_subscription(self._make_template("months", interval=3)) + self.assertEqual(sub._get_recurrence_delta(), relativedelta(months=3)) + + def test_first_invoice_date_uses_date_start(self): + start = fields.Date.today() + relativedelta(days=10) + sub = self._make_subscription(self._make_template("months"), date_start=start) + self.assertEqual(sub._get_first_invoice_date(), start) + + def test_next_invoice_date_advances_by_recurrence(self): + sub = self._make_subscription(self._make_template("months")) + base = fields.Date.today() + self.assertEqual( + sub._get_next_invoice_date(base), + base + relativedelta(months=1), + ) + + def test_set_next_invoice_date_after_invoice_advances(self): + sub = self._make_subscription(self._make_template("weeks")) + sub.recurring_next_date = fields.Date.today() + previous = sub.recurring_next_date + sub._set_next_invoice_date_after_invoice() + self.assertEqual(sub.recurring_next_date, previous + relativedelta(weeks=1)) + + def test_contract_end_date_unlimited_is_false(self): + sub = self._make_subscription(self._make_template("months", "unlimited")) + self.assertFalse(sub._get_contract_end_date()) + + def test_contract_end_date_limited_uses_template(self): + template = self._make_template("months", "limited", count=6) + start = fields.Date.today() + sub = self._make_subscription(template, date_start=start) + self.assertEqual( + sub._get_contract_end_date(), + start + relativedelta(months=6), + )