diff --git a/subscription_oca/models/__init__.py b/subscription_oca/models/__init__.py index 6fa4481885..ab569bc3e4 100644 --- a/subscription_oca/models/__init__.py +++ b/subscription_oca/models/__init__.py @@ -1,4 +1,5 @@ from . import account_move +from . import account_move_line from . import product_template from . import res_partner from . import sale_order diff --git a/subscription_oca/models/account_move_line.py b/subscription_oca/models/account_move_line.py new file mode 100644 index 0000000000..ec45af3f52 --- /dev/null +++ b/subscription_oca/models/account_move_line.py @@ -0,0 +1,17 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + subscription_id = fields.Many2one( + comodel_name="sale.subscription", + string="Subscription", + index=True, + ondelete="set null", + ) + subscription_period_start = fields.Date(string="Subscription period start") + subscription_period_end = fields.Date(string="Subscription period end") diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py index e44e9da11f..a0132852c1 100644 --- a/subscription_oca/models/sale_subscription.py +++ b/subscription_oca/models/sale_subscription.py @@ -302,15 +302,30 @@ def _prepare_account_move(self, line_ids): values["journal_id"] = self.journal_id.id return values + def _get_invoice_period(self): + self.ensure_one() + period_start = self.recurring_next_date or fields.Date.today() + type_interval = self.template_id.recurring_rule_type + interval = int(self.template_id.recurring_interval or 0) or 1 + period_end = ( + period_start + + relativedelta(**{type_interval: interval}) + - relativedelta(days=1) + ) + return period_start, period_end + def create_invoice(self): if not self.env["account.move"].has_access("create"): try: self.check_access("write") except AccessError: return self.env["account.move"] + period_start, period_end = self._get_invoice_period() line_ids = [] for line in self.sale_subscription_line_ids: - line_values = line._prepare_account_move_line() + line_values = line._prepare_account_move_line( + period_start=period_start, period_end=period_end + ) line_ids.append(Command.create(line_values)) invoice_values = self._prepare_account_move(line_ids) invoice_id = ( diff --git a/subscription_oca/models/sale_subscription_line.py b/subscription_oca/models/sale_subscription_line.py index 31cf985270..7d72dfdbca 100644 --- a/subscription_oca/models/sale_subscription_line.py +++ b/subscription_oca/models/sale_subscription_line.py @@ -1,7 +1,7 @@ # Copyright 2023 Domatix - Carlos Martínez # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import Command, api, fields, models -from odoo.tools.misc import get_lang +from odoo.tools.misc import format_date, get_lang class SaleSubscriptionLine(models.Model): @@ -298,15 +298,23 @@ def _prepare_sale_order_line(self): "analytic_distribution": self.analytic_distribution, } - def _prepare_account_move_line(self): + def _prepare_account_move_line(self, period_start=False, period_end=False): self.ensure_one() account = ( self.product_id.property_account_income_id or self.product_id.categ_id.property_account_income_categ_id ) + name = self.name + if period_start and period_end: + lang_code = get_lang( + self.env, self.sale_subscription_id.partner_id.lang + ).code + start_str = format_date(self.env, period_start, lang_code=lang_code) + end_str = format_date(self.env, period_end, lang_code=lang_code) + name = f"{name} ({start_str} - {end_str})" return { "product_id": self.product_id.id, - "name": self.name, + "name": name, "quantity": self.product_uom_qty, "price_unit": self.price_unit, "discount": self.discount, @@ -315,4 +323,7 @@ def _prepare_account_move_line(self): "product_uom_id": self.product_id.uom_id.id, "account_id": account.id, "analytic_distribution": self.analytic_distribution, + "subscription_id": self.sale_subscription_id.id, + "subscription_period_start": period_start or False, + "subscription_period_end": period_end or False, } diff --git a/subscription_oca/tests/__init__.py b/subscription_oca/tests/__init__.py index f445239d7f..68c93a3d6b 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_invoice_period diff --git a/subscription_oca/tests/test_subscription_invoice_period.py b/subscription_oca/tests/test_subscription_invoice_period.py new file mode 100644 index 0000000000..0a417e143f --- /dev/null +++ b/subscription_oca/tests/test_subscription_invoice_period.py @@ -0,0 +1,73 @@ +# 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 +from odoo.addons.product.tests.common import ProductCommon + + +class TestSubscriptionInvoicePeriod(ProductCommon, 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": "Invoice period partner"}) + cls.pricelist = cls.env["product.pricelist"].create( + {"name": "Invoice period pricelist"} + ) + cls.template_monthly = cls.env["sale.subscription.template"].create( + { + "name": "Monthly template", + "code": "PER-MTH", + "recurring_rule_type": "months", + "recurring_rule_boundary": "unlimited", + } + ) + cls.product = cls._create_product( + name="Period product", + lst_price=100.0, + subscribable=True, + uom_id=cls.uom_unit.id, + ) + cls.subscription = cls.env["sale.subscription"].create( + { + "partner_id": cls.partner.id, + "pricelist_id": cls.pricelist.id, + "template_id": cls.template_monthly.id, + "date_start": fields.Date.today(), + "recurring_next_date": fields.Date.today(), + } + ) + cls.env["sale.subscription.line"].create( + { + "sale_subscription_id": cls.subscription.id, + "product_id": cls.product.id, + } + ) + + def test_get_invoice_period_returns_start_and_end(self): + start, end = self.subscription._get_invoice_period() + self.assertEqual(start, self.subscription.recurring_next_date) + self.assertEqual( + end, + start + relativedelta(months=1) - relativedelta(days=1), + ) + + def test_manual_invoice_tracks_subscription_period(self): + period_start, period_end = self.subscription._get_invoice_period() + invoice = self.subscription.create_invoice() + for line in invoice.invoice_line_ids: + self.assertEqual(line.subscription_id, self.subscription) + self.assertEqual(line.subscription_period_start, period_start) + self.assertEqual(line.subscription_period_end, period_end) + + def test_invoice_line_description_contains_period_dates(self): + period_start, period_end = self.subscription._get_invoice_period() + invoice = self.subscription.create_invoice() + line = invoice.invoice_line_ids[:1] + self.assertIn(fields.Date.to_string(period_start)[:4], line.name) + self.assertIn(fields.Date.to_string(period_end)[:4], line.name) + self.assertIn(" - ", line.name)