# Part of Odoo. See LICENSE file for full copyright and licensing details. from dateutil.relativedelta import relativedelta from datetime import datetime from odoo import fields, Command from odoo.tests import new_test_user, tagged from odoo.exceptions import UserError from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon @tagged('-at_install', 'post_install') class TestSubscriptionTask(TestSubscriptionCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.env.user.group_ids += cls.quick_ref('project.group_project_manager') cls.env.user.group_ids += cls.env.ref('project.group_project_recurring_tasks') cls.project = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ 'name': 'Project', 'type_ids': [ Command.create({'name': 'a'}), Command.create({'name': 'b'}), ], 'allow_billable': True, }) cls.product_no_recurrence, cls.product_recurrence = cls.env['product.template'].create([{ 'name': 'Product No Recurrence', 'type': 'service', 'project_id': cls.project.id, 'service_tracking': 'task_global_project', }, { 'name': 'Product Recurrence', 'recurring_invoice': True, 'type': 'service', 'project_id': cls.project.id, 'service_tracking': 'task_global_project', }]) def test_task_recurrence(self): Order = self.env['sale.order'] OrderLine = self.env['sale.order.line'] self.product_recurrence.project_id.allow_recurring_tasks = True for product in [ self.product_no_recurrence, self.product_recurrence, ]: is_recurrent = product.recurring_invoice for end_date in fields.Date.today() + relativedelta(months=1), False: order = Order.create({ 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.partner.id, **({ 'plan_id': self.plan_month.id, 'end_date': end_date, } if is_recurrent else {}), }) order_line = OrderLine.create({ 'order_id': order.id, 'product_id': product.product_variant_id.id, }) order.action_confirm() self.assertEqual(order_line.task_id.recurring_task, is_recurrent, "The task created should be recurrent if and only if the product is recurrent") if not is_recurrent: continue task_recurrence = order_line.task_id.recurrence_id if end_date: self.assertEqual(task_recurrence.repeat_type, 'until', "A subscription with an end date must create a task with a recurrence of type 'until'") self.assertEqual(task_recurrence.repeat_until, end_date, "A subscription with an end date must set its end date on its task's recurrence") else: self.assertEqual(task_recurrence.repeat_type, 'forever', "No end date on the subscription must result in a task with a recurrence of type 'forever'") def test_task_plan_close(self): order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, }) order_line = self.env['sale.order.line'].create({ 'order_id': order.id, 'product_id': self.product_recurrence.product_variant_id.id, }) self.product_recurrence.project_id.allow_recurring_tasks = True order.action_confirm() task = order_line.task_id self.assertTrue(task.recurring_task, "Task should be recurrent") self.assertTrue(task.recurrence_id, "Task should be recurrent") order.set_close() self.assertFalse(task.recurring_task, "Closing a subscription must stop the task recurrence") self.assertFalse(task.recurrence_id, "Closing a subscription must stop the task recurrence") def test_task_plan_cancel(self): order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, }) order_line = self.env['sale.order.line'].create({ 'order_id': order.id, 'product_id': self.product_recurrence.product_variant_id.id, }) self.product_recurrence.project_id.allow_recurring_tasks = True order.action_confirm() task = order_line.task_id self.assertTrue(task.recurring_task, "Task should be recurrent") self.assertTrue(task.recurrence_id, "Task should be recurrent") order._action_cancel() self.assertFalse(task.recurring_task, "Cancelling a subscription must stop the task recurrence") self.assertFalse(task.recurrence_id, "Cancelling a subscription must stop the task recurrence") def test_task_plan_quotation_template(self): order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, }) order_line = self.env['sale.order.line'].create({ 'order_id': order.id, 'product_id': self.product_recurrence.product_variant_id.id, }) self.product_recurrence.project_id.allow_recurring_tasks = True order.action_confirm() task_recurrence = order_line.task_id.recurrence_id self.assertEqual(task_recurrence.repeat_type, 'until', "As the subscription template is of type 'limited', the task recurrence should be of type 'until'") self.assertEqual(task_recurrence.repeat_until.month, order.end_date.month, "The task recurrence should end on the same month as the subscription") def test_task_plan_upsell(self): product_plan_2 = self.env['product.template'].create({ 'name': 'Product Recurrence 2', 'recurring_invoice': True, 'type': 'service', 'project_id': self.project.id, 'service_tracking': 'task_global_project', }) self.project.allow_recurring_tasks = True order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, "end_date": fields.Date.today() + relativedelta(months=1), }) self.env['sale.order.line'].create({ 'order_id': order.id, 'product_id': self.product_recurrence.product_variant_id.id, }) self.product_recurrence.project_id.allow_recurring_tasks = True order.action_confirm() order._create_recurring_invoice() action = order.prepare_upsell_order() upsell = self.env['sale.order'].browse(action['res_id']) upsell.order_line.filtered(lambda sol: not sol.display_type).product_uom_qty = 1 upsell.order_line += self.env['sale.order.line'].create({ 'order_id': upsell.id, 'product_id': product_plan_2.product_variant_id.id, }) upsell.action_confirm() recurrences = order.order_line.task_id.recurrence_id self.assertTrue(all(recurrences.mapped(lambda r: r.repeat_type == 'until'))) self.assertTrue(all(recurrences.mapped(lambda r: r.repeat_until.month == order.end_date.month))) def test_task_generation(self): product_task = self.env['product.template'].create({ 'name': 'Product task', 'type': 'service', 'recurring_invoice': True, 'project_id': self.project.id, 'service_tracking': 'task_global_project', }) order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, }) self.env['sale.order.line'].create({ 'order_id': order.id, 'product_id': product_task.product_variant_id.id, }) order.action_confirm() self.assertEqual(len(order.tasks_ids), 1, "One task should be created") order._create_recurring_invoice() action = order.prepare_upsell_order() upsell = self.env['sale.order'].browse(action['res_id']) upsell.order_line[:1].product_uom_qty = 1 upsell.action_confirm() self.assertEqual(len(order.tasks_ids), 1, "No additional task should be created") action = order.prepare_renewal_order() renew = self.env['sale.order'].browse(action['res_id']) renew.order_line[:1].product_uom_qty = 2 renew.action_confirm() self.assertEqual(len(renew.tasks_ids), 1, "One task for renew should be created") def test_recurring_task_generation_portal(self): order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, }) order_line = self.env['sale.order.line'].create({ 'order_id': order.id, 'product_id': self.product_recurrence.product_variant_id.id, }) self.product_recurrence.project_id.allow_recurring_tasks = True order.with_user(self.user_portal).sudo().action_confirm() self.assertEqual(len(order_line.task_id.recurrence_id), 1) order_line.task_id.project_id.allow_recurring_tasks = False order = order.copy() order.with_user(self.user_portal).sudo().action_confirm() self.assertEqual(len(order_line.task_id.recurrence_id), 0) def test_task_creation_on_non_recurring_product_upsell(self): subscription = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, }) self.env['sale.order.line'].create([{ 'order_id': subscription.id, 'product_id': self.product_recurrence.product_variant_id.id, }]) subscription.action_confirm() invoice = subscription._create_invoices(final=True) invoice.action_post() task_domain = [('project_id', '=', self.project.id)] self.assertEqual(len(self.env['project.task'].search(task_domain)), 1, 'Task should be created') action = subscription.prepare_upsell_order() upsell = self.env['sale.order'].browse(action['res_id']) upsell.order_line.filtered(lambda sol: not sol.display_type).product_uom_qty = 1 self.env['sale.order.line'].create([{ 'order_id': upsell.id, 'product_id': self.product_no_recurrence.product_variant_id.id, }]) upsell.action_confirm() tasks = self.env['project.task'].search(task_domain) self.assertEqual(len(tasks), 2, 'Task should only be created on upsell if the product is non-recurring') self.assertTrue(self.product_no_recurrence.name in tasks[0].name) def test_confirm_subscription_with_recurring_service_product_as_sale_manager(self): """ Steps: 1. Create a user with Sale Manager access. 2. Create a subscription-based sale order as that user. 3. Add a recurring service product to the order line. 4. Confirm the sale order to trigger task creation with recurrence. """ user_salemanager = new_test_user(self.env, login='user_salemanager', groups='sales_team.group_sale_manager') sale_order = self.env['sale.order'].with_user(user_salemanager).create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.partner.id, }) order_line = self.env['sale.order.line'].with_user(user_salemanager).create({ 'order_id': sale_order.id, 'product_id': self.product_recurrence.product_variant_id.id, }) self.product_recurrence.project_id.allow_recurring_tasks = True sale_order.with_user(user_salemanager).action_confirm() self.assertEqual(len(order_line.task_id.recurrence_id), 1) def test_create_recurring_task_from_template_on_sale_order_confirmation(self): """ Test that confirming a subscription-based sale order with a recurring service product creates a recurring task using the specified task template. Steps: 1. Create a recurring task template. 2. Create a service product linked to the template. 3. Create a subscription sale order and add the product. 4. Confirm the sale order. 5. Assert the task is created with correct recurrence. """ self.project.allow_recurring_tasks = True task_template = self.env['project.task'].create({ 'is_template': True, 'name': 'Recurring Template', 'project_id': self.project.id, 'recurring_task': True, 'repeat_unit': 'week', 'repeat_type': 'forever', 'date_deadline': "2023-01-01 00:00:00", }) product_template = self.env['product.template'].create({ 'name': 'Recurring Service Product', 'type': 'service', 'recurring_invoice': True, 'project_id': self.project.id, 'service_tracking': 'task_global_project', 'task_template_id': task_template.id, }) product = product_template.product_variant_id sale_order = self.env['sale.order'].create({ 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.partner.id, }) order_line = self.env['sale.order.line'].create({ 'order_id': sale_order.id, 'product_id': product.id, }) sale_order.action_confirm() self.assertTrue(order_line.task_id.recurring_task, "The task should be marked as recurring.") self.assertEqual(order_line.task_id.repeat_unit, 'week', "The repeat unit should be 'week'.") self.assertEqual(order_line.task_id.repeat_type, 'forever', "The repeat type should be 'forever'.") self.assertEqual( order_line.task_id.date_deadline, datetime(2023, 1, 1, 0, 0, 0), "The task deadline should be 2023-01-01 00:00:00." ) def test_recurring_product_requires_subscription(self): """ Adding a recurring service product on a non-subscription sale order must raise a UserError. """ # Create a regular (non-subscription) sale order order = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'is_subscription': False, }) # Confirm the order to match real user scenario order.action_confirm() # Adding a recurring product without a plan must fail with self.assertRaisesRegex( UserError, "Please add a recurring plan on the subscription or remove the recurring product.", ): order.write({ 'order_line': [( 0, 0, { 'product_id': self.product_recurrence.product_variant_id.id, } )], })