# Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime from dateutil.relativedelta import relativedelta from unittest.mock import patch from odoo import Command from odoo.tests import HttpCase, tagged, freeze_time from odoo.tools import mute_logger from odoo.exceptions import AccessError, UserError from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon, UncatchableException @tagged('post_install', '-at_install') class TestSubscriptionInvoice(TestSubscriptionCommon, HttpCase): @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')[2] self.assertEqual(invoice_periods, "1 Month 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')[2] self.assertEqual(invoice_periods, "1 Month 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')[2] self.assertEqual(invoice_periods, "1 Month 03/03/2021 to 04/02/2021") self.assertEqual(inv.invoice_line_ids[0].date, datetime.date(2021, 3, 3)) def test_invoicing_with_section(self): """ Test invoicing when order has section/note.""" # create specific test products sub_product1_tmpl = self.env['product.template'].with_context(self.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(self.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(self.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(**self.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, '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({ 'display_type': 'line_subsection', 'name': 'Subscriptions', }), 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': 'Initial discount 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) preview_so = sub.action_preview_sale_order() url = preview_so.get('url') # Ensure sale order preview does not crash when collapse_prices is accessed self.assertEqual(self.url_open(url).status_code, 200) sub.invoice_ids._post() invoice = sub.invoice_ids[-1] self.assertEqual(invoice.amount_untaxed, 148.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': 'line_subsection', 'name': 'Subscriptions', 'product_id': False}, { 'display_type': 'product', 'product_id': sub_product1.id, 'name': 'Subscription #A\n1 Month 01/03/2021 to 02/02/2021', }, { 'display_type': 'product', 'product_id': sub_product2.id, 'name': 'Subscription #B\n1 Month 01/03/2021 to 02/02/2021', }, { 'display_type': 'product', 'product_id': sub_product_onetime_discount.id, 'name': 'Initial discount New subscription discount (one-time)', }, ]) with freeze_time("2021-02-03"): invoice = sub._create_invoices() invoice._post() # second invoice, should NOT include one-time discount self.assertEqual(len(sub.invoice_ids), 2) self.assertEqual(invoice.amount_untaxed, 168.0) self.assertEqual(len(invoice.invoice_line_ids), 4) self.assertRecordValues(invoice.invoice_line_ids, [ {'display_type': 'line_section', 'name': 'Products', 'product_id': False}, {'display_type': 'line_subsection', 'name': 'Subscriptions', 'product_id': False}, { 'display_type': 'product', 'product_id': sub_product1.id, 'name': 'Subscription #A\n1 Month 02/03/2021 to 03/02/2021', }, { 'display_type': 'product', 'product_id': sub_product2.id, 'name': 'Subscription #B\n1 Month 02/03/2021 to 03/02/2021', }, ]) 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, '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')[2] self.assertEqual(invoice_periods, "1 Month 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')[2] self.assertEqual(invoice_periods, "1 Month 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')[2] self.assertEqual(invoice_periods, "1 Month 03/03/2021 to 04/02/2021") self.assertEqual(inv.invoice_line_ids.date, datetime.date(2021, 3, 3)) @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_invoice_on_period_start_service_sale_order(self): """ Test behaviour of on_change_template """ with freeze_time("2021-01-15"): sub = self.subscription sub.order_line = [Command.clear()] sub_product_tmpl = self.ProductTmpl.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, 'end_date': '2021-03-18'}) sub.plan_id.billing_first_day = True sub.action_confirm() inv = sub._create_recurring_invoice() self.assertAlmostEqual(inv.amount_untaxed, 84 * 17 / 31, 1, msg="15 to 31 is 17 days (15 and 31 included)") with freeze_time("2021-02-01"): # February inv = sub._create_recurring_invoice() self.assertAlmostEqual(inv.amount_untaxed, 84) with freeze_time("2021-03-01"): # March inv = sub._create_recurring_invoice() self.assertAlmostEqual(inv.amount_untaxed, 84, 1, msg="Last period is full too 01 to 01 even if the end_date occurs in the middle of the period") 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({ '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'], [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['invoiced'], [3, 1], "To invoice should be equal to quantity") self.assertEqual(val_invoice['to_invoice'], [0, 0], "To invoice should be 0") self.assertEqual(val_invoice['delivered'], [3, 1], "Delivered qty should be set") with freeze_time("2021-01-05"): val_invoice = self._get_quantities(self.subscription.order_line) self.assertEqual(val_invoice['invoiced'], [3, 1], "To invoice should be equal to quantity") self.assertEqual(val_invoice['to_invoice'], [0, 0], "To invoice should be 0") self.assertEqual(val_invoice['delivered'], [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'], [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'], [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'], [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_product_invoice_delivery(self): sub = self.subscription sub.order_line = [Command.clear()] delivered_product_tmpl = self.env['product.template'].with_context(self.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) inv = sub._create_recurring_invoice() self.assertEqual(inv.invoice_line_ids.deferred_start_date, datetime.date(2021, 1, 3)) self.assertEqual(inv.invoice_line_ids.deferred_end_date, datetime.date(2021, 2, 2)) 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"): inv = 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) 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) 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, 2) 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_invoice_status_postpaid(self): with freeze_time("2022-05-15"): self.product.invoice_policy = 'delivery' subscription_future = self.env['sale.order'].create({ 'name': "subscription future", '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, 'price_unit': 12, })], }) subscription_now = self.env['sale.order'].create({ 'name': "subscription now", '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, 'price_unit': 12, })], }) subscription_past = subscription_now.copy({ 'name': "subscription past", 'start_date': '2022-04-15', 'next_invoice_date': '2022-04-15', }) subscriptions = subscription_future + subscription_now + subscription_past subscriptions.action_confirm() self.assertEqual(subscription_now.next_invoice_date, datetime.date(2022, 6, 15), "Postpaid subscription first invoice should be end of period") self.assertEqual(subscription_past.next_invoice_date, datetime.date(2022, 5, 15), "Postpaid subscription first invoice should be end of period") self.assertEqual(subscription_future.next_invoice_date, datetime.date(2022, 7, 1), "Postpaid subscription first invoice should be end of period") # Nothing delivered, nothing invoiced self.assertEqual(subscription_future.order_line.invoice_status, 'no', "The line qty should be black.") self.assertEqual(subscription_now.order_line.invoice_status, 'no', "The line qty should be black.") subscription_now.order_line.qty_delivered = 1 self.assertEqual(subscription_now.order_line.invoice_status, 'to invoice', "The line qty should be blue.") # 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.", ) subscriptions._create_recurring_invoice() self.assertEqual(subscription_past.order_line.invoice_status, 'invoiced', "The line qty should be invoiced.") self.assertEqual(subscription_now.order_line.invoice_status, 'to invoice', "The line qty should not be invoiced yet.") self.assertEqual(subscription_future.order_line.invoice_status, 'no', "The line qty should be black.") with freeze_time('2022-06-15'): subscriptions._create_recurring_invoice() self.assertEqual(subscription_past.order_line.invoice_status, 'invoiced', "The line qty should be invoiced.") self.assertEqual(subscription_now.order_line.invoice_status, 'invoiced', "The line qty should not be invoiced yet.") self.assertEqual(subscription_future.order_line.invoice_status, 'no', "The line qty should be black.") 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, '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_ids, refund_move, "The initial move should be reversed") refund_move._post() self.assertEqual(subscription.order_line.qty_invoiced, 0, "The products should not be invoiced") 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, })] }) 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 subscription_state != 3_progress 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"): action = self.subscription.prepare_renewal_order() renewal_so = self.env['sale.order'].browse(action['res_id']) renewal_so.action_confirm() self.assertTrue(self.subscription.locked) self.env['sale.order']._cron_recurring_create_invoice() self.subscription._cron_recurring_create_invoice() self.assertEqual(self.subscription.invoice_count, 2, "locked state don't prevent invoices anymore") 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, }) 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, msg='Downpayment price should be 10') invoice._post() self.assertEqual(self.subscription.next_invoice_date, datetime.date(2021, 1, 3)) invoice = self.subscription._create_invoices(final=True) # manual self.assertAlmostEqual(invoice.amount_total, total - 10, 4, msg='Downpayment should be deducted from the price') invoice._post() self.assertEqual(self.subscription.next_invoice_date, datetime.date(2021, 2, 3)) with freeze_time('2021-02-03'): inv = self.subscription._create_recurring_invoice() self.assertAlmostEqual(inv.amount_total, total, 4, msg='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 self.assertEqual(total, 23.1) downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({ 'advance_payment_method': 'fixed', 'fixed_amount': 10, }) 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, msg='Downpayment price should be 10') invoice._post() invoice = self.subscription._create_invoices(final=True) invoice._post() self.assertAlmostEqual(invoice.amount_total, total - 10, 4, msg='Downpayment should be deducted from the price') with freeze_time('2021-02-03'): invoice = self.subscription._create_invoices(final=True) self.assertAlmostEqual(invoice.amount_total, total, 4, msg='Downpayment should not be deducted from the price anymore') 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_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, '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_recurring_invoice_count(self): 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, 'order_line': [(0, 0, {'product_id': self.product.id})], }) sub.action_confirm() inv = self.env['sale.order']._cron_recurring_create_invoice() new_invoice = inv.copy() self.assertEqual(sub.invoice_ids, inv | new_invoice) def test_invoicing_access_rights(self): """Ensure a salesman can get amount invoiced for subscriptions with others' invoices.""" sales_user = self.company_data['default_user_salesman'] self.product_a.write({ 'name': "Non-recurring product", 'recurring_invoice': False, }) self.subscription.write({ 'user_id': sales_user.id, 'order_line': [Command.create({ 'product_id': self.product_a.id, 'tax_ids': False, })], }) self.subscription.action_confirm() self.assertAlmostEqual( self.subscription.with_user(sales_user).amount_to_invoice, sum(self.subscription.order_line.mapped('price_total')), msg="All lines should still need to be invoiced", ) # assign a different user to the invoice invoice = self.subscription._create_recurring_invoice() invoice.user_id = self.env.ref('base.user_admin') # ensure salesman doesn't have access to just any invoice invoice.invalidate_recordset(['name']) with self.assertRaises(AccessError): invoice.with_user(sales_user).user_id = sales_user self.assertAlmostEqual( self.subscription.with_user(sales_user).amount_invoiced, self.product_a.list_price, msg="We should get the amount invoiced for non-recurring products", ) @freeze_time("2025-04-14") def test_failing_sub_invoice_flag(self): """ Make sure that when the cron is crashing, we don't end up with successful subscription with an error flag. Processed subscriptions are """ self.subscription.write({ 'partner_id': self.partner.id, 'company_id': self.company.id, 'payment_token_id': self.payment_token.id, 'sale_order_template_id': self.subscription_tmpl.id, }) self.subscription._onchange_sale_order_template_id() self.subscription.action_confirm() start_date = datetime.date.today() self.subscription.start_date = start_date self.subscription.payment_token_id = False subs = self.env['sale.order'] for _i in range(3): subs |= self.subscription.copy() # last sub of the batch will make the cron crash. # _create_recurring_invoice revert the order of subscriptions and we want to crash on the last one self.crashing_sub_id = subs[0].id subs.action_confirm() self.assertEqual(subs.mapped('next_invoice_date'), [datetime.date(2025, 4, 14), datetime.date(2025, 4, 14), datetime.date(2025, 4, 14)]) # Create invoices, the third should crash but only the last one will keep is_invoice_cron value try: with patch( 'odoo.addons.sale_subscription.models.sale_order.SaleOrder._process_invoices_to_send', wraps=self._mock_subscription_process_invoice_to_send ), mute_logger('odoo.addons.sale_subscription.models.sale_order'): subs._create_recurring_invoice() except UncatchableException: pass self.assertEqual(subs.mapped('is_invoice_cron'), [True, False, False]) invs = self.env['account.move'].search([('invoice_line_ids.subscription_id', 'in', subs.ids)]) self.assertEqual(invs.mapped('state'), ["posted", "posted", "posted"]) # Make sure next time it's running it reset all is_invoice_cron self.env['sale.order']._create_recurring_invoice() self.assertEqual(subs.mapped('is_invoice_cron'), [False, False, False]) def test_invoice_delivery_free(self): # Ensure free subscription are not processed endlessly by cron with freeze_time('2025-11-11'): sub = self.subscription sub.order_line[0].unlink() sub.order_line.product_id.invoice_policy = "delivery" # ensure at confirmation the next invoice date will be today sub.start_date = datetime.date.today() - datetime.timedelta(days=31) sub.action_confirm() self.assertEqual(sub.next_invoice_date, datetime.date(2025, 11, 11)) self.assertFalse(sub.order_line.qty_delivered, "We don't deliver the product for the first period") sub._create_recurring_invoice() self.assertEqual(sub.next_invoice_date, datetime.date(2025, 12, 11)) with freeze_time('2025-12-11'): sub._create_recurring_invoice() self.assertEqual(sub.next_invoice_date, datetime.date(2026, 1, 11)) self.assertFalse(sub.invoice_ids, "No invoice should be created") def test_billing_first_day_monthly(self): """Test billing_first_day aligns invoice periods to calendar month boundaries""" with freeze_time('2025-12-02'): today = datetime.date.today() self.plan_month.billing_first_day = True common_vals = { 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.user_portal.partner_id.id, 'order_line': [Command.create({'product_id': self.product.id})], } subs = self.env['sale.order'].create([{ **common_vals, 'name': 'Test Subscription 1st', 'start_date': today + relativedelta(months=-1, day=1), }, { **common_vals, 'name': 'Test Subscription Mid', 'start_date': today + relativedelta(months=-1, day=4), }]) subs.action_confirm() # Test 1st of month invoice1 = subs[0]._create_invoices() line1 = invoice1.invoice_line_ids[0] self.assertEqual(line1.deferred_start_date, datetime.date(2025, 11, 1)) self.assertEqual(line1.deferred_end_date, datetime.date(2025, 11, 30)) self.assertEqual((line1.deferred_end_date - line1.deferred_start_date).days + 1, 30) # Test mid-month invoice2 = subs[1]._create_invoices() line2 = invoice2.invoice_line_ids[0] self.assertEqual(line2.deferred_start_date, datetime.date(2025, 11, 4)) self.assertEqual(line2.deferred_end_date, datetime.date(2025, 11, 30)) self.assertEqual((line2.deferred_end_date - line2.deferred_start_date).days + 1, 27) def test_billing_first_day_monthly_with_post_paid_advance(self): # Test with future start date and modified next_invoice_date with freeze_time('2026-03-09'): today = datetime.date.today() self.plan_month.billing_first_day = True sub_future = self.env['sale.order'].create({ 'name': 'Test Subscription', 'is_subscription': True, 'plan_id': self.plan_month.id, 'partner_id': self.user_portal.partner_id.id, 'start_date': today + relativedelta(days=3), 'order_line': [Command.create({'product_id': self.product.id})], }) sub_future.action_confirm() sub_future.next_invoice_date = today + relativedelta(months=1, day=8) invoice_future = sub_future._create_invoices() line_future = invoice_future.invoice_line_ids[0] self.assertEqual(line_future.deferred_start_date, datetime.date(2026, 4, 8)) self.assertEqual(line_future.deferred_end_date, datetime.date(2026, 4, 30)) self.assertEqual((line_future.deferred_end_date - line_future.deferred_start_date).days + 1, 23) def test_billing_multiple_months_plan(self): """ Ensures that creating an invoice for a recurring plan over multiple months correctly uses the billing_period_value. """ with freeze_time('2026-04-15'): self.plan_2_month.billing_first_day = True subscription = self.env['sale.order'].create({ 'name': 'Subscription', 'is_subscription': True, 'plan_id': self.plan_2_month.id, 'partner_id': self.user_portal.partner_id.id, 'start_date': datetime.date.today(), 'order_line': [Command.create({'product_id': self.product.id})], }) subscription.action_confirm() invoice = subscription._create_invoices() line = invoice.invoice_line_ids[0] self.assertEqual(line.deferred_start_date, datetime.date(2026, 4, 15)) self.assertEqual(line.deferred_end_date, datetime.date(2026, 5, 31))