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

1101 lines
57 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
from freezegun import freeze_time
from odoo import fields, Command
from odoo.addons.mail.tests.common import MockEmail
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('post_install', '-at_install')
class TestSubscriptionPayments(PaymentCommon, TestSubscriptionCommon, MockEmail):
def _get_payment_values(self, subscription, invoice_ids, amount=None, reference="PAYMENT"):
return {
'amount': amount or subscription.amount_total,
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'operation': 'offline',
'currency_id': subscription.currency_id.id,
'reference': reference,
'token_id': False,
'partner_id': subscription.partner_id.id,
'partner_country_id': subscription.partner_id.country_id.id,
'invoice_ids': invoice_ids,
'sale_order_ids': [(6, 0, subscription.ids)],
'state': 'draft',
}
def test_auto_payment_with_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.subscription.write({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
self.subscription._onchange_sale_order_template_id()
self.subscription.action_confirm()
self.env['sale.order']._cron_recurring_create_invoice()
self.subscription.transaction_ids._post_process()
self.assertEqual(self.subscription.subscription_state, '3_progress', 'subscription with online payment and a payment method set should stay opened when transaction succeeds')
invoice = self.subscription.invoice_ids.sorted('date')[-1]
recurring_total_with_taxes = self.subscription.amount_total
self.assertEqual(invoice.amount_total, recurring_total_with_taxes,
'website_subscription: the total of the recurring invoice created should be the subscription '
'recurring total + the products taxes')
self.assertTrue(all(line.tax_ids.ids == self.tax_10.ids for line in invoice.invoice_line_ids),
'website_subscription: All lines of the recurring invoice created should have the percent tax '
'set on the subscription products')
self.assertTrue(
all(tax_line.tax_line_id == self.tax_10 for tax_line in invoice.line_ids.filtered('tax_line_id')),
'The invoice tax lines should be set and should all use the tax set on the subscription products')
start_date = fields.Date.today() - relativedelta(months=1)
recurring_next_date = fields.Date.today() - relativedelta(days=1)
self.subscription.payment_token_id = False
failing_subs = self.env['sale.order']
subscription_mail_fail = self.subscription.copy({
'date_order': start_date,
'start_date': start_date,
'next_invoice_date': recurring_next_date,
'payment_token_id': None})
failing_subs |= subscription_mail_fail
for _i in range(5):
failing_subs |= subscription_mail_fail.copy({'is_batch': True})
failing_subs.action_confirm()
# issue: two problems:
# 1) payment failed, we want to avoid trigger it twice: (double cost) --> payment_exception
# 2) batch: we need to avoid taking subscription two time. flag remains until the end of the last trigger
failing_subs.order_line.qty_to_invoice = 1
self.env['sale.order']._create_recurring_invoice(batch_size=3)
failing_result = [not res for res in failing_subs.mapped('payment_exception')]
self.assertTrue(all(failing_result), "The subscription are not flagged anymore")
failing_result = [not res for res in failing_subs.mapped('is_batch')]
self.assertTrue(all(failing_result), "The subscription are not flagged anymore")
failing_subs.payment_token_id = self.payment_token.id
# Trigger the invoicing manually after fixing it
failing_subs._create_recurring_invoice()
vals = [sub.payment_exception for sub in failing_subs if sub.payment_exception]
self.assertFalse(vals, "The subscriptions are not flagged anymore, the payment succeeded")
def test_auto_payment_across_time(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):
subscription_tmpl = self.env['sale.order.template'].create({
'name': 'Subscription template without discount',
'is_unlimited': False,
'note': "This is the template description",
'duration_value': 4,
'duration_unit': 'month',
'plan_id': self.plan_month.id,
})
self.subscription.write({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': subscription_tmpl.id,
})
self.subscription._onchange_sale_order_template_id()
with freeze_time("2021-01-03"):
self.subscription.order_line = [Command.clear()]
self.subscription.write({
'start_date': False,
'next_invoice_date': False,
'order_line': [Command.create({'product_id': self.product.id,
'name': "month cheap",
'price_unit': 42,
'product_uom_qty': 2,
}),
Command.create({'product_id': self.product2.id,
'name': "month expensive",
'price_unit': 420,
'product_uom_qty': 3,
}),
]}
)
self.subscription.action_confirm()
self.assertEqual(self.subscription.end_date, datetime.date(2021, 5, 2))
self.env['sale.order']._cron_recurring_create_invoice()
invoice = self.subscription.invoice_ids.sorted('date')[-1]
tx = self.env['payment.transaction'].search([('invoice_ids', 'in', invoice.ids)])
tx._post_process()
# Two products are invoiced
self.assertEqual(len(invoice.invoice_line_ids), 2, 'Two lines are invoiced')
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2021, 2, 3), 'the next invoice date should be updated')
self.assertEqual(invoice.invoice_line_ids[0].name, 'BaseTestProduct\nmonth cheap\n1 Month 01/03/2021 to 02/02/2021', 'Invoice line description must be based on order line description')
self.assertEqual(invoice.invoice_line_ids[1].name, 'TestProduct2\nmonth expensive\n1 Month 01/03/2021 to 02/02/2021', 'Invoice line description must be based on order line description')
with freeze_time("2021-02-03"):
self.env.invalidate_all()
self.env['sale.order']._cron_recurring_create_invoice()
invoice = self.subscription.invoice_ids.sorted('date')[-1]
tx = self.env['payment.transaction'].search([('invoice_ids', 'in', invoice.ids)])
invoice = self.subscription.invoice_ids.sorted('date')[-1]
self.assertEqual(invoice.date, datetime.date(2021, 2, 3), 'We invoiced today')
tx._post_process()
with freeze_time("2021-03-03"):
self.env.invalidate_all()
self.env['sale.order']._cron_recurring_create_invoice()
invoice = self.subscription.invoice_ids.sorted('date')[-1]
tx = self.env['payment.transaction'].search([('invoice_ids', 'in', invoice.ids)])
tx._post_process()
invoice = self.subscription.invoice_ids.sorted('date')[-1]
self.assertEqual(invoice.date, datetime.date(2021, 3, 3), 'We invoiced today')
# We continue
with freeze_time("2021-04-03"):
self.subscription.invalidate_recordset()
self.env['sale.order']._cron_recurring_create_invoice()
invoice = self.subscription.invoice_ids.sorted('date')[-1]
tx = self.env['payment.transaction'].search([('invoice_ids', 'in', invoice.ids)])
tx._post_process()
invoice = self.subscription.invoice_ids.sorted('date')[-1]
tx = self.env['payment.transaction'].search([('invoice_ids', 'in', invoice.ids)])
tx._post_process()
self.assertEqual(invoice.date, datetime.date(2021, 4, 3), 'We invoiced today')
with freeze_time("2022-05-03"):
self.subscription.invalidate_recordset(fnames=['subscription_state'])
self.env['sale.order']._cron_recurring_create_invoice()
self.assertEqual(self.subscription.subscription_state, '6_churn', 'the end_date is passed, the subscription is automatically closed')
invoice = self.subscription.invoice_ids.sorted('date')[-1]
self.assertEqual(invoice.date, datetime.date(2021, 4, 3), 'We should not create a new invoices')
def test_do_payment_calls_send_payment_request_only_once(self):
self.invoice = self.env['account.move'].create(
self.subscription._prepare_invoice()
)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._charge_with_token'
) as patched:
self.subscription._do_payment(self._create_token(), self.invoice)
patched.assert_called_once()
def test_payment_token_is_saved(self):
"""Tests that the payment token is saved when a quotation is paid"""
portal_partner = self.user_portal.partner_id
success_payment_template_id = self.subscription_tmpl.copy()
subscription = self.env['sale.order'].create({
'partner_id': portal_partner.id,
'sale_order_template_id': success_payment_template_id.id,
})
subscription._onchange_sale_order_template_id()
# send quotation
subscription.action_quotation_sent()
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': portal_partner.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
payment_with_token = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'amount': subscription.amount_total,
'date': subscription.date_order,
'currency_id': subscription.currency_id.id,
'partner_id': portal_partner.id,
'payment_token_id': test_payment_token.id
})
transaction_ids = payment_with_token._create_payment_transaction()
transaction_ids._set_done() # dummy transaction will always be successful
subscription.write({'transaction_ids': [(6, 0, transaction_ids.ids)]})
subscription.action_confirm()
self.assertTrue(subscription.is_subscription)
self.assertEqual(subscription.payment_token_id.id, test_payment_token.id)
@mute_logger('odoo.addons.sale_subscription.models.sale_order')
def test_exception_mail(self):
self.subscription.write({'payment_token_id': self.payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
self.subscription.action_confirm()
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', side_effect=Exception("Bad Token")), self.mock_mail_gateway():
self.subscription._create_recurring_invoice()
found_mail = self._find_mail_mail_wemail('accountman@test.com', 'sent', author=self.env.user.partner_id)
mail_body = "<p>Error during renewal of contract [%s] Customer REF XXXXXXX Payment not recorded</p><p>Bad Token</p>" % self.subscription.id
self.assertEqual(found_mail.body_html, mail_body)
@mute_logger('odoo.addons.sale_subscription.models.sale_order')
def test_bad_payment_exception(self):
self.subscription.write({'payment_token_id': self.payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
self.subscription.action_confirm()
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', side_effect=Exception("Oops, network error")),\
self.mock_mail_gateway():
self.subscription._create_recurring_invoice()
invoice = self.subscription.order_line.invoice_lines.move_id
self.assertFalse(invoice, "The draft invoice should be deleted when something goes wrong in _handle_automatic_invoices")
self.assertEqual(
self.subscription.next_invoice_date, self.subscription.start_date,
"We should not have updated the next invoice date, as the invoice was unlinked",
)
@mute_logger('odoo.addons.sale_subscription.models.sale_order')
def test_bad_payment_exception_post_success(self):
self.subscription.write({'payment_token_id': self.payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
self.subscription.action_confirm()
def _mock_subscription_do_payment_and_commit(payment_method, invoice, auto_commit=False):
tx = self._mock_subscription_do_payment(payment_method, invoice, auto_commit=auto_commit)
# once the payment request succeed, we're going to reconcile
tx.env.cr.flush() # simulate commit after sucessfull `_do_payment()`
return tx
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=_mock_subscription_do_payment_and_commit),\
patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._subscription_post_success_payment', side_effect=Exception("Kaput")),\
self.mock_mail_gateway():
invoice = self.subscription._create_recurring_invoice()
with self.assertRaises(Exception):
invoice.transaction_ids._post_process()
invoice = self.subscription.order_line.invoice_lines.move_id
self.assertTrue(
invoice and invoice.state == "posted",
"The draft invoice has to be kept as we committed after the payment succeeded "
"(the next invoice date has already been updated)."
)
expected_next_invoice_date = self.subscription.start_date + self.subscription.plan_id.billing_period
self.assertEqual(
self.subscription.next_invoice_date, expected_next_invoice_date,
"The next invoice date should have been updated, as the invoice was kept after the payment succeeded",
)
@mute_logger('odoo.addons.sale_subscription.models.sale_order')
def test_bad_payment_rejected(self):
self.subscription.write({'payment_token_id': self.payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
self.subscription.action_confirm()
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment_rejected),\
self.mock_mail_gateway():
self.subscription._create_recurring_invoice()
invoice = self.subscription.order_line.invoice_lines.move_id
self.assertFalse(self.subscription.pending_transaction, "The pending transaction flag should not remain")
self.assertFalse(invoice, "The draft invoice should be deleted when something goes wrong in _handle_automatic_invoices")
self.assertEqual(
self.subscription.next_invoice_date, self.subscription.start_date,
"We should not have updated the next invoice date, as the invoice was unlinked",
)
def test_manual_invoice_with_token(self):
self.subscription.write({'payment_token_id': self.payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
with freeze_time("2021-01-03"):
self.subscription.action_confirm()
self.subscription._create_invoices()
self.subscription.order_line.invoice_lines.move_id._post()
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2021, 2, 3), 'the next invoice date should be updated')
self.assertEqual(self.subscription.invoice_count, 1)
def test_close_unpaid_contracts(self):
with freeze_time("2022-01-01"):
sub0 = self.env['sale.order'].create({
'name': 'Paid',
'partner_id': self.partner.id,
'payment_term_id': self.env.ref('account.account_payment_term_21days').id,
'sale_order_template_id': self.templ_5_days.id,
})
sub0._onchange_sale_order_template_id()
sub0.action_confirm() # we these subs alone because they will be paid
sub1 = sub0.copy(default={'name': 'Unpaid with simple date'})
sub2 = sub1.copy(default={
'name': "Partial",
'payment_term_id': self.env.ref('account.account_payment_term_advance_60days').id})
sub2.sale_order_template_id = self.templ_60_days.id
sub3 = sub1.copy(default={
'name': "Unpaid",
'payment_term_id': self.env.ref('account.account_payment_term_advance_60days').id
})
sub4 = self.env['sale.order'].create({
'name': 'Contract without template',
'is_subscription': True,
'plan_id': self.plan_year.id,
'partner_id': self.user_portal.partner_id.id,
'order_line': [
(0, 0, {
'name': self.product.name,
'product_id': self.product.id,
'product_uom_qty': 1.0,
}),
(0, 0, {
'name': self.product2.name,
'product_id': self.product2.id,
'product_uom_qty': 1.0,
})
]
})
all_subs = (sub1 | sub2 | sub3 | sub4).sorted('id')
all_subs.origin_order_id = False
for sub in all_subs - sub4:
sub._onchange_sale_order_template_id()
all_subs.action_confirm()
self.env['sale.order']._cron_recurring_create_invoice()
# Make sure, the account_moves order corresponds to the subscription order.
account_moves = sub1.invoice_ids | sub2.invoice_ids | sub3.invoice_ids | sub4.invoice_ids
with freeze_time("2022-02-01"):
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=sub1.invoice_ids.ids) \
.create({
'currency_id': sub1.currency_id.id,
'amount': 50,
})._create_payments()
self.assertEqual(sub1.invoice_ids.payment_state, 'partial')
self.env['account.payment.register'] \
.with_context(active_model='account.move',
active_ids=sub0.invoice_ids.ids).create(
{
'currency_id': sub0.currency_id.id,
'amount': 400,
})._create_payments()
self.assertTrue(sub0.invoice_ids.payment_state in ['in_payment', 'paid'])
self.assertEqual(account_moves.mapped('payment_state'), ['partial', 'not_paid', 'not_paid', 'not_paid'])
template_limit = {account_moves[0].id: 5, account_moves[1].id: 60, account_moves[2].id: 5, account_moves[3].id: 15}
date_data = [(aml.date_maturity,
aml.date_maturity + relativedelta(days=template_limit[aml.move_id.id]),
aml.move_id.id,
aml.move_id.invoice_line_ids.sale_line_ids.order_id.name) for aml in account_moves.line_ids.filtered('date_maturity')]
self.assertEqual(date_data, [
(datetime.date(2022, 1, 22), datetime.date(2022, 1, 27), account_moves[0].id, 'Unpaid with simple date'),
(datetime.date(2022, 1, 1), datetime.date(2022, 3, 2), account_moves[1].id, 'Partial'),
(datetime.date(2022, 3, 2), datetime.date(2022, 5, 1), account_moves[1].id, 'Partial'),
(datetime.date(2022, 1, 1), datetime.date(2022, 1, 6), account_moves[2].id, 'Unpaid'),
(datetime.date(2022, 3, 2), datetime.date(2022, 3, 7), account_moves[2].id, 'Unpaid'),
(datetime.date(2022, 1, 1), datetime.date(2022, 1, 16), account_moves[3].id, 'Contract without template'),
])
with freeze_time("2022-02-01"):
self.env['sale.order'].sudo()._cron_subscription_expiration()
self.assertEqual(all_subs.mapped('subscription_state'), ['3_progress', '3_progress', '3_progress', '6_churn'], "First and last invoices are never paid")
with freeze_time("2022-03-08"):
self.env['sale.order'].sudo()._cron_subscription_expiration()
self.assertEqual(all_subs.mapped('subscription_state'), ['3_progress', '3_progress', '6_churn', '6_churn'],
"Unpaid payment expire on 2022-03-02 + 5days = 2022-03-07")
with freeze_time("2022-05-02"):
self.env['sale.order'].sudo()._cron_subscription_expiration()
self.assertEqual(account_moves.mapped('payment_state'), ['partial', 'not_paid', 'not_paid', 'not_paid'])
self.assertEqual(all_subs.mapped('subscription_state'), ['3_progress', '6_churn', '6_churn', '6_churn'])
self.assertEqual(sub3.close_reason_id.id, self.env.ref("sale_subscription.close_reason_unpaid_subscription").id)
self.assertEqual(sub0.subscription_state, '3_progress')
with freeze_time("2023-01-07"):
# No new invoice, we don't increment the next_invoice_date
self.env['sale.order'].sudo()._cron_subscription_expiration()
self.assertEqual(sub0.subscription_state, '6_churn')
def test_close_unpaid_contracts_bis(self):
# We don't close the contract if the last invoice is paid but the invoice before was not paid
with freeze_time("2023-01-01"):
sub = self.env['sale.order'].create({
'name': 'Paid',
'partner_id': self.partner.id,
'payment_term_id': self.env.ref('account.account_payment_term_21days').id,
'sale_order_template_id': self.templ_5_days.id,
})
sub._onchange_sale_order_template_id()
sub.action_confirm() # we these subs alone because they will be paid
# We don't pay tbis invoice to simulate bad historical data
self.env['sale.order']._cron_recurring_create_invoice()
invoice_to_skip = sub.invoice_ids
with freeze_time("2024-01-01"):
self.env['sale.order']._cron_recurring_create_invoice()
invoice_to_pay = sub.invoice_ids - invoice_to_skip
self.env['account.payment.register'] \
.with_context(active_model='account.move',
active_ids=invoice_to_pay.ids).create(
{
'currency_id': sub.currency_id.id,
'amount': 300,
})._create_payments()
# If the last invoice is paid, we don't close the contract
with freeze_time("2025-01-02"):
self.env['sale.order'].sudo()._cron_subscription_expiration()
self.assertEqual(sub.subscription_state, '3_progress')
# The contract is expired, the next invoice date is passed since 5 days, we close it
with freeze_time("2025-01-07"):
self.env['sale.order'].sudo()._cron_subscription_expiration()
self.assertEqual(sub.subscription_state, '6_churn')
self.assertEqual(sub.close_reason_id.id, self.env.ref("sale_subscription.close_reason_auto_close_limit_reached").id)
def test_partial_payment(self):
subscription = self.subscription
subscription.action_confirm()
# /payment/pay will create a transaction, validate it and post-process-it
reference = "CONTRACT-%s-%s" % (subscription.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S%f'))
tx = self.env["payment.transaction"].create(
self._get_payment_values(subscription, [], subscription.amount_total / 2., reference))
tx._set_done()
tx._post_process()
self.assertEqual(tx.state, 'done')
self.assertFalse(tx.invoice_ids, "We should not have created an invoice")
self.assertFalse(subscription.invoice_ids, "We should not have created an invoice on the subscription")
self.assertEqual(
subscription.start_date, subscription.next_invoice_date,
"The subscription next invoice date should not have been updated"
)
def test_refund_next_invoice_date(self):
with freeze_time('2023-01-18'):
subscription = self.env['sale.order'].create({
'partner_id': self.partner.id,
'plan_id': self.plan_month.id,
'order_line': [
(0, 0, {
'name': self.product.name,
'product_id': self.product.id,
'product_uom_qty': 3.0,
'price_unit': 12,
})],
})
subscription.action_confirm()
subscription._create_recurring_invoice()
self.assertEqual(subscription.next_invoice_date, datetime.date(2023, 2, 18), "The next invoice date is incremented")
subscription._get_invoiced()
inv = subscription.invoice_ids
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'
})
payment_with_token = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'amount': subscription.amount_total,
'date': subscription.date_order,
'currency_id': subscription.currency_id.id,
'partner_id': subscription.partner_id.id,
'payment_token_id': test_payment_token.id
})
transaction_ids = payment_with_token._create_payment_transaction()
transaction_ids._set_done() # dummy transaction will always be successful
with freeze_time('2023-02-18'):
subscription._create_recurring_invoice()
self.assertEqual(subscription.next_invoice_date, datetime.date(2023, 3, 18), "The next invoice date is incremented")
# We refund the first invoice
refund_wizard = self.env['account.move.reversal'].with_context(
active_model="account.move",
active_ids=inv.ids).create({
'reason': 'Test refund tax repartition',
'journal_id': inv.journal_id.id,
})
res = refund_wizard.refund_moves()
refund_move = self.env['account.move'].browse(res['res_id'])
self.assertEqual(inv.reversal_move_ids, refund_move, "The initial move should be reversed")
self.assertEqual(subscription.next_invoice_date, datetime.date(2023, 3, 18), "The next invoice date not incremented")
def test_subscription_invoice_after_payment(self):
self.amount = self.subscription.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.subscription.id], state='done')
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx._post_process()
self.assertEqual(self.subscription.state, 'sale')
self.assertEqual(len(self.subscription.invoice_ids), 1)
self.assertEqual(self.subscription.invoice_ids.state, 'posted')
def test_subscription_invoice_after_payment_sale_automatic_invoice(self):
"""Ensure that invoice are only sent once when sale 'Automatic Invoice' option is enabled."""
self.env["ir.config_parameter"].set_param("sale.automatic_invoice", "True")
# Update account_payment `_post_process` mock so that we post invoices
# created by sale's "Automatic Invoice" option
def _post_process(self):
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
ap_reconcile_after_done_post = patch(
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._post_process',
side_effect=_post_process, autospec=_post_process,
)
self.startPatcher(ap_reconcile_after_done_post)
# Mock method that is used to send the invoice
# (simply used to registerto how many time it's called)
send_invoice_mock = self.startPatcher(
patch.object(self.env.registry["account.move.send"], '_generate_and_send_invoices', autospec=True)
)
self.amount = self.subscription.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.subscription.id], state='done',
subscription_action="assign_token")
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx._post_process()
self.assertEqual(self.subscription.state, 'sale')
self.assertEqual(len(self.subscription.invoice_ids), 1)
self.assertEqual(self.subscription.invoice_ids.state, 'posted')
def test_manually_captured_payment_providers_not_allowed(self):
self.provider.capture_manually = True
compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
self.company.id, self.partner.id, self.amount, sale_order_id=self.subscription.id
)
self.assertNotIn(self.provider, compatible_providers)
def test_exceeding_maximum_amount_providers_not_allowed(self):
self.provider.maximum_amount = 1
self.assertGreater(
self.amount,
self.provider.maximum_amount,
'The subscription amount should be greater than the maximum amount on the provider for this test.'
)
compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
self.company.id, self.partner.id, self.amount, sale_order_id=self.subscription.id
)
self.assertNotIn(
self.provider,
compatible_providers,
'The provider should not be proposed to the user because its maximum amount is too low to pay this subscription.'
)
def test_cancel_draft_invoice_unsuccessful_transaction(self):
""" Ensure that after an unsuccessful token payment is made, its draft invoice is canceled. """
with freeze_time("2024-01-23"):
subscription = self.env['sale.order'].create({
'partner_id': self.user_portal.partner_id.id,
'sale_order_template_id': self.subscription_tmpl.id,
'start_date': datetime.date(2024, 1, 15)
})
subscription._onchange_sale_order_template_id()
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': self.user_portal.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
payment_with_token = self.env['account.payment'].create({
'amount': subscription.amount_total,
'currency_id': subscription.currency_id.id,
'partner_id': self.user_portal.partner_id.id,
'payment_token_id': test_payment_token.id
})
transaction_ids = payment_with_token._create_payment_transaction()
subscription.write({'transaction_ids': [Command.set(transaction_ids.ids)]})
subscription.action_confirm()
subscription._create_invoices(final=True)
draft_invoice = subscription.order_line.invoice_lines.move_id.filtered(lambda am: am.state == 'draft')
transaction_ids._set_error("Payment declined!")
transaction_ids._post_process()
self.assertEqual(len(draft_invoice), 1, "A single draft invoice must be created after the payment was done.")
self.assertFalse(subscription.pending_transaction, "Subscription doesn't have pending transaction after unsuccessful payment.")
self.assertFalse(subscription.payment_token_id, "The payment token should not be saved after the unsuccessful payment.")
self.assertEqual(draft_invoice.state, "cancel", "Draft invoice must be canceled after unsuccessful payment.")
def test_prevent_multiple_in_progress(self):
""" make sure that when a payment link of a renewed order, it does not lead to
two in progress orders
"""
with freeze_time('2024-01-01'):
subscription = self.env['sale.order'].create({
'partner_id': self.user_portal.partner_id.id,
'sale_order_template_id': self.subscription_tmpl.id,
'start_date': datetime.date(2024, 1, 15)
})
subscription._onchange_sale_order_template_id()
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': self.user_portal.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
subscription.action_confirm()
inv = subscription._create_invoices(final=True)
inv._post()
# Renew, everything is smooth
action = subscription.prepare_renewal_order()
renewal_so = self.env['sale.order'].browse(action['res_id'])
renewal_so.action_confirm()
payment_with_token = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'amount': subscription.amount_total,
'date': subscription.date_order,
'currency_id': subscription.currency_id.id,
'partner_id': subscription.partner_id.id,
'payment_token_id': test_payment_token.id
})
transaction_ids = payment_with_token._create_payment_transaction()
transaction_ids.sale_order_ids = [Command.set(renewal_so.ids)]
transaction_ids.subscription_action = 'automatic_send_mail'
inv = renewal_so._create_invoices(final=True)
inv._post()
action = renewal_so.prepare_renewal_order()
renewal_so2 = self.env['sale.order'].browse(action['res_id'])
renewal_so2.action_confirm()
# We confirm the payment of renewal_so AFTER renewal_so2 is confirmed. It must not reopen renewal_so
# It should stays 5_renewed
transaction_ids._set_done()
transaction_ids._post_process()
self.assertEqual(subscription.subscription_state, '5_renewed')
self.assertEqual(renewal_so.subscription_state, '5_renewed')
self.assertEqual(renewal_so2.subscription_state, '3_progress')
def test_negative_payment_handling(self):
""" When the amount_total_signed is negative, the invoice is a refund. We should not do a request in that case
SO should be in exception and handled manually
"""
with freeze_time("2024-01-23"):
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': self.user_portal.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
self.subscription.write({
'start_date': False,
'next_invoice_date': False,
'payment_token_id': test_payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX',
})
# add expensive non recurring product
self.product5.product_tmpl_id.recurring_invoice = False
non_recurring = self.env['sale.order.line'].create({
'order_id': self.subscription.id,
'product_id': self.product5.id,
'price_unit': 1000,
})
self.subscription.action_confirm()
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment):
invoice = self.env['sale.order']._create_recurring_invoice()
txs_to_post_process = self.env['payment.transaction'].search([
('state', '=', 'done'),
('is_post_processed', '=', False),
('last_state_change', '>=', datetime.datetime.now() - relativedelta(days=4)),
])
txs_to_post_process._post_process()
self.assertAlmostEqual(invoice.amount_total, 1123.1)
self.assertAlmostEqual(invoice.amount_total_signed, 1123.1)
self.assertEqual(invoice.move_type, "out_invoice", "This is an invoice")
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2024, 2, 23))
with freeze_time("2024-02-23"):
# reset the quantity to 0
non_recurring.product_uom_qty = 0
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment):
invoice2 = self.env['sale.order']._create_recurring_invoice()
txs_to_post_process = self.env['payment.transaction'].search([
('state', '=', 'done'),
('is_post_processed', '=', False),
('last_state_change', '>=', datetime.datetime.now() - relativedelta(days=4)),
])
txs_to_post_process._post_process()
self.assertEqual(invoice2.move_type, "out_refund", "This is a refund")
self.assertEqual(invoice2.state, "draft", "The invoice should not be posted")
self.assertAlmostEqual(invoice2.amount_total_signed, -1076.9)
self.assertTrue(self.subscription.payment_exception)
self.assertEqual(self.subscription.next_invoice_date, datetime.date(2024, 2, 23), "The next invoice date should not be updated")
def test_automate_payments_invoice_multiple_subscriptions(self):
"""
Ensure that invoices with multiple linked subscriptions (through its invoice lines)
are able to automate its payments when paying with tokenizeable payment method.
"""
# Create two subscriptions and confirm them.
sub_params = {
'partner_id': self.partner.id,
'company_id': self.company.id,
'sale_order_template_id': self.subscription_tmpl.id
}
subscription_1 = self.subscription.create(sub_params)
subscription_2 = self.subscription.create(sub_params)
for sub in [subscription_1, subscription_2]:
sub._onchange_sale_order_template_id()
sub.action_confirm()
# Generate invoice consolidating the billing of the two subscriptions above.
invoice = self.env['sale.advance.payment.inv'].create({
'advance_payment_method': 'delivered',
'sale_order_ids': [Command.set((subscription_1 + subscription_2).ids)],
'consolidated_billing': True,
})._create_invoices(subscription_1 + subscription_2)
# Ensure that the subscriptions are present in the invoice lines.
for sub in [subscription_1, subscription_2]:
self.assertTrue(sub in invoice.invoice_line_ids.subscription_id)
# Define test payment token for later assignment.
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': subscription_1.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
# Create transaction and set it as done.
subscriptions_sudo = invoice.sudo().invoice_line_ids.subscription_id
tx_sudo = self._create_transaction(
sale_order_ids=[Command.set(subscriptions_sudo.ids)],
invoice_ids=[Command.set(invoice.ids)],
subscription_action='assign_token',
currency_id=invoice.sudo().currency_id.id,
partner_id=self.partner.sudo().id,
provider_id=self.dummy_provider.id,
payment_method_id=self.payment_method_id,
token_id=test_payment_token.id,
amount=invoice.amount_total,
flow='direct',
landing_route='/my/invoices/%s?access_token=%s' % (invoice.id, invoice.access_token),
)
tx_sudo._set_done()
tx_sudo._post_process()
# Ensure token was assigned in each subscription after completing the payment.
for sub in [subscription_1, subscription_2]:
self.assertEqual(
sub.payment_token_id.id,
test_payment_token.id,
"Token must be assigned to subscription after transaction creation."
)
def test_successful_reopen_churned_subscription(self):
"""
Ensure that a churned subscription is reopened when any
of its invoices is paid within the next invoicing date.
"""
# Confirm subscription, generate invoice and then close it.
with freeze_time("2025-08-08"):
subscription = self.subscription
subscription.action_confirm()
subscription._create_recurring_invoice()
with freeze_time("2025-09-14"):
# only some reasons can allow reopening.
close_reason = self.env.ref('sale_subscription.close_reason_auto_close_limit_reached')
subscription.set_close(close_reason_id=close_reason.id)
# Ensure that the subscription is churned, and that the next invoice
# date happens after today. The transaction date must be less than
# this date for the subscription to be successfuly re-opened.
self.assertEqual(
subscription.subscription_state,
"6_churn",
"The subscription must be churned after closing it."
)
cutoff_date = datetime.datetime.now().date() - relativedelta(days=subscription.plan_id.auto_close_limit)
self.assertTrue(
cutoff_date <= subscription.next_invoice_date,
"For sub reopening, we must have payment date <= next invoice date."
)
# Create and process the payment transaction for the invoice.
# Ensure that the subscription got re-opened after paying it.
tx = self.env["payment.transaction"].create(
self._get_payment_values(subscription, subscription.invoice_ids.ids)
)
tx._set_done()
tx._post_process()
self.assertEqual(tx.state, 'done')
self.assertEqual(
subscription.subscription_state,
"3_progress",
"Churned subscription must be re-opened after paying the invoice."
)
def test_successful_reopen_churned_subscription_manual(self):
"""
Ensure that a churned subscription is reopened when a invoices pay
through (bank transafer,..) within the next invoicing date.
"""
# Confirm subscription, generate invoice and then close it.
with freeze_time("2025-08-08"):
subscription = self.subscription
subscription.action_confirm()
invoice = subscription._create_recurring_invoice()
with freeze_time("2025-09-14"):
# only some reasons can allow reopening.
close_reason = self.env.ref('sale_subscription.close_reason_auto_close_limit_reached')
subscription.set_close(close_reason_id=close_reason.id)
# Ensure that the subscription is churned, and that the next invoice
# date happens after today. The transaction date must be less than
# this date for the subscription to be successfully re-opened.
self.assertEqual(
subscription.subscription_state,
"6_churn",
"The subscription must be churned after closing it."
)
cutoff_date = datetime.datetime.now().date() - relativedelta(days=subscription.plan_id.auto_close_limit)
self.assertTrue(
cutoff_date <= subscription.next_invoice_date,
"For sub reopening, we must have payment date cutoff_date <= next invoice date."
)
# make payment on invoice for reopen subscription
self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=invoice.ids)\
.create({})\
._create_payments()
self.assertTrue(
invoice.payment_state in ['in_payment', 'paid'],
"Ensure that the invoice's payment state is either 'in_payment' or 'paid'"
)
# Ensure that the subscription got re-opened after paying it.
self.assertEqual(
subscription.subscription_state,
"3_progress",
"Churned subscription must be re-opened after paying the invoice."
)
def test_credit_note_payment_not_reopen_churned_subscription(self):
"""
Ensure that a churned subscription is NOT reopened when a credit note
is paid. Only invoice payments should reopen churned subscriptions.
"""
with freeze_time("2025-08-08"):
subscription = self.subscription
subscription.action_confirm()
invoice = subscription._create_recurring_invoice()
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
'payment_date': invoice.date,
})._create_payments()
subscription.set_close()
self.assertEqual(
subscription.subscription_state,
"6_churn",
"The subscription must be churned after closing it."
)
refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
'reason': 'Test credit note',
'journal_id': invoice.journal_id.id,
})
res = refund_wizard.refund_moves()
credit_note = self.env['account.move'].browse(res['res_id'])
self.assertEqual(credit_note.move_type, 'out_refund', "Must be a credit note")
credit_note.action_post()
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=credit_note.ids).create({
'payment_date': credit_note.date,
})._create_payments()
self.assertEqual(credit_note.payment_state, credit_note._get_invoice_in_payment_state())
self.assertEqual(
subscription.subscription_state,
"6_churn",
"Churned subscription must NOT be re-opened after paying a credit note."
)
def test_unsuccessful_reopen_churned_subscription(self):
"""
Ensure that a churned subscription is not re-opened when
an invoice is paid after the allowed auto_close_limit
period from the next invoicing date.
"""
# Confirm subscription, generate invoice and then close it.
subscription = self.subscription
subscription.plan_id.auto_close_limit = 5
subscription.action_confirm()
subscription._create_recurring_invoice()
subscription.set_close()
# Simulate the payment transaction for the invoice one week after the
# (next_invoice_date + auto_close_limit) and ensure that the subscription
# wasn't reopened.
late_payment_date = subscription.next_invoice_date + relativedelta(
days=subscription.plan_id.auto_close_limit + 7
)
with freeze_time(late_payment_date):
tx_2 = self.env["payment.transaction"].create(
self._get_payment_values(subscription, subscription.invoice_ids.ids)
)
tx_2._set_done()
tx_2._post_process()
self.assertEqual(tx_2.state, 'done')
self.assertEqual(
subscription.subscription_state,
"6_churn",
"Churned subscription must not re-open after the invoice late payment."
)
def test_payment_pending(self):
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': self.user_portal.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
self.subscription.write({'payment_token_id': test_payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
self.subscription.action_confirm()
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment_pending),\
self.mock_mail_gateway():
self.subscription._create_recurring_invoice()
invoice = self.subscription.order_line.invoice_lines.move_id
self.assertTrue(self.subscription.pending_transaction, "The pending transaction flag should remain")
self.assertEqual(invoice.state, 'draft', "The draft invoice should exist")
self.assertEqual(
self.subscription.next_invoice_date, self.subscription.start_date,
"We should not have updated the next invoice date, as the invoice was unlinked",
)
invoice._post()
self.assertFalse(self.subscription.pending_transaction, "The pending transaction flag should not remain")
def test_manual_invoice_from_other_company_website(self):
company = self.env['res.company'].create({'name': "Website Company"})
public_user = self.env.ref('base.public_user')
public_user.sudo().write({
'company_id': company.id,
'company_ids': [Command.set([company.id])]})
with freeze_time('2024-01-01'):
subscription = self.subscription
subscription.action_confirm()
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'
})
payment_with_token = self.env['account.payment'].create({
'amount': subscription.amount_total,
'currency_id': subscription.currency_id.id,
'partner_id': self.user_portal.partner_id.id,
'payment_token_id': test_payment_token.id
})
inv = subscription._create_invoices()
payment_with_token.move_id = inv.id
payment_with_token.write({'state': 'paid'})
tx = payment_with_token._create_payment_transaction()
subscription.write({'transaction_ids': [Command.set(tx.ids)]})
tx._set_done()
tx.with_user(public_user).sudo()._post_process()
def test_subscription_invoice_after_second_period_payment(self):
"""
Ensure the second invoice is generated correctly when a non-recurring product was included in the order lines.
When first time create invoice without payment then ensure that second time payment not considered down payment.
"""
sub = self.subscription
self.product5.recurring_invoice = False
sub.order_line = [
Command.create(
{
"name": "non recurring product",
"product_id": self.product5.id, # non recurring product
"product_uom_qty": 1,
}
)
]
sub.action_confirm()
# Create first period invoice for both recurring and non-recurring products
sub._create_invoices().action_post()
invoice_1 = sub.invoice_ids
# check subscription should be not be paid
self.assertNotEqual(sub.amount_total, invoice_1.amount_paid, 'Subscription should not be paid')
self.assertEqual(sub.state, "sale")
self.assertEqual(len(sub.invoice_ids), 1)
self.assertEqual(sub.invoice_ids.state, "posted")
# Now, pay for the second period with only the recurring product
self.reference = "SECOND PERIOD"
self.amount = sub._next_billing_details()["next_invoice_amount"]
tx = self._create_transaction(
flow="redirect", sale_order_ids=[sub.id], state="done"
)
with mute_logger("odoo.addons.sale.models.payment_transaction"):
tx._post_process()
invoice_2 = sub.invoice_ids - invoice_1
# check subscription should be fully paid
self.assertEqual(self.amount, invoice_2.amount_paid, 'Subscription should not be paid')
self.assertEqual(sub.state, "sale")
self.assertEqual(len(sub.invoice_ids), 2)
self.assertEqual(sub.invoice_ids[1].state, "posted")
def test_payment_link_wizard(self):
# test auto values for payment link
with freeze_time("2025-04-15"):
test_payment_token = self.env['payment.token'].create({
'payment_details': 'Test',
'partner_id': self.user_portal.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'test'
})
self.subscription.write({
'start_date': False,
'next_invoice_date': False,
'prepayment_percent': 50,
'payment_token_id': test_payment_token.id,
'client_order_ref': 'Customer REF XXXXXXX'
})
self.subscription.action_confirm()
payment_vals = self.subscription._get_default_payment_link_values()
self.assertAlmostEqual(payment_vals['amount'], 23.1, msg="Non invoiced SO have same behavior as recular SO")
self.assertAlmostEqual(payment_vals['amount_max'], 23.1, msg="Non invoiced SO have same behavior as recular SO")
with freeze_time("2025-05-15"):
inv = self.subscription._create_recurring_invoice()
tx = self.env["payment.transaction"].create(
self._get_payment_values(self.subscription, inv.ids)
)
tx._set_done()
tx._post_process()
payment_vals = self.subscription._get_default_payment_link_values()
self.assertAlmostEqual(payment_vals['amount'], 23.1, msg="invoiced recurring SO have different behavior")
self.assertAlmostEqual(payment_vals['amount_max'], 23.1, msg="invoiced recurring SO have different behavior")
with freeze_time("2025-06-15"):
inv = self.subscription._create_recurring_invoice()
tx = self.env["payment.transaction"].create(
self._get_payment_values(self.subscription, inv.ids, reference="PAYMENT2")
)
tx._set_done()
tx._post_process()
payment_vals = self.subscription._get_default_payment_link_values()
self.assertAlmostEqual(payment_vals['amount'], 23.1, msg="invoiced recurring SO have different behavior")
self.assertAlmostEqual(payment_vals['amount_max'], 23.1, msg="invoiced recurring SO have different behavior")