[IMP] payment: allow (un)publishing of payment acquirers

Before this commit, it was impossible to migrate from one payment
acquirer to another as the only way to do so was to change a payment
acquirer’s state to ‘disabled’, subsequently disabling all tokens of that
acquirer (detrimental for eg. running subscriptions).

After this commit, payment acquirers have an additional boolean
‘Published’. With this functionality, users can safely migrate by
keeping an acquirer ‘enabled’, but invisible for customers.

There are five combined states an acquirer can take:

‘enabled’ & ‘published’: Visible to all users
‘enabled’ & ‘unpublished’: Visible only to internal users
‘disabled’ & ‘unpublished’: Same as previously ‘disabled’
‘test’ & ‘published’: Same as previously ‘test’
‘test’ & ‘unpublished’: visible only to internal users

task-2871459

closes odoo/odoo#94242

Related: odoo/upgrade#3688
Related: odoo/documentation#2308
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Laura Schauer
2022-07-19 20:12:58 +02:00
parent a26ff947ff
commit 6965066e9c
7 changed files with 114 additions and 3 deletions
+30 -2
View File
@@ -5,7 +5,7 @@ import logging
from psycopg2 import sql
from odoo import _, api, fields, models, SUPERUSER_ID
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
_logger = logging.getLogger(__name__)
@@ -31,6 +31,11 @@ class PaymentAcquirer(models.Model):
"This mode is advised when setting up the acquirer.",
selection=[('disabled', "Disabled"), ('enabled', "Enabled"), ('test', "Test Mode")],
default='disabled', required=True, copy=False)
is_published = fields.Boolean(
string="Published",
help="Whether the acquirer is visible on the website or not. Tokens remain functional but "
"are only visible on manage forms.",
)
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
string="Company", comodel_name='res.company', default=lambda self: self.env.company.id,
required=True, index=True)
@@ -236,7 +241,15 @@ class PaymentAcquirer(models.Model):
#=== ONCHANGE METHODS ===#
@api.onchange('state')
def _onchange_state(self):
def _onchange_state_switch_is_published(self):
""" Automatically publish or unpublish the acquirer depending on its state.
:return: None
"""
self.is_published = self.state == 'enabled'
@api.onchange('state')
def _onchange_state_warn_before_disabling_tokens(self):
""" Display a warning about the consequences of disabling an acquirer.
Let the user know that tokens related to an acquirer get archived if it is disabled or if
@@ -348,6 +361,17 @@ class PaymentAcquirer(models.Model):
'tag': 'reload',
}
def action_toggle_is_published(self):
""" Toggle the acquirer's is_published state.
:return: none
:raise UserError: If the acquirer is disabled.
"""
if self.state != 'disabled':
self.is_published = not self.is_published
else:
raise UserError(_("You cannot publish a disabled acquirer."))
#=== BUSINESS METHODS ===#
@api.model
@@ -372,6 +396,10 @@ class PaymentAcquirer(models.Model):
# Compute the base domain for compatible acquirers
domain = ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', company_id)]
# Handle the is_published state.
if not self.env.user._is_internal():
domain = expression.AND([domain, [('is_published', '=', True)]])
# Handle partner country
partner = self.env['res.partner'].browse(partner_id)
if partner.country_id: # The partner country must either not be set or be supported
+1
View File
@@ -5,4 +5,5 @@ from . import http_common
from . import test_account_payment
from . import test_flows
from . import test_multicompany_flows
from . import test_payment_acquirer
from . import test_payment_transaction
+1
View File
@@ -92,6 +92,7 @@ class PaymentCommon(AccountTestInvoicingCommon):
'name': "Dummy Acquirer",
'provider': 'none',
'state': 'test',
'is_published': True,
'allow_tokenization': True,
'redirect_form_view_id': redirect_form.id,
'journal_id': cls.company_data['default_journal_bank'].id,
@@ -0,0 +1,36 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentAcquirer(PaymentCommon):
def test_published_acquirer_compatible_with_all_users(self):
for user in (self.public_user, self.portal_user):
self.env = self.env(user=user)
compatible_acquirers = self.env['payment.acquirer'].sudo()._get_compatible_acquirers(
self.company.id, self.partner.id
)
self.assertIn(self.acquirer, compatible_acquirers)
def test_unpublished_acquirer_compatible_with_internal_user(self):
self.acquirer.is_published = False
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id
)
self.assertIn(self.acquirer, compatible_acquirers)
def test_unpublished_acquirer_not_compatible_with_non_internal_user(self):
self.acquirer.is_published = False
for user in (self.public_user, self.portal_user):
self.env = self.env(user=user)
compatible_acquirers = self.env['payment.acquirer'].sudo()._get_compatible_acquirers(
self.company.id, self.partner.id
)
self.assertNotIn(self.acquirer, compatible_acquirers)
@@ -6,6 +6,7 @@
<field name="model">payment.acquirer</field>
<field name="arch" type="xml">
<form string="Payment Acquirer">
<field name="is_published" invisible="1"/>
<field name="support_fees" invisible="1"/>
<field name="support_manual_capture" invisible="1"/>
<field name="support_tokenization" invisible="1"/>
@@ -21,6 +22,27 @@
<field name="show_done_msg" invisible="1"/>
<field name="show_cancel_msg" invisible="1"/>
<sheet>
<!-- === Stat Buttons === -->
<div class="oe_button_box" name="button_box">
<button name="action_toggle_is_published"
attrs="{'invisible': [('is_published', '=', False)]}"
class="oe_stat_button"
type="object"
icon="fa-globe">
<div class="o_stat_info o_field_widget">
<span class="text-success">Published</span>
</div>
</button>
<button name="action_toggle_is_published"
attrs="{'invisible': [('is_published', '=', True)]}"
class="oe_stat_button"
type="object"
icon="fa-eye-slash">
<div class="o_stat_info o_field_widget">
<span class="text-danger">Unpublished</span>
</div>
</button>
</div>
<field name="image_128" widget="image" class="oe_avatar"/>
<widget name="web_ribbon" title="Disabled" bg_color="bg-danger" attrs="{'invisible': [('state', '!=', 'disabled')]}"/>
<widget name="web_ribbon" title="Test Mode" bg_color="bg-warning" attrs="{'invisible': [('state', '!=', 'test')]}"/>
@@ -117,6 +139,7 @@
<field name="id"/>
<field name="name"/>
<field name="description"/>
<field name="is_published"/>
<field name="provider"/>
<field name="module_id"/>
<field name="module_state"/>
@@ -126,6 +149,7 @@
<t t-name="kanban-box">
<t t-set="installed" t-value="!record.module_id.value || (record.module_id.value &amp;&amp; record.module_state.raw_value === 'installed')"/>
<t t-set="to_buy" t-value="record.module_to_buy.raw_value === true"/>
<t t-set="is_published" t-value="record.is_published.raw_value === true"/>
<div t-attf-class="oe_kanban_global_click #{kanban_color(record.color.raw_value)}">
<div class="o_payment_acquirer_desc">
<div class="o_kanban_image">
@@ -138,6 +162,8 @@
<t t-if="installed">
<field name="state" widget="label_selection" options="{'classes': {'enabled': 'success', 'test': 'warning', 'disabled' : 'danger'}}"/>
</t>
<span t-if="is_published &amp;&amp; installed" class="badge text-bg-success ms-1">Published</span>
<span t-if="!is_published &amp;&amp; installed" class="badge text-bg-danger ms-1">Unpublished</span>
<button t-if="!installed and !selection_mode and !to_buy" type="object" class="btn btn-secondary float-end" name="button_immediate_install">Install</button>
<t t-if="!installed and to_buy">
<button href="https://odoo.com/pricing?utm_source=db&amp;utm_medium=module" target="_blank" class="btn btn-info btn-sm float_right">Upgrade</button>
+19 -1
View File
@@ -66,6 +66,11 @@
class="rounded-pill text-bg-warning ms-1">
Test Mode
</span>
<!-- === "Unpublished" badge === -->
<span t-if="not acquirer.is_published"
class="badge text-bg-danger ms-1">
Unpublished
</span>
<!-- === Extra fees badge === -->
<t t-if="fees_by_acquirer.get(acquirer)">
<span class="rounded-pill text-bg-secondary ms-1">
@@ -118,6 +123,10 @@
<span class="payment_option_name" t-esc="token.name"/>
<!-- === "V" check mark === -->
<t t-call="payment.verified_token_checkmark"/>
<!-- === "Unpublished" badge === -->
<span t-if="not token.acquirer_id.is_published" class="badge text-bg-danger ms-1">
Unpublished
</span>
</label>
</div>
<!-- === Token inline form === -->
@@ -195,7 +204,7 @@
data-payment-option-type="acquirer"/>
<!-- === Acquirer name === -->
<span class="payment_option_name">
<b><t t-esc="acquirer.display_as or acquirer.name"/></b>
<b t-esc="acquirer.display_as or acquirer.name"/>
</span>
<!-- === "Test Mode" badge === -->
<span t-if="acquirer.state == 'test'"
@@ -203,6 +212,10 @@
style="margin-left:5px">
Test Mode
</span>
<!-- === "Unpublished" badge === -->
<span t-if="not acquirer.is_published" class="badge text-bg-danger">
Unpublished
</span>
</label>
<!-- === Payment icon list === -->
<t t-call="payment.icon_list"/>
@@ -244,6 +257,11 @@
<span class="payment_option_name" t-esc="token.name"/>
<!-- === "V" check mark === -->
<t t-call="payment.verified_token_checkmark"/>
<!-- === "Unpublished" badge === -->
<span t-if="not token.acquirer_id.is_published and token.env.user._is_internal()"
class="badge text-bg-danger ms-1">
Unpublished
</span>
</label>
<!-- === "Delete" token button === -->
<button name="o_payment_delete_token"
@@ -4,6 +4,7 @@
<record id="payment.payment_acquirer_transfer" model="payment.acquirer">
<field name="provider">transfer</field>
<field name="state">enabled</field>
<field name="is_published">True</field>
<field name="redirect_form_view_id" ref="redirect_form"/>
<!-- Clear the default value to trigger the computation of the message -->
<field name="pending_msg" eval="False"/>