19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -1,4 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_capture_wizard
from . import payment_link_wizard
from . import payment_onboarding_wizard
from . import res_config_settings

View file

@ -0,0 +1,161 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import format_amount
class PaymentCaptureWizard(models.TransientModel):
_name = 'payment.capture.wizard'
_description = "Payment Capture Wizard"
transaction_ids = fields.Many2many( # All the source txs related to the capture request
comodel_name='payment.transaction',
default=lambda self: self.env.context.get('active_ids'),
readonly=True,
)
authorized_amount = fields.Monetary(
string="Authorized Amount", compute='_compute_authorized_amount'
)
captured_amount = fields.Monetary(string="Already Captured", compute='_compute_captured_amount')
voided_amount = fields.Monetary(string="Already Voided", compute='_compute_voided_amount')
available_amount = fields.Monetary(
string="Maximum Capture Allowed", compute='_compute_available_amount'
)
amount_to_capture = fields.Monetary(
compute='_compute_amount_to_capture', store=True, readonly=False
)
is_amount_to_capture_valid = fields.Boolean(compute='_compute_is_amount_to_capture_valid')
void_remaining_amount = fields.Boolean()
currency_id = fields.Many2one(related='transaction_ids.currency_id')
support_partial_capture = fields.Boolean(
help="Whether each of the transactions' provider supports the partial capture.",
compute='_compute_support_partial_capture',
compute_sudo=True,
)
has_draft_children = fields.Boolean(compute='_compute_has_draft_children')
has_remaining_amount = fields.Boolean(compute='_compute_has_remaining_amount')
# === COMPUTE METHODS === #
@api.depends('transaction_ids')
def _compute_authorized_amount(self):
for wizard in self:
wizard.authorized_amount = sum(wizard.transaction_ids.mapped('amount'))
@api.depends('transaction_ids')
def _compute_captured_amount(self):
for wizard in self:
full_capture_txs = wizard.transaction_ids.filtered(
lambda tx: tx.state == 'done' and not tx.child_transaction_ids
) # Transactions that have been fully captured in a single capture operation.
partial_capture_child_txs = wizard.transaction_ids.child_transaction_ids.filtered(
lambda tx: tx.state == 'done'
) # Transactions that represent a partial capture of their source transaction.
wizard.captured_amount = sum(
(full_capture_txs | partial_capture_child_txs).mapped('amount')
)
@api.depends('transaction_ids')
def _compute_voided_amount(self):
for wizard in self:
void_child_txs = wizard.transaction_ids.child_transaction_ids.filtered(
lambda tx: tx.state == 'cancel'
)
wizard.voided_amount = sum(void_child_txs.mapped('amount'))
@api.depends('authorized_amount', 'captured_amount', 'voided_amount')
def _compute_available_amount(self):
for wizard in self:
wizard.available_amount = wizard.authorized_amount \
- wizard.captured_amount \
- wizard.voided_amount
@api.depends('available_amount')
def _compute_amount_to_capture(self):
""" Set the default amount to capture to the amount available for capture. """
for wizard in self:
wizard.amount_to_capture = wizard.available_amount
@api.depends('amount_to_capture', 'available_amount')
def _compute_is_amount_to_capture_valid(self):
for wizard in self:
is_valid = 0 < wizard.amount_to_capture <= wizard.available_amount
wizard.is_amount_to_capture_valid = is_valid
@api.depends('transaction_ids')
def _compute_support_partial_capture(self):
for wizard in self:
wizard.support_partial_capture = all(
tx.provider_id.support_manual_capture == 'partial'
and tx.primary_payment_method_id.support_manual_capture == 'partial'
for tx in wizard.transaction_ids
)
@api.depends('transaction_ids')
def _compute_has_draft_children(self):
for wizard in self:
wizard.has_draft_children = bool(wizard.transaction_ids.child_transaction_ids.filtered(
lambda tx: tx.state == 'draft'
))
@api.depends('available_amount', 'amount_to_capture')
def _compute_has_remaining_amount(self):
for wizard in self:
wizard.has_remaining_amount = wizard.amount_to_capture < wizard.available_amount
if not wizard.has_remaining_amount:
wizard.void_remaining_amount = False
# === CONSTRAINT METHODS === #
@api.constrains('amount_to_capture')
def _check_amount_to_capture_within_boundaries(self):
for wizard in self:
if not wizard.is_amount_to_capture_valid:
formatted_amount = format_amount(
self.env, wizard.available_amount, wizard.currency_id
)
raise ValidationError(_(
"The amount to capture must be positive and cannot be superior to %s.",
formatted_amount
))
if not wizard.support_partial_capture \
and wizard.amount_to_capture != wizard.available_amount:
raise ValidationError(_(
"Some of the transactions you intend to capture can only be captured in full. "
"Handle the transactions individually to capture a partial amount."
))
# === ACTION METHODS === #
def action_capture(self):
self.ensure_one()
remaining_amount_to_capture = self.amount_to_capture
processed_txs_sudo = self.env['payment.transaction'].sudo()
for source_tx in self.transaction_ids.filtered(lambda tx: tx.state == 'authorized'):
partial_capture_child_txs = self.transaction_ids.child_transaction_ids.filtered(
lambda tx: tx.source_transaction_id == source_tx and tx.state == 'done'
) # We can void all the remaining amount only at once => don't check cancel state.
source_tx_remaining_amount = source_tx.currency_id.round(
source_tx.amount - sum(partial_capture_child_txs.mapped('amount'))
)
if remaining_amount_to_capture:
amount_to_capture = min(source_tx_remaining_amount, remaining_amount_to_capture)
# In sudo mode because we need to be able to read on provider fields.
processed_txs_sudo |= source_tx.sudo()._capture(
amount_to_capture=amount_to_capture
)
remaining_amount_to_capture -= amount_to_capture
source_tx_remaining_amount -= amount_to_capture
if source_tx_remaining_amount and self.void_remaining_amount:
# The source tx isn't fully captured and the user wants to void the remaining.
# In sudo mode because we need to be able to read on provider fields.
processed_txs_sudo |= source_tx.sudo()._void(
amount_to_void=source_tx_remaining_amount
)
elif not remaining_amount_to_capture and not self.void_remaining_amount:
# The amount to capture has been completely captured.
break # Skip the remaining transactions.
return processed_txs_sudo._build_action_feedback_notification()

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_capture_wizard_view_form" model="ir.ui.view">
<field name="name">payment.capture.wizard.form</field>
<field name="model">payment.capture.wizard</field>
<field name="arch" type="xml">
<form string="Capture">
<field name="transaction_ids" invisible="1"/>
<field name="is_amount_to_capture_valid" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="support_partial_capture" invisible="1"/>
<field name="has_draft_children" invisible="1"/>
<field name="has_remaining_amount" invisible="1"/>
<div id="alert_draft_capture_tx"
role="alert"
class="alert alert-warning"
invisible="not has_draft_children">
<strong>Warning!</strong> There is a partial capture pending. Please wait a
moment for it to be processed. Check your payment provider configuration if
the capture is still pending after a few minutes.
</div>
<group name="readonly_fields">
<field name="authorized_amount"/>
<field name="captured_amount"
invisible="captured_amount &lt;= 0"/>
<field name="voided_amount"
invisible="voided_amount &lt;= 0"/>
</group>
<hr/>
<group name="input_fields">
<label for="amount_to_capture" class="oe_inline"/>
<div class="o_row">
<field name="amount_to_capture"
class="oe_inline"
readonly="support_partial_capture == 'full_only'"/>
<i class="fa fa-info-circle oe_inline"
invisible="support_partial_capture != 'full_only'"
title="Some of the transactions you intend to capture can only be captured in full. Handle the transactions individually to capture a partial amount."/>
</div>
<field name="void_remaining_amount" readonly="not has_remaining_amount"/>
</group>
<div id="alert_amount_to_capture_above_authorized_amount"
role="alert"
class="alert alert-warning mb-2"
invisible="is_amount_to_capture_valid">
<strong>Warning!</strong> You can not capture a negative amount nor more
than <field name='available_amount' class='oe_inline' widget='monetary'/>.
</div>
<footer>
<button string="Capture" type="object" name="action_capture" class="btn-primary"/>
<button string="Close" special="cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -3,8 +3,6 @@
from werkzeug import urls
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare
from odoo.addons.payment import utils as payment_utils
@ -14,8 +12,8 @@ class PaymentLinkWizard(models.TransientModel):
_description = "Generate Payment Link"
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
def default_get(self, fields):
res = super().default_get(fields)
res_id = self.env.context.get('active_id')
res_model = self.env.context.get('active_model')
if res_id and res_model:
@ -32,35 +30,20 @@ class PaymentLinkWizard(models.TransientModel):
currency_id = fields.Many2one('res.currency')
partner_id = fields.Many2one('res.partner')
partner_email = fields.Char(related='partner_id.email')
description = fields.Char("Payment Ref")
link = fields.Char(string="Payment Link", compute='_compute_link')
company_id = fields.Many2one('res.company', compute='_compute_company_id')
available_provider_ids = fields.Many2many(
comodel_name='payment.provider',
string="Payment Providers Available",
compute='_compute_available_provider_ids',
compute_sudo=True,
)
has_multiple_providers = fields.Boolean(
string="Has Multiple Providers",
compute='_compute_has_multiple_providers',
)
payment_provider_selection = fields.Selection(
string="Allow Payment Provider",
help="If a specific payment provider is selected, customers will only be allowed to pay "
"via this one. If 'All' is selected, customers can pay via any available payment "
"provider.",
selection='_selection_payment_provider_selection',
default='all',
required=True,
)
warning_message = fields.Char(compute='_compute_warning_message')
@api.onchange('amount', 'description')
def _onchange_amount(self):
if float_compare(self.amount_max, self.amount, precision_rounding=self.currency_id.rounding or 0.01) == -1:
raise ValidationError(_("Please set an amount smaller than %s.", self.amount_max))
if self.amount <= 0:
raise ValidationError(_("The value of the payment amount must be positive."))
@api.depends('amount', 'amount_max')
def _compute_warning_message(self):
self.warning_message = ''
for wizard in self:
if wizard.amount_max <= 0:
wizard.warning_message = _("There is nothing to be paid.")
elif wizard.amount <= 0:
wizard.warning_message = _("Please set a positive amount.")
elif wizard.amount > wizard.amount_max:
wizard.warning_message = _("Please set an amount lower than %s.", wizard.currency_id.format(wizard.amount_max))
@api.depends('res_model', 'res_id')
def _compute_company_id(self):
@ -68,93 +51,59 @@ class PaymentLinkWizard(models.TransientModel):
record = self.env[link.res_model].browse(link.res_id)
link.company_id = record.company_id if 'company_id' in record else False
@api.depends('company_id', 'partner_id', 'currency_id')
def _compute_available_provider_ids(self):
for link in self:
link.available_provider_ids = link._get_payment_provider_available(
res_model=link.res_model,
res_id=link.res_id,
company_id=link.company_id.id,
partner_id=link.partner_id.id,
amount=link.amount,
currency_id=link.currency_id.id,
)
def _selection_payment_provider_selection(self):
""" Specify available providers in the selection field.
:return: The selection list of available providers.
:rtype: list[tuple]
"""
defaults = self.default_get(['res_model', 'res_id'])
selection = [('all', "All")]
res_model, res_id = defaults.get('res_model'), defaults.get('res_id')
if res_id and res_model in ['account.move', "sale.order"]:
# At module install, the selection method is called
# but the document context isn't specified.
related_document = self.env[res_model].browse(res_id)
company_id = related_document.company_id
partner_id = related_document.partner_id
currency_id = related_document.currency_id
selection.extend(
self._get_payment_provider_available(
res_model=res_model,
res_id=res_id,
company_id=company_id.id,
partner_id=partner_id.id,
amount=related_document.amount_total,
currency_id=currency_id.id,
).name_get()
)
return selection
def _get_payment_provider_available(self, **kwargs):
""" Select and return the providers matching the criteria.
:return: The compatible providers
:rtype: recordset of `payment.provider`
"""
return self.env['payment.provider'].sudo()._get_compatible_providers(**kwargs)
@api.depends('available_provider_ids')
def _compute_has_multiple_providers(self):
for link in self:
link.has_multiple_providers = len(link.available_provider_ids) > 1
def _get_access_token(self):
self.ensure_one()
return payment_utils.generate_access_token(
self.partner_id.id, self.amount, self.currency_id.id
)
@api.depends(
'description', 'amount', 'currency_id', 'partner_id', 'company_id',
'payment_provider_selection',
)
@api.depends('amount', 'currency_id', 'partner_id', 'company_id')
def _compute_link(self):
for payment_link in self:
related_document = self.env[payment_link.res_model].browse(payment_link.res_id)
base_url = related_document.get_base_url() # Don't generate links for the wrong website
url_params = {
'reference': payment_link.description,
'amount': self.amount,
'access_token': self._get_access_token(),
**self._get_additional_link_values(),
}
if payment_link.payment_provider_selection != 'all':
url_params['provider_id'] = str(payment_link.payment_provider_selection)
payment_link.link = f'{base_url}/payment/pay?{urls.url_encode(url_params)}'
base_url = related_document.get_base_url() # Generate links for the right website.
url = self._prepare_url(base_url, related_document)
query_params = self._prepare_query_params(related_document)
anchor = self._prepare_anchor()
if '?' in url:
payment_link.link = f'{url}&{urls.url_encode(query_params)}{anchor}'
else:
payment_link.link = f'{url}?{urls.url_encode(query_params)}{anchor}'
def _get_additional_link_values(self):
""" Return the additional values to append to the payment link.
def _prepare_url(self, base_url, related_document):
""" Build the URL of the payment link with the website's base URL and return it.
:param str base_url: The website's base URL.
:param recordset related_document: The record for which the payment link is generated.
:return: The URL of the payment link.
:rtype: str
"""
return f'{base_url}/payment/pay'
def _prepare_query_params(self, related_document):
""" Prepare the query string params to append to the payment link URL.
Note: self.ensure_one()
:return: The additional payment link values.
:param recordset related_document: The record for which the payment link is generated.
:return: The query params of the payment link.
:rtype: dict
"""
self.ensure_one()
return {
'amount': self.amount,
'access_token': self._prepare_access_token(),
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'company_id': self.company_id.id,
}
def _prepare_access_token(self):
self.ensure_one()
return payment_utils.generate_access_token(
self.partner_id.id, self.amount, self.currency_id.id, env=self.env,
)
def _prepare_anchor(self):
""" Prepare the anchor to append to the payment link.
Note: self.ensure_one()
:return: The anchor of the payment link.
:rtype: str
"""
self.ensure_one()
return ''

View file

@ -6,33 +6,42 @@
<field name="model">payment.link.wizard</field>
<field name="arch" type="xml">
<form string="Generate Payment Link">
<div class="alert alert-warning fw-bold"
<div name="no_partner_email"
class="alert alert-warning fw-bold"
role="alert"
attrs="{'invisible': [('partner_email', '!=', False)]}">
invisible="partner_email">
This partner has no email, which may cause issues with some payment providers.
Setting an email for this partner is advised.
</div>
<div name="payment_link_warning_information"
class="alert alert-warning fw-bold"
role="alert"
invisible="warning_message == ''">
<field name="warning_message"/>
</div>
<group>
<group>
<group name="payment_info" string="Payment Info" class="mt-n4">
<field name="res_id" invisible="1"/>
<field name="res_model" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="partner_email" invisible="1"/>
<field name="amount_max" invisible="1"/>
<field name="available_provider_ids" invisible="1"/>
<field name="has_multiple_providers" invisible="1"/>
<field name="description"/>
<field name="amount"/>
<field name="amount_max" invisible="1"/>
<field name="warning_message" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="payment_provider_selection"
attrs="{'invisible':[('has_multiple_providers', '=', False)]}"/>
</group>
</group>
<group>
<field name="link" readonly="1" widget="CopyClipboardChar"/>
</group>
<footer>
<button string="Close" class="btn-primary" special="cancel" data-hotkey="z"/>
<field name="link"
string="Generate and Copy Payment Link"
readonly="1"
disabled="bool(warning_message)"
widget="PaymentWizardCopyClipboardButtonField"
data-hotkey="q"/>
<button string="Close"
class="btn btn-secondary rounded-2"
special="cancel"
data-hotkey="x"/>
</footer>
</form>
</field>

View file

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_provider_onboarding_wizard_form" model="ir.ui.view">
<field name="name">payment.provider.onboarding.wizard.form</field>
<field name="model">payment.provider.onboarding.wizard</field>
<field name="arch" type="xml">
<form string="Choose a payment method" class="o_onboarding_payment_provider_wizard">
<div class="container">
<div class="row align-items-start">
<div class="col col-4" name="left-column">
<field name="payment_method" widget="radio"/>
</div>
<div class="col" name="right-column">
<div attrs="{'invisible': [('payment_method', '!=', 'paypal')]}">
<group>
<field name="paypal_email_account" attrs="{'required': [('payment_method', '=', 'paypal')]}" string="Email"/>
<field name="paypal_pdt_token" password="True" attrs="{'required': [('payment_method', '=', 'paypal')]}" />
</group>
<p>
<a href="https://www.odoo.com/documentation/16.0/applications/finance/payment_providers/paypal.html" target="_blank">
<span><i class="fa fa-arrow-right"/> How to configure your PayPal account</span>
</a>
</p>
</div>
<div attrs="{'invisible': [('payment_method', '!=', 'manual')]}">
<group>
<field name="manual_name" attrs="{'required': [('payment_method', '=', 'manual')]}"/>
<field name="journal_name" attrs="{'required': [('payment_method', '=', 'manual')]}"/>
<field name="acc_number" attrs="{'required': [('payment_method', '=', 'manual')]}"/>
<field name="manual_post_msg" attrs="{'required': [('payment_method', '=', 'manual')]}"/>
</group>
</div>
</div>
</div>
</div>
<footer>
<button name="add_payment_methods" string="Apply" class="oe_highlight"
type="object" data-hotkey="q" />
<button special="cancel" data-hotkey="z" string="Cancel" />
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -1,151 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class PaymentWizard(models.TransientModel):
_name = 'payment.provider.onboarding.wizard'
_description = 'Payment provider onboarding wizard'
payment_method = fields.Selection([
('stripe', "Credit & Debit card (via Stripe)"),
('paypal', "PayPal"),
('manual', "Custom payment instructions"),
], string="Payment Method", default=lambda self: self._get_default_payment_provider_onboarding_value('payment_method'))
paypal_user_type = fields.Selection([
('new_user', "I don't have a Paypal account"),
('existing_user', 'I have a Paypal account')], string="Paypal User Type", default='new_user')
paypal_email_account = fields.Char("Email", default=lambda self: self._get_default_payment_provider_onboarding_value('paypal_email_account'))
paypal_seller_account = fields.Char("Merchant Account ID")
paypal_pdt_token = fields.Char("PDT Identity Token", default=lambda self: self._get_default_payment_provider_onboarding_value('paypal_pdt_token'))
# Account-specific logic. It's kept here rather than moved in `account_payment` as it's not used by `account` module.
manual_name = fields.Char("Method", default=lambda self: self._get_default_payment_provider_onboarding_value('manual_name'))
journal_name = fields.Char("Bank Name", default=lambda self: self._get_default_payment_provider_onboarding_value('journal_name'))
acc_number = fields.Char("Account Number", default=lambda self: self._get_default_payment_provider_onboarding_value('acc_number'))
manual_post_msg = fields.Html("Payment Instructions")
_data_fetched = fields.Boolean(store=False)
@api.onchange('journal_name', 'acc_number')
def _set_manual_post_msg_value(self):
self.manual_post_msg = _(
'<h3>Please make a payment to: </h3><ul><li>Bank: %s</li><li>Account Number: %s</li><li>Account Holder: %s</li></ul>',
self.journal_name or _("Bank"),
self.acc_number or _("Account"),
self.env.company.name
)
_payment_provider_onboarding_cache = {}
def _get_manual_payment_provider(self, env=None):
if env is None:
env = self.env
module_id = env.ref('base.module_payment_custom').id
return env['payment.provider'].search([('module_id', '=', module_id),
('company_id', '=', env.company.id)], limit=1)
def _get_default_payment_provider_onboarding_value(self, key):
if not self.env.is_admin():
raise UserError(_("Only administrators can access this data."))
if self._data_fetched:
return self._payment_provider_onboarding_cache.get(key, '')
self._data_fetched = True
self._payment_provider_onboarding_cache['payment_method'] = self.env.company.payment_onboarding_payment_method
installed_modules = self.env['ir.module.module'].sudo().search([
('name', 'in', ('payment_paypal', 'payment_stripe')),
('state', '=', 'installed'),
]).mapped('name')
if 'payment_paypal' in installed_modules:
provider = self.env['payment.provider'].search(
[('company_id', '=', self.env.company.id), ('code', '=', 'paypal')], limit=1
)
self._payment_provider_onboarding_cache['paypal_email_account'] = provider['paypal_email_account'] or self.env.user.email or ''
self._payment_provider_onboarding_cache['paypal_pdt_token'] = provider['paypal_pdt_token']
manual_payment = self._get_manual_payment_provider()
journal = manual_payment.journal_id
self._payment_provider_onboarding_cache['manual_name'] = manual_payment['name']
self._payment_provider_onboarding_cache['manual_post_msg'] = manual_payment['pending_msg']
self._payment_provider_onboarding_cache['journal_name'] = journal.name if journal.name != "Bank" else ""
self._payment_provider_onboarding_cache['acc_number'] = journal.bank_acc_number
return self._payment_provider_onboarding_cache.get(key, '')
def add_payment_methods(self):
""" Install required payment providers, configure them and mark the
onboarding step as done."""
payment_method = self.payment_method
if self.payment_method == 'paypal':
self.env.company._install_modules(['payment_paypal', 'account_payment'])
elif self.payment_method == 'manual':
self.env.company._install_modules(['account_payment'])
if self.payment_method in ('paypal', 'manual'):
# create a new env including the freshly installed module(s)
new_env = api.Environment(self.env.cr, self.env.uid, self.env.context)
if self.payment_method == 'paypal':
provider = new_env['payment.provider'].search(
[('company_id', '=', self.env.company.id), ('code', '=', 'paypal')], limit=1
)
if not provider:
base_provider = self.env.ref('payment.payment_provider_paypal')
# Use sudo to access payment provider record that can be in different company.
provider = base_provider.sudo().copy(default={'company_id':self.env.company.id})
default_journal = new_env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', new_env.company.id)], limit=1
)
provider.write({
'paypal_email_account': self.paypal_email_account,
'paypal_pdt_token': self.paypal_pdt_token,
'state': 'enabled',
'is_published': 'True',
'journal_id': provider.journal_id or default_journal
})
elif self.payment_method == 'manual':
manual_provider = self._get_manual_payment_provider(new_env)
if not manual_provider:
raise UserError(_(
'No manual payment method could be found for this company. '
'Please create one from the Payment Provider menu.'
))
manual_provider.name = self.manual_name
manual_provider.pending_msg = self.manual_post_msg
manual_provider.state = 'enabled'
journal = manual_provider.journal_id
if journal:
journal.name = self.journal_name
journal.bank_acc_number = self.acc_number
if self.payment_method in ('paypal', 'manual', 'stripe'):
self.env.company.payment_onboarding_payment_method = self.payment_method
# delete wizard data immediately to get rid of residual credentials
self.sudo().unlink()
if payment_method == 'stripe':
return self._start_stripe_onboarding()
# the user clicked `apply` and not cancel so we can assume this step is done.
self._set_payment_provider_onboarding_step_done()
return {'type': 'ir.actions.act_window_close'}
def _set_payment_provider_onboarding_step_done(self):
self.env.company.sudo().set_onboarding_step_done('payment_provider_onboarding_state')
def _start_stripe_onboarding(self):
""" Start Stripe Connect onboarding. """
menu = self.env.ref('account_payment.payment_provider_menu', False)
menu_id = menu and menu.id # Only set if `account_payment` is installed.
return self.env.company._run_payment_onboarding_step(menu_id)

View file

@ -0,0 +1,113 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.fields import Domain
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
active_provider_id = fields.Many2one(
string="Active Provider",
comodel_name='payment.provider',
compute='_compute_active_provider_id',
)
has_enabled_provider = fields.Boolean(
string="Has Enabled Provider", compute='_compute_has_enabled_provider'
)
onboarding_payment_module = fields.Selection(
string="Onboarding Payment Module",
selection=[
('mercado_pago', "Mercado Pago"),
('razorpay', "Razorpay"),
('stripe', "Stripe"),
],
compute='_compute_onboarding_payment_module',
)
# === COMPUTE METHODS === #
@api.depends('company_id')
def _compute_active_provider_id(self):
for config in self:
active_providers_domain = config._get_active_providers_domain()
if active_providers := self.env['payment.provider'].search(
active_providers_domain, limit=1
):
config.active_provider_id = active_providers[0]
else:
config.active_provider_id = None
@api.depends('company_id')
def _compute_has_enabled_provider(self):
for config in self:
enabled_providers_domain = config._get_active_providers_domain(enabled_only=True)
config.has_enabled_provider = bool(
self.env['payment.provider'].search(enabled_providers_domain, limit=1)
)
def _get_active_providers_domain(self, enabled_only=False):
"""Return the domain to search for active providers.
:param bool enabled_only: Whether only enabled providers should be considered active.
:return: The active providers domain.
:rtype: Domain
"""
return Domain.AND([
[('state', '=', 'enabled') if enabled_only else ('state', '!=', 'disabled')],
self.env['payment.provider']._check_company_domain(self.company_id),
])
@api.depends('company_id.currency_id', 'company_id.country_id.is_stripe_supported_country')
def _compute_onboarding_payment_module(self):
for config in self:
if config.company_id.currency_id.name == 'INR':
config.onboarding_payment_module = 'razorpay'
elif config.company_id.country_id.is_stripe_supported_country:
config.onboarding_payment_module = 'stripe'
elif config.company_id.country_id.is_mercado_pago_supported_country:
config.onboarding_payment_module = 'mercado_pago'
else:
config.onboarding_payment_module = None
# === ACTION METHODS === #
def action_view_active_provider(self):
provider = self.active_provider_id.ensure_one()
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'payment.provider',
'views': [[False, 'form']],
'res_id': provider.id,
}
def _start_payment_onboarding(self, menu_id=None):
"""Install the onboarding module, configure the provider and run the onboarding action.
:param int menu_id: The menu from which the onboarding is started, as an `ir.ui.menu` id.
:return: The action returned by `action_start_onboarding`.
:rtype: dict
"""
self.ensure_one()
if not self.onboarding_payment_module:
return False
# Install the onboarding module if needed.
onboarding_module = self.env['ir.module.module'].search(
[('name', '=', f'payment_{self.onboarding_payment_module}')]
)
self._install_modules(onboarding_module)
# Create a new env including the freshly installed module.
new_env = api.Environment(self.env.cr, self.env.uid, self.env.context)
# Configure the provider.
provider_code = self.onboarding_payment_module
provider = new_env['payment.provider'].search([
('code', '=', provider_code),
*self.env['payment.provider']._check_company_domain(self.env.company),
], limit=1)
if not provider:
return False
return provider.action_start_onboarding(menu_id=menu_id)