forked from Mapan/odoo17e
4086 lines
207 KiB
Python
4086 lines
207 KiB
Python
# -*- 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('<p>original subscription description</p>'), "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('<p>This is the template description</p>'), '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('<p>internal note</p>'), '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('<p>new internal note</p>'), 'Internal Note should be updated')
|
|
self.assertEqual(self.subscription.internal_note_display, Markup('<p>new internal note</p>'), '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)
|