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

990 lines
50 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import json
from datetime import date
from unittest.mock import patch
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import Command, http
from odoo.tests import new_test_user, tagged
from odoo.tools import mute_logger
from odoo.tools.misc import get_lang
from odoo.addons.http_routing.tests.common import MockRequest
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.sale.controllers.portal import CustomerPortal
from odoo.addons.sale.models.sale_order import SaleOrder
from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon
@tagged("post_install", "-at_install", "subscription_controller")
class TestSubscriptionController(PaymentHttpCommon, PaymentCommon, TestSubscriptionCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.pricing_month = cls.sub_product_tmpl.subscription_rule_ids.filtered(
lambda rule: rule.plan_id == cls.plan_month
)
cls.pricing_year = cls.sub_product_tmpl.subscription_rule_ids.filtered(
lambda rule: rule.plan_id == cls.plan_year
)
# FIXME dunno why we need to recreate the test product here when it's 'nearly' identical to
# the one in the TestSubscriptionCommon, but some tests fail without it, so keeping it for
# now.
cls.sub_product_tmpl = cls.ProductTmpl.sudo().create({
'name': 'TestProduct',
'type': 'service',
'recurring_invoice': True,
'uom_id': cls.uom_unit.id,
})
cls.pricing_month = cls.pricing_month.copy({'product_tmpl_id': cls.sub_product_tmpl.id})
cls.pricing_year = cls.pricing_year.copy({'product_tmpl_id': cls.sub_product_tmpl.id})
cls.subscription_tmpl = cls.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': cls.plan_month.id,
'sale_order_template_line_ids': [
Command.create({
'name': "monthly",
'product_id': cls.sub_product_tmpl.product_variant_ids.id,
'product_uom_qty': 1,
'product_uom_id': cls.sub_product_tmpl.uom_id.id
}),
Command.create({
'name': "yearly",
'product_id': cls.sub_product_tmpl.product_variant_ids.id,
'product_uom_qty': 1,
'product_uom_id': cls.sub_product_tmpl.uom_id.id,
}),
]
})
def setUp(self):
super().setUp()
self.user = new_test_user(self.env, "test_user_1", email="[email protected]", tz="UTC")
self.other_user = new_test_user(self.env, "test_user_2", email="[email protected]", password="P@ssw0rd!", tz="UTC")
self.partner = self.user.partner_id
# Test Subscription
self.subscription = self.SaleOrder.create({
'name': 'TestSubscription',
'is_subscription': True,
'note': "original subscription description",
'partner_id': self.other_user.partner_id.id,
'pricelist_id': self.other_user.property_product_pricelist.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
self.subscription._onchange_sale_order_template_id()
self.subscription.end_date = False # reset the end_date
self.subscription_tmpl.flush_recordset()
self.subscription.flush_recordset()
def _pause_subscription(self, until_date):
""" method to pause a subscription from protal."""
data = {
"access_token": self.subscription.access_token,
"csrf_token": http.Request.csrf_token(self),
"until": until_date,
}
return self.url_open(f"/my/subscriptions/{self.subscription.id}/pause", allow_redirects=False, data=data)
def _resume_subscription(self):
""" method to resume a subscription from protal."""
data = {
"access_token": self.subscription.access_token,
"csrf_token": http.Request.csrf_token(self),
}
return self.url_open(f"/my/subscriptions/{self.subscription.id}/resume", allow_redirects=False, data=data)
def test_close_contract(self):
""" Test subscription close """
with freeze_time("2021-11-18"):
self.authenticate(None, None)
self.subscription.plan_id.user_closable = True
self.subscription.action_confirm()
close_reason_id = self.env.ref('sale_subscription.close_reason_1')
data = {'access_token': self.subscription.access_token, 'csrf_token': http.Request.csrf_token(self),
'close_reason_id': close_reason_id.id, 'closing_text': "I am broke"}
url = "/my/subscriptions/%s/close" % self.subscription.id
res = self.url_open(url, allow_redirects=False, data=data)
self.assertEqual(res.status_code, 303)
self.env.invalidate_all()
self.assertEqual(self.subscription.subscription_state, '6_churn', 'The subscription should be closed.')
self.assertEqual(self.subscription.end_date, date(2021, 11, 18), 'The end date of the subscription should be updated.')
def test_prevents_assigning_not_owned_payment_tokens_to_subscriptions(self):
malicious_user_subscription = self.env['sale.order'].create({
'name': 'Free Subscription',
'partner_id': self.malicious_user.partner_id.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
malicious_user_subscription._onchange_sale_order_template_id()
legit_user_subscription = self.env['sale.order'].create({
'name': 'Free Subscription',
'partner_id': self.legit_user.partner_id.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
stolen_payment_method = self.env['payment.token'].create(
{'payment_details': 'Jimmy McNulty',
'partner_id': self.malicious_user.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'Omar Little'})
legit_payment_method = self.env['payment.token'].create(
{'payment_details': 'Jimmy McNulty',
'partner_id': self.legit_user.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'Legit ref'})
legit_user_subscription._portal_ensure_token()
malicious_user_subscription._portal_ensure_token()
# Payment Token exists/does not exists
# Payment Token is accessible to user/not accessible
# SO accessible to User/Accessible thanks to the token/Not Accessible (wrong token, no token)
# First we check a legit token assignation for a legit subscription.
self.authenticate('ness', 'nessnessness')
data = {'token_id': legit_payment_method.id,
'order_id': legit_user_subscription.id,
'access_token': legit_user_subscription.access_token
}
url = self._build_url("/my/subscriptions/assign_token/%s" % legit_user_subscription.id)
self.make_jsonrpc_request(url, data)
legit_user_subscription.invalidate_recordset()
self.assertEqual(legit_user_subscription.payment_token_id, legit_payment_method)
data = {'token_id': 9999999999999999, 'order_id': legit_user_subscription.id}
with self._assertNotFound():
self.make_jsonrpc_request(url, data)
legit_user_subscription.invalidate_recordset()
self.assertEqual(legit_user_subscription.payment_token_id, legit_payment_method, "The new token should be saved on the order.")
# Payment token is inacessible to user but the SO is OK
self.authenticate('al', 'alalalal')
data = {'token_id': legit_payment_method.id, 'order_id': malicious_user_subscription.id}
url = self._build_url("/my/subscriptions/assign_token/%s" % malicious_user_subscription.id)
with self._assertNotFound():
self.make_jsonrpc_request(url, data)
malicious_user_subscription.invalidate_recordset()
self.assertFalse(malicious_user_subscription.payment_token_id, "No token should be saved on the order.")
# The SO is not accessible but the token is mine
data = {'token_id': stolen_payment_method.id, 'order_id': legit_user_subscription.id}
url = self._build_url("/my/subscriptions/assign_token/%s" % legit_user_subscription.id)
with self._assertNotFound():
self.make_jsonrpc_request(url, data)
legit_user_subscription.invalidate_recordset()
self.assertEqual(legit_user_subscription.payment_token_id, legit_payment_method, "The token should not be updated")
def test_automatic_invoice_token(self):
self.original_prepare_invoice = self.subscription._prepare_invoice
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment):
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'False')
subscription = self._portal_payment_controller_flow()
subscription.transaction_ids.unlink()
# set automatic invoice and restart
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
self._portal_payment_controller_flow()
def _portal_payment_controller_flow(self):
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
subscription._onchange_sale_order_template_id()
subscription.state = 'sent'
subscription._portal_ensure_token()
signature = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" # BASE64 of a simple image
data = {'order_id': subscription.id, 'access_token': subscription.access_token, 'signature': signature}
url = self._build_url("/my/orders/%s/accept" % subscription.id)
self.make_jsonrpc_request(url, data)
data = {
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'order_id': subscription.id,
'access_token': subscription.access_token,
'amount': subscription.amount_total,
'flow': 'direct',
'tokenization_requested': True,
'landing_route': subscription.get_portal_url(),
}
url = self._build_url("/my/orders/%s/transaction" % subscription.id)
self.make_jsonrpc_request(url, data)
subscription.transaction_ids.provider_id.support_manual_capture = 'full_only'
subscription.transaction_ids._set_authorized()
subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post()
subscription.transaction_ids.token_id = self.payment_token.id
self.assertEqual(subscription.next_invoice_date, datetime.date.today())
self.assertEqual(subscription.state, 'sale')
subscription.transaction_ids._create_or_link_to_invoice()
subscription.transaction_ids._post_process() # Create the payment
self.assertEqual(subscription.invoice_count, 1, "One invoice should be created")
# subscription has a payment_token_id, the invoice is created by the flow.
subscription.invoice_ids.invoice_line_ids.account_id.account_type = 'income'
subscription.invoice_ids.auto_post = 'at_date'
self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger()
self.assertTrue(subscription.next_invoice_date > datetime.date.today(), "the next invoice date should be updated")
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=subscription.invoice_ids.ids) \
.create({
'currency_id': subscription.currency_id.id,
'amount': subscription.amount_total,
})._create_payments()
self.assertEqual(subscription.invoice_ids.mapped('state'), ['posted'])
self.assertTrue(subscription.invoice_ids.payment_state in ['paid', 'in_payment'])
subscription._cron_recurring_create_invoice()
invoices = subscription.invoice_ids.filtered(lambda am: am.state in ['draft', 'posted']) # avoid counting canceled invoices
self.assertEqual(len(invoices), 1, "Only one invoice should be created")
# test transaction flow when paying from the portal
self.assertEqual(len(subscription.transaction_ids), 1, "Only one transaction should be created")
first_transaction_id = subscription.transaction_ids
url = self._build_url("/my/subscriptions/%s/transaction" % subscription.id)
amount_to_invoice = subscription.amount_to_invoice # force a non null amount 2.3
data = {'access_token': subscription.access_token,
'landing_route': subscription.get_portal_url(),
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': False,
'flow': 'direct',
'amount': amount_to_invoice,
}
self.make_jsonrpc_request(url, data)
last_transaction_id = subscription.transaction_ids - first_transaction_id
# the transaction is associated to the invoice in tx._post_process()
last_transaction_id._set_done()
last_transaction_id._post_process()
self.assertEqual(len(subscription.transaction_ids), 2)
self.assertEqual(last_transaction_id.sale_order_ids, subscription)
subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post()
# subscription has a payment_token_id, the invoice is created by the flow.
subscription.invoice_ids.invoice_line_ids.account_id.account_type = 'asset_cash'
subscription.invoice_ids.auto_post = 'at_date'
subscription.transaction_ids._set_done()
last_invoice_date = subscription.next_invoice_date - subscription.plan_id.billing_period
if last_invoice_date != datetime.date.today():
# sometimes, the computed last_invoice_date is different than the start_date at this step.
# it happens for example on the 31th of march:
# next_invoice_date = 30th of April - billing period = 30th of March
# but 31th of March + billing period = 30th of April too.
# As this test is only making sure that the invoice is paid, we make sure to register the payment when necessary
# registering the payment every time could trigger
# UserError: You can't register a payment because there is nothing left to pay on the selected journal items.
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=subscription.invoice_ids.ids) \
.create({
'currency_id': subscription.currency_id.id,
'amount': subscription.amount_total,
})._create_payments()
res = set(subscription.invoice_ids.mapped('payment_state')) & {'not_paid', 'partial'}
self.assertFalse(res,
"All invoices should be in paid or in_payment status")
return subscription
def test_controller_transaction_refund(self):
self.original_prepare_invoice = self.subscription._prepare_invoice
self.pricing_month.fixed_price = 10
subscription = self.env['sale.order'].create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
subscription._onchange_sale_order_template_id()
subscription.order_line.product_uom_qty = 2
self.assertEqual(subscription.order_line.pricelist_item_id, self.pricing_month)
subscription.action_confirm()
subscription._recompute_prices()
invoice = subscription._create_invoices()
invoice._post()
self.assertEqual(invoice.amount_total, 46)
# partial refund
refund_wizard = self.env['account.move.reversal'].with_context(
active_model="account.move",
active_ids=invoice.ids,
).create({
'reason': 'Test refund',
'journal_id': invoice.journal_id.id,
})
res = refund_wizard.reverse_moves()
refund_move = self.env['account.move'].browse(res['res_id'])
refund_move.invoice_line_ids.quantity = 1
refund_move._post()
self.assertEqual(refund_move.amount_total, 23, "The refund is half the invoice")
url = self._build_url("/my/subscriptions/%s/transaction/" % subscription.id)
data = {'access_token': subscription.access_token,
'landing_route': subscription.get_portal_url(),
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': False,
'flow': 'direct',
}
self.make_jsonrpc_request(url, data)
invoice_transactions = subscription.invoice_ids.transaction_ids
# the amount should be equal to the last
self.assertEqual(invoice_transactions.amount, subscription.amount_total,
"The last transaction should be equal to the total")
def test_portal_partial_payment(self):
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment',
wraps=self._mock_subscription_do_payment):
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'False')
with freeze_time("2024-01-22"):
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'sale_order_template_id': self.subscription_tmpl.id,
'require_signature': False,
})
self.pricing_month.price = 100
subscription._onchange_sale_order_template_id()
subscription.state = 'sent'
subscription._portal_ensure_token()
# test customized /payment/pay route with sale_order_id param
# partial amount specified
subscription.prepayment_percent = 0.5
self.amount = subscription.amount_total / 2.0 # self.amount is used to create the right transaction
with MockRequest(self.env):
tx_context = CustomerPortal()._get_payment_values(
subscription, is_down_payment=True, payment_amount=self.amount
)
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'landing_route': '/my/subscriptions',
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx_sudo = self._get_tx(processing_values['reference'])
# make sure to have a token on the transaction. it is needed to test the confirmation flow
tx_sudo.token_id = self.payment_token.id
self.assertEqual(tx_sudo.sale_order_ids, subscription)
# self.assertEqual(tx_sudo.amount, amount)
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx_sudo._post_process()
self.assertEqual(subscription.state, 'sale') # The prepayment amount was paid
self.assertEqual(subscription.next_invoice_date, datetime.date.today())
self.assertEqual(subscription.state, 'sale')
self.assertEqual(subscription.invoice_count, 0, "No invoice should be created")
self.assertFalse(subscription.payment_token_id, "No token should be saved")
# Renew subscription and set payment amount as half of the total amount (partial).
subscription._create_recurring_invoice()
action = subscription.prepare_renewal_order()
renewal_so = self.env['sale.order'].browse(action['res_id'])
renewal_so.require_signature = False
renewal_so.prepayment_percent = 0.5
self.amount = renewal_so.amount_total / 2.0
# Prepare renewal subscription's payment values.
with MockRequest(self.env):
tx_context = CustomerPortal()._get_payment_values(
renewal_so, is_down_payment=True, payment_amount=self.amount
)
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'landing_route': '/my/subscriptions',
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(tx_route=tx_context['transaction_route'], **tx_route_values)
# Make sure to have a token on the transaction, it is needed to test the confirmation flow.
tx_sudo = self._get_tx(processing_values['reference'])
tx_sudo.token_id = self.payment_token.id
self.assertEqual(tx_sudo.sale_order_ids, renewal_so)
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx_sudo._post_process()
# Assert that no token was saved, renewal was sent and only one invoice was registered.
self.assertFalse(renewal_so.payment_token_id, "No token should be saved")
self.assertEqual(renewal_so.state, 'sale') # The prepayment amount was paid
self.assertEqual(renewal_so.invoice_count, 1, "Only one invoice from previous subscription should be registered")
self.assertEqual(renewal_so.next_invoice_date, datetime.date.today() + datetime.timedelta(days=31))
def test_portal_payment_confirmation_email(self):
"""Check that payment confirmation emails aren't sent for validation transactions."""
def process_notification_data(data):
tx = self.env['payment.transaction'].search(
[('reference', '=', data['reference'])],
limit=1,
)
tx.token_id = tx.tokenize and self.env['payment.token'].create({
'partner_id': self.subscription.partner_id.id,
'payment_method_id': self.payment_method_id,
'provider_id': self.provider.id,
'provider_ref': 'test123',
})
tx._set_done()
self.portal_user.email = '[email protected]'
self.subscription.partner_id = self.portal_partner
self.subscription.action_confirm()
subscription_tx_url = f'/my/subscriptions/{self.subscription.id}/transaction'
base_tx_route_values = {
'access_token': None,
'amount': 0.0,
'flow': 'direct',
'is_validation': False,
'landing_route': self.subscription.get_portal_url(),
'order_id': self.subscription.id,
'payment_method_id': self.payment_method_id,
'provider_id': self.provider.id,
'token_id': None,
'tokenization_requested': False,
}
with patch.object(SaleOrder, '_send_order_notification_mail') as notification_mail_mock:
# Log in and save a payment method for a subscription
self.authenticate(self.portal_user.login, self.portal_user.login)
tx_response = self.make_jsonrpc_request(subscription_tx_url, {
**base_tx_route_values,
'is_validation': True,
'tokenization_requested': True,
})
process_notification_data(tx_response)
self.make_jsonrpc_request('/payment/status/poll', {})
self.assertFalse(
notification_mail_mock.call_count,
"Setting a payment token shouldn't send a payment succeeded email",
)
# Use saved payment method to pay subscription
tx_response = self.make_jsonrpc_request(subscription_tx_url, {
**base_tx_route_values,
'amount': self.subscription.amount_total,
'flow': 'token',
'token_id': self.subscription.payment_token_id.id,
})
process_notification_data(tx_response)
self.make_jsonrpc_request('/payment/status/poll', {})
self.assertEqual(
notification_mail_mock.call_count,
1,
"Paying a subscription should send one payment succeeded email",
)
def test_portal_quote_document(self):
product_document = self.env['product.document'].create({
'name': 'doc.txt',
'active': True,
'datas': 'TXkgYXR0YWNobWVudA==',
'res_model': 'product.product',
'res_id': self.sub_product_tmpl.product_variant_ids.id,
'attached_on_sale': 'sale_order',
})
self.subscription.action_confirm()
response = self.url_open(
self.subscription.get_portal_url('/document/' + str(product_document.id))
)
self.assertEqual(response.status_code, 200)
self.assertEqual("My attachment", response.text)
def test_save_token_automating_future_payments(self):
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment',
wraps=self._mock_subscription_do_payment):
# Create subscription and invoice.
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
self.pricing_month.price = 100
subscription._onchange_sale_order_template_id()
subscription.action_confirm()
invoice = subscription._create_recurring_invoice()
# Define test payment token for later assignment.
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': subscription.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
# Create transaction and process payment while assigning token to subscription.
tx_context = self._get_payment_context(self._make_http_get_request(
f'/my/subscriptions/{subscription.id}',
params={'access_token': subscription.access_token},
))
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': test_payment_token.id,
'amount': tx_context['amount'],
'flow': 'token',
'tokenization_requested': 'automate_payments',
'landing_route': '/my/invoices/%s' % invoice.id,
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(tx_route=tx_context['transaction_route'], **tx_route_values)
tx_sudo = self._get_tx(processing_values['reference'])
tx_sudo._set_done()
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx_sudo._post_process()
# Ensure token was assigned after completing the payment in invoices route.
self.assertEqual(subscription.payment_token_id.id, test_payment_token.id, "Token must be assigned to subscription after transaction creation.")
# Create transaction with new token without sending invoice_ids in custom_create_values.
test_payment_token_2 = self.env['payment.token'].create({
'payment_details': 'Test-2',
'partner_id': subscription.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test-2'
})
kwargs = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': test_payment_token_2.id,
'amount': tx_context['amount'],
'flow': 'token',
'landing_route': '/my/invoices/%s' % invoice.id,
'currency_id': subscription.currency_id.id,
'partner_id': self.partner.id
}
tx_sudo = self._create_transaction(**kwargs)
tx_sudo._post_process()
tx_sudo._set_done()
# Ensure token was not changed after completing the payment in invoices route.
self.assertEqual(subscription.payment_token_id.id, test_payment_token.id, "Token must remain unchanged after new payment.")
def test_anticipate_period(self):
""" When the payment is performed on subscription, we always create a new invoice
"""
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
subscription._onchange_sale_order_template_id()
subscription.order_line.price_unit = 10
subscription.action_confirm()
inv1 = subscription._create_invoices()
inv1._post() # we won't pay it
existing_tx = subscription.transaction_ids
data = {'access_token': subscription.access_token,
'landing_route': subscription.get_portal_url(),
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': False,
'amount': subscription.amount_total,
'flow': 'direct',
'subscription_anticipate': True
}
url = self._build_url("/my/subscriptions/%s/transaction" % subscription.id)
self.make_jsonrpc_request(url, data)
self.assertEqual(subscription.invoice_count, 1, "Portal don't create new invoices")
self.assertEqual(inv1.payment_state, 'not_paid', "inv 1 is not paid")
tx = subscription.transaction_ids - existing_tx
tx._set_done()
tx._post_process()
inv2 = subscription.invoice_ids - inv1
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=inv2.ids) \
.create({
'currency_id': subscription.currency_id.id,
'amount': subscription.amount_total,
})._create_payments()
tx = subscription.transaction_ids.invoice_ids.transaction_ids
tx._set_done()
tx._post_process()
self.assertTrue(inv2.payment_state in ['paid', 'in_payment'], "invoice 2 is paid")
def test_controller_reopen_subscription(self):
""" When the payment is performed on subscription, we always create a new invoice
"""
with freeze_time('2024-01-01'):
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
subscription._onchange_sale_order_template_id()
subscription.order_line.price_unit = 10
subscription.action_confirm()
inv1 = subscription._create_invoices()
inv1.button_cancel()
with freeze_time('2024-01-15'):
subscription.set_close()
existing_tx = subscription.transaction_ids
data = {'access_token': subscription.access_token,
'landing_route': subscription.get_portal_url(),
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': False,
'amount': subscription.amount_total,
'flow': 'direct',
'subscription_anticipate': True
}
url = self._build_url("/my/subscriptions/%s/transaction" % subscription.id)
self.make_jsonrpc_request(url, data)
new_tx = subscription.transaction_ids - existing_tx
new_tx._set_done()
new_tx._post_process()
self.assertEqual(subscription.invoice_count, 2, "subscription_anticipate should for a new invoice creation")
inv2 = subscription.invoice_ids - inv1
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=inv2.ids) \
.create({
'currency_id': subscription.currency_id.id,
'amount': subscription.amount_total,
})._create_payments()
tx = subscription.transaction_ids.invoice_ids.transaction_ids
tx._set_done()
tx._post_process()
self.assertTrue(inv2.payment_state in ['paid', 'in_payment'], "invoice 2 is paid")
self.assertEqual(subscription.next_invoice_date, datetime.date(2024, 2, 1))
self.assertEqual(inv2.line_ids.mapped(lambda aml: (aml.deferred_start_date, aml.deferred_end_date)),
[(datetime.date(2024, 1, 1), datetime.date(2024, 1, 31)),
(datetime.date(2024, 1, 1), datetime.date(2024, 1, 31)),
(False, False),
(False, False)],
)
def test_close_at_date_contract(self):
"""
Test the closing of a subscription at a specific date.
"""
with freeze_time("2021-11-17"):
self.authenticate(None, None)
self.subscription.plan_id.user_closable = True
self.subscription.plan_id.user_closable_options = "at_date"
self.subscription.action_confirm()
inv1 = self.subscription._create_invoices()
inv1._post() # We won't pay it
with freeze_time("2021-11-18"):
close_reason_id = self.env.ref("sale_subscription.close_reason_1")
data = {
"access_token": self.subscription.access_token,
"csrf_token": http.Request.csrf_token(self),
"close_reason_id": close_reason_id.id,
"closing_text": "I am not able to continue subs"
}
url = f"/my/subscriptions/{self.subscription.id}/close"
res = self.url_open(url, allow_redirects=False, data=data)
self.assertEqual(res.status_code, 303, "Redirection status code should be 303.")
self.env.invalidate_all()
self.env["sale.order"].sudo()._cron_subscription_expiration()
self.assertEqual(
self.subscription.subscription_state, "6_churn",
"Subscription state should be '6_churn'."
)
self.assertEqual(
self.subscription.end_date, date(2021, 11, 18),
"End date should be the closing date: 2021-11-18."
)
self.assertTrue(
self.subscription.close_reason_id,
"Close reason should be set after closing."
)
def test_close_end_of_period_contract(self):
"""
Test the closing of a subscription at the end of the period.
"""
with freeze_time("2021-11-17"):
self.authenticate(None, None)
self.subscription.plan_id.user_closable = True
self.subscription.plan_id.user_closable_options = "end_of_period"
self.subscription.action_confirm()
inv1 = self.subscription._create_invoices()
inv1._post() # We won't pay it
with freeze_time("2021-11-18"):
close_reason_id = self.env.ref("sale_subscription.close_reason_1")
data = {
"access_token": self.subscription.access_token,
"csrf_token": http.Request.csrf_token(self),
"close_reason_id": close_reason_id.id,
"closing_text": "I am not able to continue subs"
}
url = f"/my/subscriptions/{self.subscription.id}/close"
res = self.url_open(url, allow_redirects=False, data=data)
self.assertEqual(res.status_code, 303, "Redirection status code should be 303.")
self.env.invalidate_all()
self.assertEqual(self.subscription.subscription_state, "3_progress", "Subscription should still be in progress.")
self.assertEqual(
self.subscription.next_invoice_date - relativedelta(days=1), self.subscription.end_date,
"End date should be one day before next invoice date."
)
with freeze_time("2021-12-17"):
self.env["sale.order"].sudo()._cron_subscription_expiration()
self.assertEqual(
self.subscription.subscription_state, "6_churn",
"Subscription state should be '6_churn' after closing."
)
self.assertTrue(self.subscription.close_reason_id, "Close reason should be set after closing.")
def test_check_product_catalog_data(self):
"""
Test that creating a sale order with both subscription and normal products
reflects the correct pricing in the catalog and sale order lines.
"""
sub_product = self.env['product.product'].create({
'name': 'Subscription Product',
'type': 'service',
'recurring_invoice': True,
'invoice_policy': 'order',
'list_price': 100.0,
})
non_sub_product = self.env['product.product'].create({
'name': 'Non-Subscription Product',
'type': 'service',
'invoice_policy': 'order',
'list_price': 50.0,
})
sale_order = self.env['sale.order'].create({
'name': 'Test Sale Order',
'partner_id': self.partner.id,
'plan_id': self.plan_month.id,
})
data = {
"id": 0,
"jsonrpc": "2.0",
"method": "call",
"params": {
'res_model': 'sale.order',
'order_id': sale_order.id,
'product_ids': [sub_product.id, non_sub_product.id],
}
}
headers = {
"Content-Type": "application/json",
}
self.authenticate(self.env.user.login, self.env.user.login)
# Make the request
response = self.url_open(
'/product/catalog/order_lines_info',
data=json.dumps(data).encode(),
headers=headers
)
# Check the response
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
response_prices = {
int(product_id): product_data['price']
for product_id, product_data in response_data['result'].items()
}
expected_prices = {
sub_product.id: sub_product.list_price,
non_sub_product.id: non_sub_product.list_price,
}
# Ensure both are sorted before comparison
self.assertEqual(
sorted(response_prices.items()),
sorted(expected_prices.items()),
"Prices in the response should match the expected list prices."
)
def test_modification_of_subscription_addresses(self):
"""
Tests that the invoicng address linked to a subscription can be updated from the portal.
"""
fpos_1 = self.env['account.fiscal.position'].create({
'name': "Test fpos 1",
'country_id': self.env.ref('base.be').id,
'foreign_vat': 'BE0477472701',
})
self.portal_user.property_account_position_id = fpos_1.id
invoicing_partner = self.env['res.partner'].create({
'name': 'John Doe',
'street': "Example street 13",
'street2': "",
'city': "Example city",
'zip': 1234,
'state_id': False,
'country_id': self.env.ref('base.be').id,
'property_account_position_id': fpos_1.id,
'type': 'invoice',
'commercial_partner_id': self.portal_user.partner_id.commercial_partner_id.id,
})
shipping_partner_1 = self.env['res.partner'].create({
'name': 'John Doe',
'street': "Example street 13",
'street2': "",
'city': "Example city",
'zip': 1234,
'state_id': False,
'country_id': self.env.ref('base.be').id,
'property_account_position_id': fpos_1.id,
'type': 'delivery',
'commercial_partner_id': self.portal_user.partner_id.commercial_partner_id.id,
})
self.portal_user.child_ids = [Command.link(invoicing_partner.id)]
self.subscription.partner_id = self.portal_partner
self.subscription.user_id.group_ids += self.env.ref('account.group_delivery_invoice_address')
self.subscription.action_confirm()
original_shipping_partner = self.subscription.partner_shipping_id
# Update the partner
self.authenticate(self.portal_user.login, self.portal_user.login)
data = {
"csrf_token": http.Request.csrf_token(self),
"access_token": self.subscription.access_token,
"delivery_address": shipping_partner_1.id,
"invoicing_address": invoicing_partner.id,
}
url = f"/my/subscriptions/{self.subscription.id}/change_address"
res = self.url_open(url, allow_redirects=False, data=data)
self.assertEqual(res.status_code, 303, "Redirection status code should be 303.")
self.subscription.invalidate_recordset(fnames=['partner_shipping_id', 'partner_invoice_id'])
self.assertEqual(self.subscription.partner_shipping_id, original_shipping_partner, "shipping partner is not updated even when it is provided")
self.assertEqual(self.subscription.partner_invoice_id, invoicing_partner, "the invoicing partner is updated")
def test_portal_user_sees_company_subscriptions(self):
"""Test that a portal user sees all subscriptions of their company and not others."""
company = self.env['res.partner'].create({
'name': 'Test Company',
'is_company': True,
})
self.user.partner_id.parent_id = company.id
self.other_user.partner_id.parent_id = company.id
self.subscription._onchange_sale_order_template_id()
self.subscription.action_confirm()
# Create a subscription for an external unrelated partner
external_partner = self.env['res.partner'].create({'name': 'External Partner'})
external_subscription = self.subscription.copy({
'partner_id': external_partner.id,
})
external_subscription._onchange_sale_order_template_id()
external_subscription.action_confirm()
# First, remove all existing group memberships from self.user to avoid conflicts with exclusive groups
# Then, explicitly assign the Portal group to self.user to simulate portal-level access
portal_group = self.env.ref('base.group_portal')
self.user.write({
'group_ids': [Command.clear(), Command.link(portal_group.id)]
})
# Authenticate as self.user and access the subscriptions page
self.authenticate(self.user.login, self.user.password)
response = self.url_open('/my/subscriptions')
self.assertEqual(response.status_code, 200)
content = response.content.decode("utf-8")
self.assertIn(self.subscription.name, content, "Portal user should see subscriptions from their company")
self.assertNotIn(external_subscription.name, content, "Portal user should NOT see subscriptions from other companies")
def test_subscription_pause(self):
""" Test pausing a subscription from the portal and verifying its effects. """
with freeze_time("2021-11-17"):
self.authenticate(None, None)
self.subscription.plan_id.pausable_by_user = True
self.subscription.action_confirm()
self.subscription._create_invoices()._post()
with freeze_time("2021-11-18"):
pause_until = self.subscription.next_invoice_date + relativedelta(days=2)
user_lang = get_lang(self.env)
date_format = user_lang.date_format
pause_until_str = pause_until.strftime(date_format)
response = self._pause_subscription(pause_until_str)
self.assertEqual(response.status_code, 303, "Expected redirection status code 303.")
self.env.invalidate_all()
self.assertEqual(self.subscription.subscription_state, "3_progress", "Subscription should remain in progress.")
self.assertEqual(self.subscription.next_invoice_date, pause_until, "Next invoice date should be extended by 2 days.")
with freeze_time("2021-12-19"):
self.env['sale.order']._cron_recurring_create_invoice()
self.assertEqual(self.subscription.invoice_count, 2, "Invoice count should be 2.")
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2022, 1, 19), "Next invoice date should be updated correctly.")
def test_subscription_pause_and_resume(self):
""" Test pausing and then resuming a subscription from the portal. """
with freeze_time("2021-11-17"):
self.authenticate(None, None)
self.subscription.plan_id.pausable_by_user = True
self.subscription.action_confirm()
self.subscription._create_invoices()._post()
with freeze_time("2021-11-18"):
pause_until = self.subscription.next_invoice_date + relativedelta(days=3)
user_lang = get_lang(self.env)
date_format = user_lang.date_format
pause_until_str = pause_until.strftime(date_format)
response = self._pause_subscription(pause_until_str)
self.assertEqual(response.status_code, 303, "Expected redirection status code 303.")
self.env.invalidate_all()
self.assertEqual(self.subscription.subscription_state, "3_progress", "Subscription should remain in progress.")
self.assertEqual(self.subscription.next_invoice_date, pause_until, "Next invoice date should be extended by 3 days.")
with freeze_time("2021-12-18"):
response = self._resume_subscription()
self.assertEqual(response.status_code, 303, "Expected redirection status code 303.")
self.env.invalidate_all()
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2021, 12, 18), "Next invoice date should reset correctly.")
self.env['sale.order']._cron_recurring_create_invoice()
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2022, 1, 18), "Next invoice date should update after invoicing.")
def test_reset_pause_period_after_invoice_creation(self):
""" Test that the pause period is reset after an invoice is created. """
with freeze_time("2021-11-17"):
self.authenticate(None, None)
self.subscription.plan_id.pausable_by_user = True
self.subscription.action_confirm()
self.subscription._create_invoices()._post()
with freeze_time("2021-11-18"):
pause_until = self.subscription.next_invoice_date + relativedelta(days=10)
user_lang = get_lang(self.env)
date_format = user_lang.date_format
pause_until_str = pause_until.strftime(date_format)
response = self._pause_subscription(pause_until_str)
self.assertEqual(response.status_code, 303, "Expected redirection status code 303.")
self.env.invalidate_all()
self.assertEqual(self.subscription.subscription_state, "3_progress", "Subscription should remain in progress.")
with freeze_time("2021-12-19"):
# By creating a manual invoice, we reset the user_pause_start
self.subscription._create_invoices()._post()
self.assertEqual(self.subscription.invoice_count, 2, "Invoice count should be 2.")
self.assertFalse(self.subscription.user_pause_start, "User pause start should be reset after invoicing.")