This commit is contained in:
Ernad Husremovic 2025-08-29 17:40:39 +02:00
parent 12c29a983b
commit 95fcc8bd63
189 changed files with 170858 additions and 0 deletions

View file

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

View file

@ -0,0 +1,160 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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
class PaymentLinkWizard(models.TransientModel):
_name = 'payment.link.wizard'
_description = "Generate Payment Link"
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
res_id = self.env.context.get('active_id')
res_model = self.env.context.get('active_model')
if res_id and res_model:
res.update({'res_model': res_model, 'res_id': res_id})
res.update(
self.env[res_model].browse(res_id)._get_default_payment_link_values()
)
return res
res_model = fields.Char("Related Document Model", required=True)
res_id = fields.Integer("Related Document ID", required=True)
amount = fields.Monetary(currency_field='currency_id', required=True)
amount_max = fields.Monetary(currency_field='currency_id')
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,
)
@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('res_model', 'res_id')
def _compute_company_id(self):
for link in self:
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',
)
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)}'
def _get_additional_link_values(self):
""" Return the additional values to append to the payment link.
Note: self.ensure_one()
:return: The additional payment link values.
:rtype: dict
"""
self.ensure_one()
return {
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'company_id': self.company_id.id,
}

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_link_wizard_view_form" model="ir.ui.view">
<field name="name">payment.link.wizard.form</field>
<field name="model">payment.link.wizard</field>
<field name="arch" type="xml">
<form string="Generate Payment Link">
<div class="alert alert-warning fw-bold"
role="alert"
attrs="{'invisible': [('partner_email', '!=', False)]}">
This partner has no email, which may cause issues with some payment providers.
Setting an email for this partner is advised.
</div>
<group>
<group>
<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="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"/>
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,47 @@
<?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

@ -0,0 +1,151 @@
# 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)