mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 08:32:04 +02:00
payment
This commit is contained in:
parent
12c29a983b
commit
95fcc8bd63
189 changed files with 170858 additions and 0 deletions
9
odoo-bringout-oca-ocb-payment/payment/models/__init__.py
Normal file
9
odoo-bringout-oca-ocb-payment/payment/models/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_http
|
||||
from . import payment_provider
|
||||
from . import payment_icon
|
||||
from . import payment_token
|
||||
from . import payment_transaction
|
||||
from . import res_company
|
||||
from . import res_partner
|
||||
12
odoo-bringout-oca-ocb-payment/payment/models/ir_http.py
Normal file
12
odoo-bringout-oca-ocb-payment/payment/models/ir_http.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
@classmethod
|
||||
def _get_translation_frontend_modules_name(cls):
|
||||
mods = super(IrHttp, cls)._get_translation_frontend_modules_name()
|
||||
return mods + ['payment']
|
||||
21
odoo-bringout-oca-ocb-payment/payment/models/payment_icon.py
Normal file
21
odoo-bringout-oca-ocb-payment/payment/models/payment_icon.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PaymentIcon(models.Model):
|
||||
_name = 'payment.icon'
|
||||
_description = 'Payment Icon'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(string="Name")
|
||||
provider_ids = fields.Many2many(
|
||||
string="Providers", comodel_name='payment.provider',
|
||||
help="The list of providers supporting this payment icon")
|
||||
image = fields.Image(
|
||||
string="Image", max_width=64, max_height=64,
|
||||
help="This field holds the image used for this payment icon, limited to 64x64 px")
|
||||
image_payment_form = fields.Image(
|
||||
string="Image displayed on the payment form", related='image', store=True, max_width=45,
|
||||
max_height=30)
|
||||
sequence = fields.Integer('Sequence', default=1)
|
||||
623
odoo-bringout-oca-ocb-payment/payment/models/payment_provider.py
Normal file
623
odoo-bringout-oca-ocb-payment/payment/models/payment_provider.py
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from psycopg2 import sql
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentProvider(models.Model):
|
||||
_name = 'payment.provider'
|
||||
_description = 'Payment Provider'
|
||||
_order = 'module_state, state desc, sequence, name'
|
||||
|
||||
def _valid_field_parameter(self, field, name):
|
||||
return name == 'required_if_provider' or super()._valid_field_parameter(field, name)
|
||||
|
||||
# Configuration fields
|
||||
name = fields.Char(string="Name", required=True, translate=True)
|
||||
sequence = fields.Integer(string="Sequence", help="Define the display order")
|
||||
code = fields.Selection(
|
||||
string="Code",
|
||||
help="The technical code of this payment provider.",
|
||||
selection=[('none', "No Provider Set")],
|
||||
default='none',
|
||||
required=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
string="State",
|
||||
help="In test mode, a fake payment is processed through a test payment interface.\n"
|
||||
"This mode is advised when setting up the provider.",
|
||||
selection=[('disabled', "Disabled"), ('enabled', "Enabled"), ('test', "Test Mode")],
|
||||
default='disabled', required=True, copy=False)
|
||||
is_published = fields.Boolean(
|
||||
string="Published",
|
||||
help="Whether the provider is visible on the website or not. Tokens remain functional but "
|
||||
"are only visible on manage forms.",
|
||||
)
|
||||
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
|
||||
string="Company", comodel_name='res.company', default=lambda self: self.env.company.id,
|
||||
required=True, index=True)
|
||||
main_currency_id = fields.Many2one(
|
||||
related='company_id.currency_id',
|
||||
help="The main currency of the company, used to display monetary fields.",
|
||||
)
|
||||
payment_icon_ids = fields.Many2many(
|
||||
string="Supported Payment Icons", comodel_name='payment.icon')
|
||||
allow_tokenization = fields.Boolean(
|
||||
string="Allow Saving Payment Methods",
|
||||
help="This controls whether customers can save their payment methods as payment tokens.\n"
|
||||
"A payment token is an anonymous link to the payment method details saved in the\n"
|
||||
"provider's database, allowing the customer to reuse it for a next purchase.")
|
||||
capture_manually = fields.Boolean(
|
||||
string="Capture Amount Manually",
|
||||
help="Capture the amount from Odoo, when the delivery is completed.\n"
|
||||
"Use this if you want to charge your customers cards only when\n"
|
||||
"you are sure you can ship the goods to them.")
|
||||
allow_express_checkout = fields.Boolean(
|
||||
string="Allow Express Checkout",
|
||||
help="This controls whether customers can use express payment methods. Express checkout "
|
||||
"enables customers to pay with Google Pay and Apple Pay from which address "
|
||||
"information is collected at payment.",
|
||||
)
|
||||
redirect_form_view_id = fields.Many2one(
|
||||
string="Redirect Form Template", comodel_name='ir.ui.view',
|
||||
help="The template rendering a form submitted to redirect the user when making a payment",
|
||||
domain=[('type', '=', 'qweb')],
|
||||
ondelete='restrict',
|
||||
)
|
||||
inline_form_view_id = fields.Many2one(
|
||||
string="Inline Form Template", comodel_name='ir.ui.view',
|
||||
help="The template rendering the inline payment form when making a direct payment",
|
||||
domain=[('type', '=', 'qweb')],
|
||||
ondelete='restrict',
|
||||
)
|
||||
token_inline_form_view_id = fields.Many2one(
|
||||
string="Token Inline Form Template",
|
||||
comodel_name='ir.ui.view',
|
||||
help="The template rendering the inline payment form when making a payment by token.",
|
||||
domain=[('type', '=', 'qweb')],
|
||||
ondelete='restrict',
|
||||
)
|
||||
express_checkout_form_view_id = fields.Many2one(
|
||||
string="Express Checkout Form Template",
|
||||
comodel_name='ir.ui.view',
|
||||
help="The template rendering the express payment methods' form.",
|
||||
domain=[('type', '=', 'qweb')],
|
||||
ondelete='restrict',
|
||||
)
|
||||
|
||||
# Availability fields
|
||||
available_country_ids = fields.Many2many(
|
||||
string="Countries",
|
||||
comodel_name='res.country',
|
||||
help="The countries in which this payment provider is available. Leave blank to make it "
|
||||
"available in all countries.",
|
||||
relation='payment_country_rel',
|
||||
column1='payment_id',
|
||||
column2='country_id',
|
||||
)
|
||||
maximum_amount = fields.Monetary(
|
||||
string="Maximum Amount",
|
||||
help="The maximum payment amount that this payment provider is available for. Leave blank "
|
||||
"to make it available for any payment amount.",
|
||||
currency_field='main_currency_id',
|
||||
)
|
||||
|
||||
# Fees fields
|
||||
fees_active = fields.Boolean(string="Add Extra Fees")
|
||||
fees_dom_fixed = fields.Float(string="Fixed domestic fees")
|
||||
fees_dom_var = fields.Float(string="Variable domestic fees (in percents)")
|
||||
fees_int_fixed = fields.Float(string="Fixed international fees")
|
||||
fees_int_var = fields.Float(string="Variable international fees (in percents)")
|
||||
|
||||
# Message fields
|
||||
display_as = fields.Char(
|
||||
string="Displayed as", help="Description of the provider for customers",
|
||||
translate=True)
|
||||
pre_msg = fields.Html(
|
||||
string="Help Message", help="The message displayed to explain and help the payment process",
|
||||
translate=True)
|
||||
pending_msg = fields.Html(
|
||||
string="Pending Message",
|
||||
help="The message displayed if the order pending after the payment process",
|
||||
default=lambda self: _(
|
||||
"Your payment has been successfully processed but is waiting for approval."
|
||||
), translate=True)
|
||||
auth_msg = fields.Html(
|
||||
string="Authorize Message", help="The message displayed if payment is authorized",
|
||||
default=lambda self: _("Your payment has been authorized."), translate=True)
|
||||
done_msg = fields.Html(
|
||||
string="Done Message",
|
||||
help="The message displayed if the order is successfully done after the payment process",
|
||||
default=lambda self: _("Your payment has been successfully processed. Thank you!"),
|
||||
translate=True)
|
||||
cancel_msg = fields.Html(
|
||||
string="Canceled Message",
|
||||
help="The message displayed if the order is canceled during the payment process",
|
||||
default=lambda self: _("Your payment has been cancelled."), translate=True)
|
||||
|
||||
# Feature support fields
|
||||
support_tokenization = fields.Boolean(
|
||||
string="Tokenization Supported", compute='_compute_feature_support_fields'
|
||||
)
|
||||
support_manual_capture = fields.Boolean(
|
||||
string="Manual Capture Supported", compute='_compute_feature_support_fields'
|
||||
)
|
||||
support_express_checkout = fields.Boolean(
|
||||
string="Express Checkout Supported", compute='_compute_feature_support_fields'
|
||||
)
|
||||
support_refund = fields.Selection(
|
||||
string="Type of Refund Supported",
|
||||
selection=[('full_only', "Full Only"), ('partial', "Partial")],
|
||||
compute='_compute_feature_support_fields',
|
||||
)
|
||||
support_fees = fields.Boolean(
|
||||
string="Fees Supported", compute='_compute_feature_support_fields'
|
||||
)
|
||||
|
||||
# Kanban view fields
|
||||
image_128 = fields.Image(string="Image", max_width=128, max_height=128)
|
||||
color = fields.Integer(
|
||||
string="Color", help="The color of the card in kanban view", compute='_compute_color',
|
||||
store=True)
|
||||
|
||||
# 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.
|
||||
module_to_buy = fields.Boolean(string="Odoo Enterprise Module", related='module_id.to_buy')
|
||||
|
||||
# View configuration fields
|
||||
show_credentials_page = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_allow_tokenization = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_allow_express_checkout = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_payment_icon_ids = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_pre_msg = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_pending_msg = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_auth_msg = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_done_msg = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
show_cancel_msg = fields.Boolean(compute='_compute_view_configuration_fields')
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('state', 'module_state')
|
||||
def _compute_color(self):
|
||||
""" Update the color of the kanban card based on the state of the provider.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
for provider in self:
|
||||
if provider.module_id and not provider.module_state == 'installed':
|
||||
provider.color = 4 # blue
|
||||
elif provider.state == 'disabled':
|
||||
provider.color = 3 # yellow
|
||||
elif provider.state == 'test':
|
||||
provider.color = 2 # orange
|
||||
elif provider.state == 'enabled':
|
||||
provider.color = 7 # green
|
||||
|
||||
@api.depends('code')
|
||||
def _compute_view_configuration_fields(self):
|
||||
""" Compute the view configuration fields based on the provider.
|
||||
|
||||
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
|
||||
"""
|
||||
self.update({
|
||||
'show_credentials_page': True,
|
||||
'show_allow_tokenization': True,
|
||||
'show_allow_express_checkout': True,
|
||||
'show_payment_icon_ids': True,
|
||||
'show_pre_msg': True,
|
||||
'show_pending_msg': True,
|
||||
'show_auth_msg': True,
|
||||
'show_done_msg': True,
|
||||
'show_cancel_msg': True,
|
||||
})
|
||||
|
||||
def _compute_feature_support_fields(self):
|
||||
""" Compute the feature support fields based on the provider.
|
||||
|
||||
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
|
||||
"""
|
||||
self.update(dict.fromkeys((
|
||||
'support_express_checkout',
|
||||
'support_fees',
|
||||
'support_manual_capture',
|
||||
'support_refund',
|
||||
'support_tokenization',
|
||||
), None))
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
@api.onchange('state')
|
||||
def _onchange_state_switch_is_published(self):
|
||||
""" Automatically publish or unpublish the provider depending on its state.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.is_published = self.state == 'enabled'
|
||||
|
||||
@api.onchange('state')
|
||||
def _onchange_state_warn_before_disabling_tokens(self):
|
||||
""" Display a warning about the consequences of disabling a provider.
|
||||
|
||||
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: A client action with the warning message, if any.
|
||||
:rtype: dict
|
||||
"""
|
||||
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)]
|
||||
)
|
||||
if related_tokens:
|
||||
return {
|
||||
'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _(
|
||||
"This action will also archive %s tokens that are registered with this "
|
||||
"provider. Archiving tokens is irreversible.", len(related_tokens)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@api.constrains('fees_dom_var', 'fees_int_var')
|
||||
def _check_fee_var_within_boundaries(self):
|
||||
""" Check that variable fees are within realistic boundaries.
|
||||
|
||||
Variable fee values should always be positive and below 100% to respectively avoid negative
|
||||
and infinite (division by zero) fee amounts.
|
||||
|
||||
:return None
|
||||
"""
|
||||
for provider in self:
|
||||
if any(not 0 <= fee < 100 for fee in (provider.fees_dom_var, provider.fees_int_var)):
|
||||
raise ValidationError(_("Variable fees must always be positive and below 100%."))
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
providers = super().create(values_list)
|
||||
providers._check_required_if_provider()
|
||||
return providers
|
||||
|
||||
def write(self, values):
|
||||
# Handle provider disabling.
|
||||
if 'state' in values:
|
||||
state_changed_providers = self.filtered(
|
||||
lambda p: p.state not in ('disabled', values['state'])
|
||||
) # Don't handle providers being enabled or whose state is not updated.
|
||||
state_changed_providers._handle_state_change()
|
||||
|
||||
result = super().write(values)
|
||||
self._check_required_if_provider()
|
||||
|
||||
return result
|
||||
|
||||
def _check_required_if_provider(self):
|
||||
""" Check that provider-specific required fields have been filled.
|
||||
|
||||
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
|
||||
:raise ValidationError: If a provider-specific required field is empty.
|
||||
"""
|
||||
field_names = []
|
||||
enabled_providers = self.filtered(lambda p: p.state in ['enabled', 'test'])
|
||||
for field_name, field in self._fields.items():
|
||||
required_for_provider_code = getattr(field, 'required_if_provider', None)
|
||||
if required_for_provider_code and any(
|
||||
required_for_provider_code == provider.code and not provider[field_name]
|
||||
for provider in enabled_providers
|
||||
):
|
||||
ir_field = self.env['ir.model.fields']._get(self._name, field_name)
|
||||
field_names.append(ir_field.field_description)
|
||||
if field_names:
|
||||
raise ValidationError(
|
||||
_("The following fields must be filled: %s", ", ".join(field_names))
|
||||
)
|
||||
|
||||
def _handle_state_change(self):
|
||||
""" Archive all the payment tokens linked to the providers.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.env['payment.token'].search([('provider_id', 'in', self.ids)]).write({'active': False})
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_master_data(self):
|
||||
""" Prevent the deletion of the payment provider if it has an xmlid. """
|
||||
external_ids = self.get_external_id()
|
||||
for provider in self:
|
||||
external_id = external_ids[provider.id]
|
||||
if external_id and not external_id.startswith('__export__'):
|
||||
raise UserError(_(
|
||||
"You cannot delete the payment provider %s; disable it or uninstall it"
|
||||
" instead.", provider.name
|
||||
))
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def button_immediate_install(self):
|
||||
""" Install the module and reload the page.
|
||||
|
||||
Note: `self.ensure_one()`
|
||||
|
||||
:return: The action to reload the page.
|
||||
:rtype: dict
|
||||
"""
|
||||
if self.module_id and self.module_state != 'installed':
|
||||
self.module_id.button_immediate_install()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
def action_toggle_is_published(self):
|
||||
""" Toggle the field `is_published`.
|
||||
|
||||
:return: None
|
||||
:raise UserError: If the provider is disabled.
|
||||
"""
|
||||
if self.state != 'disabled':
|
||||
self.is_published = not self.is_published
|
||||
else:
|
||||
raise UserError(_("You cannot publish a disabled provider."))
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _get_compatible_providers(
|
||||
self, company_id, partner_id, amount, currency_id=None, force_tokenization=False,
|
||||
is_express_checkout=False, is_validation=False, **kwargs
|
||||
):
|
||||
""" Select and return the providers matching the criteria.
|
||||
|
||||
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 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.
|
||||
:rtype: recordset of `payment.provider`
|
||||
"""
|
||||
# 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.
|
||||
partner = self.env['res.partner'].browse(partner_id)
|
||||
if partner.country_id: # The partner country must either not be set or be supported.
|
||||
domain = expression.AND([
|
||||
domain, [
|
||||
'|',
|
||||
('available_country_ids', '=', False),
|
||||
('available_country_ids', 'in', [partner.country_id.id]),
|
||||
]
|
||||
])
|
||||
|
||||
# Handle the maximum amount.
|
||||
currency = self.env['res.currency'].browse(currency_id).exists()
|
||||
if not is_validation and currency: # The currency is required to convert the amount.
|
||||
company = self.env['res.company'].browse(company_id).exists()
|
||||
date = fields.Date.context_today(self)
|
||||
converted_amount = currency._convert(amount, company.currency_id, company, date)
|
||||
domain = expression.AND([
|
||||
domain, [
|
||||
'|', '|',
|
||||
('maximum_amount', '>=', converted_amount),
|
||||
('maximum_amount', '=', False),
|
||||
('maximum_amount', '=', 0.),
|
||||
]
|
||||
])
|
||||
|
||||
# Handle tokenization support requirements.
|
||||
if force_tokenization or self._is_tokenization_required(**kwargs):
|
||||
domain = expression.AND([domain, [('allow_tokenization', '=', True)]])
|
||||
|
||||
# Handle express checkout.
|
||||
if is_express_checkout:
|
||||
domain = expression.AND([domain, [('allow_express_checkout', '=', True)]])
|
||||
|
||||
compatible_providers = self.env['payment.provider'].search(domain)
|
||||
return compatible_providers
|
||||
|
||||
def _is_tokenization_required(self, **kwargs):
|
||||
""" Return whether tokenizing the transaction is required given its context.
|
||||
|
||||
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.
|
||||
:rtype: bool
|
||||
"""
|
||||
return False
|
||||
|
||||
def _should_build_inline_form(self, is_validation=False):
|
||||
""" Return whether the inline payment form should be instantiated.
|
||||
|
||||
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.
|
||||
:rtype: bool
|
||||
"""
|
||||
return True
|
||||
|
||||
def _compute_fees(self, amount, currency, country):
|
||||
""" Compute the transaction fees.
|
||||
|
||||
The computation is based on the fields `fees_dom_fixed`, `fees_dom_var`, `fees_int_fixed`
|
||||
and `fees_int_var`, and is performed with the formula
|
||||
:code:`fees = (amount * variable / 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.
|
||||
|
||||
:param float amount: The amount to pay for the transaction.
|
||||
:param recordset currency: The currency of the transaction, as a `res.currency` record.
|
||||
:param recordset country: The customer country, as a `res.country` record.
|
||||
:return: The computed fees.
|
||||
:rtype: float
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
fees = 0.0
|
||||
if self.fees_active:
|
||||
if country == self.company_id.country_id:
|
||||
fixed = self.fees_dom_fixed
|
||||
variable = self.fees_dom_var
|
||||
else:
|
||||
fixed = self.fees_int_fixed
|
||||
variable = self.fees_int_var
|
||||
fees = (amount * variable / 100.0 + fixed) / (1 - variable / 100.0)
|
||||
return fees
|
||||
|
||||
def _get_validation_amount(self):
|
||||
""" Return the amount to use for validation operations.
|
||||
|
||||
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()`
|
||||
|
||||
:return: The validation amount.
|
||||
:rtype: float
|
||||
"""
|
||||
self.ensure_one()
|
||||
return 0.0
|
||||
|
||||
def _get_validation_currency(self):
|
||||
""" Return the currency to use for validation operations.
|
||||
|
||||
For a provider to support tokenization, it must override this method and return the
|
||||
validation currency. If the validation amount is `0`, it is not necessary to create the
|
||||
override.
|
||||
|
||||
Note: `self.ensure_one()`
|
||||
|
||||
:return: The validation currency.
|
||||
:rtype: recordset of `res.currency`
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.company_id.currency_id
|
||||
|
||||
def _get_redirect_form_view(self, is_validation=False):
|
||||
""" Return the view of the template used to render the redirect form.
|
||||
|
||||
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()`
|
||||
|
||||
: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()
|
||||
return self.redirect_form_view_id
|
||||
|
||||
@api.model
|
||||
def _setup_provider(self, provider_code):
|
||||
""" Perform module-specific setup steps for the provider.
|
||||
|
||||
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):
|
||||
""" Remove the module-specific data of the given provider.
|
||||
|
||||
:param str provider_code: The code of the provider whose data to remove.
|
||||
:return: None
|
||||
"""
|
||||
providers = self.search([('code', '=', provider_code)])
|
||||
providers.write(self._get_removal_values())
|
||||
|
||||
def _get_removal_values(self):
|
||||
""" Return the values to update a provider with when its module is uninstalled.
|
||||
|
||||
For a module to specify additional removal values, it must override this method and complete
|
||||
the generic values with its specific values.
|
||||
|
||||
:return: The removal values to update the removed provider with.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
'code': 'none',
|
||||
'state': 'disabled',
|
||||
'is_published': False,
|
||||
'redirect_form_view_id': None,
|
||||
'inline_form_view_id': None,
|
||||
'token_inline_form_view_id': None,
|
||||
'express_checkout_form_view_id': None,
|
||||
}
|
||||
147
odoo-bringout-oca-ocb-payment/payment/models/payment_token.py
Normal file
147
odoo-bringout-oca-ocb-payment/payment/models/payment_token.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_name = 'payment.token'
|
||||
_order = 'partner_id, id desc'
|
||||
_description = 'Payment Token'
|
||||
|
||||
provider_id = fields.Many2one(string="Provider", comodel_name='payment.provider', required=True)
|
||||
provider_code = fields.Selection(related='provider_id.code')
|
||||
payment_details = fields.Char(
|
||||
string="Payment Details", help="The clear part of the payment method's payment details.",
|
||||
)
|
||||
partner_id = fields.Many2one(string="Partner", comodel_name='res.partner', required=True)
|
||||
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
|
||||
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.
|
||||
transaction_ids = fields.One2many(
|
||||
string="Payment Transactions", comodel_name='payment.transaction', inverse_name='token_id')
|
||||
verified = fields.Boolean(string="Verified")
|
||||
active = fields.Boolean(string="Active", default=True)
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
for values in values_list:
|
||||
if 'provider_id' in values:
|
||||
provider = self.env['payment.provider'].browse(values['provider_id'])
|
||||
|
||||
# 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.
|
||||
|
||||
return super().create(values_list)
|
||||
|
||||
@api.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.
|
||||
|
||||
: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()
|
||||
|
||||
def write(self, values):
|
||||
""" Prevent unarchiving tokens and handle their archiving.
|
||||
|
||||
:return: The result of the call to the parent method.
|
||||
:rtype: bool
|
||||
:raise UserError: If at least one token is being unarchived.
|
||||
"""
|
||||
if 'active' in values:
|
||||
if values['active']:
|
||||
if any(not token.active for token in self):
|
||||
raise UserError(_("A token cannot be unarchived once it has been archived."))
|
||||
else:
|
||||
# Call the handlers in sudo mode because this method might have been called by RPC.
|
||||
self.filtered('active').sudo()._handle_archiving()
|
||||
|
||||
return super().write(values)
|
||||
|
||||
def _handle_archiving(self):
|
||||
""" 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
|
||||
|
||||
def name_get(self):
|
||||
return [(token.id, token._build_display_name()) for token in self]
|
||||
|
||||
#=== 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`.
|
||||
|
||||
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 and
|
||||
return the customized display name.
|
||||
|
||||
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. 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
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
padding_length = max_length - len(self.payment_details or '')
|
||||
if not self.payment_details:
|
||||
create_date_str = self.create_date.strftime('%Y/%m/%d')
|
||||
display_name = _("Payment details saved on %(date)s", date=create_date_str)
|
||||
elif padding_length >= 2: # Enough room for padding.
|
||||
padding = '•' * min(padding_length - 1, 4) + ' ' if should_pad else ''
|
||||
display_name = ''.join([padding, self.payment_details])
|
||||
elif padding_length > 0: # Not enough room for padding.
|
||||
display_name = self.payment_details
|
||||
else: # Not enough room for neither padding nor the payment details.
|
||||
display_name = self.payment_details[-max_length:] if max_length > 0 else ''
|
||||
return display_name
|
||||
|
||||
def get_linked_records_info(self):
|
||||
""" 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 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()`
|
||||
|
||||
:return: The list of information about the linked document records.
|
||||
:rtype: list
|
||||
"""
|
||||
self.ensure_one()
|
||||
return []
|
||||
1039
odoo-bringout-oca-ocb-payment/payment/models/payment_transaction.py
Normal file
1039
odoo-bringout-oca-ocb-payment/payment/models/payment_transaction.py
Normal file
File diff suppressed because it is too large
Load diff
72
odoo-bringout-oca-ocb-payment/payment/models/res_company.py
Normal file
72
odoo-bringout-oca-ocb-payment/payment/models/res_company.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
payment_provider_onboarding_state = fields.Selection(
|
||||
string="State of the onboarding payment provider step",
|
||||
selection=[('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")],
|
||||
default='not_done')
|
||||
payment_onboarding_payment_method = fields.Selection(
|
||||
string="Selected onboarding payment method",
|
||||
selection=[
|
||||
('paypal', "PayPal"),
|
||||
('stripe', "Stripe"),
|
||||
('manual', "Manual"),
|
||||
('other', "Other"),
|
||||
])
|
||||
|
||||
def _run_payment_onboarding_step(self, menu_id):
|
||||
""" Install the suggested payment modules and configure the providers.
|
||||
|
||||
It's checked that the current company has a Chart of Account.
|
||||
|
||||
:param int menu_id: The menu from which the user started the onboarding step, as an
|
||||
`ir.ui.menu` id
|
||||
:return: The action returned by `action_stripe_connect_account`
|
||||
:rtype: dict
|
||||
"""
|
||||
self.env.company.get_chart_of_accounts_or_fail()
|
||||
|
||||
self._install_modules(['payment_stripe', 'account_payment'])
|
||||
|
||||
# Create a new env including the freshly installed module(s)
|
||||
new_env = api.Environment(self.env.cr, self.env.uid, self.env.context)
|
||||
|
||||
# Configure Stripe
|
||||
default_journal = new_env['account.journal'].search(
|
||||
[('type', '=', 'bank'), ('company_id', '=', new_env.company.id)], limit=1
|
||||
)
|
||||
|
||||
stripe_provider = new_env['payment.provider'].search(
|
||||
[('company_id', '=', self.env.company.id), ('code', '=', 'stripe')], limit=1
|
||||
)
|
||||
if not stripe_provider:
|
||||
base_provider = self.env.ref('payment.payment_provider_stripe')
|
||||
# Use sudo to access payment provider record that can be in different company.
|
||||
stripe_provider = base_provider.sudo().with_context(
|
||||
stripe_connect_onboarding=True,
|
||||
).copy(default={'company_id': self.env.company.id})
|
||||
stripe_provider.journal_id = stripe_provider.journal_id or default_journal
|
||||
|
||||
return stripe_provider.action_stripe_connect_account(menu_id=menu_id)
|
||||
|
||||
def _install_modules(self, module_names):
|
||||
modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)])
|
||||
STATES = ['installed', 'to install', 'to upgrade']
|
||||
modules_sudo.filtered(lambda m: m.state not in STATES).button_immediate_install()
|
||||
|
||||
def _mark_payment_onboarding_step_as_done(self):
|
||||
""" Mark the payment onboarding step as done.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.set_onboarding_step_done('payment_provider_onboarding_state')
|
||||
|
||||
def get_account_invoice_onboarding_steps_states_names(self):
|
||||
""" Override of account. """
|
||||
steps = super().get_account_invoice_onboarding_steps_states_names()
|
||||
return steps + ['payment_provider_onboarding_state']
|
||||
22
odoo-bringout-oca-ocb-payment/payment/models/res_partner.py
Normal file
22
odoo-bringout-oca-ocb-payment/payment/models/res_partner.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
payment_token_ids = fields.One2many(
|
||||
string="Payment Tokens", comodel_name='payment.token', inverse_name='partner_id')
|
||||
payment_token_count = fields.Integer(
|
||||
string="Payment Token Count", compute='_compute_payment_token_count')
|
||||
|
||||
@api.depends('payment_token_ids')
|
||||
def _compute_payment_token_count(self):
|
||||
payments_data = self.env['payment.token']._read_group(
|
||||
[('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']
|
||||
)
|
||||
partners_data = {payment_data['partner_id'][0]: payment_data['partner_id_count']
|
||||
for payment_data in payments_data}
|
||||
for partner in self:
|
||||
partner.payment_token_count = partners_data.get(partner.id, 0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue