# -*- coding: utf-8 -*- import datetime from dateutil.relativedelta import relativedelta from freezegun import freeze_time from markupsafe import Markup from unittest.mock import patch from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon from odoo.addons.sale_subscription.models.sale_order import SaleOrder from odoo.tests import Form, tagged from odoo.tests.common import new_test_user from odoo.tools import mute_logger from odoo import fields, Command from odoo.exceptions import AccessError, ValidationError, UserError @tagged('post_install', '-at_install') class TestSubscription(TestSubscriptionCommon): def flush_tracking(self): """ Force the creation of tracking values. """ self.env.flush_all() self.cr.flush() def setUp(self): super(TestSubscription, self).setUp() self.env.ref('base.group_user').write({"implied_ids": [(4, self.env.ref('sale_management.group_sale_order_template').id)]}) self.flush_tracking() def _get_quantities(self, order_line): order_line = order_line.sorted('id') values = { 'delivered_qty': order_line.mapped('qty_delivered'), 'qty_delivered_method': order_line.mapped('qty_delivered_method'), 'to_invoice': order_line.mapped('qty_to_invoice'), 'invoiced': order_line.mapped('qty_invoiced'), } return values @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_automatic(self): self.assertTrue(True) sub = self.subscription context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } sub_product_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Subscription Product', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'list_price': 42 }) product = sub_product_tmpl.product_variant_id template = self.env['sale.order.template'].create({ 'name': 'Subscription template without discount', 'duration_unit': 'year', 'is_unlimited': False, 'duration_value': 2, 'plan_id': self.plan_month.id, 'sale_order_template_line_ids': [Command.create({ 'name': "monthly", 'product_id': product.id, 'product_uom_id': product.uom_id.id }), Command.create({ 'name': "yearly", 'product_id': product.id, 'product_uom_id': product.uom_id.id, }) ] }) self.plan_month.auto_close_limit = 3 self.company = self.env.company self.provider = self.env['payment.provider'].create( {'name': 'The Wire', 'company_id': self.company.id, 'state': 'test', 'redirect_form_view_id': self.env['ir.ui.view'].search([('type', '=', 'qweb')], limit=1).id}) sub.sale_order_template_id = template.id sub._onchange_sale_order_template_id() with freeze_time("2021-01-03"): sub.write({'start_date': False, 'next_invoice_date': False}) sub.action_confirm() self.assertEqual(sub.invoice_count, 0) self.assertEqual(datetime.date(2021, 1, 3), sub.start_date, 'start date should be reset at confirmation') self.assertEqual(datetime.date(2021, 1, 3), sub.next_invoice_date, 'next invoice date should be updated') self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(datetime.date(2021, 2, 3), sub.next_invoice_date, 'next invoice date should be updated') inv = sub.invoice_ids.sorted('date')[-1] inv_line = inv.invoice_line_ids[0].sorted('id')[0] invoice_periods = inv_line.name.split('\n')[1] self.assertEqual(invoice_periods, "01/03/2021 to 02/02/2021") self.assertEqual(inv_line.date, datetime.date(2021, 1, 3)) with freeze_time("2021-02-03"): self.assertEqual(sub.invoice_count, 1) self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(sub.invoice_count, 2) self.assertEqual(datetime.date(2021, 1, 3), sub.start_date, 'start date should not changed') self.assertEqual(datetime.date(2021, 3, 3), sub.next_invoice_date, 'next invoice date should be in 1 month') inv = sub.invoice_ids.sorted('date')[-1] invoice_periods = inv.invoice_line_ids[1].name.split('\n')[1] self.assertEqual(invoice_periods, "02/03/2021 to 03/02/2021") self.assertEqual(inv.invoice_line_ids[1].date, datetime.date(2021, 2, 3)) with freeze_time("2021-03-03"): self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(datetime.date(2021, 4, 3), sub.next_invoice_date, 'next invoice date should be in 1 month') inv = sub.invoice_ids.sorted('date')[-1] invoice_periods = inv.invoice_line_ids[0].name.split('\n')[1] self.assertEqual(invoice_periods, "03/03/2021 to 04/02/2021") self.assertEqual(inv.invoice_line_ids[0].date, datetime.date(2021, 3, 3)) @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_template(self): """ Test behaviour of on_change_template """ Subscription = self.env['sale.order'] self.assertEqual(self.subscription.note, Markup('

original subscription description

'), "Original subscription note") # on_change_template on cached record (NOT present in the db) temp = Subscription.new({'name': 'CachedSubscription', 'partner_id': self.user_portal.partner_id.id}) temp.update({'sale_order_template_id': self.subscription_tmpl.id}) temp._onchange_sale_order_template_id() self.assertEqual(temp.note, Markup('

This is the template description

'), 'Override the subscription note') def test_template_without_selected_partner(self): """ Create a subscription by choosing a template before the customer """ with Form(self.env['sale.order']) as subscription: subscription.sale_order_template_id = self.subscription_tmpl subscription.partner_id = self.partner # mandatory to have no error def test_invoicing_with_section(self): """ Test invoicing when order has section/note.""" context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } # create specific test products sub_product1_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Subscription #A', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, }) sub_product1 = sub_product1_tmpl.product_variant_id sub_product2_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Subscription #B', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, }) sub_product2 = sub_product2_tmpl.product_variant_id sub_product_onetime_discount_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Initial discount', 'type': 'service', 'recurring_invoice': False, 'uom_id': self.env.ref('uom.product_uom_unit').id, }) sub_product_onetime_discount = sub_product_onetime_discount_tmpl.product_variant_id with freeze_time("2021-01-03"): sub = self.env["sale.order"].with_context(**context_no_mail).create({ 'name': 'TestSubscription', 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'sale_order_template_id': self.subscription_tmpl.id, }) sub._onchange_sale_order_template_id() sub.write({ 'start_date': False, 'end_date': False, 'next_invoice_date': False, }) sub.order_line = [ Command.clear(), Command.create({ 'display_type': 'line_section', 'name': 'Products', }), Command.create({ 'product_id': sub_product1.id, 'name': "Subscription #A", 'price_unit': 42, 'product_uom_qty': 2, }), Command.create({ 'product_id': sub_product2.id, 'name': "Subscription #B", 'price_unit': 42, 'product_uom_qty': 2, }), Command.create({ 'product_id': sub_product_onetime_discount.id, 'name': 'New subscription discount (one-time)', 'price_unit': -10.0, 'product_uom_qty': 2, }), Command.create({ 'display_type': 'line_section', 'name': 'Information', }), Command.create({ 'display_type': 'line_note', 'name': '...', }), ] sub.action_confirm() sub._create_invoices() # first invoice, it should include one-time discount self.assertEqual(len(sub.invoice_ids), 1) sub.invoice_ids._post() invoice = sub.invoice_ids[-1] self.assertEqual(invoice.amount_untaxed, 148.0) self.assertEqual(len(invoice.invoice_line_ids), 6) self.assertRecordValues(invoice.invoice_line_ids, [ {'display_type': 'line_section', 'name': 'Products', 'product_id': False}, { 'display_type': 'product', 'product_id': sub_product1.id, 'name': 'Subscription #A - 1 Months\n01/03/2021 to 02/02/2021', }, { 'display_type': 'product', 'product_id': sub_product2.id, 'name': 'Subscription #B - 1 Months\n01/03/2021 to 02/02/2021', }, { 'display_type': 'product', 'product_id': sub_product_onetime_discount.id, 'name': 'New subscription discount (one-time)', }, {'display_type': 'line_section', 'name': 'Information', 'product_id': False}, {'display_type': 'line_note', 'name': '...', 'product_id': False}, ]) with freeze_time("2021-02-03"): inv = sub._create_invoices() inv._post() # second invoice, should NOT include one-time discount self.assertEqual(len(sub.invoice_ids), 2) invoice = sub.invoice_ids[-1] self.assertEqual(invoice.amount_untaxed, 168.0) self.assertEqual(len(invoice.invoice_line_ids), 5) self.assertRecordValues(invoice.invoice_line_ids, [ {'display_type': 'line_section', 'name': 'Products', 'product_id': False}, { 'display_type': 'product', 'product_id': sub_product1.id, 'name': 'Subscription #A - 1 Months\n02/03/2021 to 03/02/2021', }, { 'display_type': 'product', 'product_id': sub_product2.id, 'name': 'Subscription #B - 1 Months\n02/03/2021 to 03/02/2021', }, {'display_type': 'line_section', 'name': 'Information', 'product_id': False}, {'display_type': 'line_note', 'name': '...', 'product_id': False}, ]) def test_add_aml_to_invoice(self): """ Test that it is possible to manually add a line with a start and end date to an invoice generated from a subscription sale order. """ sub_product1, sub_product2 = self.env['product.product'].create([ { 'name': 'SubA', 'type': 'service', 'recurring_invoice': True, 'invoice_policy': 'order', }, { 'name': 'SubB', 'type': 'service', 'recurring_invoice': True, } ]) sub = self.env['sale.order'].create({ 'name': 'TestSubscription', 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'order_line': [(0, 0, {'product_id': sub_product1.id})], }) sub.action_confirm() invoice = sub._create_invoices() invoice.write({ 'line_ids': [(0, 0, { 'product_id': sub_product2.id, 'deferred_start_date': '2015-03-14', 'deferred_end_date': '2030-06-28', })], }) invoice._post() # should not throw an error self.assertEqual(invoice.line_ids.product_id, sub_product1 | sub_product2) @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_unlimited_sale_order(self): """ Test behaviour of on_change_template """ with freeze_time("2021-01-03"): sub = self.subscription sub.order_line = [Command.clear()] context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } sub_product_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Subscription Product', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, }) product = sub_product_tmpl.product_variant_id sub.order_line = [Command.create({'product_id': product.id, 'name': "coucou", 'price_unit': 42, 'product_uom_qty': 2, })] sub.write({'start_date': False, 'next_invoice_date': False}) sub.action_confirm() self.assertFalse(sub.last_invoice_date) self.assertEqual("2021-01-03", sub.start_date.strftime("%Y-%m-%d")) self.assertEqual("2021-01-03", sub.next_invoice_date.strftime("%Y-%m-%d")) sub._create_recurring_invoice() # Next invoice date should not be bumped up because it is the first period self.assertEqual("2021-02-03", sub.next_invoice_date.strftime("%Y-%m-%d")) invoice_periods = sub.invoice_ids.invoice_line_ids.name.split('\n')[1] self.assertEqual(invoice_periods, "01/03/2021 to 02/02/2021") self.assertEqual(sub.invoice_ids.invoice_line_ids.date, datetime.date(2021, 1, 3)) with freeze_time("2021-02-03"): # February sub._create_recurring_invoice() self.assertEqual("2021-02-03", sub.last_invoice_date.strftime("%Y-%m-%d")) self.assertEqual("2021-03-03", sub.next_invoice_date.strftime("%Y-%m-%d")) inv = sub.invoice_ids.sorted('date')[-1] invoice_periods = inv.invoice_line_ids.name.split('\n')[1] self.assertEqual(invoice_periods, "02/03/2021 to 03/02/2021") self.assertEqual(inv.invoice_line_ids.date, datetime.date(2021, 2, 3)) with freeze_time("2021-03-03"): # March sub._create_recurring_invoice() self.assertEqual("2021-03-03", sub.last_invoice_date.strftime("%Y-%m-%d")) self.assertEqual("2021-04-03", sub.next_invoice_date.strftime("%Y-%m-%d")) inv = sub.invoice_ids.sorted('date')[-1] invoice_periods = inv.invoice_line_ids.name.split('\n')[1] self.assertEqual(invoice_periods, "03/03/2021 to 04/02/2021") self.assertEqual(inv.invoice_line_ids.date, datetime.date(2021, 3, 3)) @mute_logger('odoo.models.unlink') def test_renewal(self): """ Test subscription renewal """ with freeze_time("2021-11-18"): # We reset the renew alert to make sure it will run with freezetime self.subscription.write({ 'start_date': False, 'next_invoice_date': False, 'partner_invoice_id': self.partner_a_invoice.id, 'partner_shipping_id': self.partner_a_shipping.id, 'internal_note': 'internal note', }) # add an so line with a different uom uom_dozen = self.env.ref('uom.product_uom_dozen').id self.subscription_tmpl.duration_value = 2 # end after 2 months to adapt to the following line self.subscription_tmpl.duration_unit = 'month' self.env['sale.order.line'].create({'name': self.product.name, 'order_id': self.subscription.id, 'product_id': self.product3.id, 'product_uom_qty': 4, 'product_uom': uom_dozen, 'price_unit': 42}) self.subscription.action_confirm() self.subscription._create_recurring_invoice() self.assertEqual(self.subscription.end_date, datetime.date(2022, 1, 17), 'The end date of the subscription should be updated according to the template') self.assertEqual(self.subscription.next_invoice_date, datetime.date(2021, 12, 18)) self.env['account.payment.register'] \ .with_context(active_model='account.move', active_ids=self.subscription.invoice_ids.ids) \ .create({ 'currency_id': self.subscription.currency_id.id, 'amount': self.subscription.amount_total, })._create_payments() self.assertEqual(self.subscription.invoice_count, 1) self.assertTrue(self.subscription.invoice_ids.payment_state in ['in_payment', 'paid'], "the invoice is considered paid, depending on the settings.") with freeze_time("2021-12-18"): action = self.subscription.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) self.assertEqual(renewal_so.partner_invoice_id, self.partner_a_invoice) self.assertEqual(renewal_so.partner_shipping_id, self.partner_a_shipping) # check produt_uom_qty self.assertEqual(renewal_so.sale_order_template_id.id, self.subscription.sale_order_template_id.id, 'sale_subscription: renewal so should have the same template') renewal_start_date = renewal_so.start_date with self.assertRaises(ValidationError): # try to start the renewal before the parent next invoice date renewal_so.start_date = self.subscription.next_invoice_date - relativedelta(days=1) renewal_so.action_confirm() renewal_so.start_date = renewal_start_date renewal_so.action_confirm() self.assertEqual(renewal_so.internal_note_display, Markup('

internal note

'), 'Internal Note should redirect to the parent') self.assertEqual(self.subscription.recurring_monthly, 189, 'Should be closed but with an MRR') self.assertEqual(renewal_so.subscription_state, '3_progress', 'so should now be in progress') self.assertEqual(self.subscription.subscription_state, '5_renewed') self.assertEqual(renewal_so.date_order.date(), self.subscription.end_date, 'renewal start date should depends on the parent end date') self.assertEqual(renewal_so.start_date, self.subscription.end_date, 'The renewal subscription start date and the renewed end_date should be aligned') self.assertEqual(renewal_so.plan_id, self.plan_month, 'the plan should be propagated') self.assertEqual(renewal_so.next_invoice_date, datetime.date(2021, 12, 18)) self.assertEqual(renewal_so.start_date, datetime.date(2021, 12, 18)) self.assertTrue(renewal_so.is_subscription) renewal_so._create_recurring_invoice() with freeze_time("2024-11-17"): invoice = self.subscription._create_recurring_invoice() self.assertFalse(invoice, "Locked contract should not generate invoices") renewal_so.internal_note_display = 'new internal note' self.assertEqual(renewal_so.internal_note_display, Markup('

new internal note

'), 'Internal Note should be updated') self.assertEqual(self.subscription.internal_note_display, Markup('

new internal note

'), 'Internal Note should be updated') with freeze_time("2024-11-19"): self.subscription._create_recurring_invoice() # it will close self.subscription renew_close_reason_id = self.env.ref('sale_subscription.close_reason_renew').id self.assertEqual(self.subscription.subscription_state, '5_renewed') self.assertEqual(self.subscription.close_reason_id.id, renew_close_reason_id) (self.subscription | renewal_so).invalidate_recordset(['invoice_ids', 'invoice_count']) self.assertEqual(self.subscription.invoice_count, 2) self.assertEqual(renewal_so.invoice_count, 2) def test_upsell_no_start_date(self): self.sub_product_tmpl.product_subscription_pricing_ids = [(5, 0, 0)] self.subscription_tmpl.sale_order_template_option_ids = [Command.create({ 'name': "Option 1", 'product_id': self.product5.id, 'quantity': 1, 'uom_id': self.product5.uom_id.id, })] self.subscription.write({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [Command.create({'product_id': self.product.id, 'name': "Monthly cheap", 'price_unit': 42, 'product_uom_qty': 2, }), Command.create({'product_id': self.product2.id, 'name': "Monthly expensive", 'price_unit': 420, 'product_uom_qty': 3, }), ] }) self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_so.order_line.filtered(lambda l: not l.display_type).product_uom_qty = 6 upsell_so.start_date = False upsell_so.action_confirm() upsell_so._create_invoices() self.assertEqual(self.subscription.order_line.sorted('id').mapped('product_uom_qty'), [7.0, 7.0, 8.0, 9.0]) def test_upsell_via_so(self): # Test the upsell flow using an intermediary upsell quote. self.sub_product_tmpl.product_subscription_pricing_ids = [(5, 0, 0)] self.subscription_tmpl.sale_order_template_option_ids = [Command.create({ 'name': "Option 1", 'product_id': self.product5.id, 'quantity': 1, 'uom_id': self.product5.uom_id.id, })] self.product_tmpl_2.product_subscription_pricing_ids = [(5, 0, 0)] self.env['sale.subscription.pricing'].create({'plan_id': self.plan_month.id, 'product_template_id': self.sub_product_tmpl.id, 'price': 42}) self.env['sale.subscription.pricing'].create({'plan_id': self.plan_month.id, 'product_template_id': self.product_tmpl_2.id, 'price': 420}) with freeze_time("2021-01-01"): self.subscription.order_line = False self.subscription.start_date = False self.subscription.next_invoice_date = False self.subscription.write({ 'partner_id': self.partner.id, 'partner_invoice_id': self.partner_a_invoice.id, 'partner_shipping_id': self.partner_a_shipping.id, 'plan_id': self.plan_month.id, 'order_line': [Command.create({'product_id': self.product.id, 'name': "Monthly cheap", 'price_unit': 42, 'product_uom_qty': 2, }), Command.create({'product_id': self.product2.id, 'name': "Monthly expensive", 'price_unit': 420, 'product_uom_qty': 3, }), ] }) self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() self.assertEqual(self.subscription.order_line.sorted('id').mapped('product_uom_qty'), [2, 3], "Quantities should be equal to 2 and 3") with freeze_time("2021-01-15"): action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) self.assertEqual(upsell_so.partner_invoice_id, self.partner_a_invoice) self.assertEqual(upsell_so.partner_shipping_id, self.partner_a_shipping) self.assertEqual(upsell_so.order_line.mapped('product_uom_qty'), [0, 0, 0], 'The upsell order has 0 quantity') note = upsell_so.order_line.filtered('display_type') self.assertEqual(note.name, 'Recurring products are discounted according to the prorated period from 01/15/2021 to 01/31/2021') self.assertEqual(upsell_so.order_line.product_id, self.subscription.order_line.product_id) upsell_so.order_line.filtered(lambda l: not l.display_type).product_uom_qty = 1 # When the upsell order is created, all quantities are equal to 0 # add line to quote manually, it must be taken into account in the subscription after validation upsell_so.order_line = [(0, 0, { 'name': self.product2.name, 'order_id': upsell_so.id, 'product_id': self.product2.id, 'product_uom_qty': 2, 'product_uom': self.product2.uom_id.id, 'price_unit': self.product2.list_price, }), (0, 0, { 'name': self.product3.name, 'order_id': upsell_so.id, 'product_id': self.product3.id, 'product_uom_qty': 1, 'product_uom': self.product3.uom_id.id, 'price_unit': self.product3.list_price, })] upsell_so.action_confirm() self.subscription._create_recurring_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() discounts = [round(v, 2) for v in upsell_so.order_line.sorted('discount').mapped('discount')] self.assertEqual(discounts, [0.0, 45.16, 45.16, 45.16, 45.16], 'Prorated prices should be applied') prices = [round(v, 2) for v in upsell_so.order_line.sorted('id').mapped('price_subtotal')] self.assertEqual(prices, [23.03, 230.33, 0, 21.94, 23.03], 'Prorated prices should be applied') with freeze_time("2021-02-01"): self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() with freeze_time("2021-03-01"): self.env['sale.order']._cron_recurring_create_invoice() upsell_so._create_invoices() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() sorted_lines = self.subscription.order_line.sorted('id') self.assertEqual(sorted_lines.mapped('product_uom_qty'), [3.0, 4.0, 2.0, 1.0], "Quantities should be equal to 3.0, 4.0, 2.0, 1.0") with freeze_time("2021-04-01"): self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() with freeze_time("2021-05-01"): self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() with freeze_time("2021-06-01"): self.subscription._create_recurring_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() with freeze_time("2021-07-01"): self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() with freeze_time("2021-08-01"): self.env['sale.order']._cron_recurring_create_invoice() self.subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() inv = self.subscription.invoice_ids.sorted('date')[-1] invoice_periods = inv.invoice_line_ids.sorted('id').mapped('name') first_period = invoice_periods[0].split('\n')[1] self.assertEqual(first_period, "08/01/2021 to 08/31/2021") second_period = invoice_periods[1].split('\n')[1] self.assertEqual(second_period, "08/01/2021 to 08/31/2021") self.assertEqual(len(self.subscription.order_line), 4) def test_upsell_prorata(self): """ Test the prorated values obtained when creating an upsell. complementary to the previous one where new lines had no existing default values. """ self.env['sale.subscription.pricing'].create({'plan_id': self.plan_2_month.id, 'product_template_id': self.sub_product_tmpl.id, 'price': 42}) self.env['sale.subscription.pricing'].create( {'plan_id': self.plan_2_month.id, 'product_template_id': self.product_tmpl_2.id, 'price': 42}) with freeze_time("2021-01-01"): self.subscription.order_line = False self.subscription.start_date = False self.subscription.next_invoice_date = False self.subscription.write({ 'partner_id': self.partner.id, 'plan_id': self.plan_2_month.id, 'order_line': [ Command.create({ 'product_id': self.product.id, 'name': "month: original", 'price_unit': 50, 'product_uom_qty': 1, }), Command.create({ 'product_id': self.product2.id, 'name': "2 month: original", 'price_unit': 50, 'product_uom_qty': 1, }), ] }) self.subscription.action_confirm() self.subscription._create_recurring_invoice() with freeze_time("2021-01-20"): action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) # Create new lines that should be aligned with existing ones so_line_vals = [{ 'name': 'Upsell added: 1 month', 'order_id': upsell_so.id, 'product_id': self.product2.id, 'product_uom_qty': 1, 'product_uom': self.product2.uom_id.id, 'price_unit': self.product.list_price, }, { 'name': 'Upsell added: 2 month', 'order_id': upsell_so.id, 'product_id': self.product3.id, 'product_uom_qty': 1, 'product_uom': self.product3.uom_id.id, 'price_unit': self.product3.list_price, }] self.env['sale.order.line'].create(so_line_vals) upsell_so.order_line.product_uom_qty = 1 discounts = [round(v) for v in upsell_so.order_line.sorted('discount').mapped('discount')] # discounts for: 40/59 days self.assertEqual(discounts, [0, 32, 32, 32, 32], 'Prorated prices should be applied') self.assertEqual(self.subscription.order_line.ids, upsell_so.order_line.parent_line_id.ids, "The parent line id should correspond to the first two lines") # discounts for: 12d/31d; 40d/59d; 21d/31d (shifted); 31d/41d; 59d/78d; self.assertEqual(discounts, [0, 32, 32, 32, 32], 'Prorated prices should be applied') prices = [round(v, 2) for v in upsell_so.order_line.sorted('price_subtotal').mapped('price_subtotal')] self.assertEqual(prices, [0.0, 28.48, 28.48, 28.48, 28.48], 'Prorated prices should be applied') def test_recurring_revenue(self): """Test computation of recurring revenue""" # Initial subscription is $100/y self.subscription_tmpl.write({'duration_value': 1, 'duration_unit': 'year'}) self.subscription.write({ 'plan_id': self.plan_2_month.id, 'start_date': False, 'next_invoice_date': False, 'partner_id': self.partner.id, 'company_id': self.company.id, 'payment_token_id': self.payment_token.id, }) self.subscription.order_line[0].write({'price_unit': 1200}) self.subscription.order_line[1].write({'price_unit': 200}) self.subscription.action_confirm() self.assertAlmostEqual(self.subscription.amount_untaxed, 1400, msg="unexpected price after setup") self.assertAlmostEqual(self.subscription.recurring_monthly, 700, msg="Half because invoice every two months") # Change periodicity self.subscription.order_line.product_id.product_subscription_pricing_ids = [(6, 0, 0)] # remove all pricings to fallaback on list price self.subscription.plan_id = self.plan_year self.assertAlmostEqual(self.subscription.amount_untaxed, 70, msg='Recompute price_unit : 50 (product) + 20 (product2)') # 1200 over 4 year = 25/year + 100 per month self.assertAlmostEqual(self.subscription.recurring_monthly, 5.84, msg='70 / 12') def test_compute_kpi(self): self.env['sale.order.alert'].create([ { 'name': 'Bad domain Setup', 'trigger_condition': 'on_create_or_write', 'mrr_min': 0, 'mrr_max': 80, 'subscription_state': '3_progress', 'action': 'set_health_value', 'health': 'bad' }, { 'name': 'Good domain Setup', 'trigger_condition': 'on_create_or_write', 'mrr_min': 120, 'mrr_max': 9999, 'action': 'set_health_value', 'health': 'done' }, ]) self.subscription.action_confirm() self.env['sale.order']._cron_update_kpi() self.assertEqual(self.subscription.health, 'bad') # 16 to 6 weeks: 80 # 6 to 2 weeks: 100 # 2weeks - today : 120 date_log = datetime.date.today() - relativedelta(weeks=16) self.env['sale.order.log'].sudo().create({ 'event_type': '1_expansion', 'event_date': date_log, 'create_date': date_log, 'order_id': self.subscription.id, 'recurring_monthly': 80, 'amount_signed': 80, 'currency_id': self.subscription.currency_id.id, 'subscription_state': self.subscription.subscription_state, 'user_id': self.subscription.user_id.id, 'team_id': self.subscription.team_id.id, }) date_log = datetime.date.today() - relativedelta(weeks=6) self.env['sale.order.log'].sudo().create({ 'event_type': '1_expansion', 'event_date': date_log, 'create_date': date_log, 'order_id': self.subscription.id, 'recurring_monthly': 100, 'amount_signed': 20, 'currency_id': self.subscription.currency_id.id, 'subscription_state': self.subscription.subscription_state, 'user_id': self.subscription.user_id.id, 'team_id': self.subscription.team_id.id, }) self.subscription.recurring_monthly = 120.0 date_log = datetime.date.today() - relativedelta(weeks=2) self.env['sale.order.log'].sudo().create({ 'event_type': '1_expansion', 'event_date': date_log, 'create_date': date_log, 'order_id': self.subscription.id, 'recurring_monthly': 120, 'amount_signed': 20, 'currency_id': self.subscription.currency_id.id, 'subscription_state': self.subscription.subscription_state, 'user_id': self.subscription.user_id.id, 'team_id': self.subscription.team_id.id, }) self.subscription._cron_update_kpi() self.assertEqual(self.subscription.kpi_1month_mrr_delta, 20.0) self.assertEqual(self.subscription.kpi_1month_mrr_percentage, 0.2) self.assertEqual(self.subscription.kpi_3months_mrr_delta, 40.0) self.assertEqual(self.subscription.kpi_3months_mrr_percentage, 0.5) self.assertEqual(self.subscription.health, 'done') def test_onchange_date_start(self): recurring_bound_tmpl = self.env['sale.order.template'].create({ 'name': 'Recurring Bound Template', 'plan_id': self.plan_month.id, 'is_unlimited': False, 'duration_unit': 'month', 'duration_value': 3, 'sale_order_template_line_ids': [Command.create({ 'name': "monthly", 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom_id': self.product.uom_id.id })] }) sub_form = Form(self.env['sale.order']) sub_form.partner_id = self.user_portal.partner_id sub_form.sale_order_template_id = recurring_bound_tmpl sub = sub_form.save() sub._onchange_sale_order_template_id() # The end date is set upon confirmation sub.action_confirm() self.assertEqual(sub.sale_order_template_id.is_unlimited, False) self.assertIsInstance(sub.end_date, datetime.date) def test_changed_next_invoice_date(self): # Test wizard to change next_invoice_date manually with freeze_time("2022-01-01"): self.subscription.write({'start_date': False, 'next_invoice_date': False}) self.env['sale.order.line'].create({ 'name': self.product2.name, 'order_id': self.subscription.id, 'product_id': self.product2.id, 'product_uom_qty': 3, 'product_uom': self.product2.uom_id.id, 'price_unit': 42}) self.subscription.action_confirm() self.subscription._create_recurring_invoice() today = fields.Date.today() self.assertEqual(self.subscription.start_date, today, "start date should be set to today") self.assertEqual(self.subscription.next_invoice_date, datetime.date(2022, 2, 1)) # We decide to invoice the monthly subscription on the 5 of february self.subscription.next_invoice_date = fields.Date.from_string('2022-02-05') # check the invoice state self.assertEqual(self.subscription.invoice_status, 'invoiced') with freeze_time("2022-02-01"): # Nothing should be invoiced self.subscription._cron_recurring_create_invoice() # next_invoice_date : 2022-02-5 but the previous invoice deferred_end_date was set on the 2022-02-01 # We can't prevent it to be re-invoiced. inv = self.subscription.invoice_ids.sorted('date') # Nothing was invoiced self.assertEqual(inv.date, datetime.date(2022, 1, 1)) with freeze_time("2022-02-05"): self.subscription._cron_recurring_create_invoice() inv = self.subscription.invoice_ids.sorted('date') self.assertEqual(inv[-1].date, datetime.date(2022, 2, 5)) self.assertEqual(self.subscription.invoice_status, 'invoiced') def test_product_change(self): """Check behaviour of the product onchange (taxes mostly).""" # check default tax self.sub_product_tmpl.product_subscription_pricing_ids = [(6, 0, self.pricing_month.ids)] self.pricing_month.price = 50 self.subscription.order_line.unlink() sub_form = Form(self.subscription) sub_form.plan_id = self.plan_month with sub_form.order_line.new() as line: line.product_id = self.product sub = sub_form.save() self.assertEqual(sub.order_line.tax_id, self.tax_10, 'Default tax for product should have been applied.') self.assertEqual(sub.amount_tax, 5.0, 'Default tax for product should have been applied.') self.assertEqual(sub.amount_total, 55.0, 'Default tax for product should have been applied.') # Change the product line_id = sub.order_line.ids sub.write({ 'order_line': [(1, line_id[0], {'product_id': self.product4.id})] }) self.assertEqual(sub.order_line.tax_id, self.tax_20, 'Default tax for product should have been applied.') self.assertEqual(sub.amount_tax, 3, 'Default tax for product should have been applied.') self.assertEqual(sub.amount_total, 18, 'Default tax for product should have been applied.') def test_log_change_pricing(self): """ Test subscription log generation when template_id is changed """ self.sub_product_tmpl.product_subscription_pricing_ids.price = 120 # 120 for monthly and yearly # Create a subscription and add a line, should have logs with MMR 120 subscription = self.env['sale.order'].create({ 'name': 'TestSubscription', 'start_date': False, 'next_invoice_date': False, 'plan_id': self.plan_month.id, 'partner_id': self.user_portal.partner_id.id, 'sale_order_template_id': self.subscription_tmpl.id, }) self.cr.precommit.clear() subscription.write({'order_line': [(0, 0, { 'name': 'TestRecurringLine', 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id})]}) subscription.action_confirm() self.flush_tracking() init_nb_log = len(subscription.order_log_ids) self.assertEqual(subscription.order_line.recurring_monthly, 120) subscription.plan_id = self.plan_year self.assertEqual(subscription.order_line.recurring_monthly, 10) self.flush_tracking() # Should get one more log with MRR 10 (so change is -110) self.assertEqual(len(subscription.order_log_ids), init_nb_log + 1, "Subscription log not generated after change of the subscription template") self.assertRecordValues(subscription.order_log_ids[-1], [{'recurring_monthly': 10.0, 'amount_signed': -110}]) def test_fiscal_position(self): # Test that the fiscal postion FP is applied on recurring invoice. # FP must mapped an included tax of 21% to an excluded one of 0% tax_include_id = self.env['account.tax'].create({'name': "Include tax", 'amount': 21.0, 'price_include': True, 'type_tax_use': 'sale'}) tax_exclude_id = self.env['account.tax'].create({'name': "Exclude tax", 'amount': 0.0, 'type_tax_use': 'sale'}) product_tmpl = self.env['product.template'].create(dict(name="Voiture", list_price=121, taxes_id=[(6, 0, [tax_include_id.id])])) fp = self.env['account.fiscal.position'].create({'name': "fiscal position", 'sequence': 1, 'auto_apply': True, 'tax_ids': [(0, 0, {'tax_src_id': tax_include_id.id, 'tax_dest_id': tax_exclude_id.id})]}) self.subscription.fiscal_position_id = fp.id self.subscription.partner_id.property_account_position_id = fp sale_order = self.env['sale.order'].create({ 'name': 'TestSubscription', 'fiscal_position_id': fp.id, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'order_line': [Command.create({ 'product_id': product_tmpl.product_variant_id.id, 'product_uom': self.env.ref('uom.product_uom_unit').id, 'product_uom_qty': 1 })] }) sale_order.action_confirm() inv = sale_order._create_invoices() self.assertEqual(100, inv.invoice_line_ids[0].price_unit, "The included tax must be subtracted to the price") def test_quantity_on_product_invoice_ordered_qty(self): # This test checks that the invoiced qty and to_invoice qty have the right behavior # Service product self.product.write({ 'detailed_type': 'service' }) with freeze_time("2021-01-01"): self.subscription.order_line = False self.subscription.write({ 'start_date': False, 'next_invoice_date': False, 'plan_id': self.plan_month.id, 'partner_id': self.partner.id, 'order_line': [Command.create({'product_id': self.product2.id, 'price_unit': 420, 'product_uom_qty': 3, }), Command.create({'product_id': self.product.id, 'price_unit': 42, 'product_uom_qty': 1, }), ] }) self.subscription.action_confirm() val_confirm = self._get_quantities(self.subscription.order_line) self.assertEqual(val_confirm['to_invoice'], [3, 1], "To invoice should be equal to quantity") self.assertEqual(val_confirm['invoiced'], [0, 0], "To invoice should be equal to quantity") self.assertEqual(val_confirm['delivered_qty'], [0, 0], "Delivered qty not should be set") self.env['sale.order']._cron_recurring_create_invoice() self.subscription.order_line[0].write({'qty_delivered': 3}) self.subscription.order_line[1].write({'qty_delivered': 1}) val_invoice = self._get_quantities(self.subscription.order_line) self.assertEqual(val_invoice['to_invoice'], [0, 0], "To invoice should be 0") self.assertEqual(val_invoice['invoiced'], [3, 1], "To invoice should be equal to quantity") self.assertEqual(val_invoice['delivered_qty'], [3, 1], "Delivered qty should be set") with freeze_time("2021-02-02"): self.env['sale.order']._cron_recurring_create_invoice() val_invoice = self._get_quantities(self.subscription.order_line) self.assertEqual(val_invoice['to_invoice'], [0, 0], "To invoice should be 0") self.assertEqual(val_invoice['invoiced'], [3, 1], "To invoice should be equal to quantity") self.assertEqual(val_invoice['delivered_qty'], [3, 1], "Delivered qty should be equal to quantity") with freeze_time("2021-02-15"): self.subscription.order_line[1].write({'qty_delivered': 3, 'product_uom_qty': 3}) val_invoice = self._get_quantities( self.subscription.order_line ) self.assertEqual(val_invoice['to_invoice'], [0, 2], "To invoice should be equal to quantity") self.assertEqual(val_invoice['invoiced'], [3, 1], "invoiced should be correct") self.assertEqual(val_invoice['delivered_qty'], [3, 3], "Delivered qty should be equal to quantity") with freeze_time("2021-03-01"): self.env['sale.order']._cron_recurring_create_invoice() self.env.invalidate_all() val_invoice = self._get_quantities(self.subscription.order_line) self.assertEqual(val_invoice['to_invoice'], [0, 0], "To invoice should be equal to quantity") self.assertEqual(val_invoice['delivered_qty'], [3, 3], "Delivered qty should be equal to quantity") self.assertEqual(val_invoice['invoiced'], [3, 3], "To invoice should be equal to quantity delivered") def test_update_prices_template(self): recurring_bound_tmpl = self.env['sale.order.template'].create({ 'name': 'Subscription template without discount', 'duration_unit': 'year', 'is_unlimited': False, 'duration_value': 2, 'plan_id': self.plan_month.id, 'note': "This is the template description", 'sale_order_template_line_ids': [ Command.create({ 'name': "monthly", 'product_id': self.product.id, 'product_uom_id': self.product.uom_id.id }), Command.create({ 'name': "yearly", 'product_id': self.product.id, 'product_uom_id': self.product.uom_id.id, }), ], 'sale_order_template_option_ids': [ Command.create({ 'name': "option", 'product_id': self.product.id, 'quantity': 1, 'uom_id': self.product2.uom_id.id }), ], }) sub_form = Form(self.env['sale.order']) sub_form.partner_id = self.user_portal.partner_id sub_form.sale_order_template_id = recurring_bound_tmpl sub = sub_form.save() self.assertEqual(len(sub.order_line.ids), 2) def test_product_invoice_delivery(self): sub = self.subscription sub.order_line = [Command.clear()] context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } delivered_product_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Delivery product', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'invoice_policy': 'delivery', }) product = delivered_product_tmpl.product_variant_id product.write({ 'list_price': 50.0, 'taxes_id': [(6, 0, [self.tax_10.id])], 'property_account_income_id': self.account_income.id, }) with freeze_time("2021-01-03"): # January sub.plan_id = self.plan_month sub.start_date = False sub.next_invoice_date = False sub.order_line = [Command.create({'product_id': product.id, 'name': "coucou", 'price_unit': 42, 'product_uom_qty': 1, })] sub.action_confirm() sub._create_recurring_invoice() self.assertFalse(sub.order_line.qty_delivered) # We only invoice what we deliver self.assertFalse(sub.order_line.qty_to_invoice) self.assertFalse(sub.invoice_count, "We don't invoice if we don't deliver the product") self.assertEqual(sub.next_invoice_date, datetime.date(2021, 2, 3), 'But we still update the next invoice date') with freeze_time("2021-02-03"): # Deliver some product sub.order_line.qty_delivered = 1 self.assertEqual(sub.order_line.qty_to_invoice, 1) sub._create_recurring_invoice() sub.invoice_ids.filtered(lambda am: am.state == 'draft')._post() self.assertTrue(sub.invoice_count, "We should have invoiced") self.assertEqual(sub.next_invoice_date, datetime.date(2021, 3, 3)) with freeze_time("2021-03-03"): sub._create_recurring_invoice() # The quantity to invoice and delivered are reset after the creation of the invoice self.assertTrue(sub.order_line.qty_delivered) inv = sub.invoice_ids.sorted('date')[-1] self.assertEqual(inv.invoice_line_ids.quantity, 1) with freeze_time("2021-04-03"): # February sub.order_line.qty_delivered = 1 sub._create_recurring_invoice() self.assertEqual(sub.order_line.qty_delivered, 1) inv = sub.invoice_ids.sorted('date')[-1] self.assertEqual(inv.invoice_line_ids.quantity, 1) with freeze_time("2021-05-03"): # March sub.order_line.qty_delivered = 2 sub._create_recurring_invoice() inv = sub.invoice_ids.sorted('date')[-1] self.assertEqual(inv.invoice_line_ids.quantity, 2) self.assertEqual(sub.order_line.product_uom_qty, 1) def test_recurring_invoices_from_interface(self): # From the interface, all the subscription lines are invoiced sub = self.subscription sub.end_date = datetime.date(2029, 4, 1) with freeze_time("2021-01-01"): self.subscription.write({'start_date': False, 'next_invoice_date': False, 'plan_id': self.plan_month.id}) sub.action_confirm() # first invoice: automatic or not, it's the same behavior. All line are invoiced sub._create_invoices() sub.order_line.invoice_lines.move_id._post() self.assertEqual("2021-02-01", sub.next_invoice_date.strftime("%Y-%m-%d")) inv = sub.invoice_ids.sorted('date')[-1] invoice_start_periods = inv.invoice_line_ids.mapped('deferred_start_date') invoice_end_periods = inv.invoice_line_ids.mapped('deferred_end_date') self.assertEqual(invoice_start_periods, [datetime.date(2021, 1, 1), datetime.date(2021, 1, 1)]) self.assertEqual(invoice_end_periods, [datetime.date(2021, 1, 31), datetime.date(2021, 1, 31)]) with freeze_time("2021-02-01"): sub._create_invoices() inv = sub.invoice_ids.sorted('date')[-1] invoice_start_periods = inv.invoice_line_ids.mapped('deferred_start_date') invoice_end_periods = inv.invoice_line_ids.mapped('deferred_end_date') self.assertEqual(invoice_start_periods, [datetime.date(2021, 2, 1), datetime.date(2021, 2, 1)], "monthly is updated everytime in manual action") self.assertEqual(invoice_end_periods, [datetime.date(2021, 2, 28), datetime.date(2021, 2, 28)], "both lines are invoiced") with self.assertRaisesRegex(UserError, 'The following recurring orders have draft invoices. Please Confirm them or cancel them'): sub._create_invoices() inv._post() self.assertEqual("2021-03-01", sub.next_invoice_date.strftime("%Y-%m-%d"), "Next invoice date should be updated") sub._create_invoices() inv = sub.invoice_ids.sorted('id')[-1] inv._post() self.assertEqual("2021-04-01", sub.next_invoice_date.strftime("%Y-%m-%d")) invoice_start_periods = inv.invoice_line_ids.mapped('deferred_start_date') invoice_end_periods = inv.invoice_line_ids.mapped('deferred_end_date') self.assertEqual(invoice_start_periods, [datetime.date(2021, 3, 1), datetime.date(2021, 3, 1)], "monthly is updated everytime in manual action") self.assertEqual(invoice_end_periods, [datetime.date(2021, 3, 31), datetime.date(2021, 3, 31)], "monthly is updated everytime in manual action") with freeze_time("2021-04-01"): # Automatic invoicing, only one line generated inv = sub._create_recurring_invoice() invoice_start_periods = inv.invoice_line_ids.mapped('deferred_start_date') invoice_end_periods = inv.invoice_line_ids.mapped('deferred_end_date') self.assertEqual(invoice_start_periods, [datetime.date(2021, 4, 1), datetime.date(2021, 4, 1)], "Monthly is updated because it is due") self.assertEqual(invoice_end_periods, [datetime.date(2021, 4, 30), datetime.date(2021, 4, 30)], "Monthly is updated because it is due") self.assertEqual(inv.date, datetime.date(2021, 4, 1)) with freeze_time("2021-05-01"): # Automatic invoicing, only one line generated sub._create_recurring_invoice() inv = sub.invoice_ids.sorted('date')[-1] invoice_start_periods = inv.invoice_line_ids.mapped('deferred_start_date') invoice_end_periods = inv.invoice_line_ids.mapped('deferred_end_date') self.assertEqual(invoice_start_periods, [datetime.date(2021, 5, 1), datetime.date(2021, 5, 1)], "Monthly is updated because it is due") self.assertEqual(invoice_end_periods, [datetime.date(2021, 5, 31), datetime.date(2021, 5, 31)], "Monthly is updated because it is due") self.assertEqual(inv.date, datetime.date(2021, 5, 1)) with freeze_time("2022-02-02"): # We prevent the subscription to be automatically closed because the next invoice date is passed for too long sub.plan_id.auto_close_limit = 999 # With non-automatic, we invoice all line prior to today once inv = sub._create_invoices() inv._post() self.assertEqual("2021-07-01", sub.next_invoice_date.strftime("%Y-%m-%d"), "on the 1st of may, nid is updated to 1fst of june and here we force the line to be apdated again") inv = sub.invoice_ids.sorted('date')[-1] invoice_start_periods = inv.invoice_line_ids.mapped('deferred_start_date') invoice_end_periods = inv.invoice_line_ids.mapped('deferred_end_date') self.assertEqual(invoice_start_periods, [datetime.date(2021, 6, 1), datetime.date(2021, 6, 1)], "monthly is updated when prior to today") self.assertEqual(invoice_end_periods, [datetime.date(2021, 6, 30), datetime.date(2021, 6, 30)], "monthly is updated when prior to today") def test_renew_kpi_mrr(self): # Test that renew with MRR transfer give correct result # First, whe create a sub with MRR = 21 # Then we renew it with a MRR of 42 # After a few months the MRR of the renewal is 63 # We also create and renew a free subscription SaleOrder = self.env["sale.order"] with freeze_time("2021-01-01"), patch.object(type(SaleOrder), '_get_unpaid_subscriptions', lambda x: []): self.subscription.plan_id.auto_close_limit = 5000 # don't close automatically contract if unpaid invoices # so creation with mail tracking context_mail = {'tracking_disable': False} sub = self.env['sale.order'].with_context(context_mail).create({ 'name': 'Parent Sub', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'sale_order_template_id': self.subscription_tmpl.id, }) free_sub = self.env['sale.order'].with_context(context_mail).create({ 'name': 'Parent free Sub', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'plan_id': self.plan_month.id, 'client_order_ref': 'free', 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, 'price_unit': 0, })], }) future_sub = self.env['sale.order'].with_context(context_mail).create({ 'name': 'FutureSub', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'plan_id': self.plan_month.id, 'start_date': '2021-06-01', 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, })], }) self.assertFalse(free_sub.amount_total) self.flush_tracking() sub._onchange_sale_order_template_id() # Same product for both lines sub.order_line.product_uom_qty = 1 (free_sub | sub).end_date = datetime.date(2022, 1, 1) (free_sub | sub | future_sub).action_confirm() self.flush_tracking() self.assertEqual(sub.recurring_monthly, 21, "20 + 1 for both lines") self.assertEqual(sub.subscription_state, "3_progress") self.env['sale.order'].with_context(tracking_disable=False)._cron_recurring_create_invoice() with freeze_time("2021-02-01"): self.env['sale.order'].with_context(tracking_disable=False)._cron_recurring_create_invoice() with freeze_time("2021-03-01"): self.env['sale.order'].with_context(tracking_disable=False)._cron_recurring_create_invoice() with freeze_time("2021-04-01"): # We create a renewal order in april for the new year self.env['sale.order']._cron_recurring_create_invoice() action = sub.with_context(tracking_disable=False).prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so = renewal_so.with_context(tracking_disable=False) renewal_so.order_line.product_uom_qty = 3 renewal_so.name = "Renewal" self.flush_tracking() action = free_sub.with_context(tracking_disable=False).prepare_renewal_order() free_renewal_so = self.env['sale.order'].browse(action['res_id']) free_renewal_so = free_renewal_so.with_context(tracking_disable=False) free_renewal_so.order_line.write({'product_uom_qty': 2, 'price_unit': 0}) self.flush_tracking() self.assertEqual(renewal_so.subscription_state, '2_renewal') (sub | free_sub).pause_subscription() # we pause the contracts to make sure no parasite log are created self.flush_tracking() self.env['sale.order']._cron_recurring_create_invoice() self.flush_tracking() (renewal_so | free_renewal_so).action_confirm() self.flush_tracking() self.assertEqual(sub.subscription_state, '5_renewed') self.assertEqual(renewal_so.subscription_state, '3_progress') (sub | free_sub).resume_subscription() # we resume the contracts to make sure no parasite log are created self.flush_tracking() # Most of the time, the renewal invoice is created by the salesman # before the renewal start date renewal_invoices = (free_renewal_so | renewal_so)._create_invoices() renewal_invoices._post() self.flush_tracking() # "upsell" of the simple sub that did not start yet future_sub.order_line.product_uom_qty = 4 self.flush_tracking() self.assertEqual(sub.recurring_monthly, 21, "MRR should still be non null") self.assertEqual(sub.subscription_state, '5_renewed') self.assertEqual(renewal_so.recurring_monthly, 63, "MRR of renewal should not be computed before start_date of the lines") self.flush_tracking() # renew is still not ongoing; Total MRR is 21 coming from the original sub self.env['sale.order'].sudo()._cron_subscription_expiration() self.assertEqual(sub.recurring_monthly, 21) self.assertEqual(renewal_so.recurring_monthly, 63) self.env['sale.order']._cron_recurring_create_invoice() self.flush_tracking() self.subscription._cron_update_kpi() self.assertEqual(sub.kpi_1month_mrr_delta, 0) self.assertEqual(sub.kpi_1month_mrr_percentage, 0) self.assertEqual(sub.kpi_3months_mrr_delta, 0) self.assertEqual(sub.kpi_3months_mrr_percentage, 0) self.assertEqual(sub.subscription_state, '5_renewed') with freeze_time("2021-04-20"): # We upsell the renewal after it's confirmation but before its start_date The event date must be "today" self.flush_tracking() renewal_so.order_line[1].product_uom_qty += 1 self.flush_tracking() with freeze_time("2021-05-05"): # We switch the cron the X of may to make sure the day of the cron does not affect the numbers # Renewal period is from 2021-05 to 2021-06 self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(sub.recurring_monthly, 21) self.assertEqual(sub.subscription_state, '5_renewed') self.assertEqual(renewal_so.next_invoice_date, datetime.date(2021, 6, 1)) self.assertEqual(renewal_so.recurring_monthly, 83) self.flush_tracking() with freeze_time("2021-05-15"): self.env['sale.order']._cron_recurring_create_invoice() sub.order_line._compute_recurring_monthly() self.flush_tracking() with freeze_time("2021-06-01"): self.subscription._cron_update_kpi() self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(sub.recurring_monthly, 21) self.assertEqual(renewal_so.recurring_monthly, 83) self.flush_tracking() with freeze_time("2021-07-01"), patch.object(type(SaleOrder), '_get_unpaid_subscriptions', lambda x: []): # Total MRR is 42 coming from renew self.subscription._cron_update_kpi() self.env['sale.order']._cron_recurring_create_invoice() self.env['sale.order']._cron_subscription_expiration() # we trigger the compute because it depends on today value. self.assertEqual(sub.recurring_monthly, 21) self.assertEqual(renewal_so.recurring_monthly, 83) self.flush_tracking() with freeze_time("2021-08-03"), patch.object(type(SaleOrder), '_get_unpaid_subscriptions', lambda x: []): # We switch the cron the X of august to make sure the day of the cron does not affect the numbers renewal_so.end_date = datetime.date(2032, 1, 1) self.flush_tracking() # Total MRR is 80 coming from renewed sub self.env['sale.order']._cron_recurring_create_invoice() self.env['sale.order'].sudo()._cron_subscription_expiration() self.assertEqual(sub.recurring_monthly, 21) self.assertEqual(renewal_so.recurring_monthly, 83) self.assertEqual(sub.subscription_state, '5_renewed') self.flush_tracking() with freeze_time("2021-09-01"), patch.object(type(SaleOrder), '_get_unpaid_subscriptions', lambda x: []): renewal_so.order_line.product_uom_qty = 5 # We update the MRR of the renewed self.env['sale.order']._cron_recurring_create_invoice() self.env['sale.order']._cron_subscription_expiration() self.assertEqual(renewal_so.recurring_monthly, 105) # free subscription is not free anymore free_renewal_so.order_line.price_unit = 10 self.flush_tracking() self.subscription._cron_update_kpi() self.assertEqual(sub.kpi_1month_mrr_delta, 0) self.assertEqual(sub.kpi_1month_mrr_percentage, 0) self.assertEqual(sub.kpi_3months_mrr_delta, 0) self.assertEqual(sub.kpi_3months_mrr_percentage, 0) self.assertEqual(renewal_so.kpi_1month_mrr_delta, 22) self.assertEqual(round(renewal_so.kpi_1month_mrr_percentage, 2), 0.27) self.assertEqual(renewal_so.kpi_3months_mrr_delta, 22) self.assertEqual(round(renewal_so.kpi_3months_mrr_percentage, 2), 0.27) order_log_ids = sub.order_log_ids.sorted('event_date') sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', datetime.date(2021, 1, 1), '3_progress', 21, 21), ('3_transfer', datetime.date(2021, 4, 1), '5_renewed', -21, 0)]) renew_logs = renewal_so.order_log_ids.sorted(key=lambda log: (log.event_date, log.id)) renew_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in renew_logs] self.assertEqual(renew_data, [('3_transfer', datetime.date(2021, 4, 1), '3_progress', 21.0, 21.0), ('1_expansion', datetime.date(2021, 4, 1), '3_progress', 42.0, 63.0), ('1_expansion', datetime.date(2021, 4, 20), '3_progress', 20.0, 83.0), ('1_expansion', datetime.date(2021, 9, 1), '3_progress', 22, 105.0)]) self.assertEqual(renewal_so.start_date, datetime.date(2021, 5, 1), "the renewal starts on the firsts of May even if transfer occurs on first of April") free_log_ids = free_sub.order_log_ids.sorted(key=lambda log: (log.event_date, log.id)) sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in free_log_ids] self.assertEqual(sub_data, [('0_creation', datetime.date(2021, 1, 1), '3_progress', 0, 0), ('3_transfer', datetime.date(2021, 4, 1), '5_renewed', 0, 0)]) renew_logs = free_renewal_so.order_log_ids.sorted(key=lambda log: (log.event_date, log.id)) renew_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in renew_logs] self.assertEqual(renew_data, [('3_transfer', datetime.date(2021, 4, 1), '3_progress', 0, 0), ('1_expansion', datetime.date(2021, 9, 1), '3_progress', 20.0, 20.0)]) future_data = future_sub.order_log_ids.sorted(key=lambda log: (log.event_date, log.id)) # several events aggregated on the same date simple_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in future_data] self.assertEqual(simple_data, [('0_creation', datetime.date(2021, 1, 1), '3_progress', 1.0, 1.0), ('1_expansion', datetime.date(2021, 4, 1), '3_progress', 3.0, 4.0)]) self.assertEqual(future_sub.start_date, datetime.date(2021, 6, 1), "the start date is in june but the events are recorded as today") def test_option_template(self): self.product.product_tmpl_id.product_subscription_pricing_ids = [(6, 0, 0)] self.env['sale.subscription.pricing'].create({ 'price': 10, 'plan_id': self.plan_year.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'product_template_id': self.product.product_tmpl_id.id }) other_pricelist = self.env['product.pricelist'].create({ 'name': 'New pricelist', 'currency_id': self.company.currency_id.id, }) self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_year.id, 'pricelist_id': other_pricelist.id, 'price': 15, 'product_template_id': self.product.product_tmpl_id.id }) template = self.env['sale.order.template'].create({ 'name': 'Subscription template without discount', 'is_unlimited': True, 'note': "This is the template description", 'plan_id': self.plan_year.id, 'sale_order_template_line_ids': [Command.create({ 'name': "monthly", 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom_id': self.product.uom_id.id })], 'sale_order_template_option_ids': [Command.create({ 'name': "line 1", 'product_id': self.product.id, 'quantity': 1, 'uom_id': self.product.uom_id.id, })], }) subscription = self.env['sale.order'].create({ 'name': 'TestSubscription', 'is_subscription': True, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'sale_order_template_id': template.id, }) subscription._onchange_sale_order_template_id() self.assertEqual(subscription.order_line.price_unit, 10, "The second pricing should be applied") self.assertEqual(subscription.sale_order_option_ids.price_unit, 10, "The second pricing should be applied") subscription.pricelist_id = other_pricelist.id subscription._onchange_sale_order_template_id() self.assertEqual(subscription.pricelist_id.id, other_pricelist.id, "The second pricelist should be applied") self.assertEqual(subscription.order_line.price_unit, 15, "The second pricing should be applied") self.assertEqual(subscription.sale_order_option_ids.price_unit, 15, "The second pricing should be applied") # Note: the pricing_id on the line is not saved on the line, but it is used to calculate the price. def test_update_subscription_company(self): """ Update the taxes of confirmed lines when the subscription company is updated """ tax_group_1 = self.env['account.tax.group'].create({ 'name': 'Test tax group', 'tax_receivable_account_id': self.company_data['default_account_receivable'].copy().id, 'tax_payable_account_id': self.company_data['default_account_payable'].copy().id, }) sale_tax_percentage_incl_1 = self.env['account.tax'].create({ 'name': 'sale_tax_percentage_incl_1', 'amount': 20.0, 'amount_type': 'percent', 'type_tax_use': 'sale', 'price_include': True, 'tax_group_id': tax_group_1.id, }) other_company_data = self.setup_company_data("Company 3", chart_template=self.env.company.chart_template) tax_group_2 = self.env['account.tax.group'].create({ 'name': 'Test tax group', 'company_id': other_company_data['company'].id, 'tax_receivable_account_id': other_company_data['default_account_receivable'].copy().id, 'tax_payable_account_id': other_company_data['default_account_payable'].copy().id, }) sale_tax_percentage_incl_2 = self.env['account.tax'].create({ 'name': 'sale_tax_percentage_incl_2', 'amount': 40.0, 'amount_type': 'percent', 'type_tax_use': 'sale', 'price_include': True, 'tax_group_id': tax_group_2.id, 'company_id': other_company_data['company'].id, }) self.product.write({ 'taxes_id': [(6, 0, [sale_tax_percentage_incl_1.id, sale_tax_percentage_incl_2.id])], }) simple_product = self.product.copy({'recurring_invoice': False}) simple_so = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, 'company_id': self.company_data['company'].id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': simple_product.id, 'product_uom_qty': 2.0, 'product_uom': simple_product.uom_id.id, 'price_unit': 12, })], }) self.assertEqual(simple_so.order_line.tax_id.id, sale_tax_percentage_incl_1.id, 'The so has the first tax') subscription = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, 'company_id': self.company_data['company'].id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) self.assertEqual(subscription.order_line.tax_id.id, sale_tax_percentage_incl_1.id) (simple_so | subscription).write({'company_id': other_company_data['company'].id}) self.assertEqual(simple_so.order_line.tax_id.id, sale_tax_percentage_incl_2.id, "Simple SO taxes must be recomputed on company change") self.assertEqual(subscription.order_line.tax_id.id, sale_tax_percentage_incl_2.id, "Subscription taxes must be recomputed on company change") def test_onchange_product_quantity_with_different_currencies(self): # onchange_product_quantity compute price unit into the currency of the sale_order pricelist # when currency of the product (Gold Coin) is different from subscription pricelist (USD) self.subscription.order_line = False self.subscription.plan_id = self.plan_month self.pricing_month.pricelist_id = self.subscription.pricelist_id self.pricing_month.price = 50 self.sub_product_tmpl.product_subscription_pricing_ids = [(6, 0, self.pricing_month.ids)] self.subscription.write({ 'order_line': [(0, 0, { 'name': 'TestRecurringLine', 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id, })], }) self.assertEqual(self.subscription.currency_id.name, 'USD') line = self.subscription.order_line self.assertEqual(line.price_unit, 50, 'Price unit should not have changed') currency = self.currency_data['currency'] self.product.currency_id = currency self.pricing_month.currency_id = currency line._compute_price_unit() conversion_rate = self.env['res.currency']._get_conversion_rate( self.product.currency_id, self.subscription.currency_id, self.product.company_id or self.env.company, fields.Date.today()) self.assertEqual(line.price_unit, self.subscription.currency_id.round(50 * conversion_rate), 'Price unit must be converted into the currency of the pricelist (USD)') def test_archive_partner_invoice_shipping(self): # archived a partner must not remain set on invoicing/shipping address in subscription # here, they are set manually on subscription self.subscription.action_confirm() self.subscription.write({ 'partner_invoice_id': self.partner_a_invoice.id, 'partner_shipping_id': self.partner_a_shipping.id, }) self.assertEqual(self.partner_a_invoice, self.subscription.partner_invoice_id, "Invoice address should have been set manually on the subscription.") self.assertEqual(self.partner_a_shipping, self.subscription.partner_shipping_id, "Delivery address should have been set manually on the subscription.") invoice = self.subscription._create_recurring_invoice() self.assertEqual(self.partner_a_invoice, invoice.partner_id, "On the invoice, invoice address should be the same as on the subscription.") self.assertEqual(self.partner_a_shipping, invoice.partner_shipping_id, "On the invoice, delivery address should be the same as on the subscription.") with self.assertRaises(ValidationError): self.partner_a.child_ids.write({'active': False}) def test_subscription_invoice_shipping_address(self): """Test to check that subscription invoice first try to use partner_shipping_id and partner_id from subscription""" partner = self.env['res.partner'].create( {'name': 'Stevie Nicks', 'email': 'sti@fleetwood.mac', 'company_id': self.env.company.id}) partner2 = self.env['res.partner'].create( {'name': 'Partner 2', 'email': 'sti@fleetwood.mac', 'company_id': self.env.company.id}) subscription = self.env['sale.order'].create({ 'partner_id': partner.id, 'company_id': self.company_data['company'].id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) subscription.action_confirm() invoice_id = subscription._create_recurring_invoice() addr = subscription.partner_id.address_get(['delivery', 'invoice']) self.assertEqual(invoice_id.partner_shipping_id.id, addr['invoice']) self.assertEqual(invoice_id.partner_id.id, addr['delivery']) subscription.write({ 'partner_id': partner.id, 'partner_shipping_id': partner2.id, }) invoice_id = subscription._create_invoices() # force a new invoice with all lines self.assertEqual(invoice_id.partner_shipping_id.id, partner2.id) self.assertEqual(invoice_id.partner_id.id, partner.id) def test_portal_pay_subscription(self): # When portal pays a subscription, a success mail is sent. # This calls AccountMove.amount_by_group, which triggers _compute_invoice_taxes_by_group(). # As this method writes on this field and also reads tax_ids, which portal has no rights to, # it might cause some access rights issues. This test checks that no error is raised. portal_partner = self.user_portal.partner_id portal_partner.country_id = self.env['res.country'].search([('code', '=', 'US')]) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', }) provider = self.env['payment.provider'].create({ 'name': 'Test', }) tx = self.env['payment.transaction'].create({ 'amount': 100, 'provider_id': provider.id, 'payment_method_id': self.payment_method_id, 'currency_id': self.env.company.currency_id.id, 'partner_id': portal_partner.id, }) self.subscription.with_user(self.user_portal).sudo()._send_success_mail(invoice, tx) def test_upsell_date_check(self): """ Test what happens when the upsell invoice is not generated before the next invoice cron call """ self.pricing_year.price = 100 self.sub_product_tmpl.write({ 'product_subscription_pricing_ids': [(6, 0, self.pricing_year.ids)] }) self.product_tmpl_2.write({ 'product_subscription_pricing_ids': [(6, 0, self.pricing_year_2.ids)] }) self.product_tmpl_3.write({ 'product_subscription_pricing_ids': [(6, 0, self.pricing_year_3.ids)] }) with freeze_time("2022-01-01"): sub = self.env['sale.order'].create({ 'name': 'TestSubscription', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'plan_id': self.plan_year.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, }), (0, 0, { 'name': self.product2.name, 'product_id': self.product2.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, }) ] }) sub.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() inv = sub.invoice_ids line_names = inv.invoice_line_ids.mapped('name') periods = [n.split('\n')[1] for n in line_names] for p in periods: self.assertEqual(p, '01/01/2022 to 12/31/2022', 'the first year should be invoiced') with freeze_time("2022-06-20"): action = sub.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_so.order_line[0].product_uom_qty = 2 upsell_so.order_line = [(0, 0, { 'product_id': self.product3.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, })] self.assertEqual(upsell_so.next_invoice_date, datetime.date(2023, 1, 1), "The end date is the same than the parent sub") discounts = upsell_so.order_line.mapped('discount') self.assertEqual(discounts, [46.58, 46.58, 0.0, 46.58], "The discount is almost equal to 50%") self.assertEqual(sub.next_invoice_date, datetime.date(2023, 1, 1), 'the first year should be invoiced') upsell_so.action_confirm() self.assertEqual(upsell_so.next_invoice_date, datetime.date(2023, 1, 1), 'the first year should be invoiced') # We trigger the invoice cron before the generation of the upsell invoice self.env['sale.order']._cron_recurring_create_invoice() inv = sub.invoice_ids.sorted('date')[-1] self.assertEqual(inv.date, datetime.date(2022, 1, 1), "No invoice should be created") with freeze_time("2022-07-01"): discount = upsell_so.order_line.mapped('discount')[0] self.assertEqual(discount, 46.58, "The discount is almost equal to 50% and should not be updated for confirmed SO") self.assertEqual(upsell_so.order_line.mapped('qty_to_invoice'), [2, 0, 0, 1]) upsell_invoice = upsell_so._create_invoices() inv_line_ids = upsell_invoice.invoice_line_ids.filtered('product_id') self.assertEqual(inv_line_ids.mapped('subscription_id'), upsell_so.subscription_id) self.assertEqual(inv_line_ids.mapped('deferred_start_date'), [datetime.date(2022, 6, 20), datetime.date(2022, 6, 20)]) self.assertEqual(inv_line_ids.mapped('deferred_end_date'), [datetime.date(2022, 12, 31), datetime.date(2022, 12, 31)]) (upsell_so | sub)._cron_recurring_create_invoice() inv = sub.invoice_ids.sorted('date')[-1] self.assertEqual(inv.date, datetime.date(2022, 1, 1), "No invoice should be created") self.assertEqual(upsell_invoice.amount_untaxed, 267.1, "The upsell amount should be equal to 267.1") # (1-0.4658)*(200+300) with freeze_time("2023-01-01"): (upsell_so | sub)._cron_recurring_create_invoice() inv = sub.invoice_ids.sorted('date')[-1] self.assertEqual(inv.date, datetime.date(2023, 1, 1), "A new invoice should be created") self.assertEqual(inv.amount_untaxed, 800, "A new invoice should be created, all the lines should be invoiced") def test_subscription_starts_in_future(self): """ Start a subscription in 2 weeks. The next invoice date should be aligned with start_date """ with freeze_time("2022-05-15"): subscription = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, 'plan_id': self.plan_month.id, 'start_date': '2022-06-01', 'next_invoice_date': '2022-06-01', 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) subscription.action_confirm() self.assertEqual(subscription.order_line.invoice_status, 'no', "The line qty should be black.") self.assertEqual(subscription.start_date, datetime.date(2022, 6, 1), 'Start date should be in the future') self.assertEqual(subscription.next_invoice_date, datetime.date(2022, 6, 1), 'next_invoice_date should be in the future') subscription._create_invoices() with self.assertRaisesRegex(UserError, 'The following recurring orders have draft invoices. Please Confirm them or cancel them'): subscription._create_invoices() subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() self.assertEqual(subscription.next_invoice_date, datetime.date(2022, 7, 1), 'next_invoice_date should updated') subscription._create_invoices() subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post() self.assertEqual(subscription.next_invoice_date, datetime.date(2022, 8, 1), 'next_invoice_date should updated') def test_invoice_status(self): with freeze_time("2022-05-15"): self.product.invoice_policy = 'delivery' subscription_future = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, 'plan_id': self.plan_month.id, 'start_date': '2022-06-01', 'next_invoice_date': '2022-06-01', 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) subscription_now = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, 'plan_id': self.plan_month.id, 'start_date': '2022-05-15', 'next_invoice_date': '2022-05-15', 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) subscription_past = subscription_now.copy({ 'start_date': '2022-04-15', 'next_invoice_date': '2022-04-15', }) subscriptions = subscription_future + subscription_now + subscription_past subscriptions.action_confirm() # Nothing delivered, nothing invoiced for subscription in subscriptions: self.assertEqual( subscription.order_line.invoice_status, 'no', "The line qty should be black.", ) # Status after delivery subscriptions.order_line.qty_delivered = 1 self.assertEqual( subscription_future.order_line.invoice_status, 'no', "Nothing to invoice for future subscription yet.", ) for subscription in (subscription_now, subscription_past): self.assertEqual( subscription.order_line.invoice_status, 'to invoice', "The line qty should be blue.", ) # Status after invoice creation subscriptions._create_recurring_invoice() self.assertEqual( subscription_future.order_line.invoice_status, 'no', "Nothing to invoice for future subscription yet.", ) self.assertEqual( subscription_now.order_line.invoice_status, 'invoiced', "Current subscription has been invoiced.", ) self.assertEqual( subscription_past.order_line.invoice_status, 'to invoice', "Past subscription should be ready to get invoiced again.", ) # Status after closing subscriptions.set_close() self.assertEqual( subscription_future.order_line.invoice_status, 'to invoice', "Future subscription still needs to be invoiced after closing due to delivery.", ) for subscription in (subscription_now, subscription_past): self.assertEqual( subscription.order_line.invoice_status, 'invoiced', "Nothing new to invoice after closing.", ) def test_product_pricing_respects_variants(self): # create a product with 2 variants ProductTemplate = self.env['product.template'] ProductAttributeVal = self.env['product.attribute.value'] SaleOrderTemplate = self.env['sale.order.template'] Pricing = self.env['sale.subscription.pricing'] product_attribute = self.env['product.attribute'].create({'name': 'Weight'}) product_attribute_val1 = ProductAttributeVal.create({ 'name': '1kg', 'attribute_id': product_attribute.id }) product_attribute_val2 = ProductAttributeVal.create({ 'name': '2kg', 'attribute_id': product_attribute.id }) product = ProductTemplate.create({ 'recurring_invoice': True, 'detailed_type': 'service', 'name': 'Variant Products', 'list_price': 5, }) product.attribute_line_ids = [(Command.create({ 'attribute_id': product_attribute.id, 'value_ids': [Command.set([product_attribute_val1.id, product_attribute_val2.id])], }))] product_product_1 = product.product_variant_ids[0] product_product_2 = product.product_variant_ids[-1] # Define extra price for variant without temporal pricing self.assertEqual(product_product_2.list_price, 5.0) self.assertEqual(product_product_2.lst_price, 5.0) product_product_2.product_template_attribute_value_ids.price_extra = 15.0 self.assertEqual(product_product_2.lst_price, 20.0) template = SaleOrderTemplate.create({ 'name': 'Variant Products Plan', 'plan_id': self.plan_week.id, 'sale_order_template_line_ids': [Command.create({ 'product_id': product_product_2.id })] }) sale_order_form = Form(self.env['sale.order']) sale_order_form.partner_id = self.user_portal.partner_id sale_order_form.sale_order_template_id = template sale_order = sale_order_form.save() self.assertEqual(sale_order.order_line.price_unit, 20.0) # set pricing for variants. make sure the cheaper one is not for the variant we're testing cheaper_pricing = Pricing.create({ 'plan_id': self.plan_week.id, 'price': 10, 'product_template_id': product.id, 'product_variant_ids': [Command.link(product_product_1.id)], }) pricing2 = Pricing.create({ 'plan_id': self.plan_week.id, 'price': 25, 'product_template_id': product.id, 'product_variant_ids': [Command.link(product_product_2.id)], }) product.write({ 'product_subscription_pricing_ids': [Command.set([cheaper_pricing.id, pricing2.id])] }) # create SO with product variant having the most expensive pricing sale_order = self.env['sale.order'].create({ 'name': 'TestSubscription', 'is_subscription': True, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'plan_id': self.plan_week.id, 'order_line': [ Command.create({ 'product_id': product_product_2.id, 'product_uom_qty': 1 }), Command.create({ 'product_id': product_product_1[0].id, 'product_uom_qty': 1 }) ] }) # check that correct pricings are being used self.assertEqual(sale_order.order_line[0].price_unit, pricing2.price) self.assertEqual(sale_order.order_line[1].price_unit, cheaper_pricing.price) # test constraints product2 = ProductTemplate.create({ 'recurring_invoice': True, 'detailed_type': 'service', 'name': 'Variant Products', 'list_price': 5, }) product2.attribute_line_ids = [(Command.create({ 'attribute_id': product_attribute.id, 'value_ids': [Command.set([product_attribute_val1.id, product_attribute_val2.id])], }))] product2_product_2 = product2.product_variant_ids[-1] Pricing.create({ 'plan_id': self.plan_week.id, 'price': 25, 'product_template_id': product2.id, 'product_variant_ids': [Command.link(product2_product_2.id)], }) product2_product_1 = product2.product_variant_ids[0] product2_product_2 = product2.product_variant_ids[-1] with self.assertRaises(UserError): Pricing.create({ 'plan_id': self.plan_week.id, 'price': 32, 'product_template_id': product2.id, 'product_variant_ids': [Command.set([product2_product_1.id, product2_product_2.id])], }) with self.assertRaises(UserError): # Check constraint without product variants Pricing.create({ 'plan_id': self.plan_month.id, 'price': 32, 'product_template_id': product2.id, 'product_variant_ids': [], }) Pricing.create({ 'plan_id': self.plan_month.id, 'price': 40, 'product_template_id': product2.id, 'product_variant_ids': [], }) Pricing.create({ 'plan_id': self.plan_month.id, 'price': 88, 'product_template_id': product2.id, 'product_variant_ids': [], }) def test_upsell_parent_line_id(self): with freeze_time("2022-01-01"): self.subscription.order_line = False self.subscription.write({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'start_date': False, 'next_invoice_date': False, 'order_line': [ Command.create({ 'product_id': self.product.id, 'name': "month: original", 'price_unit': 50, 'product_uom_qty': 1, }) ] }) self.subscription.action_confirm() self.subscription._create_recurring_invoice() with freeze_time("2022-01-20"): action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) # Create new lines that should be aligned with existing ones parent_line_id = upsell_so.order_line.parent_line_id self.assertEqual(self.subscription.order_line, parent_line_id, "The parent line is the one from the subscription") first_line_id = upsell_so.order_line[0] # line 0 is the upsell line first_line_id.product_id = self.product2 self.assertFalse(first_line_id.parent_line_id, "The new line should not have a parent line") upsell_so.currency_id = False self.assertFalse(first_line_id.parent_line_id, "The new line should not have a parent line even without currency_id") self.subscription._compute_pricelist_id() # reset the currency_id upsell_so._compute_pricelist_id() first_line_id.product_id = self.product upsell_so.order_line[0].price_unit = parent_line_id.price_unit + 0.004 # making sure that rounding issue will not affect computed result self.assertEqual(upsell_so.order_line[0].parent_line_id, parent_line_id, "The parent line is the one from the subscription") self.assertEqual(upsell_so.order_line.parent_line_id, parent_line_id, "The parent line is still the one from the subscription") # reset the product to another one to lose the link first_line_id.product_id = self.product2 so_line_vals = [{ 'name': 'Upsell added: 1 month', 'order_id': upsell_so.id, 'product_id': self.product3.id, 'product_uom_qty': 3, 'product_uom': self.product.uom_id.id, }] self.env['sale.order.line'].create(so_line_vals) self.assertFalse(upsell_so.order_line[2].parent_line_id, "The new line should not have any parent line") upsell_so.order_line[2].product_id = self.product3 upsell_so.order_line[2].product_id = self.product # it should recreate a link upsell_so.order_line[0].product_uom_qty = 2 self.assertEqual(upsell_so.order_line.parent_line_id, parent_line_id, "The parent line is the one from the subscription") upsell_so.action_confirm() self.assertEqual(self.subscription.order_line[0].product_uom_qty, 4, "The original line qty should be 4 (1 + 3 upsell line 1)") self.assertEqual(self.subscription.order_line[1].product_uom_qty, 2, "The new line qty should be 2 (upsell line 0)") action = self.subscription.prepare_renewal_order() renew_so = self.env['sale.order'].browse(action['res_id']) parent_line_id = renew_so.order_line.parent_line_id self.assertEqual(self.subscription.order_line, parent_line_id, "The parent line is the one from the subscription") renew_so.plan_id = self.plan_year self.assertFalse(renew_so.order_line.parent_line_id, "The lines should not have parent lines anymore") # test the general behavior of so when the compute_price_unit is called self.product_tmpl_4.recurring_invoice = False order = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, }), (0, 0, { 'name': self.product5.name, # non recurring product 'product_id': self.product5.id, 'product_uom_qty': 1.0, 'product_uom': self.product5.uom_id.id, 'price_unit': 12, }) ], }) self.assertTrue(order.is_subscription) self.assertEqual(order.order_line[1].price_unit, 12) order.order_line[1].product_id = self.product_tmpl_4.product_variant_id self.assertEqual(order.order_line[1].price_unit, 15, "The price should be updated") def test_subscription_constraint(self): sub = self.subscription.copy() self.subscription.plan_id = False with self.assertRaisesRegex(UserError, 'You cannot save a sale order with recurring product and no subscription plan.'): self.subscription.action_confirm() self.subscription.plan_id = self.plan_month self.product.recurring_invoice = False self.product2.recurring_invoice = False sub2 = self.subscription.copy() with self.assertRaisesRegex(UserError, 'You cannot save a sale order with a subscription plan and no recurring product.'): sub2.action_confirm() # order linked to subscription with recurring product and no recurrence: it was created before the upgrade # of sale.subscription into sale.order context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } delivered_product_tmpl = self.env['product.template'].with_context(context_no_mail).create({ 'name': 'Delivery product', 'type': 'service', 'recurring_invoice': False, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'invoice_policy': 'delivery', }) self.product.recurring_invoice = True self.product2.recurring_invoice = True sub.action_confirm() # Simulate the order without recurrence but linked to a subscription order = self.env['sale.order'].create({ 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'subscription_id': sub.id, 'order_line': [Command.create({ 'name': "recurring line", 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom': self.product.uom_id.id }), Command.create({ 'name': "None recurring line", 'product_id': delivered_product_tmpl.product_variant_id.id, 'product_uom_qty': 1, 'product_uom': delivered_product_tmpl.product_variant_id.uom_id.id }), ], }) # Make sure the _constraint_subscription_recurrence is not triggered self.assertFalse(order.subscription_state) order.action_confirm() order.write({'order_line': [Command.create({ 'name': "None recurring line", 'product_id': delivered_product_tmpl.product_variant_id.id, 'product_uom_qty': 1, 'qty_delivered': 3, 'product_uom': delivered_product_tmpl.product_variant_id.uom_id.id })],}) def test_multiple_renew(self): """ Prevent to confirm several renewal quotation for the same subscription """ self.subscription.write({'start_date': False, 'next_invoice_date': False}) self.subscription.action_confirm() self.subscription._cron_recurring_create_invoice() action = self.subscription.prepare_renewal_order() renewal_so_1 = self.env['sale.order'].browse(action['res_id']) action = self.subscription.prepare_renewal_order() renewal_so_2 = self.env['sale.order'].browse(action['res_id']) renewal_so_1.action_confirm() self.assertEqual(renewal_so_2.state, 'cancel', 'The other quotation should be canceled') def test_next_invoice_date(self): with freeze_time("2022-01-20"): subscription = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'sale_order_template_id': self.subscription_tmpl.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 1.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) self.assertFalse(subscription.next_invoice_date) self.assertFalse(subscription.start_date) with freeze_time("2022-02-10"): subscription.action_confirm() self.assertEqual(subscription.next_invoice_date, datetime.date(2022, 2, 10)) self.assertEqual(subscription.start_date, datetime.date(2022, 2, 10)) def test_refund_qty_invoiced(self): with freeze_time("2024-09-01"): subscription = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) subscription.action_confirm() subscription._create_recurring_invoice() self.assertEqual(subscription.order_line.qty_invoiced, 3, "The 3 products should be invoiced") subscription._get_invoiced() inv = subscription.invoice_ids inv.payment_state = 'paid' # We refund the invoice refund_wizard = self.env['account.move.reversal'].with_context( active_model="account.move", active_ids=inv.ids).create({ 'reason': 'Test refund tax repartition', 'journal_id': inv.journal_id.id, }) res = refund_wizard.refund_moves() refund_move = self.env['account.move'].browse(res['res_id']) self.assertEqual(inv.reversal_move_id, refund_move, "The initial move should be reversed") self.assertEqual(subscription.order_line.qty_invoiced, 0, "The products should be not be invoiced") def test_discount_parent_line(self): with freeze_time("2022-01-01"): self.subscription.start_date = False self.subscription.next_invoice_date = False self.subscription.write({ 'partner_id': self.partner.id, 'plan_id': self.plan_year.id, }) self.subscription.order_line.discount = 10 self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() with freeze_time("2022-10-31"): self.env['sale.order']._cron_recurring_create_invoice() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) # Discount is 55.61: 83% for pro rata temporis and 10% coming from the parent order # price_unit must be multiplied by (1-0.831) * 0,9 # 100 * [1 - ((1 - 0.831) * 0.9)] = ~84% discount = [round(v, 2) for v in upsell_so.order_line.mapped('discount')] self.assertAlmostEqual(discount, [84.71, 84.71, 0]) def test_upsell_renewal(self): """ Upselling a invoiced renewed order before it started should create a negative discount to invoice the previous period. If the renewal has not been invoiced yet, we should only invoice for the previous period. """ with freeze_time("2022-01-01"): self.subscription.start_date = False self.subscription.next_invoice_date = False self.subscription.write({ 'partner_id': self.partner.id, 'plan_id': self.plan_year.id, }) subscription_2 = self.subscription.copy() (self.subscription | subscription_2).action_confirm() self.env['sale.order']._cron_recurring_create_invoice() with freeze_time("2022-09-10"): action = self.subscription.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so.action_confirm() renewal_so._create_invoices() renewal_so.order_line.invoice_lines.move_id._post() self.assertEqual(renewal_so.start_date, datetime.date(2023, 1, 1)) self.assertEqual(renewal_so.next_invoice_date, datetime.date(2024, 1, 1)) action = subscription_2.prepare_renewal_order() renewal_so_2 = self.env['sale.order'].browse(action['res_id']) renewal_so_2.action_confirm() # We don't invoice renewal_so_2 yet to see what happens. self.assertEqual(renewal_so_2.start_date, datetime.date(2023, 1, 1)) self.assertEqual(renewal_so_2.next_invoice_date, datetime.date(2023, 1, 1)) with freeze_time("2022-10-2"): self.env['sale.order']._cron_recurring_create_invoice() action = renewal_so.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_so.order_line.filtered(lambda l: not l.display_type).product_uom_qty = 1 renewal_so_2.next_invoice_date += relativedelta(days=1) # prevent validation error action = renewal_so_2.prepare_upsell_order() upsell_so_2 = self.env['sale.order'].browse(action['res_id']) upsell_so_2.order_line.filtered(lambda l: not l.display_type).product_uom_qty = 1 parents = upsell_so.order_line.mapped('parent_line_id') line_match = [ renewal_so.order_line[0], renewal_so.order_line[1], ] for idx in range(2): self.assertEqual(parents[idx], line_match[idx]) self.assertEqual(self.subscription.order_line.mapped('product_uom_qty'), [1, 1]) self.assertEqual(renewal_so.order_line.mapped('product_uom_qty'), [1, 1]) upsell_so.action_confirm() self.assertEqual(upsell_so.order_line.mapped('product_uom_qty'), [1.0, 1.0, 0]) self.assertEqual(renewal_so.order_line.mapped('product_uom_qty'), [2, 2]) self.assertEqual(upsell_so.order_line.mapped('discount'), [-24.93, -24.93, 0]) self.assertEqual(upsell_so.start_date, datetime.date(2022, 10, 2)) self.assertEqual(upsell_so.next_invoice_date, datetime.date(2024, 1, 1)) self.assertEqual(upsell_so_2.amount_untaxed, 30.25) # upsell_so_2.order_line.flush() line = upsell_so_2.order_line.filtered('display_type') self.assertEqual(line.display_type, 'line_note') self.assertFalse(line.product_uom_qty) self.assertFalse(line.price_unit) self.assertFalse(line.customer_lead) self.assertFalse(line.product_id) self.assertEqual(upsell_so_2.order_line.mapped('product_uom_qty'), [1.0, 1.0, 0]) for discount, value in zip(upsell_so_2.order_line.mapped('discount'), [74.79, 74.79, 0.0]): self.assertAlmostEqual(discount, value) self.assertEqual(upsell_so_2.next_invoice_date, datetime.date(2023, 1, 2), 'We only invoice until the start of the renewal') def test_free_product_do_not_invoice(self): sub_product_tmpl = self.env['product.template'].create({ 'name': 'Free product', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'list_price': 0, }) self.subscription.start_date = False self.subscription.next_invoice_date = False self.subscription.order_line = [Command.clear()] self.subscription.write({ 'partner_id': self.partner.id, 'plan_id': self.plan_year.id, 'order_line': [Command.create({ 'name': sub_product_tmpl.name, 'product_id': sub_product_tmpl.product_variant_id.id, 'product_uom_qty': 1.0, 'product_uom': sub_product_tmpl.uom_id.id, })] }) self.assertEqual(self.subscription.amount_untaxed, 0, "The price shot be 0") self.assertEqual(self.subscription.order_line.price_subtotal, 0, "The price line should be 0") self.assertEqual(self.subscription.order_line.invoice_status, 'no', "Nothing to invoice here") def test_invoice_done_order(self): # Prevent to invoice order in done state with freeze_time("2021-01-03"): self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(self.subscription.invoice_count, 1, "one invoice is created normally") with freeze_time("2021-02-03"): self.subscription.action_lock() self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(self.subscription.invoice_count, 2, "locked state don't prevent invoices anymore") def test_create_alternative(self): self.subscription.next_invoice_date = fields.Date.today() + relativedelta(months=1) action = self.subscription.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) copy_so = renewal_so.copy() alternative_action = renewal_so.create_alternative() alternative_so = self.env['sale.order'].browse(alternative_action['res_id']) self.assertFalse(copy_so.origin_order_id) self.assertFalse(copy_so.subscription_id) self.assertEqual(renewal_so.origin_order_id.id, alternative_so.origin_order_id.id) self.assertEqual(renewal_so.subscription_id.id, alternative_so.subscription_id.id) def test_subscription_state(self): # test default value for subscription_state sub_1 = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) self.assertEqual(sub_1.subscription_state, '1_draft') sub_2 = self.env['sale.order'].create({ 'partner_id': self.partner.id, }) self.assertFalse(sub_2.subscription_state, ) sub_2.plan_id = self.plan_month sub_2.order_line = [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })] self.assertEqual(sub_2.subscription_state, '1_draft') sub_2.write({ 'order_line': False, 'plan_id': False, }) self.assertFalse(sub_2.is_subscription, "Subscription quotation without plan_id isn't a subscription") self.assertEqual(sub_2.subscription_state, '1_draft', "Draft subscription quotation without plan_id should retain subscription_state") sub_2.action_confirm() self.assertFalse(sub_2.subscription_state, "SO without subscription plan should lose subscription_state on confirmation") def test_free_subscription(self): with freeze_time("2023-01-01"): pricelist = self.env['product.pricelist'].create({ 'name': 'Pricelist A', }) # We don't want to create invoice when the sum of recurring line is 0 nr_product = self.env['product.template'].create({ 'name': 'Non recurring product', 'type': 'service', 'uom_id': self.product.uom_id.id, 'list_price': 25, 'invoice_policy': 'order', }) # nr_product.taxes_id = False # we avoid using taxes in this example self.pricing_year.unlink() self.pricing_month.price = 25 self.product2.list_price = -25.0 # total = 0 & recurring amount = 0 sub_0_0 = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'pricelist_id': pricelist.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, }), (0, 0, { 'name': self.product.name, 'product_id': self.product2.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': -25, }) ], }) # total = 0 & recurring amount > 0 sub_0_1 = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'pricelist_id': pricelist.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, }), (0, 0, { 'name': nr_product.name, 'product_id': nr_product.product_variant_id.id, 'product_uom_qty': 2.0, 'product_uom': nr_product.uom_id.id, 'price_unit': -25, }) ], }) # total > 0 & recurring amount = 0 sub_1_0 = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'pricelist_id': pricelist.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, }), (0, 0, { 'name': self.product.name, 'product_id': self.product2.id, 'product_uom_qty': 2.0, 'product_uom': self.product2.uom_id.id, }), (0, 0, { 'name': nr_product.name, 'product_id': nr_product.product_variant_id.id, 'product_uom_qty': 2.0, 'product_uom': nr_product.uom_id.id, }), ], }) sub_negative_recurring = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'pricelist_id': pricelist.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': -30 }), (0, 0, { 'name': self.product.name, 'product_id': self.product2.id, 'product_uom_qty': 2.0, 'product_uom': self.product2.uom_id.id, 'price_unit': -10 }), ], }) # negative_nonrecurring_sub negative_nonrecurring_sub = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'pricelist_id': pricelist.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': -30 }), (0, 0, { 'name': self.product.name, 'product_id': self.product2.id, 'product_uom_qty': 2.0, 'product_uom': self.product2.uom_id.id, 'price_unit': -10 }), (0, 0, { 'name': nr_product.name, 'product_id': nr_product.product_variant_id.id, 'product_uom_qty': 4.0, 'product_uom': nr_product.uom_id.id, }), ], }) (sub_0_0 | sub_0_1 | sub_1_0 | sub_negative_recurring | negative_nonrecurring_sub).order_line.tax_id = False (sub_0_0 | sub_0_1 | sub_1_0 | sub_negative_recurring | negative_nonrecurring_sub).action_confirm() invoice_0_0 = sub_0_0._create_recurring_invoice() self.assertTrue(sub_0_0.currency_id.is_zero(sub_0_0.amount_total)) self.assertFalse(invoice_0_0, "Free contract with recurring products should not create invoice") self.assertEqual(sub_0_0.order_line.mapped('invoice_status'), ['no', 'no'], 'No invoice needed') self.assertTrue(sub_0_1.currency_id.is_zero(sub_0_1.amount_total)) self.assertTrue(sub_0_1.order_line.filtered(lambda l: l.recurring_invoice).price_subtotal > 0) invoice_0_1 = sub_0_1._create_recurring_invoice() self.assertEqual(invoice_0_1.amount_total, 0, "Total is 0 but an invoice should be created.") self.assertEqual(sub_0_1.order_line.mapped('invoice_status'), ['invoiced', 'invoiced'], 'No invoice needed') self.assertTrue(sub_1_0.amount_total > 0) invoice_1_0 = sub_1_0._create_recurring_invoice() self.assertEqual(invoice_1_0.amount_total, 50, "Total is 0 and an invoice should be created.") self.assertEqual(sub_1_0.order_line.mapped('invoice_status'), ['no', 'no', 'invoiced'], 'No invoice needed') self.assertFalse(all(invoice_1_0.invoice_line_ids.sale_line_ids.product_id.mapped('recurring_invoice')), "The recurring line should be invoiced") # Negative subscription will be invoiced by cron the next day negative_invoice = sub_negative_recurring._create_recurring_invoice() self.assertEqual(sub_negative_recurring.amount_total, -80) self.assertFalse(negative_invoice, "Free contract with recurring products should not create invoice") self.assertEqual(sub_negative_recurring.order_line.mapped('invoice_status'), ['no', 'no'], 'No invoice needed') negative_non_recurring_inv = negative_nonrecurring_sub._create_recurring_invoice() self.assertEqual(negative_nonrecurring_sub.amount_total, 20) self.assertFalse(negative_non_recurring_inv, "negative contract with non recurring products should not create invoice") self.assertEqual(sub_negative_recurring.order_line.mapped('invoice_status'), ['no', 'no'], 'No invoice needed') self.assertTrue(negative_nonrecurring_sub.payment_exception, "The contract should be in exception") def test_subscription_unlink_flow(self): """ Check that the user receives the correct messages when he deletes a subscription. Check that the flow to delete a subscription is confirm => close => cancel """ subscription_a = self.env['sale.order'].create({ 'partner_id': self.user_portal.partner_id.id, 'sale_order_template_id': self.subscription_tmpl.id, }) subscription_b = self.env['sale.order'].create({ 'partner_id': self.user_portal.partner_id.id, 'sale_order_template_id': self.subscription_tmpl.id, }) subscription_c = self.env['sale.order'].create({ 'partner_id': self.user_portal.partner_id.id, 'sale_order_template_id': self.subscription_tmpl.id, }) subscription_d = self.env['sale.order'].create({ 'partner_id': self.user_portal.partner_id.id, 'sale_order_template_id': self.subscription_tmpl.id, }) subscription_a._onchange_sale_order_template_id() subscription_b._onchange_sale_order_template_id() subscription_c._onchange_sale_order_template_id() subscription_d._onchange_sale_order_template_id() # Subscription can be deleted if it is in draft subscription_a.unlink() # Subscription cannot be deleted if it was confirmed once before and it is not closed subscription_b.action_confirm() with self.assertRaisesRegex(UserError, r'You can not delete a confirmed subscription. You must first close and cancel it before you can delete it.'): subscription_b.unlink() # Subscription cannot be deleted if it is closed subscription_c.action_confirm() subscription_c.set_close() with self.assertRaisesRegex(UserError, r'You can not delete a sent quotation or a confirmed sales order. You must first cancel it.'): subscription_c.unlink() # Subscription can be deleted if it is cancel subscription_d.action_confirm() subscription_d.set_close() subscription_d._action_cancel() subscription_d.unlink() def test_downpayment_automatic_invoice(self): """ Test invoice with a way of downpayment and check downpayment's SO line is created and also check a total amount of invoice is equal to a respective sale order's total amount """ context = { 'active_model': 'sale.order', 'active_ids': [self.subscription.id], 'active_id': self.subscription.id, 'default_journal_id': self.company_data['default_journal_sale'].id, } with freeze_time('2021-01-03'): self.subscription.action_confirm() total = self.subscription.amount_total downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({ 'advance_payment_method': 'fixed', 'fixed_amount': 10, 'deposit_account_id': self.company_data['default_account_revenue'].id }) downpayment.create_invoices() downpayment_line = self.subscription.order_line.filtered(lambda l: l.is_downpayment and not l.display_type) self.assertEqual(len(downpayment_line), 1, 'SO line downpayment should be created on SO') self.assertEqual(self.subscription.invoice_count, 1) invoice = self.subscription.invoice_ids.sorted('id')[-1] self.assertAlmostEqual(invoice.amount_total, 10, 4, 'Downpayment price should be 10') invoice._post() invoice = self.subscription._create_recurring_invoice() self.assertAlmostEqual(invoice.amount_total, total - 10, 4, 'Downpayment should be deducted from the price') with freeze_time('2021-02-03'): self.subscription._create_recurring_invoice() invoice = self.subscription.invoice_ids.sorted('id')[-1] self.assertAlmostEqual(invoice.amount_total, total, 4, 'Downpayment should not be deducted from the price anymore') def test_downpayment_manual_invoice(self): """ Test invoice with a way of downpayment and check downpayment's SO line is created and also check a total amount of invoice is equal to a respective sale order's total amount """ context = { 'active_model': 'sale.order', 'active_ids': [self.subscription.id], 'active_id': self.subscription.id, 'default_journal_id': self.company_data['default_journal_sale'].id, } with freeze_time('2021-01-03'): self.subscription.action_confirm() total = self.subscription.amount_total downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({ 'advance_payment_method': 'fixed', 'fixed_amount': 10, 'deposit_account_id': self.company_data['default_account_revenue'].id }) downpayment.create_invoices() downpayment_line = self.subscription.order_line.filtered(lambda l: l.is_downpayment and not l.display_type) self.assertEqual(len(downpayment_line), 1, 'SO line downpayment should be created on SO') self.assertEqual(self.subscription.invoice_count, 1) invoice = self.subscription.invoice_ids.sorted('id')[-1] self.assertAlmostEqual(invoice.amount_total, 10, 4, 'Downpayment price should be 10') invoice._post() self.subscription._create_invoices(final=True) invoice = self.subscription.invoice_ids.sorted('id')[-1] invoice._post() self.assertAlmostEqual(invoice.amount_total, total - 10, 4, 'Downpayment should be deducted from the price') with freeze_time('2021-02-03'): self.subscription._create_invoices(final=True) invoice = self.subscription.invoice_ids.sorted('id')[-1] self.assertAlmostEqual(invoice.amount_total, total, 4, 'Downpayment should not be deducted from the price anymore') def test_upsell_with_different_currency_throws_error(self): pricelist_eur = self.env['product.pricelist'].create({ 'name': 'Euro pricelist', 'currency_id': self.env.ref('base.EUR').id, }) self.subscription.action_confirm() self.subscription._create_recurring_invoice() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) with self.assertRaises(ValidationError): upsell_so.pricelist_id = pricelist_eur.id def test_modify_discount_on_upsell(self): """ Makes sure that you can edit the discount on an upsell, save it, and then confirm it, and it doesn't change/reset to default """ with freeze_time("2022-10-31"): self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_line = upsell_so.order_line.filtered(lambda l: not l.display_type)[0] old_discount = upsell_line.discount new_discount = 42 self.assertTrue(old_discount != new_discount, "These discounts should be different, change the value of new_discount if this test fail.") upsell_line.write({'discount': new_discount}) self.assertEqual(upsell_line.discount, new_discount, "The line should have the new discount written.") upsell_so.action_confirm() self.assertEqual(upsell_line.discount, new_discount, "The line should have the new discount after confirmation.") def test_subscription_change_partner(self): # This test check that action_confirm is only called once on SO when the partner is updated. sub = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) self.assertEqual(sub.partner_id, self.partner) sub.action_confirm() self.assertEqual(sub.subscription_state, '3_progress') action_confirm_orig = SaleOrder.action_confirm self.call_count = 0 self1 = self def _action_confirm_mock(*args, **kwargs): self1.call_count += 1 return action_confirm_orig(*args, **kwargs) with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder.action_confirm', _action_confirm_mock): sub.partner_id = self.partner_a_invoice.id self.assertEqual(sub.partner_id, self.partner_a_invoice) self.assertEqual(self.call_count, 0) def test_reopen(self): with freeze_time("2023-03-01"): sub = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, })], }) sub_mrr_change = sub.copy() self.flush_tracking() (sub | sub_mrr_change).action_confirm() self.flush_tracking() with freeze_time("2023-03-02"): sub_mrr_change.order_line.product_uom_qty = 10 sub.order_line.product_uom_qty = 10 self.flush_tracking() with freeze_time("2023-03-05"): close_reason_id = self.env.ref('sale_subscription.close_reason_1').id (sub | sub_mrr_change).set_close(close_reason_id=close_reason_id) self.flush_tracking() # We change the quantity after cloing to see what happens to the logs when we reopen sub_mrr_change.order_line.product_uom_qty = 6 self.flush_tracking() (sub | sub_mrr_change).set_close() self.flush_tracking() churn_log = sub.order_log_ids.sorted('event_date')[-1] self.assertEqual((churn_log.event_type, churn_log.amount_signed, churn_log.recurring_monthly), ('2_churn', -10, 0), "The churn log should be created") with freeze_time("2023-03-10"): (sub | sub_mrr_change).reopen_order() self.flush_tracking() order_log_ids = sub.order_log_ids.sorted('event_date') sub_data = [ (log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', datetime.date(2023, 3, 1), '3_progress', 3.0, 3.0), ('1_expansion', datetime.date(2023, 3, 2), '3_progress', 7.0, 10.0)]) order_log_ids = sub_mrr_change.order_log_ids.sorted('event_date') sub_data = [ (log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', datetime.date(2023, 3, 1), '3_progress', 3.0, 3.0), ('1_expansion', datetime.date(2023, 3, 2), '3_progress', 7.0, 10.0), ('15_contraction', datetime.date(2023, 3, 10), '3_progress', -4.0, 6.0)]) def test_cancel_constraint(self): sub_progress = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 3.0, 'product_uom': self.product.uom_id.id, })], }) sub_paused = sub_progress.copy() sub_progress_no_invoice = sub_progress.copy() with freeze_time('2022-02-02'): (sub_progress | sub_paused | sub_progress_no_invoice).action_confirm() (sub_progress | sub_paused)._create_recurring_invoice() sub_paused.subscription_state = '4_paused' sub_progress_no_invoice._action_cancel() self.assertEqual(sub_progress_no_invoice.state, 'cancel') with self.assertRaises(ValidationError): sub_paused._action_cancel() sub_paused.subscription_state = '6_churn' sub_paused._action_cancel() with self.assertRaises(ValidationError): sub_paused.subscription_state = '4_paused' with self.assertRaises(ValidationError): sub_progress._action_cancel() sub_progress.subscription_state = '6_churn' sub_progress._action_cancel() with self.assertRaises(ValidationError): sub_progress.subscription_state = '3_progress' action = sub_progress.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so.action_confirm() self.assertEqual(sub_progress.state, 'cancel') self.assertEqual(sub_progress.subscription_state, '6_churn', "sub was churned") inv = renewal_so._create_invoices() inv._post() self.assertEqual(renewal_so.subscription_state, '3_progress') action = renewal_so.prepare_renewal_order() renewal_so2 = self.env['sale.order'].browse(action['res_id']) renewal_so2.action_confirm() self.assertEqual(renewal_so2.subscription_state, '3_progress') self.assertEqual(renewal_so.subscription_state, '5_renewed') self.assertEqual(renewal_so.state, 'sale') self.assertTrue(renewal_so.locked) with self.assertRaises(ValidationError): renewal_so._action_cancel() def test_renew_different_currency(self): with freeze_time("2023-03-28"): self.product.product_subscription_pricing_ids.unlink() default_pricelist = self.company_data['default_pricelist'] other_currency = self.env.ref('base.EUR') other_currency.action_unarchive() other_pricelist = self.env['product.pricelist'].create({ 'name': 'Test Pricelist (EUR)', 'currency_id': other_currency.id, }) other_currency.write({ 'rate_ids': [(0, 0, { 'rate': 20, })] }) pricing_month_1 = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_month.id, 'price': 10, 'pricelist_id': default_pricelist.id, }) pricing_month_2 = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_month.id, 'price': 200, 'pricelist_id': other_pricelist.id, }) sub_product_tmpl = self.env['product.template'].create({ 'name': 'BaseTestProduct', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'product_subscription_pricing_ids': [(6, 0, (pricing_month_1 | pricing_month_2).ids)] }) subscription_tmpl = self.env['sale.order.template'].create({ 'name': 'Subscription template without discount', 'duration_unit': 'year', 'is_unlimited': False, 'duration_value': 2, 'note': "This is the template description", 'plan_id': self.plan_month.copy(default={'auto_close_limit': 5}).id, 'sale_order_template_line_ids': [Command.create({ 'name': "Product 1", 'product_id': sub_product_tmpl.product_variant_id.id, 'product_uom_qty': 1, 'product_uom_id': sub_product_tmpl.product_variant_id.uom_id.id, })] }) sub = self.subscription.create({ 'name': 'Company1 - Currency1', 'sale_order_template_id': subscription_tmpl.id, 'partner_id': self.user_portal.partner_id.id, 'currency_id': self.company.currency_id.id, 'plan_id': self.plan_month.id, 'order_line': [(0, 0, { 'name': "Product 1", 'product_id': sub_product_tmpl.product_variant_id.id, 'product_uom_qty': 1, 'product_uom': sub_product_tmpl.uom_id.id })] }) sub.pricelist_id = default_pricelist.id sub._onchange_sale_order_template_id() # recompute the pricings self.flush_tracking() sub.action_confirm() self.assertEqual(sub.recurring_monthly, 10) self.flush_tracking() self.env['sale.order']._cron_recurring_create_invoice() self.flush_tracking() with freeze_time("2023-04-29"): action = sub.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so.write({ 'pricelist_id': other_pricelist.id, }) renewal_so._onchange_sale_order_template_id() renewal_so.order_line.product_uom_qty = 3 self.flush_tracking() renewal_so.action_confirm() self.flush_tracking() self.env['sale.order']._cron_recurring_create_invoice() order_log_ids = sub.order_log_ids.sorted(key=lambda log: (log.event_date, log.id)) sub_data = [(log.event_type, log.event_date, log.amount_signed, log.recurring_monthly, log.currency_id) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', datetime.date(2023, 3, 28), 10, 10, default_pricelist.currency_id), ('3_transfer', datetime.date(2023, 4, 29), -10, 0, default_pricelist.currency_id) ]) renew_logs = renewal_so.order_log_ids.sorted(key=lambda log: (log.event_date, log.id)) renew_data = [(log.event_type, log.event_date, log.amount_signed, log.recurring_monthly, log.currency_id) for log in renew_logs] self.assertEqual(renew_data, [ ('3_transfer', datetime.date(2023, 4, 29), 200, 200, other_currency), ('1_expansion', datetime.date(2023, 4, 29), 400, 600, other_currency) ]) def test_protected_close_reason(self): close_reason = self.env['sale.order.close.reason'].create({ 'name': 'Super close reason', 'is_protected': True, }) with self.assertRaises(AccessError): close_reason.unlink() def test_amount_to_invoice(self): sub = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 10.0, 'product_uom': self.product.uom_id.id, })], }) sub.order_line.tax_id = [Command.clear()] nr_product = self.env['product.template'].create({ 'name': 'Non recurring product', 'type': 'service', 'uom_id': self.product.uom_id.id, 'list_price': 25, 'invoice_policy': 'order', }) sub.action_confirm() self.assertEqual(sub.amount_to_invoice, 10) sub._create_recurring_invoice() sub.order_line = [Command.link(self.env['sale.order.line'].create({ 'name': nr_product.name, 'order_id': sub.id, 'product_id': nr_product.product_variant_id.id, 'product_uom_qty': 1, }).id)] sub.order_line.tax_id = [Command.clear()] self.assertEqual(sub.amount_to_invoice, (10 + 25)) sub._create_recurring_invoice() self.assertEqual(sub.amount_to_invoice, 10) def test_amount_to_invoice_with_downpayment(self): with freeze_time("2024-10-01"): sub = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ Command.create({ 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 10.0, 'product_uom': self.product.uom_id.id, }), Command.create({ 'name': self.product_a.name, 'product_id': self.product_a.id, 'product_uom_qty': 1, 'product_uom': self.product_a.uom_id.id, }) ], }) currency = sub.currency_id sub.order_line.tax_id = [Command.clear()] sub.action_confirm() self.assertTrue(currency.is_zero(sub.amount_to_invoice - 1010), "Both products will be invoiced") sub._create_recurring_invoice() self.assertTrue(currency.is_zero(sub.amount_to_invoice - 10), "Only the recurring product will be invoiced") # downpayment of 5 to reimburse bad prestation self.env['sale.advance.payment.inv'].create({ 'sale_order_ids': sub.ids, 'advance_payment_method': 'fixed', 'fixed_amount': 5 })._create_invoices(sub).action_post() self.assertTrue(currency.is_zero(sub.amount_to_invoice - 5), "We only have to pay 5") with freeze_time("2024-11-01"): sub._create_recurring_invoice() self.assertTrue(currency.is_zero(sub.amount_to_invoice - 10), "We fall back on the normal price") with freeze_time("2024-12-01"): sub._create_recurring_invoice() self.assertTrue(currency.is_zero(sub.amount_to_invoice - 10), "Same") def test_close_reason_end_of_contract(self): sub = self.subscription end_date = datetime.date(2022, 6, 20) sub.end_date = end_date with freeze_time(end_date): sub.action_confirm() sub._create_recurring_invoice() self.assertEqual(sub.close_reason_id.id, self.env.ref('sale_subscription.close_reason_end_of_contract').id) def test_sale_subscription_post_invoice(self): """ Test that the post invoice hook is correctly called """ def patched_reset(self): self.name = "Called" with patch('odoo.addons.sale_subscription.models.sale_order_line.SaleOrderLine._reset_subscription_quantity_post_invoice', patched_reset), freeze_time("2021-01-01"): sub = self.subscription sub.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(sub.order_line.mapped('name'), ['Called']*2) def test_close_reason_automatic_renewal_failed(self): sub = self.subscription sub.plan_id.auto_close_limit = 1 start_date = datetime.date(2022, 6, 20) sub.start_date = start_date sub.payment_token_id = self.payment_token.id sub.action_confirm() with freeze_time(start_date + relativedelta(days=sub.plan_id.auto_close_limit)): with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment_rejected): sub._create_recurring_invoice() self.assertEqual(sub.close_reason_id.id, self.env.ref('sale_subscription.close_reason_auto_close_limit_reached').id) def test_renewal_churn(self): # Test what we expect when we # 1) create a renewal quote # 2) close the parent # 3) confirm the renewal SaleOrder = self.env["sale.order"] with freeze_time("2021-01-01"), patch.object(type(SaleOrder), '_get_unpaid_subscriptions', lambda x: []): # so creation with mail tracking context_mail = {'tracking_disable': False} sub = self.env['sale.order'].with_context(context_mail).create({ 'name': 'Parent Sub', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'sale_order_template_id': self.subscription_tmpl.id, }) sub._onchange_sale_order_template_id() # Same product for both lines sub.order_line.product_uom_qty = 1 self.flush_tracking() sub.action_confirm() sub._create_recurring_invoice() self.flush_tracking() action = sub.with_context(tracking_disable=False).prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so = renewal_so.with_context(tracking_disable=False) renewal_so.order_line.product_uom_qty = 3 renewal_so.name = "Renewal" self.flush_tracking() sub.set_close() self.flush_tracking() renewal_so.action_confirm() self.flush_tracking() order_log_ids = sub.order_log_ids.sorted('id') sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', datetime.date(2021, 1, 1), '3_progress', 21.0, 21.0), ('3_transfer', datetime.date(2021, 1, 1), '5_renewed', -21.0, 0.0)]) order_log_ids = renewal_so.order_log_ids.sorted('id') renew_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(renew_data, [('3_transfer', datetime.date(2021, 1, 1), '3_progress', 21, 21), ('1_expansion', datetime.date(2021, 1, 1), '3_progress', 42.0, 63)]) def test_subscription_pricelist_discount(self): context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } pricelist = self.company_data['default_pricelist'] pricelist.discount_policy = 'without_discount' pricelist.item_ids.create({ 'pricelist_id': pricelist.id, 'compute_price': 'percentage', 'percent_price': 50, }) sub = self.env["sale.order"].with_context(**context_no_mail).create({ 'name': 'TestSubscription', 'is_subscription': True, 'plan_id': self.plan_month.id, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'sale_order_template_id': self.subscription_tmpl.id, }) sub._onchange_sale_order_template_id() sub.order_line.create({ 'order_id': sub.id, 'product_id': self.product_a.id, # non-subscription product }) self.assertEqual(sub.order_line.mapped('discount'), [0, 0, 50], "Regular pricelist discounts should't affect temporal items.") sub.order_line.discount = 20 self.assertEqual(sub.order_line.mapped('discount'), [20, 20, 20]) sub.action_confirm() self.assertEqual(sub.order_line.mapped('discount'), [20, 20, 20], "Discounts should not be reset on confirmation.") def test_non_subscription_pricelist_discount(self): context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True, } pricelist = self.company_data['default_pricelist'] pricelist.discount_policy = 'without_discount' pricelist.item_ids.create({ 'pricelist_id': pricelist.id, 'compute_price': 'percentage', 'percent_price': 50, }) so = self.env["sale.order"].with_context(**context_no_mail).create({ 'name': 'TestNonSubscription', 'is_subscription': False, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': pricelist.id, 'order_line': [(0, 0, {'product_id': self.product_a.id})], }) self.assertEqual(so.order_line.discount, 50) so.order_line.discount = 20 self.assertEqual(so.order_line.discount, 20) so.action_confirm() self.assertEqual(so.order_line.discount, 20, "Discounts should not be reset on confirmation.") def test_churn_log_renew(self): """ Test the behavior of the logs when we confirm a renewal quote after the parent has been closed. """ self.flush_tracking() with freeze_time("2024-01-22 08:00:00"): today = datetime.date.today() context_mail = {'tracking_disable': False} sub = self.env['sale.order'].with_context(context_mail).create({ 'name': 'TestSubscription', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'sale_order_template_id': self.subscription_tmpl.id, }) sub._onchange_sale_order_template_id() # Same product for both lines sub.order_line.product_uom_qty = 1 self.flush_tracking() sub.action_confirm() self.flush_tracking() sub.order_line.product_uom_qty = 2 self.flush_tracking() self.env['sale.order'].with_context(tracking_disable=False)._cron_recurring_create_invoice() self.flush_tracking() action = sub.with_context(tracking_disable=False).prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so = renewal_so.with_context(tracking_disable=False) renewal_so.order_line.product_uom_qty = 3 renewal_so.name = "Renewal" self.flush_tracking() sub.set_close() self.flush_tracking() renewal_so.action_confirm() self.flush_tracking() # Most of the time, the renewal invoice is created by the salesman # before the renewal start date renewal_invoices = renewal_so._create_invoices() renewal_invoices._post() order_log_ids = sub.order_log_ids.sorted('id') sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', today, '3_progress', 21, 21), ('1_expansion', today, '3_progress', 21.0, 42.0), ('3_transfer', today, '5_renewed', -42, 0)]) renew_logs = renewal_so.order_log_ids.sorted('id') renew_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in renew_logs] self.assertEqual(renew_data, [('3_transfer', today, '3_progress', 42, 42), ('1_expansion', today, '3_progress', 21.0, 63.0)]) def test_paused_resume_logs(self): self.flush_tracking() today = datetime.date.today() context_mail = {'tracking_disable': False} sub = self.env['sale.order'].with_context(context_mail).create({ 'name': 'TestSubscription', 'is_subscription': True, 'note': "original subscription description", 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'sale_order_template_id': self.subscription_tmpl.id, }) sub._onchange_sale_order_template_id() self.flush_tracking() sub.action_confirm() self.flush_tracking() sub.pause_subscription() self.flush_tracking() sub.pause_subscription() self.flush_tracking() sub.resume_subscription() self.flush_tracking() order_log_ids = sub.order_log_ids.sorted('id') sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [('0_creation', today, '3_progress', 21, 21)]) def test_renewal_different_period(self): """ When a renewal quote is negotiated for more than a month, we need to update the start date of the renewal quote if the parent is prolonged. """ with freeze_time("2023-01-1"): # We reset the renew alert to make sure it will run with freezetime self.subscription.write({'start_date': False, 'next_invoice_date': False}) self.subscription._onchange_sale_order_template_id() self.assertEqual(self.subscription.plan_id, self.plan_month) self.subscription.action_confirm() self.subscription._create_recurring_invoice() action = self.subscription.with_context(tracking_disable=False).prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so = renewal_so.with_context(tracking_disable=False) renewal_so.order_line.product_uom_qty = 3 renewal_so.name = "Renewal" renewal_so.plan_id = self.plan_year self.assertEqual(self.subscription.next_invoice_date, datetime.date(2023, 2, 1)) self.assertEqual(renewal_so.start_date, datetime.date(2023, 2, 1)) self.assertEqual(renewal_so.next_invoice_date, datetime.date(2023, 2, 1)) with freeze_time("2023-02-01"): # the new invoice is created and validated by the customer self.subscription._create_recurring_invoice() self.assertEqual(self.subscription.next_invoice_date, datetime.date(2023, 3, 1)) self.assertEqual(renewal_so.start_date, datetime.date(2023, 3, 1)) self.assertEqual(renewal_so.next_invoice_date, datetime.date(2023, 3, 1)) def test_close_reason_wizard(self): self.subscription._onchange_sale_order_template_id() self.subscription.action_confirm() self.subscription._create_recurring_invoice() new_reason = self.env['sale.order.close.reason'].create({'name': "test reason"}) wiz = self.env['sale.subscription.close.reason.wizard'].with_context(active_id=self.subscription.id).create({ 'close_reason_id': new_reason.id }) wiz.set_close() self.assertEqual(self.subscription.close_reason_id, new_reason, "The reason should be saved on the order") def test_renew_with_different_currency(self): pricelist_eur = self.env['product.pricelist'].create({ 'name': 'Euro pricelist', 'currency_id': self.env.ref('base.EUR').id, }) self.pricing_month.write({'pricelist_id': self.subscription.pricelist_id.id, 'price': 42}) pricing_month_eur = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_month.id, 'pricelist_id': pricelist_eur.id, 'price': 420 }) self.sub_product_tmpl.product_subscription_pricing_ids = [Command.link(pricing_month_eur.id)] self.subscription_tmpl.sale_order_template_line_ids[1].unlink() self.subscription.order_line.product_id.taxes_id = [Command.clear()] self.subscription._onchange_sale_order_template_id() self.subscription.action_confirm() self.assertEqual(self.subscription.amount_total, 42) self.subscription._create_recurring_invoice() action = self.subscription.prepare_renewal_order() renew_so = self.env['sale.order'].browse(action['res_id']) self.assertEqual(renew_so.amount_total, 42) renew_so.pricelist_id = pricelist_eur.id renew_so.action_update_prices() self.assertEqual(renew_so.amount_total, 420) def test_renew_pricelist_currency_update(self): """ Assert that after renewing a subscription, changing the pricelist to another one will recompute the order lines pricings. """ with freeze_time("2023-04-04"): default_pricelist = self.company_data['default_pricelist'] other_currency = self.env.ref('base.EUR') other_currency.action_unarchive() other_pricelist = self.env['product.pricelist'].create({ 'name': 'Test Pricelist (EUR)', 'currency_id': other_currency.id, }) other_currency.rate_ids = [Command.create({'rate': 20})] pricing_month_1_usd = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_month.id, 'price': 100, 'pricelist_id': default_pricelist.id, }) pricing_month_2_eur = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_month.id, 'price': 200, 'pricelist_id': other_pricelist.id, }) sub_product_tmpl = self.env['product.template'].create({ 'name': 'BaseTestProduct', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'product_subscription_pricing_ids': [Command.set((pricing_month_1_usd | pricing_month_2_eur).ids)] }) sub = self.subscription.create({ 'name': 'Company1 - Currency1', 'partner_id': self.user_portal.partner_id.id, 'currency_id': self.company.currency_id.id, 'plan_id': self.plan_month.id, 'pricelist_id': default_pricelist.id, 'order_line': [Command.create({ 'name': "Product 1", 'product_id': sub_product_tmpl.product_variant_id.id, 'product_uom_qty': 1, 'product_uom': sub_product_tmpl.uom_id.id })] }) sub.action_confirm() self.flush_tracking() # Assert that order line was created with correct pricing and currency. self.assertEqual(sub.order_line[0].price_unit, 100.0, "Subscription product's order line must be created with default pricelist pricing (USD) having the price unit as 100.0.") self.assertEqual(sub.order_line[0].order_id.currency_id.id, self.company.currency_id.id, "Subscription product's order line must be created with the default company currency (USD).") self.assertEqual(sub.pricelist_id.id, default_pricelist.id, "Subscription must be created with the default company pricelist (in USD).") self.env['sale.order']._cron_recurring_create_invoice() self.flush_tracking() with freeze_time("2023-04-05"): action = sub.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) # Assert that parent_line_id is saved in renewed subscription. self.assertEqual(renewal_so.order_line[0].parent_line_id.id, sub.order_line[0].id, "The parent line of the order line should have been saved after subscription renewal.") renewal_so.pricelist_id = other_pricelist.id # Computes the updated price unit through 'Update Prices' button. renewal_so.action_update_prices() renewal_so.invalidate_recordset() # Assert that updated pricing has the correct currency, price_unit and pricelist. self.assertEqual(renewal_so.pricelist_id.id, other_pricelist.id, "Pricelist must update to the new one (in EUR) after performing a manual update.") self.assertEqual(renewal_so.order_line[0].currency_id.id, other_currency.id, "Order line's currency should have been updated from USD to EUR after changing the pricelist.") self.assertEqual(renewal_so.order_line[0].price_unit, 200.0, "Order line's price unit must update to 200.0 according to the new pricelist pricing (in EUR).") # Update prices button removes the parent_line_id of order lines to recalculate pricings. self.assertFalse(renewal_so.order_line[0].parent_line_id, "Parent order line should not exist anymore after updating prices, it was intentionally deleted for forcing price recalculation.") def test_plan_field_automatic_price_unit_update(self): """ Assert that after changing the 'Recurrence' field of a subscription, prices will recompute automatically ONLY for subscription products. """ default_pricelist = self.company_data['default_pricelist'] other_currency = self.env.ref('base.EUR') other_currency.action_unarchive() pricing_month_1_eur = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_month.id, 'price': 100, 'pricelist_id': default_pricelist.id, }) pricing_year_1_eur = self.env['sale.subscription.pricing'].create({ 'plan_id': self.plan_year.id, 'price': 1000, 'pricelist_id': default_pricelist.id, }) simple_product = self.product.copy({'recurring_invoice': False}) simple_product_order_line = { 'name': self.product.name, 'product_id': simple_product.id, 'product_uom_qty': 2.0, 'product_uom': simple_product.uom_id.id } sub_product_tmpl = self.env['product.template'].create({ 'name': 'BaseTestProduct', 'type': 'service', 'recurring_invoice': True, 'uom_id': self.env.ref('uom.product_uom_unit').id, 'product_subscription_pricing_ids': [Command.set((pricing_month_1_eur | pricing_year_1_eur).ids)] }) sub_product_order_line = { 'name': "Product 1", 'product_id': sub_product_tmpl.product_variant_id.id, 'product_uom_qty': 1, 'product_uom': sub_product_tmpl.uom_id.id } sub = self.subscription.create({ 'name': 'Company1 - Currency1', 'partner_id': self.user_portal.partner_id.id, 'currency_id': self.company.currency_id.id, 'plan_id': self.plan_month.id, 'pricelist_id': default_pricelist.id, 'order_line': [ Command.create(sub_product_order_line), Command.create(simple_product_order_line) ] }) sub.action_confirm() self.flush_tracking() # Assert that order lines were created with correct pricing and currency. self.assertEqual(sub.order_line[0].price_unit, 100.0, "Subscription product's order line should have its price unit as 100.0 according to the 'Monthly' pricing during creation.") self.assertEqual(sub.order_line[1].price_unit, 50.0, "Simple product's order line must have its default price unit of 50.0 during creation.") # Change the 'Recurrence' field and check if price unit updated ONLY in the recurring order line. sub.plan_id = self.plan_year.id self.assertEqual(sub.order_line[0].price_unit, 1000.0, "Subscription product's order line must have its unit price as 1000.0 after 'Recurrence' is changed to 'Yearly'.") self.assertEqual(sub.order_line[1].price_unit, 50.0, "Simple product's order line must not update its price unit, it must be kept as 50.0 during the 'Recurrence' field changes.") # Update price of normal product and check if it is updated in recurrence (it should not!) sub.order_line[1].product_id.list_price = 70.0 self.assertEqual(sub.order_line[1].price_unit, 50.0, "Simple product's price unit must be kept as 50.0 even though the product price was updated outside the subscription scope.") self.env['sale.order']._cron_recurring_create_invoice() self.flush_tracking() # Change again the 'Recurrence' field and check if the price unit update during renewal was done in the recurring order line. action = sub.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so.plan_id = self.plan_month.id self.assertEqual(renewal_so.order_line[0].price_unit, 100.0, "Subscription product's order line must have its unit price as 100.0 after 'Recurrence' is changed to 'Monthly'.") # Change the 'Recurrence' field to yearly and ensure that price was updated accordingly for the subscription product. renewal_so.plan_id = self.plan_year.id self.assertEqual(renewal_so.order_line[0].price_unit, 1000.0, "Subscription product's order line must have its unit price as 1000.0 after 'Recurrence' is changed to 'Yearly'.") def test_new_plan_id_optional_products_price_update(self): """ Assert that after changing the 'Recurrence' field of a subscription, prices will be recomputed for Optional Products with time-based pricing linked to the subscription template. """ # Define a subscription template with a optional product having time-based pricing. self.product.product_tmpl_id.product_subscription_pricing_ids.unlink() self.env['sale.subscription.pricing'].create({ 'price': 150, 'plan_id': self.plan_month.id, 'product_template_id': self.product.product_tmpl_id.id }) self.env['sale.subscription.pricing'].create({ 'price': 1000, 'plan_id': self.plan_year.id, 'product_template_id': self.product.product_tmpl_id.id }) template = self.env['sale.order.template'].create({ 'name': 'Subscription template with time-based pricing on optional product', 'note': "This is the template description", 'plan_id': self.plan_year.id, 'sale_order_template_line_ids': [Command.create({ 'name': "monthly", 'product_id': self.product.id, 'product_uom_qty': 1, 'product_uom_id': self.product.uom_id.id })], 'sale_order_template_option_ids': [Command.create({ 'name': "line 1", 'product_id': self.product.id, 'quantity': 1, 'uom_id': self.product.uom_id.id, })], }) # Create the subscription based on the subscription template. subscription = self.env['sale.order'].create({ 'name': 'TestSubscription', 'is_subscription': True, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'plan_id': self.plan_month.id, 'sale_order_template_id': template.id, }) subscription._onchange_sale_order_template_id() # Assert that optional product has its price updated after changing the 'recurrence' field. self.assertEqual(subscription.sale_order_option_ids.price_unit, 150, "The price unit for the optional product must be 150.0 due to 'Monthly' value in the 'Recurrence' field.") subscription.plan_id = self.plan_year.id self.assertEqual(subscription.sale_order_option_ids.price_unit, 1000, "The price unit for the optional product must update to 1000.0 after changing the 'Recurrence' field to 'Yearly'.") def test_qty_invoiced_after_revert(self): """ Test invoice quantity is correctly updated after a revert with modify move creation """ self.subscription.write({ 'order_line': [ Command.clear(), Command.create({ 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() invoice = self.subscription.invoice_ids move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({ 'reason': 'no reason', 'journal_id': invoice.journal_id.id, }) reversal = move_reversal.modify_moves() new_move = self.env['account.move'].browse(reversal['res_id']) new_move.action_post() self.assertEqual(self.subscription.order_line.qty_invoiced, 2.0, "Invoiced quantity on the order line is not correct") def test_negative_subscription(self): nr_product = self.env['product.template'].create({ 'name': 'Non recurring product', 'type': 'service', 'uom_id': self.product.uom_id.id, 'list_price': 25, 'invoice_policy': 'order', }) # nr_product.taxes_id = False # we avoid using taxes in this example self.pricing_year.unlink() self.pricing_month.price = 25 self.product2.list_price = -25.0 self.product.product_subscription_pricing_ids.unlink() self.sub_product_tmpl.list_price = -30 self.product_tmpl_2.list_price = -10 self.product2.product_subscription_pricing_ids.unlink() sub_negative_recurring = self.env['sale.order'].create({ 'name': 'sub_negative_recurring (1)', 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, }), (0, 0, { 'name': self.product.name, 'product_id': self.product2.id, 'product_uom_qty': 2.0, 'product_uom': self.product2.uom_id.id, }), ], }) negative_nonrecurring_sub = self.env['sale.order'].create({ 'name': 'negative_nonrecurring_sub (2)', 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, }), (0, 0, { 'name': self.product.name, 'product_id': self.product2.id, 'product_uom_qty': 2.0, 'product_uom': self.product2.uom_id.id, }), (0, 0, { 'name': nr_product.name, 'product_id': nr_product.product_variant_id.id, 'product_uom_qty': 4.0, 'product_uom': nr_product.uom_id.id, }), ], }) all_subs = sub_negative_recurring | negative_nonrecurring_sub with freeze_time("2023-01-01"): self.flush_tracking() all_subs.write({'start_date': False, 'next_invoice_date': False}) all_subs.action_confirm() self.flush_tracking() all_subs.next_invoice_date = datetime.datetime(2023, 2, 1) self.flush_tracking() with freeze_time("2023-02-01"): sub_negative_recurring.order_line.product_uom_qty = 6 # update quantity negative_nonrecurring_sub.order_line[1].product_uom_qty = 4 self.flush_tracking() all_subs._create_recurring_invoice() # should not create any invoice because negative self.flush_tracking() with freeze_time("2023-02-15"): action = sub_negative_recurring.prepare_renewal_order() renewal_so1 = self.env['sale.order'].browse(action['res_id']) renewal_so1.name = 'renewal_so1' renewal_so1.order_line.product_uom_qty = 12 action = negative_nonrecurring_sub.prepare_renewal_order() renewal_so2 = self.env['sale.order'].browse(action['res_id']) renewal_so2.name = 'renewal_so2' renewal_so2.order_line[1].product_uom_qty = 8 self.flush_tracking() all_subs |= renewal_so1|renewal_so2 (renewal_so1|renewal_so2).action_confirm() self.flush_tracking() with freeze_time("2023-03-01"): (all_subs)._create_recurring_invoice() self.flush_tracking() with freeze_time("2023-04-01"): self.flush_tracking() self.assertFalse(renewal_so2.invoice_ids, "no invoice should have been created") close_reason_id = self.env.ref('sale_subscription.close_reason_1').id renewal_so2.set_close(close_reason_id=close_reason_id) self.flush_tracking() renewal_so2.reopen_order() self.flush_tracking() order_log_ids = self.env['sale.order.log'].search([('order_id', 'in', (sub_negative_recurring|renewal_so1).ids)], order='id') sub_data1 = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data1, [('0_creation', datetime.date(2023, 1, 1), '3_progress', 0, 0), ('3_transfer', datetime.date(2023, 2, 15), '3_progress', 0, 0), ('3_transfer', datetime.date(2023, 2, 15), '5_renewed', 0, 0)]) order_log_ids = self.env['sale.order.log'].search([('order_id', 'in', (negative_nonrecurring_sub|renewal_so2).ids)], order='id') sub_data2 = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data2, [('0_creation', datetime.date(2023, 1, 1), '3_progress', 0, 0), ('3_transfer', datetime.date(2023, 2, 15), '3_progress', 0, 0), ('3_transfer', datetime.date(2023, 2, 15), '5_renewed', 0, 0)]) self.assertEqual(renewal_so1.recurring_monthly, -480, "The MRR field is negative but it does not produce logs") self.assertEqual(renewal_so2.recurring_monthly, -140, "The MRR field is negative but it does not produce logs") def test_reopen_parent_child_canceled(self): """ Renew a contract a few time, invoice it, check the computed amount of invoices Then cancel a non invoiced renewal and see if it restart the parent """ with freeze_time("2023-11-03"): self.flush_tracking() self.subscription.write({ 'start_date': False, 'next_invoice_date': False, 'partner_invoice_id': self.partner_a_invoice.id, 'partner_shipping_id': self.partner_a_shipping.id, }) self.subscription.action_confirm() self.flush_tracking() self.subscription._create_recurring_invoice() self.assertEqual(self.subscription.invoice_count, 1) self.flush_tracking() with freeze_time("2023-12-03"): action = self.subscription.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) self.flush_tracking() renewal_so.action_confirm() self.flush_tracking() renewal_so._create_recurring_invoice() self.assertEqual(renewal_so.invoice_count, 2) self.flush_tracking() with freeze_time("2024-01-03"): action = renewal_so.prepare_renewal_order() renewal_so2 = self.env['sale.order'].browse(action['res_id']) self.flush_tracking() renewal_so2.action_confirm() self.flush_tracking() self.assertEqual(renewal_so.subscription_state, '5_renewed') self.assertEqual(renewal_so2.subscription_state, '3_progress') renewal_so2._action_cancel() renewal_so.end_date = False self.flush_tracking() self.assertEqual(renewal_so.subscription_state, '3_progress') self.assertFalse(renewal_so2.subscription_state) with freeze_time("2024-02-03"): renewal_so._create_recurring_invoice() self.flush_tracking() (self.subscription | renewal_so | renewal_so2).invalidate_recordset(['invoice_ids', 'invoice_count']) self.assertEqual(renewal_so.invoice_count, 3, "All contracts have the same count") self.assertEqual(renewal_so2.invoice_count, 3, "All contracts have the same count") self.assertEqual(self.subscription.invoice_count, 3, "All contracts have the same count") def test_renew_simple_user(self): user_sales_salesman = self.company_data['default_user_salesman'] subscription = self.env['sale.order'].with_user(user_sales_salesman).create({ 'partner_id': self.partner_a.id, 'company_id': self.company_data['company'].id, 'plan_id': self.plan_month.id, 'order_line': [ (0, 0, { 'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 2.0, 'product_uom': self.product.uom_id.id, 'price_unit': 12, })], }) subscription.with_user(user_sales_salesman).action_confirm() self.env['sale.order']._cron_recurring_create_invoice() action = subscription.with_user(user_sales_salesman).prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so.with_user(user_sales_salesman).action_confirm() def test_alert_next_activity(self): ''' Ensure correct functionality of sale order creation. This function validates the process of creating sale orders. Previously, there was an issue of infinite recursion during alert creation. The recursion occurred because calling _configure_alerts led to a call to write, which in turn would call _configure_alerts again. ''' self.env['sale.order.alert'].create([{ 'name': 'Test Alert', 'trigger_condition': 'on_create_or_write', 'subscription_state_from': '3_progress', 'subscription_state': '6_churn', 'action': 'next_activity', }]) def test_multiple_churn_log(self): with freeze_time("2024-01-22"): subscription = self.env['sale.order'].create({ 'name': 'TestSubscription', 'is_subscription': True, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'plan_id': self.plan_month.id, 'sale_order_template_id': self.subscription_tmpl.id, }) subscription._onchange_sale_order_template_id() self.flush_tracking() subscription.action_confirm() self.flush_tracking() self.env['sale.order']._cron_recurring_create_invoice() self.flush_tracking() # create crappy logs to simulate issues on history logs self.env['sale.order.log'].sudo().create([ { 'event_type': '2_churn', 'event_date': fields.Date.today() + relativedelta(days=6), 'order_id': subscription.id, 'origin_order_id': subscription.id, 'amount_signed': - subscription.recurring_monthly, 'recurring_monthly': 0, 'currency_id': subscription.currency_id.id, 'subscription_state': '6_churn', }, { 'event_type': '2_churn', 'event_date': fields.Date.today(), 'order_id': subscription.id, 'origin_order_id': subscription.id, 'amount_signed': - subscription.recurring_monthly, 'recurring_monthly': 0, 'currency_id': subscription.currency_id.id, 'subscription_state': '6_churn', }, { 'event_type': '0_creation', 'event_date': fields.Date.today() + relativedelta(days=5), 'order_id': subscription.id, 'origin_order_id': subscription.id, 'amount_signed': subscription.recurring_monthly, 'recurring_monthly': subscription.recurring_monthly, 'currency_id': subscription.currency_id.id, 'subscription_state': '3_progress', } ]) with freeze_time("2024-02-02"): subscription.set_close() self.flush_tracking() order_log_ids = subscription.order_log_ids.sorted('id') sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [ ('0_creation', datetime.date(2024, 1, 22), '3_progress', 21.0, 21.0), ('2_churn', datetime.date(2024, 1, 28), '6_churn', -21.0, 0.0), # weird order by design to make sure it does not affect the business logic ('2_churn', datetime.date(2024, 1, 22), '6_churn', -21.0, 0.0), # order by date is correct ('0_creation', datetime.date(2024, 1, 27), '3_progress', 21.0, 21.0), ('2_churn', datetime.date(2024, 2, 2), '6_churn', -21.0, 0.0), ]) with freeze_time("2024-02-03"): subscription.reopen_order() self.flush_tracking() order_log_ids = subscription.order_log_ids.sorted('id') sub_data = [(log.event_type, log.event_date, log.subscription_state, log.amount_signed, log.recurring_monthly) for log in order_log_ids] self.assertEqual(sub_data, [ ('0_creation', datetime.date(2024, 1, 22), '3_progress', 21.0, 21.0), ('2_churn', datetime.date(2024, 1, 28), '6_churn', -21.0, 0.0), # weird order by id, order by date is more logical ('2_churn', datetime.date(2024, 1, 22), '6_churn', -21.0, 0.0), ('0_creation', datetime.date(2024, 1, 27), '3_progress', 21.0, 21.0), ], "The last churn is removed") def test_recurring_plan_price_recalc_adding_optional_product(self): """ Test that when an optional recurring product is added to a subscription sale order that its price unit is correctly recalculated after subsequent edits to the order's recurring plan """ self.sub_product_tmpl.write({'product_subscription_pricing_ids': [Command.set(self.pricing_year.id)]}) product_a = self.sub_product_tmpl.product_variant_id product_a.list_price = 1.0 self.product_tmpl_2.write({'product_subscription_pricing_ids': [Command.set(self.pricing_year_2.id)]}) product_b = self.product_tmpl_2.product_variant_id product_b.list_price = 1.0 sale_order = self.env['sale.order'].create({ 'plan_id': self.plan_month.id, 'partner_id': self.user_portal.partner_id.id, 'company_id': self.company_data['company'].id, 'order_line': [ Command.create({'product_id': product_a.id}), Command.create({'product_id': product_b.id}) ], 'sale_order_option_ids': [Command.create({'product_id': product_b.id})], }) sale_order.sale_order_option_ids.line_id = sale_order.order_line[1].id sale_order.write({'plan_id': self.plan_year}) self.assertEqual(sale_order.order_line[1].price_unit, 200.0) def test_upsell_total_qty(self): self.subscription.action_confirm() self.subscription._create_recurring_invoice() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_so.order_line.filtered(lambda l: not l.display_type).product_uom_qty = 2 upsell_so.action_confirm() for line in upsell_so.order_line.filtered(lambda l: not l.display_type): self.assertEqual(line.upsell_total, 3) def test_sale_subscription_upsell_does_not_copy_non_recurring_products(self): nr_product = self.env['product.template'].create({ 'name': 'Non recurring product', 'type': 'service', 'uom_id': self.product.uom_id.id, 'list_price': 25, 'invoice_policy': 'order', }) self.subscription.action_confirm() self.subscription._create_recurring_invoice() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_so.order_line = [(6, 0, self.env['sale.order.line'].create({ 'name': nr_product.name, 'order_id': upsell_so.id, 'product_id': nr_product.product_variant_id.id, 'product_uom_qty': 1, }).ids)] upsell_so._confirm_upsell() self.assertEqual(len(upsell_so.order_line), 1) self.assertEqual(len(self.subscription.order_line), 2) self.assertEqual(upsell_so.order_line.name, nr_product.name) self.assertFalse(nr_product in self.subscription.order_line.product_template_id) def test_change_recurrence_plan_with_option(self): """ A recurring order with a line for a recurring produce and a sale order option for a recurring product yields an exception when changing the recurring plan via Form, preventing the plan from being changed """ order_1 = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'order_line': [Command.create({'product_id': self.product.id})], }) self.env['sale.order.option'].create({ 'order_id': order_1.id, 'product_id': self.product.id, }) with Form(order_1) as order_form: order_form.plan_id = self.plan_week self.assertEqual(order_1.plan_id, self.plan_week) def test_subscription_lock_settings(self): """ The settings to automatically lock SO upon confirmation should never be applied to subscription orders. """ self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting') self.subscription.write({'start_date': False, 'next_invoice_date': False}) self.subscription.action_confirm() self.assertEqual(self.subscription.state, 'sale') def test_upsell_descriptions(self): """ On invoicing upsells, only subscription-based items should display a duration. """ with freeze_time("2022-10-31"): self.subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() action = self.subscription.prepare_upsell_order() upsell_so = self.env['sale.order'].browse(action['res_id']) upsell_so.order_line = [Command.create({'product_id': self.product_a.id})] upsell_so.order_line.filtered('product_id').product_uom_qty = 1 upsell_so.action_confirm() invoice = upsell_so._create_invoices() self.assertEqual(len(invoice.invoice_line_ids), 4) for line in invoice.invoice_line_ids: name = line.name sol_name = line.sale_line_ids.name if line.sale_line_ids.recurring_invoice: self.assertRegex(name, rf"^{sol_name} - 1 Month", "Sub lines require duration") else: self.assertEqual(name, sol_name, "Non-sub lines shouldn't add duration") def test_stock_user_without_sale_permission_can_access_product_form(self): stock = self.env['ir.module.module']._get('stock') if stock.state != 'installed': self.skipTest("stock module is not installed") stock_manager = new_test_user( self.env, 'temp_stock_manager', 'stock.group_stock_manager', ) Form(self.env['product.product'].with_user(stock_manager)) def test_product_subscription_pricing_copy(self): """Check that product variants on product pricings after copying a product template. """ product = self.product_tmpl_2 product_attribute = self.env['product.attribute'].create({ 'name': 'Color', 'value_ids': [Command.create({'name': name}) for name in ('Blue', 'Red')], }) product.attribute_line_ids = 2 * [Command.create({ 'attribute_id': product_attribute.id, 'value_ids': product_attribute.value_ids.ids, })] for i, variant in enumerate(product.product_variant_ids, start=1): self.env['sale.subscription.pricing'].create([{ 'product_template_id': product.id, 'product_variant_ids': [Command.link(variant.id)], 'plan_id': self.plan_week.id, 'price': 10.0 * i, 'pricelist_id': self.company_data['default_pricelist'].id, }, { 'product_template_id': product.id, 'product_variant_ids': [Command.link(variant.id)], 'plan_id': self.plan_month.id, 'price': 25.0 * i, }]) pricings_1 = product.product_subscription_pricing_ids.sorted() pricings_2 = product.copy().product_subscription_pricing_ids.sorted() self.assertEqual( len(pricings_2), 8, # 2 attributes * 2 values * 2 plans = 8 pricings "copied product should get 8 pricings", ) self.assertNotEqual( pricings_2.product_variant_ids, pricings_1.product_variant_ids, "copied pricings shouldn't be linked to the original products", ) for pricing_1, pricing_2 in zip(pricings_1, pricings_2): self.assertEqual(pricing_2.price, pricing_1.price) self.assertEqual(pricing_2.plan_id, pricing_1.plan_id) self.assertEqual(pricing_2.pricelist_id, pricing_1.pricelist_id) def test_recurring_create_invoice_branch(self): self.company.child_ids = [Command.create({'name': 'Branch'})] branch = self.company.child_ids[0] self.product.company_id = branch sub = self.env['sale.order'].with_company(branch).create({ 'name': 'TestSubscription', 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.user_portal.partner_id.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'order_line': [(0, 0, {'product_id': self.product.id})], }) sub.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() self.assertEqual(sub.invoice_count, 1) self.assertEqual(sub.invoice_ids.company_id, branch) self.assertEqual(sub.invoice_ids.state, 'posted') def test_sale_determine_order(self): """ If a subscription order is locked but has a renewal (child order), an attempt to expense some purchase order which is linked to the analytic account of the subscription should use the non-locked, renewed order. """ required_modules = ('project_hr_expense', 'purchase') if not all(self.env['ir.module.module']._get(module).state == 'installed' for module in required_modules): self.skipTest(f'This test requires the installation of the following modules: {required_modules}') analytic_plan = self.env['account.analytic.plan'].create({'name': 'TSDO analytic plan'}) analytic_account1, analytic_account2 = self.env['account.analytic.account'].create([{ 'name': f'TSDO analytic account {i}', 'plan_id': analytic_plan.id, } for i in (1, 2)]) project = self.env['project.project'].with_context({'mail_create_nolog': True}).create({ 'name': 'Project', 'partner_id': self.partner_a.id, 'analytic_account_id': analytic_account1.id, }) recurring_service_with_linked_project = self.env['product.product'].create({ 'name': 'service_with_linked_project', 'standard_price': 30, 'list_price': 90, 'type': 'service', 'service_tracking': 'task_global_project', 'project_id': project.id, 'recurring_invoice': True, }) expensable_service_product = self.env['product.product'].create({ 'name': 'Material', 'type': 'service', 'standard_price': 5, 'list_price': 10, 'can_be_expensed': True, 'expense_policy': 'sales_price', 'purchase_method': 'purchase', }) subscription1, subscription2 = self.env['sale.order'].create([{ 'name': 'TestSubscription', 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.partner_a.id, 'order_line': [(0, 0, { 'product_id': recurring_service_with_linked_project.id, 'product_uom_qty': 1, 'price_unit': 10, })], 'analytic_account_id': analytic_account_id, } for analytic_account_id in [analytic_account1.id, analytic_account2.id]]) purchase_order = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [(0, 0, { 'product_id': expensable_service_product.id, 'product_uom_qty': 1, 'analytic_distribution': {subscription1.analytic_account_id.id: 100.0}, }), (0, 0, { 'product_id': expensable_service_product.id, 'product_uom_qty': 1, 'analytic_distribution': {subscription2.analytic_account_id.id: 100.0}, })], }) subscriptions = subscription1 + subscription2 subscriptions.action_confirm() subscriptions._create_invoices() subscriptions.invoice_ids[0].action_post() subscription1.prepare_renewal_order() subscription2.prepare_renewal_order() first_renewal_sub1 = subscription1.subscription_child_ids[0] first_renewal_sub1.action_confirm() first_renewal_sub1.with_context(disable_cancel_warning=True).action_cancel() subscription1.subscription_state = '3_progress' subscription1.prepare_renewal_order() second_renewal_sub1 = subscription1.subscription_child_ids[1] second_renewal_sub1.action_confirm() second_renewal_sub1._create_invoices() second_renewal_sub1.invoice_ids[1].action_post() second_renewal_sub1.prepare_renewal_order() second_renewal_renewal_sub1 = second_renewal_sub1.subscription_child_ids[0] second_renewal_renewal_sub1.action_confirm() purchase_order.button_confirm() purchase_order.action_create_invoice() purchase_order_invoice = purchase_order.invoice_ids[0] purchase_order_invoice.invoice_date = '2000-05-05' purchase_order_invoice.action_post() self.assertEqual(purchase_order_invoice.state, 'posted') def test_uspell_same_product_different_discount(self): subscription = self.env['sale.order'].create({ 'name': 'Original subscription', 'is_subscription': True, 'partner_id': self.user_portal.partner_id.id, 'plan_id': self.plan_month.id, 'order_line': [ Command.create({ 'name': self.product2.name, 'product_id': self.product2.id, 'product_uom_qty': 1, 'product_uom': self.env.ref('uom.product_uom_unit').id, 'discount': 0, }), Command.create({ 'name': self.product2.name, 'product_id': self.product2.id, 'product_uom_qty': 1, 'product_uom': self.env.ref('uom.product_uom_unit').id, 'discount': 50, }) ] }) subscription.action_confirm() self.env['sale.order']._cron_recurring_create_invoice() action = subscription.prepare_upsell_order() upsell = self.env['sale.order'].browse(action['res_id']) self.assertEqual(set(upsell.order_line.mapped('discount')), {0, 50}, 'Upsell discounts should match subscription discounts') def test_prevent_advance_payment_delivered_quantity_product(self): """ Test that recurring products with invoice policy set on delivery quantity that are not yet delivered are not accounted in the subscription.amount_to_invoice """ product_ordered, product_delivered = self.env['product.product'].create([ { 'name': 'Product Ordered', 'type': 'service', 'recurring_invoice': True, 'invoice_policy': 'order', }, { 'name': 'Product Delivered', 'type': 'service', 'recurring_invoice': True, 'invoice_policy': 'delivery', } ]) subscription = self.env['sale.order'].create({ 'name': 'subscription', 'partner_id': self.partner.id, 'plan_id': self.plan_month.id, 'order_line': [ Command.create({ 'product_id': product_ordered.id, 'price_unit': 10, 'qty_to_deliver': 1, }), Command.create({ 'product_id': product_delivered.id, 'price_unit': 5, 'qty_to_deliver': 3, }) ] }) self.assertEqual(subscription.amount_to_invoice, 11.5) delivered_line = subscription.order_line.filtered(lambda line: line.product_id == product_delivered) delivered_line.qty_delivered = 1.0 subscription.action_confirm() self.assertEqual(subscription.amount_to_invoice, 17.25)