[CLN] payment: update docstrings for Sphinx's autodoc
task-2804999 closes odoo/odoo#102716 X-original-commit: 5800c773184dce836a0fc3b67189ec6a14124178 Related: odoo/documentation#2797 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
@@ -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='<provider>'` attribute are made required
|
||||
for all `payment.provider` records with the `code` field equal to <provider> and with the
|
||||
`state` field equal to 'enabled' or 'test'.
|
||||
The fields that have the `required_if_provider='<provider_code>'` attribute are made
|
||||
required for all `payment.provider` records with the `code` field equal to `<provider_code>`
|
||||
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,))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user