diff --git a/addons/payment/models/payment_provider.py b/addons/payment/models/payment_provider.py index 4cd2d4d6fdd..e67efd2195f 100644 --- a/addons/payment/models/payment_provider.py +++ b/addons/payment/models/payment_provider.py @@ -92,7 +92,7 @@ class PaymentProvider(models.Model): ondelete='restrict', ) - # Availability fields. + # Availability fields available_country_ids = fields.Many2many( string="Countries", comodel_name='res.country', @@ -170,7 +170,7 @@ class PaymentProvider(models.Model): # Module-related fields module_id = fields.Many2one(string="Corresponding Module", comodel_name='ir.module.module') module_state = fields.Selection( - string="Installation State", related='module_id.state', store=True) # Stored for sorting + string="Installation State", related='module_id.state', store=True) # Stored for sorting. module_to_buy = fields.Boolean(string="Odoo Enterprise Module", related='module_id.to_buy') # View configuration fields @@ -204,11 +204,25 @@ class PaymentProvider(models.Model): @api.depends('code') def _compute_view_configuration_fields(self): - """ Compute view configuration fields based on the provider. + """ Compute the view configuration fields based on the provider. - By default, all fields are set to `True`. - For a provider to hide generic elements (pages, fields) in a view, it must override this - method and set their corresponding view configuration field to `False`. + View configuration fields are used to hide specific elements (notebook pages, fields, etc.) + from the form view of payment providers. These fields are set to `True` by default and are + as follows: + + - `show_credentials_page`: Whether the "Credentials" notebook page should be shown. + - `show_allow_tokenization`: Whether the `allow_tokenization` field should be shown. + - `show_allow_express_checkout`: Whether the `allow_express_checkout` field should be shown. + - `show_payment_icon_ids`: Whether the `payment_icon_ids` field should be shown. + - `show_pre_msg`: Whether the `pre_msg` field should be shown. + - `show_pending_msg`: Whether the `pending_msg` field should be shown. + - `show_auth_msg`: Whether the `auth_msg` field should be shown. + - `show_done_msg`: Whether the `done_msg` field should be shown. + - `show_cancel_msg`: Whether the `cancel_msg` field should be shown. + + For a provider to hide specific elements of the form view, it must override this method and + set the related view configuration fields to `False` on the appropriate `payment.provider` + records. :return: None """ @@ -225,9 +239,24 @@ class PaymentProvider(models.Model): }) def _compute_feature_support_fields(self): - """ Compute the feature support fields. + """ Compute the feature support fields based on the provider. - For an provider to support one or more additional feature, it must override this method. + Feature support fields are used to specify which additional features are supported by a + given provider. These fields are as follows: + + - `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`, + `'full_only'`, or `'partial'`. `None` by default. + - `support_tokenization`: Whether the "tokenization feature" is supported. `False` by + default. + + For a provider to specify that it supports additional features, it must override this method + and set the related feature support fields to the desired value on the appropriate + `payment.provider` records. :return: None """ @@ -251,16 +280,14 @@ class PaymentProvider(models.Model): @api.onchange('state') def _onchange_state_warn_before_disabling_tokens(self): - """ Display a warning about the consequences of disabling an provider. + """ Display a warning about the consequences of disabling a provider. - Let the user know that tokens related to an provider get archived if it is disabled or if - its state is changed from 'test' to 'enabled' and vice versa. + Let the user know that tokens related to a provider get archived if it is disabled or if its + state is changed from 'test' to 'enabled', and vice versa. - :return: The warning message in a client action. + :return: A client action with the warning message, if any. :rtype: dict """ - self.ensure_one() - if self._origin.state in ('test', 'enabled') and self._origin.state != self.state: related_tokens = self.env['payment.token'].search( [('provider_id', '=', self._origin.id)] @@ -282,8 +309,8 @@ class PaymentProvider(models.Model): def _check_fee_var_within_boundaries(self): """ Check that variable fees are within realistic boundaries. - Variable fees values should always be positive and below 100% to respectively avoid negative - and infinite (division by zero) fees amount. + Variable fee values should always be positive and below 100% to respectively avoid negative + and infinite (division by zero) fee amounts. :return None """ @@ -315,9 +342,10 @@ class PaymentProvider(models.Model): def _check_required_if_provider(self): """ Check that provider-specific required fields have been filled. - The fields that have the `required_if_provider=''` attribute are made required - for all `payment.provider` records with the `code` field equal to and with the - `state` field equal to 'enabled' or 'test'. + The fields that have the `required_if_provider=''` attribute are made + required for all `payment.provider` records with the `code` field equal to `` + and with the `state` field equal to `'enabled'` or `'test'`. + Provider-specific views should make the form fields required under the same conditions. :return: None @@ -339,7 +367,7 @@ class PaymentProvider(models.Model): ) def _handle_state_change(self): - """ Archive all the payment tokens linked to these providers. + """ Archive all the payment tokens linked to the providers. :return: None """ @@ -348,11 +376,11 @@ class PaymentProvider(models.Model): #=== ACTION METHODS ===# def button_immediate_install(self): - """ Install the provider's module and reload the page. + """ Install the module and reload the page. - Note: self.ensure_one() + Note: `self.ensure_one()` - :return: The action to reload the page + :return: The action to reload the page. :rtype: dict """ if self.module_id and self.module_state != 'installed': @@ -363,9 +391,9 @@ class PaymentProvider(models.Model): } def action_toggle_is_published(self): - """ Toggle the provider's is_published state. + """ Toggle the field `is_published`. - :return: none + :return: None :raise UserError: If the provider is disabled. """ if self.state != 'disabled': @@ -382,30 +410,31 @@ class PaymentProvider(models.Model): ): """ Select and return the providers matching the criteria. - The base criteria are that providers must not be disabled, be in the company that is - provided, and support the country of the partner if it exists. + The criteria are that providers must not be disabled, be in the company that is provided, + and support the country of the partner if it exists. The criteria can be further refined + by providing the keyword arguments. - :param int company_id: The company to which providers must belong, as a `res.company` id - :param int partner_id: The partner making the payment, as a `res.partner` id - :param float amount: The amount to pay, `0` for validation transactions. - :param int currency_id: The payment currency if known beforehand, as a `res.currency` id - :param bool force_tokenization: Whether only providers allowing tokenization can be matched + :param int company_id: The company to which providers must belong, as a `res.company` id. + :param int partner_id: The partner making the payment, as a `res.partner` id. + :param float amount: The amount to pay. `0` for validation transactions. + :param int currency_id: The payment currency, if known beforehand, as a `res.currency` id. + :param bool force_tokenization: Whether only providers allowing tokenization can be matched. :param bool is_express_checkout: Whether the payment is made through express checkout. - :param bool is_validation: Whether the operation is a validation - :param dict kwargs: Optional data. This parameter is not used here - :return: The compatible providers + :param bool is_validation: Whether the operation is a validation. + :param dict kwargs: Optional data. This parameter is not used here. + :return: The compatible providers. :rtype: recordset of `payment.provider` """ - # Compute the base domain for compatible providers + # Compute the base domain for compatible providers. 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 + # 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 + if partner.country_id: # The partner country must either not be set or be supported. domain = expression.AND([ domain, [ '|', @@ -429,7 +458,7 @@ class PaymentProvider(models.Model): ] ]) - # Handle tokenization support requirements + # Handle tokenization support requirements. if force_tokenization or self._is_tokenization_required(**kwargs): domain = expression.AND([domain, [('allow_tokenization', '=', True)]]) @@ -446,21 +475,21 @@ class PaymentProvider(models.Model): For a module to make the tokenization required based on the transaction context, it must override this method and return whether it is required. - :param dict kwargs: The transaction context. This parameter is not used here - :return: Whether tokenizing the transaction is required + :param dict kwargs: The transaction context. This parameter is not used here. + :return: Whether tokenizing the transaction is required. :rtype: bool """ return False def _should_build_inline_form(self, is_validation=False): - """ Return whether the inline form should be instantiated if it exists. + """ Return whether the inline payment form should be instantiated. - For a provider to handle both direct payments and payment with redirection, it should - override this method and return whether the inline form should be instantiated (i.e. if the - payment should be direct) based on the operation (online payment or validation). + For a provider to handle both direct payments and payments with redirection, it must + override this method and return whether the inline payment form should be instantiated (i.e. + if the payment should be direct) based on the operation (online payment or validation). - :param bool is_validation: Whether the operation is a validation - :return: Whether the inline form should be instantiated + :param bool is_validation: Whether the operation is a validation. + :return: Whether the inline form should be instantiated. :rtype: bool """ return True @@ -468,20 +497,19 @@ class PaymentProvider(models.Model): def _compute_fees(self, amount, currency, country): """ Compute the transaction fees. - The computation is based on the generic fields `fees_dom_fixed`, `fees_dom_var`, - `fees_int_fixed` and `fees_int_var` and is done according to the following formula: - - `fees = (amount * variable / 100.0 + fixed) / (1 - variable / 100.0)` where the value - of `fixed` and `variable` is taken either from the domestic (dom) or international (int) - field depending on whether the country matches the company's country. + 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 / 100.0 + fixed) / (1 - variable / 100.0)` 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 as a float. + 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 + :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() @@ -498,30 +526,29 @@ class PaymentProvider(models.Model): return fees def _get_validation_amount(self): - """ Get the amount to transfer in a payment method validation operation. + """ Return the amount to use for validation operations. - For a provider to support tokenization, it must override this method and return the amount - to be transferred in a payment method validation operation *if the validation amount is not - null*. + For a provider to support tokenization, it must override this method and return the + validation amount. If it is `0`, it is not necessary to create the override. - Note: self.ensure_one() + Note: `self.ensure_one()` - :return: The validation amount + :return: The validation amount. :rtype: float """ self.ensure_one() return 0.0 def _get_validation_currency(self): - """ Get the currency of the transfer in a payment method validation operation. + """ Return the currency to use for validation operations. For a provider to support tokenization, it must override this method and return the - currency to be used in a payment method validation operation *if the validation amount is - not null*. + validation currency. If the validation amount is `0`, it is not necessary to create the + override. - Note: self.ensure_one() + Note: `self.ensure_one()` - :return: The validation currency + :return: The validation currency. :rtype: recordset of `res.currency` """ self.ensure_one() @@ -533,10 +560,10 @@ class PaymentProvider(models.Model): For a provider to return a different view depending on whether the operation is a validation, it must override this method and return the appropriate view. - Note: self.ensure_one() + Note: `self.ensure_one()` - :param bool is_validation: Whether the operation is a validation - :return: The redirect form template + :param bool is_validation: Whether the operation is a validation. + :return: The view of the redirect form template. :rtype: record of `ir.ui.view` """ self.ensure_one() @@ -544,20 +571,21 @@ class PaymentProvider(models.Model): @api.model def _setup_provider(self, provider_code): - """ Prepare module-specific data for a given provider. + """ Perform module-specific setup steps for the provider. - This method is called after a new provider module is installed and also for all existing - providers when `account_payment` is installed. + This method is called after the module of a provider is installed, with its code passed as + `provider_code`. :param str provider_code: The code of the provider to setup. :return: None """ + return @api.model def _remove_provider(self, provider_code): - """ Clean module-specific data for a given provider. + """ Remove the module-specific data of the given provider. - :param str provider_code: The code of the provider to setup. + :param str provider_code: The code of the provider whose data to remove. :return: None """ providers = self.search([('code', '=', provider_code)]) @@ -576,10 +604,10 @@ class PaymentProvider(models.Model): """) def _neutralize_fields(self, provider_code, field_names): - """ Helper to neutralize API keys for a specific provider. + """ Helper to neutralize API keys for the given provider. - :param str provider_code: The code of the provider. - :param list field_names: The names of the fields to nullify. + :param str provider_code: The code of the provider whose fields to neutralize. + :param list field_names: The names of the fields to neutralize. :return: None """ self.flush_model() @@ -592,4 +620,4 @@ class PaymentProvider(models.Model): fields=sql.SQL(','.join(field_names)), vals=sql.SQL(', '.join(['NULL'] * len(field_names))), ) - self.env.cr.execute(query, (provider_code, )) + self.env.cr.execute(query, (provider_code,)) diff --git a/addons/payment/models/payment_token.py b/addons/payment/models/payment_token.py index 6578c314935..57ae04c09f8 100644 --- a/addons/payment/models/payment_token.py +++ b/addons/payment/models/payment_token.py @@ -22,7 +22,7 @@ class PaymentToken(models.Model): related='provider_id.company_id', store=True, index=True) provider_ref = fields.Char( string="Provider Reference", help="The provider reference of the token of the transaction", - required=True) # This is not the same thing as the provider reference of the transaction + required=True) # This is not the same thing as the provider reference of the transaction. transaction_ids = fields.One2many( string="Payment Transactions", comodel_name='payment.transaction', inverse_name='token_id') verified = fields.Boolean(string="Verified") @@ -39,7 +39,7 @@ class PaymentToken(models.Model): # Include provider-specific create values values.update(self._get_specific_create_values(provider.code, values)) else: - pass # Let psycopg warn about the missing required field + pass # Let psycopg warn about the missing required field. return super().create(values_list) @@ -51,9 +51,9 @@ class PaymentToken(models.Model): dict of values. Provider-specific values take precedence over those of the dict of generic create values. - :param str provider_code: The code of the provider managing the token - :param dict values: The original create values - :return: The dict of provider-specific create values + :param str provider_code: The code of the provider managing the token. + :param dict values: The original create values. + :return: The dict of provider-specific create values. :rtype: dict """ return dict() @@ -76,14 +76,14 @@ class PaymentToken(models.Model): return super().write(values) def _handle_archiving(self): - """ Handle the archiving of the current tokens. + """ Handle the archiving of tokens. For a module to perform additional operations when a token is archived, it must override this method. :return: None """ - return None + return def name_get(self): return [(token.id, token._build_display_name()) for token in self] @@ -91,20 +91,22 @@ class PaymentToken(models.Model): #=== BUSINESS METHODS ===# def _build_display_name(self, *args, max_length=34, should_pad=True, **kwargs): - """ Build a token name of the desired maximum length with the format •••• 1234. + """ Build a token name of the desired maximum length with the format `•••• 1234`. The payment details are padded on the left with up to four padding characters. The padding is only added if there is enough room for it. If not, it is either reduced or not added at all. If there is not enough room for the payment details either, they are trimmed from the left. - For a module to customize the display name of a token, it must override this method. + For a module to customize the display name of a token, it must override this method and + return the customized display name. - Note: self.ensure_one() + Note: `self.ensure_one()` :param list args: The arguments passed by QWeb when calling this method. - :param int max_length: The desired maximum length of the token name. - :param bool should_pad: Whether the token should be padded or not. + :param int max_length: The desired maximum length of the token name. The default is `34` to + fit the largest IBANs. + :param bool should_pad: Whether the token should be padded. :param dict kwargs: Optional data used in overrides of this method. :return: The padded token name. :rtype: str @@ -128,17 +130,18 @@ class PaymentToken(models.Model): """ Return a list of information about records linked to the current token. For a module to implement payments and link documents to a token, it must override this - method and add information about linked records to the returned list. + method and add information about linked document records to the returned list. The information must be structured as a dict with the following keys: - - description: The description of the record's model (e.g. "Subscription") - - id: The id of the record - - name: The name of the record - - url: The url to access the record. - Note: self.ensure_one() + - `description`: The description of the record's model (e.g. "Subscription"). + - `id`: The id of the record. + - `name`: The name of the record. + - `url`: The url to access the record. - :return: The list of information about linked documents + Note: `self.ensure_one()` + + :return: The list of information about the linked document records. :rtype: list """ self.ensure_one() diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index b2f61bdbf31..80d7ba7af29 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -36,10 +36,10 @@ class PaymentTransaction(models.Model): related='provider_id.company_id', store=True, index=True) reference = fields.Char( string="Reference", help="The internal reference of the transaction", readonly=True, - required=True) # Already has an index from the UNIQUE SQL constraint + required=True) # Already has an index from the UNIQUE SQL constraint. provider_reference = fields.Char( string="Provider Reference", help="The provider reference of the transaction", - readonly=True) # This is not the same thing as the provider reference of the token + readonly=True) # This is not the same thing as the provider reference of the token. amount = fields.Monetary( string="Amount", currency_field='currency_id', readonly=True, required=True) currency_id = fields.Many2one( @@ -61,8 +61,8 @@ class PaymentTransaction(models.Model): last_state_change = fields.Datetime( string="Last State Change Date", readonly=True, default=fields.Datetime.now) - # Fields used for traceability - operation = fields.Selection( # This should not be trusted if the state is 'draft' or 'pending' + # Fields used for traceability. + operation = fields.Selection( # This should not be trusted if the state is draft or pending. string="Operation", selection=[ ('online_redirect', "Online payment with redirection"), @@ -103,13 +103,13 @@ class PaymentTransaction(models.Model): string="Callback Document Model", comodel_name='ir.model', groups='base.group_system') callback_res_id = fields.Integer(string="Callback Record ID", groups='base.group_system') callback_method = fields.Char(string="Callback Method", groups='base.group_system') - # Hash for additional security on top of the callback fields' group in case a bug exposes a sudo + # Hash for extra security on top of the callback fields' group in case a bug exposes a sudo. callback_hash = fields.Char(string="Callback Hash", groups='base.group_system') callback_is_done = fields.Boolean( string="Callback Done", help="Whether the callback has already been executed", groups="base.group_system", readonly=True) - # Duplicated partner values allowing to keep a record of them, should they be later updated + # Duplicated partner values allowing to keep a record of them, should they be later updated. partner_id = fields.Many2one( string="Customer", comodel_name='res.partner', readonly=True, required=True, ondelete='restrict') @@ -143,7 +143,7 @@ class PaymentTransaction(models.Model): @api.constrains('state') def _check_state_authorized_supported(self): - """ Check that authorization is supported for a transaction in the 'authorized' state. """ + """ Check that authorization is supported for a transaction in the `authorized` state. """ illegal_authorize_state_txs = self.filtered( lambda tx: tx.state == 'authorized' and not tx.provider_id.support_manual_capture ) @@ -169,7 +169,7 @@ class PaymentTransaction(models.Model): if not values.get('reference'): values['reference'] = self._compute_reference(provider.code, **values) - # Duplicate partner values + # Duplicate partner values. partner = self.env['res.partner'].browse(values['partner_id']) values.update({ 'partner_name': partner.name, @@ -185,7 +185,7 @@ class PaymentTransaction(models.Model): 'partner_phone': partner.phone, }) - # Compute fees, for validation transactions fees are zero + # Compute fees. For validation transactions, fees are zero. if values.get('operation') == 'validation': values['fees'] = 0 else: @@ -197,7 +197,7 @@ class PaymentTransaction(models.Model): # Include provider-specific create values values.update(self._get_specific_create_values(provider.code, values)) - # Generate the hash for the callback if one has be configured on the tx + # Generate the hash for the callback if one has be configured on the tx. values['callback_hash'] = self._generate_callback_hash( values.get('callback_model_id'), values.get('callback_res_id'), @@ -222,13 +222,13 @@ class PaymentTransaction(models.Model): def _get_specific_create_values(self, provider_code, values): """ Complete the values of the `create` method with provider-specific values. - For a provider to add its own create values, it must overwrite this method and return a - dict of values. Provider-specific values take precedence over those of the dict of generic - create values. + For a provider to add its own create values, it must overwrite this method and return a dict + of values. Provider-specific values take precedence over those of the dict of generic create + values. - :param str provider_code: The code of the provider that handled the transaction - :param dict values: The original create values - :return: The dict of provider-specific create values + :param str provider_code: The code of the provider that handled the transaction. + :param dict values: The original create values. + :return: The dict of provider-specific create values. :rtype: dict """ return dict() @@ -236,11 +236,11 @@ class PaymentTransaction(models.Model): #=== ACTION METHODS ===# def action_view_refunds(self): - """ Return the action for the views of the refund transactions linked to the transaction. + """ Return the windows action to browse the refund transactions linked to the transaction. - Note: self.ensure_one() + Note: `self.ensure_one()` - :return: The action + :return: The window action to browse the refund transactions. :rtype: dict """ self.ensure_one() @@ -284,7 +284,7 @@ class PaymentTransaction(models.Model): def action_refund(self, amount_to_refund=None): """ Check the state of the transactions and request their refund. - :param float amount_to_refund: The amount to be refunded + :param float amount_to_refund: The amount to be refunded. :return: None """ if any(tx.state != 'done' for tx in self): @@ -299,46 +299,51 @@ class PaymentTransaction(models.Model): def _compute_reference(self, provider_code, prefix=None, separator='-', **kwargs): """ Compute a unique reference for the transaction. - The reference either corresponds to the prefix if no other transaction with that prefix - already exists, or follows the pattern `{computed_prefix}{separator}{sequence_number}` where - - {computed_prefix} is: - - The provided custom prefix, if any. - - The computation result of `_compute_reference_prefix` if the custom prefix is not - filled but the kwargs are. - - 'tx-{datetime}', if neither the custom prefix nor the kwargs are filled. - - {separator} is a custom string also used in `_compute_reference_prefix`. - - {sequence_number} is the next integer in the sequence of references sharing the exact - same prefix, '1' if there is only one matching reference (hence without sequence number) + The reference corresponds to the prefix if no other transaction with that prefix already + exists. Otherwise, it follows the pattern `{computed_prefix}{separator}{sequence_number}` + where: - Examples: - - Given the custom prefix 'example' which has no match with an existing reference, the - full reference will be 'example'. - - Given the custom prefix 'example' which matches the existing reference 'example', and - the custom separator '-', the full reference will be 'example-1'. - - Given the kwargs {'invoice_ids': [1, 2]}, the custom separator '-' and no custom prefix, - the full reference will be 'INV1-INV2' (or similar) if no existing reference has the - same prefix, or 'INV1-INV2-n' if n existing references have the same prefix. + - `{computed_prefix}` is: - :param str provider_code: The code of the provider handling the transaction - :param str prefix: The custom prefix used to compute the full reference - :param str separator: The custom separator used to separate the prefix from the suffix, and - passed to `_compute_reference_prefix` if it is called - :param dict kwargs: Optional values passed to `_compute_reference_prefix` if no custom - prefix is provided - :return: The unique reference for the transaction + - The provided custom prefix, if any. + - The computation result of :meth:`_compute_reference_prefix` if the custom prefix is not + filled, but the kwargs are. + - `'tx-{datetime}'` if neither the custom prefix nor the kwargs are filled. + + - `{separator}` is the string that separates the prefix from the sequence number. + - `{sequence_number}` is the next integer in the sequence of references sharing the same + prefix. The sequence starts with `1` if there is only one matching reference. + + .. example:: + + - Given the custom prefix `'example'` which has no match with an existing reference, the + full reference will be `'example'`. + - Given the custom prefix `'example'` which matches the existing reference `'example'`, + and the custom separator `'-'`, the full reference will be `'example-1'`. + - Given the kwargs `{'invoice_ids': [1, 2]}`, the custom separator `'-'` and no custom + prefix, the full reference will be `'INV1-INV2'` (or similar) if no existing reference + has the same prefix, or `'INV1-INV2-n'` if `n` existing references have the same + prefix. + + :param str provider_code: The code of the provider handling the transaction. + :param str prefix: The custom prefix used to compute the full reference. + :param str separator: The custom separator used to separate the prefix from the suffix. + :param dict kwargs: Optional values passed to :meth:`_compute_reference_prefix` if no custom + prefix is provided. + :return: The unique reference for the transaction. :rtype: str """ - # Compute the prefix + # Compute the prefix. if prefix: # Replace special characters by their ASCII alternative (é -> e ; ä -> a ; ...) prefix = unicodedata.normalize('NFKD', prefix).encode('ascii', 'ignore').decode('utf-8') - if not prefix: # Prefix not provided or voided above, compute it based on the kwargs + if not prefix: # Prefix not provided or voided above, compute it based on the kwargs. prefix = self.sudo()._compute_reference_prefix(provider_code, separator, **kwargs) if not prefix: # Prefix not computed from the kwargs, fallback on time-based value prefix = payment_utils.singularize_reference_prefix() - # Compute the sequence number - reference = prefix # The first reference of a sequence has no sequence number + # Compute the sequence number. + reference = prefix # The first reference of a sequence has no sequence number. if self.sudo().search([('reference', '=', prefix)]): # The reference already has a match # We now execute a second search on `payment.transaction` to fetch all the references # starting with the given prefix. The load of these two searches is mitigated by the @@ -358,16 +363,16 @@ class PaymentTransaction(models.Model): # 'example', 'example-1' and 'example-ref', in that order. Trusting the order to infer # the sequence number would lead to a collision with 'example-1'. search_pattern = re.compile(rf'^{re.escape(prefix)}{separator}(\d+)$') - max_sequence_number = 0 # If no match is found, start the sequence with this reference + max_sequence_number = 0 # If no match is found, start the sequence with this reference. for existing_reference in same_prefix_references: search_result = re.search(search_pattern, existing_reference) if search_result: # The reference has the same prefix and is from the same sequence - # Find the largest sequence number, if any + # Find the largest sequence number, if any. current_sequence = int(search_result.group(1)) if current_sequence > max_sequence_number: max_sequence_number = current_sequence - # Compute the full reference + # Compute the full reference. reference = f'{prefix}{separator}{max_sequence_number + 1}' return reference @@ -375,12 +380,14 @@ class PaymentTransaction(models.Model): def _compute_reference_prefix(self, provider_code, separator, **values): """ Compute the reference prefix from the transaction values. - Note: This method should be called in sudo mode to give access to documents (INV, SO, ...). + Note: This method should be called in sudo mode to give access to the documents (invoices, + sales orders) referenced in the transaction values. - :param str provider_code: The code of the provider handling the transaction - :param str separator: The custom separator used to separate data references + :param str provider_code: The code of the provider handling the transaction. + :param str separator: The custom separator used to separate parts of the computed + reference prefix. :param dict values: The transaction values used to compute the reference prefix. - :return: an empty string + :return: The computed reference prefix. :rtype: str """ return '' @@ -390,11 +397,11 @@ class PaymentTransaction(models.Model): """ Return the hash for the callback on the transaction. :param int callback_model_id: The model on which the callback method is defined, as a - `res.model` id + `res.model` id. :param int callback_res_id: The record on which the callback method must be called, as an id - of the callback model - :param str callback_method: The name of the callback method - :return: The callback hash + of the callback method's model. + :param str callback_method: The name of the callback method. + :return: The callback hash. :rtype: str """ if callback_model_id and callback_res_id and callback_method: @@ -405,21 +412,22 @@ class PaymentTransaction(models.Model): return None def _get_processing_values(self): - """ Return a dict of values used to process the transaction. + """ Return the values used to process the transaction. - The returned dict contains the following entries: - - tx_id: The transaction, as a `payment.transaction` id - - provider_id: The provider handling the transaction, as a `payment.provider` id - - provider_code: The code of the provider - - reference: The reference of the transaction - - amount: The rounded amount of the transaction - - currency_id: The currency of the transaction, as a res.currency id - - partner_id: The partner making the transaction, as a res.partner id - - Additional provider-specific entries + The values are returned as a dict containing entries with the following keys: - Note: self.ensure_one() + - `tx_id`: The transaction, as a `payment.transaction` id. + - `provider_id`: The provider handling the transaction, as a `payment.provider` id. + - `provider_code`: The code of the provider. + - `reference`: The reference of the transaction. + - `amount`: The rounded amount of the transaction. + - `currency_id`: The currency of the transaction, as a `res.currency` id. + - `partner_id`: The partner making the transaction, as a `res.partner` id. + - Additional provider-specific entries. - :return: The dict of processing values + Note: `self.ensure_one()` + + :return: The processing values. :rtype: dict """ self.ensure_one() @@ -434,7 +442,7 @@ class PaymentTransaction(models.Model): 'partner_id': self.partner_id.id, } - # Complete generic processing values with provider-specific values + # Complete generic processing values with provider-specific values. processing_values.update(self._get_specific_processing_values(processing_values)) _logger.info( "generic and provider-specific processing values for transaction with reference " @@ -442,12 +450,12 @@ class PaymentTransaction(models.Model): {'ref': self.reference, 'values': pprint.pformat(processing_values)}, ) - # Render the html form for the redirect flow if available + # Render the html form for the redirect flow if available. if self.operation in ('online_redirect', 'validation'): redirect_form_view = self.provider_id._get_redirect_form_view( is_validation=self.operation == 'validation' ) - if redirect_form_view: # Some provider don't need a redirect form + if redirect_form_view: # Some provider don't need a redirect form. rendering_values = self._get_specific_rendering_values(processing_values) _logger.info( "provider-specific rendering values for transaction with reference " @@ -467,8 +475,8 @@ class PaymentTransaction(models.Model): Provider-specific values take precedence over those of the dict of generic processing values. - :param dict processing_values: The generic processing values of the transaction - :return: The dict of provider-specific processing values + :param dict processing_values: The generic processing values of the transaction. + :return: The dict of provider-specific processing values. :rtype: dict """ return dict() @@ -480,19 +488,22 @@ class PaymentTransaction(models.Model): dict of provider-specific values based on the processing values (provider-specific processing values included). - :param dict processing_values: The processing values of the transaction - :return: The dict of provider-specific rendering values + :param dict processing_values: The processing values of the transaction. + :return: The dict of provider-specific rendering values. :rtype: dict """ return dict() def _send_payment_request(self): - """ Request the provider handling the transaction to execute the payment. + """ Request the provider handling the transaction to make the payment. - For a provider to support tokenization, it must override this method and call it to log the - 'sent' message, then request a money transfer to its provider. + This method is exclusively used to make payments by token, which correspond to both the + `online_token` and the `offline` transaction's `operation` field. - Note: self.ensure_one() + For a provider to support tokenization, it must override this method and make an API request + to make a payment. + + Note: `self.ensure_one()` :return: None """ @@ -503,12 +514,12 @@ class PaymentTransaction(models.Model): def _send_refund_request(self, amount_to_refund=None): """ Request the provider handling the transaction to refund it. - For a provider to support refunds, it must override this method and request a refund - to its provider. + For a provider to support refunds, it must override this method and make an API request to + make a refund. - Note: self.ensure_one() + Note: `self.ensure_one()` - :param float amount_to_refund: The amount to be refunded + :param float amount_to_refund: The amount to be refunded. :return: The refund transaction created to process the refund request. :rtype: recordset of `payment.transaction` """ @@ -520,11 +531,12 @@ class PaymentTransaction(models.Model): return refund_tx def _create_refund_transaction(self, amount_to_refund=None, **custom_create_values): - """ Create a new transaction with operation 'refund' and link it to the current transaction. + """ Create a new transaction with the operation `refund` and the current transaction as + source transaction. :param float amount_to_refund: The strictly positive amount to refund, in the same currency - as the source transaction - :return: The refund transaction + as the source transaction. + :return: The refund transaction. :rtype: recordset of `payment.transaction` """ self.ensure_one() @@ -542,12 +554,12 @@ class PaymentTransaction(models.Model): }) def _send_capture_request(self): - """ Request the provider handling the transaction to capture it. + """ Request the provider handling the transaction to capture the payment. - For a provider to support authorization, it must override this method and request a capture - to its provider. + For a provider to support authorization, it must override this method and make an API + request to capture the payment. - Note: self.ensure_one() + Note: `self.ensure_one()` :return: None """ @@ -555,12 +567,12 @@ class PaymentTransaction(models.Model): self._ensure_provider_is_not_disabled() def _send_void_request(self): - """ Request the provider handling the transaction to void it. + """ Request the provider handling the transaction to void the payment. - For a provider to support authorization, it must override this method and request the - transaction to be voided to its provider. + For a provider to support authorization, it must override this method and make an API + request to void the payment. - Note: self.ensure_one() + Note: `self.ensure_one()` :return: None """ @@ -568,11 +580,11 @@ class PaymentTransaction(models.Model): self._ensure_provider_is_not_disabled() def _ensure_provider_is_not_disabled(self): - """ Ensure that the provider's state is not 'disabled' before sending a request to its + """ Ensure that the provider's state is not `disabled` before sending a request to its provider. :return: None - :raise UserError: If the provider's state is 'disabled'. + :raise UserError: If the provider's state is `disabled`. """ if self.provider_id.state == 'disabled': raise UserError(_( @@ -582,9 +594,9 @@ class PaymentTransaction(models.Model): def _handle_notification_data(self, provider_code, notification_data): """ Match the transaction with the notification data, update its state and return it. - :param str provider_code: The code of the provider that handled the transaction - :param dict notification_data: The notification data sent by the provider - :return: The transaction + :param str provider_code: The code of the provider handling the transaction. + :param dict notification_data: The notification data sent by the provider. + :return: The transaction. :rtype: recordset of `payment.transaction` """ tx = self._get_tx_from_notification_data(provider_code, notification_data) @@ -598,9 +610,9 @@ class PaymentTransaction(models.Model): For a provider to handle transaction processing, it must overwrite this method and return the transaction matching the notification data. - :param str provider_code: The code of the provider that handled the transaction - :param dict notification_data: The notification data sent by the provider - :return: The transaction if found + :param str provider_code: The code of the provider handling the transaction. + :param dict notification_data: The notification data sent by the provider. + :return: The transaction, if found. :rtype: recordset of `payment.transaction` """ return self @@ -608,25 +620,25 @@ class PaymentTransaction(models.Model): def _process_notification_data(self, notification_data): """ Update the transaction state and the provider reference based on the notification data. - This method should normally never be called directly. The correct method to call upon - receiving notification data is `_handle_notification_data`. + This method should usually not be called directly. The correct method to call upon receiving + notification data is :meth:`_handle_notification_data`. For a provider to handle transaction processing, it must overwrite this method and process the notification data. - Note: self.ensure_one() + Note: `self.ensure_one()` - :param dict notification_data: The notification data sent by the provider + :param dict notification_data: The notification data sent by the provider. :return: None """ self.ensure_one() def _set_pending(self, state_message=None): - """ Update the transactions' state to 'pending'. + """ Update the transactions' state to `pending`. - :param str state_message: The reason for which the transaction is set in 'pending' state - :return: updated transactions - :rtype: `payment.transaction` recordset + :param str state_message: The reason for setting the transactions in the state `pending`. + :return: The updated transactions. + :rtype: recordset of `payment.transaction` """ allowed_states = ('draft',) target_state = 'pending' @@ -635,11 +647,11 @@ class PaymentTransaction(models.Model): return txs_to_process def _set_authorized(self, state_message=None): - """ Update the transactions' state to 'authorized'. + """ Update the transactions' state to `authorized`. - :param str state_message: The reason for which the transaction is set in 'authorized' state - :return: updated transactions - :rtype: `payment.transaction` recordset + :param str state_message: The reason for setting the transactions in the state `authorized`. + :return: The updated transactions. + :rtype: recordset of `payment.transaction` """ allowed_states = ('draft', 'pending') target_state = 'authorized' @@ -648,11 +660,11 @@ class PaymentTransaction(models.Model): return txs_to_process def _set_done(self, state_message=None): - """ Update the transactions' state to 'done'. + """ Update the transactions' state to `done`. - :param str state_message: The reason for which the transaction is set in 'done' state - :return: updated transactions - :rtype: `payment.transaction` recordset + :param str state_message: The reason for setting the transactions in the state `done`. + :return: The updated transactions. + :rtype: recordset of `payment.transaction` """ allowed_states = ('draft', 'pending', 'authorized', 'error') target_state = 'done' @@ -661,25 +673,25 @@ class PaymentTransaction(models.Model): return txs_to_process def _set_canceled(self, state_message=None): - """ Update the transactions' state to 'cancel'. + """ Update the transactions' state to `cancel`. - :param str state_message: The reason for which the transaction is set in 'cancel' state - :return: updated transactions - :rtype: `payment.transaction` recordset + :param str state_message: The reason for setting the transactions in the state `cancel`. + :return: The updated transactions. + :rtype: recordset of `payment.transaction` """ allowed_states = ('draft', 'pending', 'authorized', 'done') # 'done' for Authorize refunds. target_state = 'cancel' txs_to_process = self._update_state(allowed_states, target_state, state_message) - # Cancel the existing payments + # Cancel the existing payments. txs_to_process._log_received_message() return txs_to_process def _set_error(self, state_message): - """ Update the transactions' state to 'error'. + """ Update the transactions' state to `error`. - :param str state_message: The reason for which the transaction is set in 'error' state - :return: updated transactions - :rtype: `payment.transaction` recordset + :param str state_message: The reason for setting the transactions in the state `error`. + :return: The updated transactions. + :rtype: recordset of `payment.transaction` """ allowed_states = ('draft', 'pending', 'authorized', 'done') # 'done' for Stripe refunds. target_state = 'error' @@ -690,36 +702,37 @@ class PaymentTransaction(models.Model): def _update_state(self, allowed_states, target_state, state_message): """ Update the transactions' state to the target state if the current state allows it. - If the current state is the same as the target state, the transaction is skipped. + If the current state is the same as the target state, the transaction is skipped and a log + with level INFO is created. - :param tuple[str] allowed_states: The allowed source states for the target state - :param str target_state: The target state - :param str state_message: The message to set as `state_message` - :return: The recordset of transactions whose state was correctly updated + :param tuple[str] allowed_states: The allowed source states for the target state. + :param str target_state: The target state. + :param str state_message: The message to set as `state_message`. + :return: The recordset of transactions whose state was updated. :rtype: recordset of `payment.transaction` """ - - def _classify_by_state(_transactions): - """Classify the transactions according to their current state. + def classify_by_state(transactions_): + """ Classify the transactions according to their current state. For each transaction of the current recordset, if: - - The state is an allowed state: the transaction is flagged as 'to process'. - - The state is equal to the target state: the transaction is flagged as 'processed'. - - The state matches none of above: the transaction is flagged as 'in wrong state'. - :param recordset _transactions: The transactions to classify, as a `payment.transaction` - recordset + - The state is an allowed state: the transaction is flagged as `to process`. + - The state is equal to the target state: the transaction is flagged as `processed`. + - The state matches none of above: the transaction is flagged as `in wrong state`. + + :param recordset transactions_: The transactions to classify, as a `payment.transaction` + recordset. :return: A 3-items tuple of recordsets of classified transactions, in this order: - transactions 'to process', 'processed', and 'in wrong state' + transactions `to process`, `processed`, and `in wrong state`. :rtype: tuple(recordset) """ - _txs_to_process = _transactions.filtered(lambda _tx: _tx.state in allowed_states) - _txs_already_processed = _transactions.filtered(lambda _tx: _tx.state == target_state) - _txs_wrong_state = _transactions - _txs_to_process - _txs_already_processed + txs_to_process_ = transactions_.filtered(lambda _tx: _tx.state in allowed_states) + txs_already_processed_ = transactions_.filtered(lambda _tx: _tx.state == target_state) + txs_wrong_state_ = transactions_ - txs_to_process_ - txs_already_processed_ - return _txs_to_process, _txs_already_processed, _txs_wrong_state + return txs_to_process_, txs_already_processed_, txs_wrong_state_ - txs_to_process, txs_already_processed, txs_wrong_state = _classify_by_state(self) + txs_to_process, txs_already_processed, txs_wrong_state = classify_by_state(self) for tx in txs_already_processed: _logger.info( "tried to write on transaction with reference %s with the same value for the " @@ -748,31 +761,30 @@ class PaymentTransaction(models.Model): def _execute_callback(self): """ Execute the callbacks defined on the transactions. - Callbacks that have already been executed are silently ignored. This case can happen when a - transaction is first authorized before being confirmed, for instance. In this case, both - status updates try to execute the callback. + Callbacks that have already been executed are silently ignored. For example, the callback is + called twice when a transaction is first authorized then confirmed. - Only successful callbacks are marked as done. This allows callbacks to reschedule themselves - should the conditions not be met in the present call. + Only successful callbacks are marked as done. This allows callbacks to reschedule + themselves, should the conditions be unmet in the present call. :return: None """ for tx in self.filtered(lambda t: not t.sudo().callback_is_done): - # Only use sudo to check, not to execute + # Only use sudo to check, not to execute. tx_sudo = tx.sudo() model_sudo = tx_sudo.callback_model_id res_id = tx_sudo.callback_res_id method = tx_sudo.callback_method callback_hash = tx_sudo.callback_hash if not (model_sudo and res_id and method): - continue # Skip transactions with unset (or not properly defined) callbacks + continue # Skip transactions with unset (or not properly defined) callbacks. valid_callback_hash = self._generate_callback_hash(model_sudo.id, res_id, method) if not consteq(ustr(valid_callback_hash), callback_hash): _logger.warning( "invalid callback signature for transaction with reference %s", tx.reference ) - continue # Ignore tampered callbacks + continue # Ignore tampered callbacks. record = self.env[model_sudo.model].browse(res_id).exists() if not record: @@ -785,10 +797,10 @@ class PaymentTransaction(models.Model): 'ref': tx.reference, } ) - continue # Ignore invalidated callbacks + continue # Ignore invalidated callbacks. - success = getattr(record, method)(tx) # Execute the callback - tx_sudo.callback_is_done = success or success is None # Missing returns are successful + success = getattr(record, method)(tx) # Execute the callback. + tx_sudo.callback_is_done = success or success is None # Missing returns are successful. #=== BUSINESS METHODS - POST-PROCESSING ===# @@ -800,19 +812,21 @@ class PaymentTransaction(models.Model): generic post-processing values. The returned dict contains the following entries: - - provider_code: The code of the provider - - reference: The reference of the transaction - - amount: The rounded amount of the transaction - - currency_id: The currency of the transaction, as a res.currency id - - state: The transaction state: draft, pending, authorized, done, cancel or error - - state_message: The information message about the state - - is_post_processed: Whether the transaction has already been post-processed - - landing_route: The route the user is redirected to after the transaction - - Additional provider-specific entries - Note: self.ensure_one() + - `provider_code`: The code of the provider. + - `reference`: The reference of the transaction. + - `amount`: The rounded amount of the transaction. + - `currency_id`: The currency of the transaction, as a `res.currency` id. + - `state`: The transaction state: `draft`, `pending`, `authorized`, `done`, `cancel`, or + `error`. + - `state_message`: The information message about the state. + - `is_post_processed`: Whether the transaction has already been post-processed. + - `landing_route`: The route the user is redirected to after the transaction. + - Additional provider-specific entries. - :return: The dict of processing values + Note: `self.ensure_one()` + + :return: The dict of processing values. :rtype: dict """ self.ensure_one() @@ -833,14 +847,6 @@ class PaymentTransaction(models.Model): ) # DEBUG level because this can get spammy with transactions in non-final states return post_processing_values - def _finalize_post_processing(self): - """ Trigger the final post-processing tasks and mark the transactions as post-processed. - - :return: None - """ - self._reconcile_after_done() - self.is_post_processed = True - def _cron_finalize_post_processing(self): """ Finalize the post-processing of recently done transactions not handled by the client. @@ -865,8 +871,8 @@ class PaymentTransaction(models.Model): try: tx._finalize_post_processing() self.env.cr.commit() - except psycopg2.OperationalError: # A collision of accounting sequences occurred - self.env.cr.rollback() # Rollback and try later + except psycopg2.OperationalError: + self.env.cr.rollback() # Rollback and try later. except Exception as e: _logger.exception( "encountered an error while post-processing transaction with reference %s:\n%s", @@ -874,12 +880,19 @@ class PaymentTransaction(models.Model): ) self.env.cr.rollback() - def _reconcile_after_done(self): - """ Post relevant fiscal documents and create missing payments. + def _finalize_post_processing(self): + """ Trigger the final post-processing tasks and mark the transactions as post-processed. - As there is nothing to reconcile for validation transactions, no payment is created for - them. This is also true for validations with a validity check (transfer of a small amount - with immediate refund) because validation amounts are not included in payouts. + :return: None + """ + self._reconcile_after_done() + self.is_post_processed = True + + def _reconcile_after_done(self): + """ Perform compute-intensive operations on related documents. + + For a provider to handle transaction post-processing, it must overwrite this method and + execute its compute-intensive operations on documents linked to confirmed transactions. :return: None """ @@ -888,7 +901,7 @@ class PaymentTransaction(models.Model): #=== BUSINESS METHODS - LOGGING ===# def _log_sent_message(self): - """ Log in the chatter of relevant documents that the transactions have been initiated. + """ Log that the transactions have been initiated in the chatter of relevant documents. :return: None """ @@ -897,10 +910,10 @@ class PaymentTransaction(models.Model): tx._log_message_on_linked_documents(message) def _log_received_message(self): - """ Log in the chatter of relevant documents that the transactions have been received. + """ Log that the transactions have been received in the chatter of relevant documents. - A transaction is 'received' when a response is received from the system of the provider - handling the transaction. + A transaction is 'received' when a payment status is received from the provider handling the + transaction. :return: None """ @@ -912,11 +925,11 @@ class PaymentTransaction(models.Model): """ Log a message on the records linked to the transaction. For a module to implement payments and link documents to a transaction, it must override - this method and call super, then log the message on documents linked to the transaction. + this method and call it, then log the message on documents linked to the transaction. - Note: self.ensure_one() + Note: `self.ensure_one()` - :param str message: The message to be logged + :param str message: The message to log. :return: None """ self.ensure_one() @@ -926,14 +939,14 @@ class PaymentTransaction(models.Model): def _get_sent_message(self): """ Return the message stating that the transaction has been requested. - Note: self.ensure_one() + Note: `self.ensure_one()` - :return: The 'transaction sent' message + :return: The 'transaction sent' message. :rtype: str """ self.ensure_one() - # Choose the message based on the payment flow + # Choose the message based on the payment flow. if self.operation in ('online_redirect', 'online_direct'): message = _( "A transaction with reference %(ref)s has been initiated (%(provider_name)s).", @@ -966,7 +979,10 @@ class PaymentTransaction(models.Model): def _get_received_message(self): """ Return the message stating that the transaction has been received by the provider. - Note: self.ensure_one() + Note: `self.ensure_one()` + + :return: The 'transaction received' message. + :rtype: str """ self.ensure_one() @@ -1014,7 +1030,7 @@ class PaymentTransaction(models.Model): def _get_last(self): """ Return the last transaction of the recordset. - :return: The last transaction of the recordset, sorted by id + :return: The last transaction of the recordset, sorted by id. :rtype: recordset of `payment.transaction` """ return self.filtered(lambda t: t.state != 'draft').sorted()[:1] diff --git a/addons/payment/static/src/js/express_checkout_form.js b/addons/payment/static/src/js/express_checkout_form.js index ce7ef520fc8..2170f788417 100644 --- a/addons/payment/static/src/js/express_checkout_form.js +++ b/addons/payment/static/src/js/express_checkout_form.js @@ -50,7 +50,7 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({ /** * Prepare the provider-specific express checkout form based on the provided data. * - * For an provider to manage an express checkout form, it must override this method. + * For a provider to manage an express checkout form, it must override this method. * * @private * @param {Object} providerData - The provider-specific data. @@ -63,8 +63,8 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({ /** * Prepare the params to send to the transaction route. * - * For an provider to overwrite generic params or to add provider-specific ones, it must - * override this method and return the extended transaction route params. + * For a provider to overwrite generic params or to add provider-specific ones, it must override + * this method and return the extended transaction route params. * * @private * @param {number} providerId - The id of the provider handling the transaction. diff --git a/addons/payment/tests/test_payment_provider.py b/addons/payment/tests/test_payment_provider.py index c85d29279f2..aa4ebb9561b 100644 --- a/addons/payment/tests/test_payment_provider.py +++ b/addons/payment/tests/test_payment_provider.py @@ -49,7 +49,7 @@ class TestPaymentprovider(PaymentCommon): self.assertIn(self.provider, compatible_providers) def test_provider_compatible_when_payment_below_maximum_amount(self): - """ Test that an provider is compatible when the payment amount is less than the maximum + """ Test that a provider is compatible when the payment amount is less than the maximum amount. """ self.provider.maximum_amount = self.amount + 10.0 @@ -59,7 +59,7 @@ class TestPaymentprovider(PaymentCommon): self.assertIn(self.provider, compatible_providers) def test_provider_not_compatible_when_payment_above_maximum_amount(self): - """ Test that an provider is not compatible when the payment amount is more than the maximum + """ Test that a provider is not compatible when the payment amount is more than the maximum amount. """ self.provider.maximum_amount = self.amount - 10.0