[REM] payment(_alipay,_demo,_paypal), website_payment: drop fees support
The "fees" or "extra fees" or "customer fees" feature was meant to make customers pay for the processing fee charged by the payment provider they choose to make their payment. The fee was also displayed on the payment form to deter customers and encourage them to choose another, cheaper, payment provider. In practice, it didn't hold up because: 1. the provider's API must allow sending the fee as a separate amount, and PayPal was the only supported provider to do it; 2. charging extra fees is highly discouraged by providers, and forbidden in Europe; 3. the final fee amount depends on the customer, country, payment method, risk profile... rendering charging the actual fees amount infeasible; 4. most of the time, only one payment provider is enabled at a time, thus alienating customers who would have no other choice than paying the fee; task-3358581 closes odoo/odoo#132104 Related: odoo/documentation#5517 Related: odoo/upgrade#5053 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
@@ -39,12 +39,6 @@ class PortalAccount(portal.PortalAccount):
|
||||
partner_sudo, invoice_company
|
||||
)
|
||||
|
||||
fees_by_provider = {
|
||||
pro_sudo: pro_sudo._compute_fees(
|
||||
invoice.amount_total, invoice.currency_id, invoice.partner_id.country_id
|
||||
) for pro_sudo in providers_sudo.filtered('fees_active')
|
||||
}
|
||||
|
||||
portal_page_values = {
|
||||
'company_mismatch': company_mismatch,
|
||||
'expected_company': invoice_company,
|
||||
@@ -52,7 +46,6 @@ class PortalAccount(portal.PortalAccount):
|
||||
payment_form_values = {
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens,
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping(
|
||||
providers_sudo
|
||||
),
|
||||
|
||||
@@ -119,12 +119,6 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
# Make sure that the partner's company matches the company passed as parameter.
|
||||
company_mismatch = not PaymentPortal._can_partner_pay_in_company(partner_sudo, company)
|
||||
|
||||
# Compute the fees taken by providers supporting the feature
|
||||
fees_by_provider = {
|
||||
provider_sudo: provider_sudo._compute_fees(amount, currency, partner_sudo.country_id)
|
||||
for provider_sudo in providers_sudo.filtered('fees_active')
|
||||
}
|
||||
|
||||
# Generate a new access token in case the partner id or the currency id was updated
|
||||
access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
|
||||
|
||||
@@ -136,7 +130,6 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
payment_form_values = {
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens_sudo,
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'show_tokenize_input': self._compute_show_tokenize_input_mapping(
|
||||
providers_sudo, **kwargs
|
||||
),
|
||||
|
||||
@@ -123,17 +123,6 @@ class PaymentProvider(models.Model):
|
||||
currency_field='main_currency_id',
|
||||
)
|
||||
|
||||
# Fees fields
|
||||
fees_active = fields.Boolean(string="Add Extra Fees")
|
||||
fees_dom_fixed = fields.Monetary(
|
||||
string="Fixed domestic fees", currency_field='main_currency_id'
|
||||
)
|
||||
fees_dom_var = fields.Float(string="Variable domestic fees")
|
||||
fees_int_fixed = fields.Monetary(
|
||||
string="Fixed international fees", currency_field='main_currency_id'
|
||||
)
|
||||
fees_int_var = fields.Float(string="Variable international fees")
|
||||
|
||||
# Message fields
|
||||
display_as = fields.Char(
|
||||
string="Displayed as", help="Description of the provider for customers",
|
||||
@@ -177,9 +166,6 @@ class PaymentProvider(models.Model):
|
||||
selection=[('full_only', "Full Only"), ('partial', "Partial")],
|
||||
compute='_compute_feature_support_fields',
|
||||
)
|
||||
support_fees = fields.Boolean(
|
||||
string="Fees Supported", compute='_compute_feature_support_fields'
|
||||
)
|
||||
|
||||
# Kanban view fields
|
||||
image_128 = fields.Image(string="Image", max_width=128, max_height=128)
|
||||
@@ -286,7 +272,6 @@ class PaymentProvider(models.Model):
|
||||
|
||||
- `support_express_checkout`: Whether the "express checkout" feature is supported. `False`
|
||||
by default.
|
||||
- `support_fees`: Whether the "extra fees" feature is supported. `False` by default.
|
||||
- `support_manual_capture`: Whether the "manual capture" feature is supported. `False` by
|
||||
default.
|
||||
- `support_refund`: Which type of the "refunds" feature is supported: `None`,
|
||||
@@ -302,7 +287,6 @@ class PaymentProvider(models.Model):
|
||||
"""
|
||||
self.update(dict.fromkeys((
|
||||
'support_express_checkout',
|
||||
'support_fees',
|
||||
'support_manual_capture',
|
||||
'support_refund',
|
||||
'support_tokenization',
|
||||
@@ -343,21 +327,6 @@ class PaymentProvider(models.Model):
|
||||
}
|
||||
}
|
||||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@api.constrains('fees_dom_var', 'fees_int_var')
|
||||
def _check_fee_var_within_boundaries(self):
|
||||
""" Check that variable fees are within realistic boundaries.
|
||||
|
||||
Variable fee values should always be positive and below 100% to respectively avoid negative
|
||||
and infinite (division by zero) fee amounts.
|
||||
|
||||
:return None
|
||||
"""
|
||||
for provider in self:
|
||||
if any(not 0 <= fee < 1 for fee in (provider.fees_dom_var, provider.fees_int_var)):
|
||||
raise ValidationError(_("Variable fees must always be positive and below 100%."))
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
@@ -573,40 +542,6 @@ class PaymentProvider(models.Model):
|
||||
"""
|
||||
return True
|
||||
|
||||
def _compute_fees(self, amount, currency, country):
|
||||
""" Compute the transaction fees.
|
||||
|
||||
The computation is based on the fields `fees_dom_fixed`, `fees_dom_var`, `fees_int_fixed`
|
||||
and `fees_int_var`, and is performed with the formula
|
||||
:code:`fees = (amount * variable + fixed) / (1 - variable)` where the values of `fixed` and
|
||||
`variable` are taken from either the domestic (`dom`) or international (`int`) fields,
|
||||
depending on whether the country matches the company's country.
|
||||
|
||||
For a provider to base the computation on different variables, or to use a different
|
||||
formula, it must override this method and return the resulting fees.
|
||||
|
||||
:param float amount: The amount to pay for the transaction.
|
||||
:param recordset currency: The currency of the transaction, as a `res.currency` record.
|
||||
:param recordset country: The customer country, as a `res.country` record.
|
||||
:return: The computed fees.
|
||||
:rtype: float
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
fees = 0.0
|
||||
if self.fees_active:
|
||||
if country == self.company_id.country_id:
|
||||
fixed = self.fees_dom_fixed
|
||||
variable = self.fees_dom_var
|
||||
else:
|
||||
fixed = self.fees_int_fixed
|
||||
variable = self.fees_int_var
|
||||
fixed_converted = self.main_currency_id._convert(
|
||||
fixed, currency, self.company_id, fields.Date.context_today(self)
|
||||
)
|
||||
fees = (amount * variable + fixed_converted) / (1 - variable)
|
||||
return fees
|
||||
|
||||
def _get_validation_amount(self):
|
||||
""" Return the amount to use for validation operations.
|
||||
|
||||
|
||||
@@ -45,9 +45,6 @@ class PaymentTransaction(models.Model):
|
||||
string="Amount", currency_field='currency_id', readonly=True, required=True)
|
||||
currency_id = fields.Many2one(
|
||||
string="Currency", comodel_name='res.currency', readonly=True, required=True)
|
||||
fees = fields.Monetary(
|
||||
string="Fees", currency_field='currency_id',
|
||||
help="The fees amount; set by the system as it depends on the provider", readonly=True)
|
||||
token_id = fields.Many2one(
|
||||
string="Payment Token", comodel_name='payment.token', readonly=True,
|
||||
domain='[("provider_id", "=", "provider_id")]', ondelete='restrict')
|
||||
@@ -187,15 +184,6 @@ class PaymentTransaction(models.Model):
|
||||
'partner_phone': partner.phone,
|
||||
})
|
||||
|
||||
# Compute fees. For validation transactions, fees are zero.
|
||||
if values.get('operation') == 'validation':
|
||||
values['fees'] = 0
|
||||
else:
|
||||
currency = self.env['res.currency'].browse(values.get('currency_id')).exists()
|
||||
values['fees'] = provider._compute_fees(
|
||||
values.get('amount', 0), currency, partner.country_id,
|
||||
)
|
||||
|
||||
# Include provider-specific create values
|
||||
values.update(self._get_specific_create_values(provider.code, values))
|
||||
|
||||
@@ -212,11 +200,11 @@ class PaymentTransaction(models.Model):
|
||||
# can lead to inconsistent string representation of the amounts sent to the providers.
|
||||
# E.g., tx.create(amount=1111.11) -> tx.amount == 1111.1100000000001
|
||||
# To ensure a proper string representation, we invalidate this request's cache values of the
|
||||
# `amount` and `fees` fields for the created transactions. This forces the ORM to read the
|
||||
# values from the DB where there were stored using `float_repr`, which produces a result
|
||||
# consistent with the format expected by providers.
|
||||
# `amount` field for the created transactions. This forces the ORM to read the values from
|
||||
# the DB where there were stored using `float_repr`, which produces a result consistent with
|
||||
# the format expected by providers.
|
||||
# E.g., tx.create(amount=1111.11) ; tx.invalidate_recordset() -> tx.amount == 1111.11
|
||||
txs.invalidate_recordset(['amount', 'fees'])
|
||||
txs.invalidate_recordset(['amount'])
|
||||
|
||||
return txs
|
||||
|
||||
|
||||
@@ -97,59 +97,3 @@ class TestPaymentProvider(PaymentCommon):
|
||||
self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
|
||||
)
|
||||
self.assertIn(self.provider, compatible_providers)
|
||||
|
||||
def test_fees_null_when_deactivated(self):
|
||||
""" Test that extra fees of a provider are null if these fees are deactivated. """
|
||||
self.provider.write({
|
||||
'fees_active': False,
|
||||
'fees_dom_fixed': 1.0,
|
||||
'fees_dom_var': 0.35,
|
||||
'fees_int_fixed': 0.1,
|
||||
'fees_int_var': 0.2,
|
||||
})
|
||||
transaction_fees = self.provider._compute_fees(
|
||||
self.amount, self.currency, self.partner.country_id
|
||||
)
|
||||
self.assertEqual(transaction_fees, 0)
|
||||
|
||||
def test_fees_formula(self):
|
||||
""" Test that the extra fees of a provider are correctly computed in a domestic case. """
|
||||
self.provider.write({
|
||||
'fees_active': True,
|
||||
'fees_dom_fixed': 1.0,
|
||||
'fees_dom_var': 0.0035,
|
||||
'fees_int_fixed': 0.,
|
||||
'fees_int_var': 0.,
|
||||
})
|
||||
transaction_fees = self.currency.round(
|
||||
self.provider._compute_fees(
|
||||
self.amount,
|
||||
self.provider.company_id.currency_id,
|
||||
self.provider.company_id.country_id,
|
||||
)
|
||||
)
|
||||
self.assertEqual(transaction_fees, 4.91)
|
||||
|
||||
def test_fees_with_currency_conversion(self):
|
||||
""" Test that the conversion of the fees is correctly computed. """
|
||||
# Some modules change the company settings and may make this test useless.
|
||||
if self.currency_euro == self.provider.main_currency_id or (
|
||||
self.country_france == self.provider.company_id.country_id
|
||||
):
|
||||
return
|
||||
self.provider.write({
|
||||
'fees_active': True,
|
||||
'fees_dom_fixed': 0.,
|
||||
'fees_dom_var': 0.,
|
||||
'fees_int_fixed': 1.,
|
||||
'fees_int_var': 0.,
|
||||
})
|
||||
|
||||
with patch(
|
||||
'odoo.addons.base.models.res_currency.Currency._get_rates',
|
||||
return_value={self.currency_euro.id: 2., self.provider.main_currency_id.id: 1.},
|
||||
):
|
||||
transaction_fees = self.currency.round(
|
||||
self.provider._compute_fees(self.amount, self.currency_euro, self.country_france)
|
||||
)
|
||||
self.assertEqual(transaction_fees, 2.)
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_published" invisible="1"/>
|
||||
<field name="main_currency_id" invisible="1"/>
|
||||
<field name="support_fees" invisible="1"/>
|
||||
<field name="support_manual_capture" invisible="1"/>
|
||||
<field name="support_tokenization" invisible="1"/>
|
||||
<field name="support_express_checkout" invisible="1"/>
|
||||
@@ -108,23 +107,6 @@
|
||||
<group string="Payment Followup" name="payment_followup" invisible="1"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Fees" name="fees" invisible="not support_fees">
|
||||
<group name="payment_fees">
|
||||
<group class="oe_inline">
|
||||
<field name="fees_active"/>
|
||||
<field name="fees_dom_fixed"
|
||||
invisible="not fees_active"/>
|
||||
<field name="fees_dom_var"
|
||||
widget="percentage"
|
||||
invisible="not fees_active"/>
|
||||
<field name="fees_int_fixed"
|
||||
invisible="not fees_active"/>
|
||||
<field name="fees_int_var"
|
||||
widget="percentage"
|
||||
invisible="not fees_active"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Messages"
|
||||
name="messages"
|
||||
invisible="module_id and module_state != 'installed'">
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
- 'providers' - The payment providers compatible with the current transaction
|
||||
- 'tokens' - The payment tokens of the current partner and payment providers
|
||||
- 'default_token_id' - The id of the token that should be pre-selected. Optional
|
||||
- 'fees_by_provider' - The dict of transaction fees for each provider. Optional
|
||||
- 'show_tokenize_input' - Whether the option to save the payment method is shown
|
||||
- 'reference_prefix' - The custom prefix to compute the full transaction reference
|
||||
- 'amount' - The amount to pay. Optional (sale_subscription)
|
||||
@@ -52,7 +51,6 @@
|
||||
t-value="default_token_id if default_token_id and token_count > 0
|
||||
else providers[0].id if provider_count == 1 and token_count == 0
|
||||
else None"/>
|
||||
<t t-set="fees_by_provider" t-value="fees_by_provider or dict()"/>
|
||||
<t t-set="footer_template_id"
|
||||
t-value="footer_template_id or 'payment.footer'"/>
|
||||
|
||||
@@ -75,13 +73,6 @@
|
||||
<span class="payment_option_name" t-esc="token.display_name"/>
|
||||
<!-- === "V" check mark === -->
|
||||
<t t-call="payment.verified_token_checkmark"/>
|
||||
<!-- === "Fees" badge === -->
|
||||
<span t-if="fees_by_provider.get(provider)"
|
||||
class="badge rounded-pill text-bg-secondary ms-1">
|
||||
+ <t t-esc="fees_by_provider.get(provider)"
|
||||
t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
Fees
|
||||
</span>
|
||||
<!-- === "Test token" badge === -->
|
||||
<t t-call="payment.test_token_badge"/>
|
||||
<!-- === "Unpublished" badge === -->
|
||||
@@ -142,14 +133,6 @@
|
||||
class="badge rounded-pill text-bg-danger ms-1">
|
||||
Unpublished
|
||||
</span>
|
||||
<!-- === Extra fees badge === -->
|
||||
<t t-if="fees_by_provider.get(provider)">
|
||||
<span class="badge rounded-pill text-bg-secondary ms-1">
|
||||
+ <t t-esc="fees_by_provider.get(provider)"
|
||||
t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
Fees
|
||||
</span>
|
||||
</t>
|
||||
</label>
|
||||
<!-- === Payment method icons list === -->
|
||||
<t t-call="payment.icon_list"/>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<field name="source_transaction_id"
|
||||
invisible="not source_transaction_id"/>
|
||||
<field name="amount"/>
|
||||
<field name="fees" invisible="fees == 0.0"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="provider_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
@@ -78,7 +77,6 @@
|
||||
<!-- Needed to display the currency of the amounts -->
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
<field name="amount"/>
|
||||
<field name="fees"/>
|
||||
<field name="state"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
</tree>
|
||||
|
||||
@@ -28,15 +28,6 @@ class PaymentProvider(models.Model):
|
||||
alipay_seller_email = fields.Char(
|
||||
string="Alipay Seller Email", help="The public Alipay partner email")
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
def _compute_feature_support_fields(self):
|
||||
""" Override of `payment` to enable additional features. """
|
||||
super()._compute_feature_support_fields()
|
||||
self.filtered(lambda p: p.code == 'alipay').update({
|
||||
'support_fees': True,
|
||||
})
|
||||
|
||||
# === BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -30,13 +30,6 @@ class PaymentTransaction(models.Model):
|
||||
return res
|
||||
|
||||
base_url = self.provider_id.get_base_url()
|
||||
if self.fees:
|
||||
# Similarly to what is done in `payment::payment.transaction.create`, we need to round
|
||||
# the sum of the amount and of the fees to avoid inconsistent string representations.
|
||||
# E.g., str(1111.11 + 7.09) == '1118.1999999999998'
|
||||
total_fee = self.currency_id.round(self.amount + self.fees)
|
||||
else:
|
||||
total_fee = self.amount
|
||||
rendering_values = {
|
||||
'_input_charset': 'utf-8',
|
||||
'notify_url': urls.url_join(base_url, AlipayController._webhook_url),
|
||||
@@ -44,7 +37,7 @@ class PaymentTransaction(models.Model):
|
||||
'partner': self.provider_id.alipay_merchant_partner_id,
|
||||
'return_url': urls.url_join(base_url, AlipayController._return_url),
|
||||
'subject': self.reference,
|
||||
'total_fee': f'{total_fee:.2f}',
|
||||
'total_fee': f'{self.amount:.2f}',
|
||||
}
|
||||
if self.provider_id.alipay_payment_method == 'standard_checkout':
|
||||
# https://global.alipay.com/docs/ac/global/create_forex_trade
|
||||
|
||||
@@ -14,7 +14,6 @@ class AlipayCommon(PaymentCommon):
|
||||
'alipay_merchant_partner_id': 'dummy',
|
||||
'alipay_md5_signature_key': 'dummy',
|
||||
'alipay_seller_email': 'dummy',
|
||||
'fees_active': False, # Only activate fees in dedicated tests
|
||||
})
|
||||
|
||||
# override defaults for helpers
|
||||
|
||||
@@ -55,7 +55,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
'partner': self.alipay.alipay_merchant_partner_id,
|
||||
'return_url': self._build_url(AlipayController._return_url),
|
||||
'subject': self.reference,
|
||||
'total_fee': str(self.amount), # Fees disabled by default
|
||||
'total_fee': str(self.amount),
|
||||
}
|
||||
|
||||
if self.alipay.alipay_payment_method == 'standard_checkout':
|
||||
@@ -91,35 +91,6 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
"Alipay: invalid inputs specified in the redirect form.",
|
||||
)
|
||||
|
||||
def test_03_redirect_form_with_fees(self):
|
||||
# update provider: compute fees
|
||||
self.alipay.write({
|
||||
'fees_active': True,
|
||||
'fees_dom_fixed': 1.0,
|
||||
'fees_dom_var': 0.0035,
|
||||
'fees_int_fixed': 1.5,
|
||||
'fees_int_var': 0.005,
|
||||
})
|
||||
|
||||
transaction_fees = self.currency.round(
|
||||
self.alipay._compute_fees(
|
||||
self.amount,
|
||||
self.currency,
|
||||
self.partner.country_id,
|
||||
)
|
||||
)
|
||||
self.assertEqual(transaction_fees, 18.78)
|
||||
total_fee = self.currency.round(self.amount + transaction_fees)
|
||||
self.assertEqual(total_fee, 1129.89)
|
||||
|
||||
tx = self._create_transaction(flow='redirect')
|
||||
self.assertEqual(tx.fees, 18.78)
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = tx._get_processing_values()
|
||||
redirect_form_data = self._extract_values_from_html_form(processing_values['redirect_form_html'])
|
||||
|
||||
self.assertEqual(redirect_form_data['inputs']['total_fee'], f'{total_fee:.2f}')
|
||||
|
||||
def test_21_standard_checkout_feedback(self):
|
||||
self.alipay.alipay_payment_method = 'standard_checkout'
|
||||
self.currency = self.currency_euro
|
||||
|
||||
@@ -24,7 +24,6 @@ class PaymentProvider(models.Model):
|
||||
""" Override of `payment` to enable additional features. """
|
||||
super()._compute_feature_support_fields()
|
||||
self.filtered(lambda p: p.code == 'demo').update({
|
||||
'support_fees': True,
|
||||
'support_manual_capture': 'partial',
|
||||
'support_refund': 'partial',
|
||||
'support_tokenization': True,
|
||||
|
||||
@@ -24,15 +24,6 @@ class PaymentProvider(models.Model):
|
||||
)
|
||||
paypal_pdt_token = fields.Char(string="PDT Identity Token", groups='base.group_system')
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
def _compute_feature_support_fields(self):
|
||||
""" Override of `payment` to enable additional features. """
|
||||
super()._compute_feature_support_fields()
|
||||
self.filtered(lambda p: p.code == 'paypal').update({
|
||||
'support_fees': True,
|
||||
})
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
def _get_supported_currencies(self):
|
||||
|
||||
@@ -51,7 +51,6 @@ class PaymentTransaction(models.Model):
|
||||
'currency_code': self.currency_id.name,
|
||||
'email': self.partner_email,
|
||||
'first_name': partner_first_name,
|
||||
'handling': self.fees,
|
||||
'item_name': f"{self.company_id.name}: {self.reference}",
|
||||
'item_number': self.reference,
|
||||
'last_name': partner_last_name,
|
||||
|
||||
@@ -11,7 +11,6 @@ class PaypalCommon(PaymentCommon):
|
||||
|
||||
cls.paypal = cls._prepare_provider('paypal', update_values={
|
||||
'paypal_email_account': 'dummy@test.mail.com',
|
||||
'fees_active': False,
|
||||
})
|
||||
|
||||
# Override default values
|
||||
|
||||
@@ -23,7 +23,7 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon):
|
||||
'tx_ref': self.reference,
|
||||
'access_token': self._generate_test_access_token(self.reference),
|
||||
}
|
||||
values = {
|
||||
return {
|
||||
'address1': 'Huge Street 2/543',
|
||||
'amount': str(self.amount),
|
||||
'business': self.paypal.paypal_email_account,
|
||||
@@ -44,14 +44,6 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon):
|
||||
'zip': '1000',
|
||||
}
|
||||
|
||||
if self.paypal.fees_active:
|
||||
fees = self.currency.round(self.paypal._compute_fees(self.amount, self.currency, self.partner.country_id))
|
||||
if fees:
|
||||
# handling input is only specified if truthy
|
||||
values['handling'] = float_repr(fees, self.currency.decimal_places)
|
||||
|
||||
return values
|
||||
|
||||
@mute_logger('odoo.addons.payment.models.payment_transaction')
|
||||
def test_redirect_form_values(self):
|
||||
tx = self._create_transaction(flow='redirect')
|
||||
@@ -72,29 +64,6 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon):
|
||||
"Paypal: invalid inputs specified in the redirect form.",
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.payment.models.payment_transaction')
|
||||
def test_redirect_form_with_fees(self):
|
||||
self.paypal.write({
|
||||
'fees_active': True,
|
||||
'fees_dom_fixed': 1.0,
|
||||
'fees_dom_var': 0.35,
|
||||
'fees_int_fixed': 1.5,
|
||||
'fees_int_var': 0.50,
|
||||
})
|
||||
expected_values = self._get_expected_values()
|
||||
|
||||
tx = self._create_transaction(flow='redirect')
|
||||
with patch(
|
||||
'odoo.addons.payment.utils.generate_access_token', new=self._generate_test_access_token
|
||||
):
|
||||
processing_values = tx._get_processing_values()
|
||||
form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
|
||||
|
||||
self.assertEqual(form_info['action'], 'https://www.sandbox.paypal.com/cgi-bin/webscr')
|
||||
self.assertDictEqual(
|
||||
expected_values, form_info['inputs'],
|
||||
"Paypal: invalid inputs specified in the redirect form.")
|
||||
|
||||
def test_feedback_processing(self):
|
||||
# Unknown transaction
|
||||
with self.assertRaises(ValidationError):
|
||||
@@ -119,18 +88,6 @@ class PaypalTest(PaypalCommon, PaymentHttpCommon):
|
||||
self.assertEqual(tx.state, 'pending')
|
||||
self.assertEqual(tx.state_message, payload['pending_reason'])
|
||||
|
||||
def test_fees_computation(self):
|
||||
# If the merchant needs to keep 100€, the transaction will be equal to 103.30€.
|
||||
# In this way, Paypal will take 103.30 * 2.9% + 0.30 = 3.30€
|
||||
# And the merchant will take 103.30 - 3.30 = 100€
|
||||
self.paypal.write({
|
||||
'fees_active': True,
|
||||
'fees_int_fixed': 0.30,
|
||||
'fees_int_var': 0.029,
|
||||
})
|
||||
total_fee = self.paypal._compute_fees(100, self.paypal.main_currency_id, False)
|
||||
self.assertEqual(round(total_fee, 2), 3.3, 'Wrong computation of the Paypal fees')
|
||||
|
||||
def test_parsing_pdt_validation_response_returns_notification_data(self):
|
||||
""" Test that the notification data are parsed from the content of a validation response."""
|
||||
response_content = 'SUCCESS\nkey1=val1\nkey2=val+2\n'
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
<input type="hidden" name="currency_code" t-att-value="currency_code"/>
|
||||
<input type="hidden" name="email" t-att-value="email"/>
|
||||
<input type="hidden" name="first_name" t-att-value="first_name"/>
|
||||
<input t-if="handling"
|
||||
type="hidden" name="handling" t-att-value="handling"/>
|
||||
<input type="hidden" name="item_name" t-att-value="item_name"/>
|
||||
<input type="hidden" name="item_number" t-att-value="item_number"/>
|
||||
<input type="hidden" name="last_name" t-att-value="last_name"/>
|
||||
|
||||
@@ -128,13 +128,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
|
||||
|
||||
providers_sudo = self._get_allowed_providers_sudo(pos_order_sudo, partner_sudo.id, amount_to_pay)
|
||||
|
||||
# Compute the fees taken by providers supporting the feature
|
||||
fees_by_provider = {
|
||||
provider_sudo: provider_sudo._compute_fees(
|
||||
amount_to_pay, currency_id, partner_sudo.country_id)
|
||||
for provider_sudo in providers_sudo.filtered('fees_active')
|
||||
}
|
||||
|
||||
if logged_in:
|
||||
tokens_sudo = request.env['payment.token']._get_available_tokens(
|
||||
providers_sudo.ids, partner_sudo.id)
|
||||
@@ -147,7 +140,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
|
||||
rendering_context.update({
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens_sudo,
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'show_tokenize_input': show_tokenize_input,
|
||||
**self._get_custom_rendering_context_values(**kwargs),
|
||||
})
|
||||
|
||||
@@ -189,17 +189,11 @@ class CustomerPortal(portal.CustomerPortal):
|
||||
sale_order_id=order_sudo.id,
|
||||
**kwargs,
|
||||
) # In sudo mode to read the fields of providers and partner (if not logged in).
|
||||
fees_by_provider = {
|
||||
provider: provider._compute_fees(
|
||||
amount, currency, partner.country_id
|
||||
) for provider in providers_sudo.filtered('fees_active')
|
||||
}
|
||||
payment_form_values = {
|
||||
'providers': providers_sudo,
|
||||
'tokens': request.env['payment.token']._get_available_tokens(
|
||||
providers_sudo.ids, partner.id, **kwargs
|
||||
),
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping(
|
||||
providers_sudo, sale_order_id=order_sudo.id
|
||||
),
|
||||
|
||||
@@ -33,19 +33,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
|
||||
|
||||
return self.payment_pay(**kwargs)
|
||||
|
||||
@http.route('/donation/get_provider_fees', type='json', auth='public', website=True, sitemap=False)
|
||||
def get_provider_fees(self, provider_ids=None, amount=None, currency_id=None, country_id=None):
|
||||
providers_sudo = request.env['payment.provider'].sudo().browse(provider_ids)
|
||||
currency = request.env['res.currency'].browse(currency_id)
|
||||
country = request.env['res.country'].browse(country_id)
|
||||
|
||||
# Compute the fees taken by providers supporting the feature
|
||||
fees_by_provider = {
|
||||
pro_sudo.id: pro_sudo._compute_fees(amount, currency, country)
|
||||
for pro_sudo in providers_sudo.filtered('fees_active')
|
||||
}
|
||||
return fees_by_provider
|
||||
|
||||
@http.route('/donation/transaction/<minimum_amount>', type='json', auth='public', website=True, sitemap=False)
|
||||
def donation_transaction(self, amount, currency_id, partner_id, access_token, minimum_amount=0, **kwargs):
|
||||
if float(amount) < float(minimum_amount):
|
||||
|
||||
@@ -4,9 +4,6 @@ import core, { _t } from "@web/legacy/js/services/core";
|
||||
import checkoutForm from '@payment/js/checkout_form';
|
||||
|
||||
checkoutForm.include({
|
||||
events: Object.assign({}, checkoutForm.prototype.events || {}, {
|
||||
'change .o_wpayment_fee_impact': '_onFeeParameterChange',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -102,60 +99,5 @@ checkoutForm.include({
|
||||
_updateShippingCost: function (amount) {
|
||||
this.txContext.amount = amount;
|
||||
},
|
||||
/**
|
||||
* Update the fees associated to each provider.
|
||||
*
|
||||
* Called upon change of any parameter that might impact the fees (marked with
|
||||
* .o_wpayment_fee_impact).
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onFeeParameterChange: function (ev) {
|
||||
const targetId = ev.target.id;
|
||||
if (targetId.indexOf("amount") >= 0) {
|
||||
this.txContext.amount = ev.target.value;
|
||||
}
|
||||
const providerIds = [];
|
||||
for (const card of this.$('.o_payment_option_card:has(.o_payment_fee)')) {
|
||||
const radio = $(card).find('input[name="o_payment_radio"]');
|
||||
if (radio.data("paymentOptionType") === 'provider') {
|
||||
providerIds.push(radio.data("paymentOptionId"));
|
||||
}
|
||||
}
|
||||
const countryId = this.$('select[name="country_id"]').val();
|
||||
if (providerIds && this.txContext.amount) {
|
||||
this._rpc({
|
||||
route: '/donation/get_provider_fees',
|
||||
params: {
|
||||
'provider_ids': providerIds,
|
||||
'amount': this.txContext.amount !== undefined
|
||||
? parseFloat(this.txContext.amount) : null,
|
||||
'currency_id': this.txContext.currencyId
|
||||
? parseInt(this.txContext.currencyId) : null,
|
||||
'country_id': countryId,
|
||||
},
|
||||
}).then(feesPerProvider => {
|
||||
for (const card of this.$('.o_payment_option_card:has(.o_payment_fee)')) {
|
||||
const radio = $(card).find('input[name="o_payment_radio"]');
|
||||
let providerId;
|
||||
if (radio.data("paymentOptionType") === 'provider') {
|
||||
providerId = radio.data("paymentOptionId");
|
||||
} else { // token
|
||||
providerId = radio.data("paymentProviderId");
|
||||
}
|
||||
const chunk = $(card).find('.o_payment_fee .oe_currency_value')[0];
|
||||
chunk.innerText = (feesPerProvider[providerId] || 0).toFixed(2);
|
||||
}
|
||||
}).guardedCatch(error => {
|
||||
error.event.preventDefault();
|
||||
this._displayError(
|
||||
_t("Server Error"),
|
||||
_t("We could not obtain payment fees."),
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<label class="col-form-label fw-bold" for="country_id">Country
|
||||
<span class="s_website_form_mark"> *</span>
|
||||
</label>
|
||||
<select t-att-disabled="'1' if country_id and partner_id else None" id="country_id" name="country_id" t-attf-class="o_wpayment_fee_impact form-select #{error.get('country_id') and 'is-invalid' or ''}">
|
||||
<select t-att-disabled="'1' if country_id and partner_id else None" id="country_id" name="country_id" t-attf-class="form-select #{error.get('country_id') and 'is-invalid' or ''}">
|
||||
<option value="">Country...</option>
|
||||
<t t-foreach="countries" t-as="c">
|
||||
<option t-att-value="c.id" t-att-selected="c.id == (country_id or -1)">
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="form-check my-2">
|
||||
<t t-set="is_checked" t-value="float(amount) == float(donation_amount)"/>
|
||||
<t t-set="has_checked" t-value="has_checked or is_checked"/>
|
||||
<input class="o_wpayment_fee_impact form-check-input" type="radio" name="amount"
|
||||
<input class="form-check-input" type="radio" name="amount"
|
||||
t-attf-id="amount_#{donation_amount_index}" t-att-value="donation_amount"
|
||||
t-att-checked="is_checked or None"/>
|
||||
<label class="form-check-label mt-0" t-attf-for="amount_#{donation_amount_index}">
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</t>
|
||||
<div t-attf-class="form-check my-2 #{not donation_layout and 'd-none' or ''}">
|
||||
<input class="o_wpayment_fee_impact form-check-input" type="radio" id="other_amount" name="amount"
|
||||
<input class="form-check-input" type="radio" id="other_amount" name="amount"
|
||||
t-att-value="amount" t-att-checked="not has_checked or None"/>
|
||||
<label class="form-check-label mt-0 d-block" for="other_amount">
|
||||
<t t-call="website_payment.donation_input">
|
||||
@@ -86,13 +86,6 @@
|
||||
<h3 class="o_page_header mt16 mb4">Payment Details</h3>
|
||||
</t>
|
||||
</xpath>
|
||||
<!-- Make fee badges recognizable so that they can be updated upon amount changes -->
|
||||
<xpath expr="//t[@t-if='fees_by_provider.get(provider)']/span" position="attributes">
|
||||
<attribute name="class" add="o_payment_fee" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//span[@t-if='fees_by_provider.get(provider)']" position="attributes">
|
||||
<attribute name="class" add="o_payment_fee" separator=" "/>
|
||||
</xpath>
|
||||
<!-- Allow to find the provider id from the radio of tokens -->
|
||||
<xpath expr="//t[@t-foreach='tokens']//input[@name='o_payment_radio']" position="attributes">
|
||||
<attribute name="t-att-data-payment-provider-id">provider.id</attribute>
|
||||
@@ -139,7 +132,7 @@
|
||||
</template>
|
||||
|
||||
<template id="website_payment.donation_input" name="Donation input">
|
||||
<input type="number" class="o_wpayment_fee_impact form-control o_amount_input" t-att-min="donation_options.get('minimumAmount')"
|
||||
<input type="number" class="form-control o_amount_input" t-att-min="donation_options.get('minimumAmount')"
|
||||
t-att-max="donation_options.get('maximumAmount')" t-att-value="amount" placeholder="Custom Amount" id="other_amount_value"/>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user