Files
enterprise-addons/sale_subscription/tests/test_subscription_invoice.py
T

1011 lines
51 KiB
Python

# 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))