[IMP] sale,payment: custom payment link
**Added custom payment link** - Added action 'Generate a payment link' in dropdown menu - Reference id is prefilled. - Based on change in amount, new link is generated Task 1938635 closes odoo/odoo#31575 Signed-off-by: Damien Bouvy (dbo) <dbo@odoo.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
'views/res_partner_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/payment_security.xml',
|
||||
'wizards/payment_link_wizard_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
from unicodedata import normalize
|
||||
import psycopg2
|
||||
import werkzeug
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, consteq, ustr
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
@@ -129,10 +132,27 @@ class WebsitePayment(http.Controller):
|
||||
return request.render("payment.pay_methods", values)
|
||||
|
||||
@http.route(['/website_payment/pay'], type='http', auth='public', website=True, sitemap=False)
|
||||
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw):
|
||||
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, partner_id=False, access_token=None, **kw):
|
||||
"""
|
||||
Generic payment page allowing public and logged in users to pay an arbitrary amount.
|
||||
|
||||
In the case of a public user access, we need to ensure that the payment is made anonymously - e.g. it should not be
|
||||
possible to pay for a specific partner simply by setting the partner_id GET param to a random id. In the case where
|
||||
a partner_id is set, we do an access_token check based on the payment.link.wizard model (since links for specific
|
||||
partners should be created from there and there only). Also noteworthy is the filtering of s2s payment methods -
|
||||
we don't want to create payment tokens for public users.
|
||||
|
||||
In the case of a logged in user, then we let access rights and security rules do their job.
|
||||
"""
|
||||
env = request.env
|
||||
user = env.user.sudo()
|
||||
reference = normalize('NFKD', reference).encode('ascii','ignore').decode('utf-8')
|
||||
if partner_id and not access_token:
|
||||
raise werkzeug.exceptions.NotFound
|
||||
if partner_id and access_token:
|
||||
token_ok = request.env['payment.link.wizard'].check_token(access_token, int(partner_id), float(amount), int(currency_id))
|
||||
if not token_ok:
|
||||
raise werkzeug.exceptions.NotFound
|
||||
|
||||
# Default values
|
||||
values = {
|
||||
@@ -181,7 +201,15 @@ class WebsitePayment(http.Controller):
|
||||
acquirers = env['payment.acquirer'].search([('state', 'in', ['enabled', 'test']), ('company_id', '=', user.company_id.id)])
|
||||
|
||||
# Check partner
|
||||
partner_id = user.partner_id.id if not user._is_public() else False
|
||||
if not user._is_public():
|
||||
# NOTE: this means that if the partner was set in the GET param, it gets overwritten here
|
||||
# This is something we want, since security rules are based on the partner - assuming the
|
||||
# access_token checked out at the start, this should have no impact on the payment itself
|
||||
# existing besides making reconciliation possibly more difficult (if the payment partner is
|
||||
# not the same as the invoice partner, for example)
|
||||
partner_id = user.partner_id.id
|
||||
elif partner_id:
|
||||
partner_id = int(partner_id)
|
||||
|
||||
values.update({
|
||||
'partner_id': partner_id,
|
||||
@@ -189,15 +217,17 @@ class WebsitePayment(http.Controller):
|
||||
'error_msg': kw.get('error_msg')
|
||||
})
|
||||
|
||||
values['acquirers'] = [acq for acq in acquirers if acq.payment_flow in ['form', 's2s']]
|
||||
# s2s mode will always generate a token, which we don't want for public users
|
||||
valid_flows = ['form', 's2s'] if not user._is_public() else ['form']
|
||||
values['acquirers'] = [acq for acq in acquirers if acq.payment_flow in valid_flows]
|
||||
values['pms'] = request.env['payment.token'].search([('acquirer_id', 'in', acquirers.ids)])
|
||||
|
||||
return request.render('payment.pay', values)
|
||||
|
||||
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',
|
||||
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',], type='json', auth='public')
|
||||
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
|
||||
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
|
||||
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',
|
||||
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>/<int:partner_id>'], type='json', auth='public')
|
||||
def transaction(self, acquirer_id, reference, amount, currency_id, partner_id=False, **kwargs):
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
order_id = kwargs.get('order_id')
|
||||
|
||||
@@ -220,7 +250,10 @@ class WebsitePayment(http.Controller):
|
||||
reference_values.update(acquirer_id=int(acquirer_id))
|
||||
values['reference'] = request.env['payment.transaction']._compute_reference(values=reference_values, prefix=reference)
|
||||
tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values)
|
||||
tx.return_url = '/website_payment/confirm?tx_id=%d' % tx.id
|
||||
secret = request.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
token_str = '%s%s%s' % (tx.id, tx.reference, tx.amount)
|
||||
token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
tx.return_url = '/website_payment/confirm?tx_id=%d&access_token=%s' % (tx.id, token)
|
||||
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
|
||||
@@ -231,36 +264,38 @@ class WebsitePayment(http.Controller):
|
||||
return acquirer.sudo().render(tx.reference, float(amount), int(currency_id), values=render_values)
|
||||
|
||||
@http.route(['/website_payment/token/<string:reference>/<string:amount>/<string:currency_id>',
|
||||
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>'], type='http', auth='public', website=True)
|
||||
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
|
||||
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>',
|
||||
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>/<int:partner_id>'], type='http', auth='public', website=True)
|
||||
def payment_token(self, pm_id, reference, amount, currency_id, partner_id=False, return_url=None, **kwargs):
|
||||
token = request.env['payment.token'].browse(int(pm_id))
|
||||
order_id = kwargs.get('order_id')
|
||||
|
||||
if not token:
|
||||
return request.redirect('/website_payment/pay?error_msg=%s' % _('Cannot setup the payment.'))
|
||||
|
||||
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
|
||||
|
||||
values = {
|
||||
'acquirer_id': token.acquirer_id.id,
|
||||
'reference': reference,
|
||||
'amount': float(amount),
|
||||
'currency_id': int(currency_id),
|
||||
'partner_id': partner_id,
|
||||
'payment_token_id': pm_id,
|
||||
'partner_id': int(partner_id),
|
||||
'payment_token_id': int(pm_id),
|
||||
'type': 'server2server',
|
||||
'return_url': return_url,
|
||||
}
|
||||
|
||||
if order_id:
|
||||
values['sale_order_ids'] = [(6, 0, [order_id])]
|
||||
values['sale_order_ids'] = [(6, 0, [int(order_id)])]
|
||||
|
||||
tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values)
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
|
||||
try:
|
||||
tx.s2s_do_transaction()
|
||||
tx.return_url = return_url or '/website_payment/confirm?tx_id=%d' % tx.id
|
||||
secret = request.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
token_str = '%s%s%s' % (tx.id, tx.reference, tx.amount)
|
||||
token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
tx.return_url = return_url or '/website_payment/confirm?tx_id=%d&access_token=%s' % (tx.id, token)
|
||||
except Exception as e:
|
||||
_logger.exception(e)
|
||||
return request.redirect('/payment/process')
|
||||
@@ -268,8 +303,17 @@ class WebsitePayment(http.Controller):
|
||||
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True, sitemap=False)
|
||||
def confirm(self, **kw):
|
||||
tx_id = int(kw.get('tx_id', 0))
|
||||
access_token = kw.get('access_token')
|
||||
if tx_id:
|
||||
tx = request.env['payment.transaction'].browse(tx_id)
|
||||
if access_token:
|
||||
tx = request.env['payment.transaction'].sudo().browse(tx_id)
|
||||
secret = request.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
valid_token_str = '%s%s%s' % (tx.id, tx.reference, tx.amount)
|
||||
valid_token = hmac.new(secret.encode('utf-8'), valid_token_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
if not consteq(ustr(valid_token), access_token):
|
||||
raise werkzeug.exceptions.NotFound
|
||||
else:
|
||||
tx = request.env['payment.transaction'].browse(tx_id)
|
||||
if tx.state in ['done', 'authorized']:
|
||||
status = 'success'
|
||||
message = tx.acquirer_id.done_msg
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
<field name="payment_method_id" position="after">
|
||||
<field name="payment_method_code" invisible="1"/>
|
||||
<field name="payment_token_id" options="{'no_create': True}"
|
||||
attrs="{'invisible': [('payment_method_code', '!=', 'electronic')],
|
||||
'required': [('payment_method_code', '=', 'electronic')]}"/>
|
||||
attrs="{'invisible': [('payment_method_code', '!=', 'electronic')], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
<p><b>Amount:</b> <t t-esc="amount" t-options="{'widget': 'monetary', 'display_currency': currency}"/></p>
|
||||
<t t-call="payment.payment_tokens_list" t-if="reference and amount and currency">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
|
||||
<t t-set="form_action" t-value="'/website_payment/token/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
|
||||
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference + '/' + str(partner_id)"/>
|
||||
<t t-set="form_action" t-value="'/website_payment/token/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference + '/' + str(partner_id)"/>
|
||||
</t>
|
||||
<div t-if="not acquirers" class="alert alert-danger" role="alert">
|
||||
<p>No payment acquirer found.</p>
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment_acquirer_onboarding_wizard
|
||||
from . import payment_link_wizard
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import ustr, consteq
|
||||
|
||||
|
||||
class PaymentLinkWizard(models.TransientModel):
|
||||
_name = "payment.link.wizard"
|
||||
_description = "Generate Payment Link"
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(PaymentLinkWizard, self).default_get(fields)
|
||||
res_id = self._context.get('active_id')
|
||||
res_model = self._context.get('active_model')
|
||||
res.update({'res_id': res_id, 'res_model': res_model})
|
||||
amount_field = 'amount_residual' if res_model == 'account.move' else 'amount_total'
|
||||
if res_id and res_model == 'account.move':
|
||||
record = self.env[res_model].browse(res_id)
|
||||
res.update({
|
||||
'description': record.invoice_payment_ref,
|
||||
'amount': record[amount_field],
|
||||
'currency_id': record.currency_id.id,
|
||||
'partner_id': record.partner_id.id,
|
||||
'amount_max': record[amount_field],
|
||||
})
|
||||
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')
|
||||
link = fields.Char(string='Payment Link', compute='_compute_values')
|
||||
description = fields.Char('Payment Ref')
|
||||
access_token = fields.Char(compute='_compute_values')
|
||||
|
||||
@api.onchange('amount', 'description')
|
||||
def _onchange_amount(self):
|
||||
if self.amount_max < self.amount:
|
||||
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', 'description', 'partner_id', 'currency_id')
|
||||
def _compute_values(self):
|
||||
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
for payment_link in self:
|
||||
token_str = '%s%s%s' % (payment_link.partner_id.id, payment_link.amount, payment_link.currency_id.id)
|
||||
payment_link.access_token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
# must be called after token generation, obvsly - the link needs an up-to-date token
|
||||
self._generate_link()
|
||||
|
||||
def _generate_link(self):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for payment_link in self:
|
||||
payment_link.link = '%s/website_payment/pay?reference=%s&amount=%s¤cy_id=%s&partner_id=%s&access_token=%s' % (base_url, payment_link.description, payment_link.amount, payment_link.currency_id.id, payment_link.partner_id.id, payment_link.access_token)
|
||||
|
||||
@api.model
|
||||
def check_token(self, access_token, partner_id, amount, currency_id):
|
||||
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
token_str = '%s%s%s' % (partner_id, amount, currency_id)
|
||||
correct_token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
if consteq(ustr(access_token), correct_token):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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">
|
||||
<group>
|
||||
<field name="res_id" invisible="1"/>
|
||||
<field name="res_model" invisible="1"/>
|
||||
<field name="partner_id" invisible="1"/>
|
||||
<field name="amount_max" invisible="1"/>
|
||||
<field name="description"/>
|
||||
<field name="amount"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="access_token" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="link" readonly="1" widget="CopyClipboardChar"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Close" class="btn-primary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<act_window
|
||||
name="Generate a Payment Link"
|
||||
res_model="payment.link.wizard"
|
||||
binding_model="account.move"
|
||||
binding_views="form"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
view_id="payment_link_wizard_view_form"
|
||||
id="action_invoice_order_generate_link"
|
||||
/>
|
||||
</odoo>
|
||||
@@ -34,7 +34,8 @@ This module contains all the common features of Sales Management and eCommerce.
|
||||
'views/payment_views.xml',
|
||||
'views/product_attribute_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/utm_campaign_views.xml'
|
||||
'views/utm_campaign_views.xml',
|
||||
'wizard/sale_payment_link_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/sale_demo.xml',
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import payment_acquirer_onboarding_wizard
|
||||
from . import sale_make_invoice_advance
|
||||
from . import sale_payment_link
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class SalePaymentLink(models.TransientModel):
|
||||
_inherit = "payment.link.wizard"
|
||||
_description = "Generate Sales Payment Link"
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SalePaymentLink, self).default_get(fields)
|
||||
if res['res_id'] and res['res_model'] == 'sale.order':
|
||||
record = self.env[res['res_model']].browse(res['res_id'])
|
||||
res.update({
|
||||
'description': record.name,
|
||||
'amount': record.amount_total - sum(record.invoice_ids.mapped('amount_total')),
|
||||
'currency_id': record.currency_id.id,
|
||||
'partner_id': record.partner_id.id,
|
||||
'amount_max': record.amount_total
|
||||
})
|
||||
return res
|
||||
|
||||
def _generate_link(self):
|
||||
""" Override of the base method to add the order_id in the link. """
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for payment_link in self:
|
||||
payment_link.link = '%s/website_payment/pay?reference=%s&amount=%s¤cy_id=%s&partner_id=%s&order_id=%s&access_token=%s' % (base_url, payment_link.description, payment_link.amount, payment_link.currency_id.id, payment_link.partner_id.id, payment_link.res_id, payment_link.access_token)
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<act_window
|
||||
name="Generate a Payment Link"
|
||||
res_model="payment.link.wizard"
|
||||
binding_model="sale.order"
|
||||
binding_views="form"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
view_id="payment.payment_link_wizard_view_form"
|
||||
id="action_sale_order_generate_link"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user