[MERGE] (account_)payment,*: extract accounting logic from payment

The goal of this work is two-fold:
- First, it aims at clarifying the role of the `account_payment` module
in regard to the `payment` module. The former was meant to
single-handledly handle invoice payments but it failed to do so:
the logic responsible for portal payments was split in two parts with
the main part being located in the `account_payment` module, and the
dangling parts being scattered all over the `payment` module;
regarding payment links, they were not handled by the `account_payment`
module at all.
- Second, it increases the modularity of the `payment` module by
promoting it to the role of a true "engine" that can work without the
Invoicing/Accounting app and, therefore, that can be installed
in standalone and used by community apps that don't depend
on the `account` module.

To meet these objectives, everything that is account-related is moved
to the `account_payment` module while the dependency of the `payment`
module to `account` is removed.
The business features are kept untouched by overriding the hooks already
present in `payment` for the `sale` module.
Doing so allows for some code cleaning with, for example,
`account.payment.method.line` records being automatically created as
new payment acquirer modules are installed rather than having them
override lots of methods and load data files to achieve the same result.

Task-2786690

See also:
- https://github.com/odoo/enterprise/pull/29945
- https://github.com/odoo/upgrade/pull/3832

closes odoo/odoo#97047

Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Odoo's Mergebot
2022-09-06 22:31:07 +02:00
committed by GitHub
191 changed files with 1232 additions and 1041 deletions
+23 -1
View File
@@ -1,4 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import wizards
from odoo import api, SUPERUSER_ID
def post_init_hook(cr, registry):
""" Create `account.payment.method` records for the installed payment providers. """
env = api.Environment(cr, SUPERUSER_ID, {})
PaymentAcquirer = env['payment.acquirer']
installed_providers = PaymentAcquirer.search([('module_id.state', '=', 'installed')])
for provider_code in set(installed_providers.mapped('provider')):
PaymentAcquirer._setup_payment_method(provider_code)
def uninstall_hook(cr, registry):
""" Delete `account.payment.method` records created for the installed payment providers. """
env = api.Environment(cr, SUPERUSER_ID, {})
installed_providers = env['payment.acquirer'].search([('module_id.state', '=', 'installed')])
env['account.payment.method'].search([
('code', 'in', installed_providers.mapped('provider')),
('payment_type', '=', 'inbound'),
]).unlink()
+27 -13
View File
@@ -1,21 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Payment - Account',
'name': "Payment - Account",
'category': 'Accounting/Accounting',
'summary': 'Account and Payment Link and Portal',
'version': '1.0',
'description': """Link Account and Payment and add Portal Payment
Provide tools for account-related payment as well as portal options to
enable payment.
* UPDATE ME
""",
'depends': ['payment'],
'summary': "Enable customers to pay invoices on the portal and post payments when transactions are processed.",
'version': '2.0',
'depends': ['account', 'payment'],
'auto_install': ['account'],
'data': [
'security/ir.model.access.csv',
'security/ir_rules.xml',
'views/account_payment_menus.xml',
'views/account_portal_templates.xml',
'views/payment_templates.xml',
'views/account_move_views.xml',
'views/account_journal_views.xml',
'views/account_payment_views.xml',
'views/payment_acquirer_views.xml',
'views/payment_transaction_views.xml',
'wizards/account_payment_register_views.xml',
'wizards/payment_link_wizard_views.xml',
'wizards/payment_refund_wizard_views.xml',
],
'installable': True,
'assets': {
'web.assets_frontend': [
'account_payment/static/src/js/payment_form.js',
],
},
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
}
+79 -2
View File
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _
from odoo.exceptions import AccessError, MissingError, ValidationError
from odoo.fields import Command
from odoo.http import route
from odoo.http import request, route
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.controllers import portal as payment_portal
@@ -36,3 +37,79 @@ class PaymentPortal(payment_portal.PaymentPortal):
)
return tx_sudo._get_processing_values()
# Payment overrides
@route()
def payment_pay(self, *args, amount=None, invoice_id=None, access_token=None, **kwargs):
""" Override of `payment` to replace the missing transaction values by that of the invoice.
This is necessary for the reconciliation as all transaction values, excepted the amount,
need to match exactly that of the invoice.
:param str amount: The (possibly partial) amount to pay used to check the access token.
:param str invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param str access_token: The access token used to authenticate the partner.
:return: The result of the parent method.
:rtype: str
:raise ValidationError: If the invoice id is invalid.
"""
# Cast numeric parameters as int or float and void them if their str value is malformed.
amount = self._cast_as_float(amount)
invoice_id = self._cast_as_int(invoice_id)
if invoice_id:
invoice_sudo = request.env['account.move'].sudo().browse(invoice_id).exists()
if not invoice_sudo:
raise ValidationError(_("The provided parameters are invalid."))
# Check the access token against the invoice values. Done after fetching the invoice
# as we need the invoice fields to check the access token.
if not payment_utils.check_access_token(
access_token, invoice_sudo.partner_id.id, amount, invoice_sudo.currency_id.id
):
raise ValidationError(_("The provided parameters are invalid."))
kwargs.update({
'currency_id': invoice_sudo.currency_id.id,
'partner_id': invoice_sudo.partner_id.id,
'company_id': invoice_sudo.company_id.id,
'invoice_id': invoice_id,
})
return super().payment_pay(*args, amount=amount, access_token=access_token, **kwargs)
def _get_custom_rendering_context_values(self, invoice_id=None, **kwargs):
""" Override of `payment` to add the invoice id in the custom rendering context values.
:param int invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param dict kwargs: Optional data. This parameter is not used here.
:return: The extended rendering context values.
:rtype: dict
"""
rendering_context_values = super()._get_custom_rendering_context_values(**kwargs)
if invoice_id:
rendering_context_values['invoice_id'] = invoice_id
# Interrupt the payment flow if the invoice has been canceled.
invoice_sudo = request.env['account.move'].sudo().browse(invoice_id)
if invoice_sudo.state == 'cancel':
rendering_context_values['amount'] = 0.0
return rendering_context_values
def _create_transaction(self, *args, invoice_id=None, custom_create_values=None, **kwargs):
""" Override of `payment` to add the invoice id in the custom create values.
:param int invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param dict custom_create_values: Additional create values overwriting the default ones.
:param dict kwargs: Optional data. This parameter is not used here.
:return: The result of the parent method.
:rtype: recordset of `payment.transaction`
"""
if invoice_id:
if custom_create_values is None:
custom_create_values = {}
custom_create_values['invoice_ids'] = [Command.set([int(invoice_id)])]
return super()._create_transaction(
*args, invoice_id=invoice_id, custom_create_values=custom_create_values, **kwargs
)
@@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_journal
from . import account_move
from . import account_payment
from . import account_payment_method
from . import account_payment_method_line
from . import payment_acquirer
from . import payment_transaction
@@ -22,12 +22,16 @@ class AccountJournal(models.Model):
self._cr.execute('''
SELECT acquirer.id
FROM payment_acquirer acquirer
JOIN account_payment_method apm ON apm.code = acquirer.provider
LEFT JOIN account_payment_method_line apml ON apm.id = apml.payment_method_id AND apml.journal_id IS NOT NULL
WHERE acquirer.state IN ('enabled', 'test') AND apm.payment_type = 'inbound'
AND apml.id IS NULL
AND acquirer.company_id IN %(company_ids)s
FROM payment_acquirer acquirer
JOIN account_payment_method apm
ON apm.code = acquirer.provider
LEFT JOIN account_payment_method_line apml
ON apm.id = apml.payment_method_id AND apml.journal_id IS NOT NULL
WHERE acquirer.state IN ('enabled', 'test')
AND acquirer.provider != 'custom'
AND apm.payment_type = 'inbound'
AND apml.id IS NULL
AND acquirer.company_id IN %(company_ids)s
''', {'company_ids': tuple(self.company_id.ids)})
ids = [r[0] for r in self._cr.fetchall()]
if ids:
@@ -65,3 +65,13 @@ class AccountMove(models.Model):
action['domain'] = [('id', 'in', self.transaction_ids.ids)]
return action
def _get_default_payment_link_values(self):
self.ensure_one()
return {
'description': self.payment_reference,
'amount': self.amount_residual,
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'amount_max': self.amount_residual,
}
@@ -43,6 +43,8 @@ class AccountPayment(models.Model):
)
refunds_count = fields.Integer(string="Refunds Count", compute='_compute_refunds_count')
#=== COMPUTE METHODS ===#
def _compute_amount_available_for_refund(self):
for payment in self:
tx_sudo = payment.payment_transaction_id.sudo()
@@ -97,6 +99,8 @@ class AccountPayment(models.Model):
for payment in self:
payment.refunds_count = data.get(payment.id, 0)
#=== ONCHANGE METHODS ===#
@api.onchange('partner_id', 'payment_method_line_id', 'journal_id')
def _onchange_set_payment_token_id(self):
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))]
@@ -117,6 +121,8 @@ class AccountPayment(models.Model):
('acquirer_id', '=', self.payment_method_line_id.payment_acquirer_id.id),
], limit=1)
#=== ACTION METHODS ===#
def action_post(self):
# Post the payments "normally" if no transactions are needed.
# If not, let the acquirer update the state.
@@ -174,6 +180,8 @@ class AccountPayment(models.Model):
action['domain'] = [('source_payment_id', '=', self.id)]
return action
#=== BUSINESS METHODS - PAYMENT FLOW ===#
def _create_payment_transaction(self, **extra_create_values):
for payment in self:
if payment.payment_transaction_id:
@@ -205,3 +213,11 @@ class AccountPayment(models.Model):
'payment_id': self.id,
**extra_create_values,
}
def _get_payment_refund_wizard_values(self):
self.ensure_one()
return {
'transaction_id': self.payment_transaction_id.id,
'payment_amount': self.amount,
'amount_available_for_refund': self.amount_available_for_refund,
}
@@ -10,5 +10,11 @@ class AccountPaymentMethod(models.Model):
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['adyen'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
for provider, _desc in self.env['payment.acquirer']._fields['provider'].selection:
if provider in ('none', 'transfer'):
continue
res[provider] = {
'mode': 'unique',
'domain': [('type', '=', 'bank')],
}
return res
@@ -0,0 +1,90 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
journal_id = fields.Many2one(
string="Payment Journal",
help="The journal in which the successful transactions are posted.",
comodel_name='account.journal',
compute='_compute_journal_id',
inverse='_inverse_journal_id',
domain='[("type", "=", "bank"), ("company_id", "=", company_id)]',
)
#=== COMPUTE METHODS ===#
@api.depends('provider')
def _compute_journal_id(self):
for acquirer in self:
payment_method = self.env['account.payment.method.line'].search([
('journal_id.company_id', '=', acquirer.company_id.id),
('code', '=', acquirer.provider)
], limit=1)
if payment_method:
acquirer.journal_id = payment_method.journal_id
else:
acquirer.journal_id = False
def _inverse_journal_id(self):
for acquirer in self:
payment_method_line = self.env['account.payment.method.line'].search([
('journal_id.company_id', '=', acquirer.company_id.id),
('code', '=', acquirer.provider),
], limit=1)
if acquirer.journal_id:
if not payment_method_line:
default_payment_method_id = acquirer._get_default_payment_method_id(
acquirer.provider
)
existing_payment_method_line = self.env['account.payment.method.line'].search([
('payment_method_id', '=', default_payment_method_id),
('journal_id', '=', acquirer.journal_id.id),
], limit=1)
if not existing_payment_method_line:
self.env['account.payment.method.line'].create({
'payment_method_id': default_payment_method_id,
'journal_id': acquirer.journal_id.id,
})
else:
payment_method_line.journal_id = acquirer.journal_id
elif payment_method_line:
payment_method_line.unlink()
@api.model
def _get_default_payment_method_id(self, provider):
provider_payment_method = self._get_provider_payment_method(provider)
if provider_payment_method:
return provider_payment_method.id
return self.env.ref('account.account_payment_method_manual_in').id
@api.model
def _get_provider_payment_method(self, provider):
return self.env['account.payment.method'].search([('code', '=', provider)], limit=1)
#=== BUSINESS METHODS ===#
@api.model
def _setup_provider(self, provider_code):
""" Override of `payment` to create the payment method of the provider. """
super()._setup_provider(provider_code)
self._setup_payment_method(provider_code)
@api.model
def _setup_payment_method(self, provider):
if provider not in ('none', 'transfer') and not self._get_provider_payment_method(provider):
providers_description = dict(self._fields['provider']._description_selection(self.env))
self.env['account.payment.method'].create({
'name': providers_description[provider],
'code': provider,
'payment_type': 'inbound',
})
@api.model
def _remove_provider(self, provider):
""" Override of `payment` to delete the payment method of the provider. """
super()._remove_provider(provider)
self._get_provider_payment_method(provider).unlink()
@@ -0,0 +1,205 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
payment_id = fields.Many2one(
string="Payment", comodel_name='account.payment', readonly=True)
invoice_ids = fields.Many2many(
string="Invoices", comodel_name='account.move', relation='account_invoice_transaction_rel',
column1='transaction_id', column2='invoice_id', readonly=True, copy=False,
domain=[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))])
invoices_count = fields.Integer(string="Invoices Count", compute='_compute_invoices_count')
#=== COMPUTE METHODS ===#
@api.depends('invoice_ids')
def _compute_invoices_count(self):
self.env.cr.execute(
'''
SELECT transaction_id, count(invoice_id)
FROM account_invoice_transaction_rel
WHERE transaction_id IN %s
GROUP BY transaction_id
''',
[tuple(self.ids)]
)
tx_data = dict(self.env.cr.fetchall()) # {id: count}
for tx in self:
tx.invoices_count = tx_data.get(tx.id, 0)
#=== ACTION METHODS ===#
def action_view_invoices(self):
""" Return the action for the views of the invoices linked to the transaction.
Note: self.ensure_one()
:return: The action
:rtype: dict
"""
self.ensure_one()
action = {
'name': _("Invoices"),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'target': 'current',
}
invoice_ids = self.invoice_ids.ids
if len(invoice_ids) == 1:
invoice = invoice_ids[0]
action['res_id'] = invoice
action['view_mode'] = 'form'
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
else:
action['view_mode'] = 'tree,form'
action['domain'] = [('id', 'in', invoice_ids)]
return action
#=== BUSINESS METHODS - PAYMENT FLOW ===#
@api.model
def _compute_reference_prefix(self, provider, separator, **values):
""" Compute the reference prefix from the transaction values.
If the `values` parameter has an entry with 'invoice_ids' as key and a list of (4, id, O) or
(6, 0, ids) X2M command as value, the prefix is computed based on the invoice name(s).
Otherwise, an empty string is returned.
Note: This method should be called in sudo mode to give access to documents (INV, SO, ...).
:param str provider: The provider of the acquirer handling the transaction
:param str separator: The custom separator used to separate data references
:param dict values: The transaction values used to compute the reference prefix. It should
have the structure {'invoice_ids': [(X2M command), ...], ...}.
:return: The computed reference prefix if invoice ids are found, an empty string otherwise
:rtype: str
"""
command_list = values.get('invoice_ids')
if command_list:
# Extract invoice id(s) from the X2M commands
invoice_ids = self._fields['invoice_ids'].convert_to_cache(command_list, self)
invoices = self.env['account.move'].browse(invoice_ids).exists()
if len(invoices) == len(invoice_ids): # All ids are valid
return separator.join(invoices.mapped('name'))
return super()._compute_reference_prefix(provider, separator, **values)
def _set_canceled(self, state_message=None):
""" Update the transactions' state to 'cancel'.
:param str state_message: The reason for which the transaction is set in 'cancel' state
:return: updated transactions
:rtype: `payment.transaction` recordset
"""
processed_txs = super()._set_canceled(state_message)
# Cancel the existing payments
processed_txs.payment_id.action_cancel()
return processed_txs
#=== BUSINESS METHODS - POST-PROCESSING ===#
def _reconcile_after_done(self):
""" Post relevant fiscal documents and create missing payments.
As there is nothing to reconcile for validation transactions, no payment is created for
them. This is also true for validations with a validity check (transfer of a small amount
with immediate refund) because validation amounts are not included in payouts.
:return: None
"""
super()._reconcile_after_done()
# Validate invoices automatically once the transaction is confirmed
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
# Create and post missing payments for transactions requiring reconciliation
for tx in self.filtered(lambda t: t.operation != 'validation' and not t.payment_id):
tx._create_payment()
def _create_payment(self, **extra_create_values):
"""Create an `account.payment` record for the current transaction.
If the transaction is linked to some invoices, their reconciliation is done automatically.
Note: self.ensure_one()
:param dict extra_create_values: Optional extra create values
:return: The created payment
:rtype: recordset of `account.payment`
"""
self.ensure_one()
payment_method_line = self.acquirer_id.journal_id.inbound_payment_method_line_ids\
.filtered(lambda l: l.code == self.provider)
payment_values = {
'amount': abs(self.amount), # A tx may have a negative amount, but a payment must >= 0
'payment_type': 'inbound' if self.amount > 0 else 'outbound',
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.commercial_partner_id.id,
'partner_type': 'customer',
'journal_id': self.acquirer_id.journal_id.id,
'company_id': self.acquirer_id.company_id.id,
'payment_method_line_id': payment_method_line.id,
'payment_token_id': self.token_id.id,
'payment_transaction_id': self.id,
'ref': self.reference,
**extra_create_values,
}
payment = self.env['account.payment'].create(payment_values)
payment.action_post()
# Track the payment to make a one2one.
self.payment_id = payment
if self.invoice_ids:
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
(payment.line_ids + self.invoice_ids.line_ids).filtered(
lambda line: line.account_id == payment.destination_account_id
and not line.reconciled
).reconcile()
return payment
#=== BUSINESS METHODS - LOGGING ===#
def _log_message_on_linked_documents(self, message):
""" Log a message on the payment and the invoices linked to the transaction.
For a module to implement payments and link documents to a transaction, it must override
this method and call super, then log the message on documents linked to the transaction.
Note: self.ensure_one()
:param str message: The message to be logged
:return: None
"""
self.ensure_one()
if self.source_transaction_id.payment_id:
self.source_transaction_id.payment_id.message_post(body=message)
for invoice in self.source_transaction_id.invoice_ids:
invoice.message_post(body=message)
for invoice in self.invoice_ids:
invoice.message_post(body=message)
#=== BUSINESS METHODS - GETTERS ===#
def _get_received_message(self):
""" Return the message stating that the transaction has been received by the provider.
Note: self.ensure_one()
"""
message = super()._get_received_message()
if self.state == 'done' and self.payment_id:
message += "<br />" + _(
"The related payment is posted: %s",
self.payment_id._get_html_link()
)
return message
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
payment_link_wizard,payment.link.wizard,payment.model_payment_link_wizard,account.group_account_user,1,1,1,0
payment_refund_wizard,payment.refund.wizard,model_payment_refund_wizard,account.group_account_invoice,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 payment_link_wizard payment.link.wizard payment.model_payment_link_wizard account.group_account_user 1 1 1 0
3 payment_refund_wizard payment.refund.wizard model_payment_refund_wizard account.group_account_invoice 1 1 1 0
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Transactions -->
<record id="payment_transaction_billing_rule" model="ir.rule">
<field name="name">Access every transaction</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
<!-- Reset the domain defined by payment.transaction_user_rule -->
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
</record>
<!-- Tokens -->
<record id="payment_token_billing_rule" model="ir.rule">
<field name="name">Access every token</field>
<field name="model_id" ref="payment.model_payment_token"/>
<!-- Reset the domain defined by payment.token_user_rule -->
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
</record>
</odoo>
@@ -0,0 +1,36 @@
odoo.define('account_payment.payment_form', require => {
'use strict';
const checkoutForm = require('payment.checkout_form');
const manageForm = require('payment.manage_form');
const PaymentMixin = {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Add `invoice_id` to the transaction route params if it is provided.
*
* @override method from payment.payment_form_mixin
* @private
* @param {string} provider - The provider of the selected payment option's acquirer.
* @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} flow - The online payment flow of the selected payment option.
* @return {object} The extended transaction route params.
*/
_prepareTransactionRouteParams: function (provider, paymentOptionId, flow) {
const transactionRouteParams = this._super(...arguments);
return {
...transactionRouteParams,
'invoice_id': this.txContext.invoiceId ? parseInt(this.txContext.invoiceId) : null,
};
},
};
checkoutForm.include(PaymentMixin);
manageForm.include(PaymentMixin);
});
+4
View File
@@ -0,0 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_account_payment
from . import test_payment_flows
+82
View File
@@ -0,0 +1,82 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.payment.tests.common import PaymentCommon
class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, *kw):
# chart_template_ref is dropped on purpose because not needed for account_payment tests.
super().setUpClass()
Method_get_payment_method_information = AccountPaymentMethod._get_payment_method_information
def _get_payment_method_information(self):
res = Method_get_payment_method_information(self)
res['none'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]}
return res
with patch.object(AccountPaymentMethod, '_get_payment_method_information', _get_payment_method_information):
cls.env['account.payment.method'].create({
'name': 'Dummy method',
'code': 'none',
'payment_type': 'inbound'
})
cls.dummy_acquirer.journal_id = cls.company_data['default_journal_bank'].id,
cls.account = cls.company.account_journal_payment_credit_account_id
cls.invoice = cls.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'currency_id': cls.currency_euro.id,
'partner_id': cls.partner.id,
'line_ids': [
(0, 0, {
'account_id': cls.account.id,
'debit': 100.0,
'credit': 0.0,
'amount_currency': 200.0,
}),
(0, 0, {
'account_id': cls.account.id,
'debit': 0.0,
'credit': 100.0,
'amount_currency': -200.0,
}),
],
})
def setUp(self):
super().setUp()
# Disable _reconcile_after_done patcher
self.reconcile_after_done_patcher.stop()
#=== Utils ===#
@classmethod
def _prepare_acquirer(cls, provider='none', company=None, update_values=None):
""" Override of `payment` to prepare and return the first acquirer matching the given
provider and company.
If no acquirer is found in the given company, we duplicate the one from the base company.
All other acquirers belonging to the same company are disabled to avoid any interferences.
:param str provider: The provider of the acquirer to prepare.
:param recordset company: The company of the acquirer to prepare, as a `res.company` record.
:param dict update_values: The values used to update the acquirer.
:return: The acquirer to prepare, if found.
:rtype: recordset of `payment.acquirer`
"""
acquirer = super()._prepare_acquirer(provider, company, update_values)
if not acquirer.journal_id:
acquirer.journal_id = cls.env['account.journal'].search(
[('company_id', '=', acquirer.company_id.id), ('type', '=', 'bank')],
limit=1,
)
return acquirer
@@ -4,11 +4,11 @@ from unittest.mock import patch
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
@tagged('-at_install', 'post_install')
class TestAccountPayment(PaymentCommon):
class TestAccountPayment(AccountPaymentCommon):
def test_no_amount_available_for_refund_when_not_supported(self):
self.acquirer.support_refund = False
@@ -141,3 +141,11 @@ class TestAccountPayment(PaymentCommon):
patched.assert_not_called()
payment_with_token.action_post()
patched.assert_called_once()
def test_no_payment_for_validations(self):
tx = self._create_transaction(flow='dummy', operation='validation') # Overwrite the flow
tx._reconcile_after_done()
payment_count = self.env['account.payment'].search_count(
[('payment_transaction_id', '=', tx.id)]
)
self.assertEqual(payment_count, 0, msg="validation transactions should not create payments")
@@ -0,0 +1,47 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
@tagged('post_install', '-at_install')
class TestFlows(AccountPaymentCommon, PaymentHttpCommon):
def test_invoice_payment_flow(self):
"""Test the payment of an invoice through the payment/pay route"""
# Pay for this invoice (no impact even if amounts do not match)
route_values = self._prepare_pay_values()
route_values['invoice_id'] = self.invoice.id
tx_context = self._get_tx_checkout_context(**route_values)
self.assertEqual(tx_context['invoice_id'], self.invoice.id)
# payment/transaction
route_values = {
k: tx_context[k]
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'invoice_id',
]
}
route_values.update({
'flow': 'direct',
'payment_option_id': self.acquirer.id,
'tokenization_requested': False,
})
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
tx_sudo = self._get_tx(processing_values['reference'])
# Note: strangely, the check
# self.assertEqual(tx_sudo.invoice_ids, invoice)
# doesn't work, and cache invalidation doesn't work either.
self.invoice.invalidate_recordset(['transaction_ids'])
self.assertEqual(self.invoice.transaction_ids, tx_sudo)
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem action="payment.action_payment_acquirer"
id="payment_acquirer_menu"
parent="account.root_payment_menu"
sequence="10"/>
<menuitem action="payment.action_payment_icon"
id="payment_icon_menu"
parent="account.root_payment_menu"
groups="base.group_no_one"
sequence="15"/>
<menuitem action="payment.action_payment_token"
id="payment_token_menu"
parent="account.root_payment_menu"
groups="base.group_no_one"
sequence="20"/>
<menuitem action="payment.action_payment_transaction"
id="payment_transaction_menu"
parent="account.root_payment_menu"
groups="base.group_no_one"
sequence="25"/>
</odoo>
@@ -8,8 +8,8 @@
<field name="arch" type="xml">
<xpath expr="//header/button[@name='action_draft']" position="after">
<field name="amount_available_for_refund" invisible="1"/>
<button type="object"
name="action_refund_wizard"
<button name="action_refund_wizard"
type="object"
string="Refund"
groups="account.group_account_invoice"
attrs="{'invisible': [('amount_available_for_refund', '&lt;=', 0)]}"
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="payment_acquirer_form" model="ir.ui.view">
<field name="name">payment.acquirer.form</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
<field name="arch" type="xml">
<group name="payment_followup" position="attributes">
<attribute name="invisible">
0
</attribute>
</group>
<group name="payment_followup" position="inside">
<field name="journal_id"
context="{'default_type': 'bank'}"
attrs="{'required': [('state', '!=', 'disabled'), ('provider', 'not in', ['none', 'transfer'])]}"/>
</group>
</field>
</record>
</odoo>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Include account-related values in payment checkout form to pass them to the client -->
<template id="payment_checkout_inherit" inherit_id="payment.checkout">
<xpath expr="//form[@name='o_payment_checkout']" position="attributes">
<attribute name="t-att-data-invoice-id">invoice_id</attribute>
</xpath>
</template>
<!-- Include account-related values in payment manage form to pass them to the client -->
<template id="payment_manage_inherit" inherit_id="payment.manage">
<xpath expr="//form[@name='o_payment_manage']" position="attributes">
<attribute name="t-att-data-invoice-id">invoice_id</attribute>
</xpath>
</template>
</odoo>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_transaction_form" model="ir.ui.view">
<field name="name">payment.transaction.form</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment.payment_transaction_form"/>
<field name="arch" type="xml">
<button name="action_view_refunds" position="before">
<button name="action_view_invoices" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('invoices_count', '=', 0)]}">
<field name="invoices_count" widget="statinfo" string="Invoice(s)"/>
</button>
</button>
<field name="reference" position="after">
<field name="payment_id"/>
</field>
</field>
</record>
</odoo>
@@ -0,0 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_register
from . import payment_link_wizard
from . import payment_refund_wizard
@@ -0,0 +1,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class PaymentLinkWizard(models.TransientModel):
_inherit = 'payment.link.wizard'
def _get_additional_link_values(self):
res = super()._get_additional_link_values()
if self.res_model != 'account.move':
return res
# Invoice-related fields are retrieved in the controller.
return {
'invoice_id': self.res_id,
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="action_invoice_order_generate_link" model="ir.actions.act_window">
<field name="name">Generate a Payment Link</field>
<field name="res_model">payment.link.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="payment.payment_link_wizard_view_form"/>
<field name="target">new</field>
<field name="binding_model_id" ref="model_account_move"/>
<field name="binding_view_types">form</field>
</record>
</odoo>
+6 -5
View File
@@ -8,10 +8,11 @@ from . import wizards
from odoo import api, SUPERUSER_ID
def setup_provider(cr, registry, provider_code):
env = api.Environment(cr, SUPERUSER_ID, {})
env['payment.acquirer']._setup_provider(provider_code)
def reset_payment_acquirer(cr, registry, provider):
env = api.Environment(cr, SUPERUSER_ID, {})
acquirers = env['payment.acquirer'].search([('provider', '=', provider)])
acquirers.write({
'provider': 'none',
'state': 'disabled',
})
env['payment.acquirer']._remove_provider(provider)
+4 -7
View File
@@ -6,7 +6,7 @@
'category': 'Hidden',
'summary': 'Base Module for Payment Acquirers',
'description': """Payment Acquirer Base Module""",
'depends': ['account'],
'depends': ['portal'],
'data': [
'data/payment_icon_data.xml',
'data/payment_acquirer_data.xml',
@@ -15,9 +15,6 @@
'views/payment_portal_templates.xml',
'views/payment_templates.xml',
'views/account_invoice_views.xml',
'views/account_journal_views.xml',
'views/account_payment_views.xml',
'views/payment_acquirer_views.xml',
'views/payment_icon_views.xml',
'views/payment_transaction_views.xml',
@@ -27,12 +24,12 @@
'security/ir.model.access.csv',
'security/payment_security.xml',
'wizards/account_payment_register_views.xml',
'wizards/payment_link_wizard_views.xml',
'wizards/payment_onboarding_views.xml',
'wizards/payment_refund_wizard_views.xml',
],
'auto_install': True,
'demo': [
'data/payment_demo.xml',
],
'assets': {
'web.assets_frontend': [
'payment/static/src/scss/portal_payment.scss',
+4 -21
View File
@@ -39,7 +39,7 @@ class PaymentPortal(portal.CustomerPortal):
)
def payment_pay(
self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
acquirer_id=None, access_token=None, invoice_id=None, **kwargs
acquirer_id=None, access_token=None, **kwargs
):
""" Display the payment form with optional filtering of payment options.
@@ -60,15 +60,14 @@ class PaymentPortal(portal.CustomerPortal):
:param str company_id: The related company, as a `res.company` id
:param str acquirer_id: The desired acquirer, as a `payment.acquirer` id
:param str access_token: The access token used to authenticate the partner
:param str invoice_id: The account move for which a payment id made, as a `account.move` id
:param dict kwargs: Optional data passed to helper methods.
:return: The rendered checkout form
:rtype: str
:raise: werkzeug.exceptions.NotFound if the access token is invalid
"""
# Cast numeric parameters as int or float and void them if their str value is malformed
currency_id, acquirer_id, partner_id, company_id, invoice_id = tuple(map(
self._cast_as_int, (currency_id, acquirer_id, partner_id, company_id, invoice_id)
currency_id, acquirer_id, partner_id, company_id = tuple(map(
self._cast_as_int, (currency_id, acquirer_id, partner_id, company_id)
))
amount = self._cast_as_float(amount)
@@ -103,15 +102,6 @@ class PaymentPortal(portal.CustomerPortal):
company = request.env['res.company'].sudo().browse(company_id)
currency_id = currency_id or company.currency_id.id
if invoice_id:
invoice_sudo = request.env['account.move'].sudo().browse(invoice_id).exists()
if not invoice_sudo:
raise ValidationError(_("The provided parameters are invalid."))
# Interrupt the payment flow if the invoice has been canceled.
if invoice_sudo.state == 'cancel':
amount = 0.0
# Make sure that the company passed as parameter matches the partner's company.
PaymentPortal._ensure_matching_companies(partner_sudo, company)
@@ -155,7 +145,6 @@ class PaymentPortal(portal.CustomerPortal):
'landing_route': '/payment/confirmation',
'res_company': company, # Display the correct logo in a multi-company environment
'partner_is_different': partner_is_different,
'invoice_id': invoice_id,
**self._get_custom_rendering_context_values(**kwargs),
}
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
@@ -257,7 +246,7 @@ class PaymentPortal(portal.CustomerPortal):
def _create_transaction(
self, payment_option_id, reference_prefix, amount, currency_id, partner_id, flow,
tokenization_requested, landing_route, is_validation=False, invoice_id=None,
tokenization_requested, landing_route, is_validation=False,
custom_create_values=None, **kwargs
):
""" Create a draft transaction based on the payment context and return it.
@@ -274,7 +263,6 @@ class PaymentPortal(portal.CustomerPortal):
:param bool tokenization_requested: Whether the user requested that a token is created
:param str landing_route: The route the user is redirected to after the transaction
:param bool is_validation: Whether the operation is a validation
:param int invoice_id: The account move for which a payment id made, as an `account.move` id
:param dict custom_create_values: Additional create values overwriting the default ones
:param dict kwargs: Locally unused data passed to `_is_tokenization_required` and
`_compute_reference`
@@ -310,11 +298,6 @@ class PaymentPortal(portal.CustomerPortal):
_("The payment should either be direct, with redirection, or made by a token.")
)
if invoice_id:
if custom_create_values is None:
custom_create_values = {}
custom_create_values['invoice_ids'] = [Command.set([int(invoice_id)])]
reference = request.env['payment.transaction']._compute_reference(
acquirer_sudo.provider,
prefix=reference_prefix,
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Enable the EUR currency since it's the currency of the company. -->
<function model="res.currency" name="action_unarchive" eval="[[ref('base.EUR')]]"/>
</odoo>
-4
View File
@@ -1,8 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment
from . import account_payment_method
from . import account_move
from . import ir_http
from . import ir_ui_view
from . import payment_acquirer
@@ -11,4 +8,3 @@ from . import payment_token
from . import payment_transaction
from . import res_company
from . import res_partner
from . import account_journal
+25 -44
View File
@@ -81,11 +81,6 @@ class PaymentAcquirer(models.Model):
help="The template rendering the express payment methods' form.",
domain=[('type', '=', 'qweb')],
)
journal_id = fields.Many2one(
string="Payment Journal", comodel_name='account.journal',
compute='_compute_journal_id', inverse='_inverse_journal_id',
help="The journal in which the successful transactions are posted",
domain="[('type', '=', 'bank'), ('company_id', '=', company_id)]")
# Availability fields.
available_country_ids = fields.Many2many(
@@ -219,40 +214,6 @@ class PaymentAcquirer(models.Model):
'show_cancel_msg': True,
})
def _compute_journal_id(self):
for acquirer in self:
payment_method = self.env['account.payment.method.line'].search([
('journal_id.company_id', '=', acquirer.company_id.id),
('code', '=', acquirer.provider)
], limit=1)
if payment_method:
acquirer.journal_id = payment_method.journal_id
else:
acquirer.journal_id = False
def _inverse_journal_id(self):
for acquirer in self:
payment_method_line = self.env['account.payment.method.line'].search([
('journal_id.company_id', '=', acquirer.company_id.id),
('code', '=', acquirer.provider)
], limit=1)
if acquirer.journal_id:
if not payment_method_line:
default_payment_method_id = acquirer._get_default_payment_method_id()
existing_payment_method_line = self.env['account.payment.method.line'].search([
('payment_method_id', '=', default_payment_method_id),
('journal_id', '=', acquirer.journal_id.id)
], limit=1)
if not existing_payment_method_line:
self.env['account.payment.method.line'].create({
'payment_method_id': default_payment_method_id,
'journal_id': acquirer.journal_id.id,
})
else:
payment_method_line.journal_id = acquirer.journal_id
elif payment_method_line:
payment_method_line.unlink()
def _compute_feature_support_fields(self):
""" Compute the feature support fields.
@@ -268,10 +229,6 @@ class PaymentAcquirer(models.Model):
'support_tokenization',
), None))
def _get_default_payment_method_id(self):
self.ensure_one()
return self.env.ref('account.account_payment_method_manual_in').id
#=== ONCHANGE METHODS ===#
@api.onchange('state')
@@ -558,7 +515,7 @@ class PaymentAcquirer(models.Model):
:rtype: recordset of `res.currency`
"""
self.ensure_one()
return self.journal_id.currency_id or self.company_id.currency_id
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.
@@ -575,6 +532,30 @@ class PaymentAcquirer(models.Model):
self.ensure_one()
return self.redirect_form_view_id
@api.model
def _setup_provider(self, provider_code):
""" Prepare module-specific data for a given provider.
This method is called after a new provider module is installed and also for all existing
providers when `account_payment` is installed.
:param str provider_code: The code of the provider to setup.
:return: None
"""
@api.model
def _remove_provider(self, provider_code):
""" Clean module-specific data for a given provider.
:param str provider_code: The code of the provider to setup.
:return: None
"""
acquirers = self.search([('provider', '=', provider_code)])
acquirers.write({
'provider': 'none',
'state': 'disabled',
})
def _neutralize(self):
super()._neutralize()
self.flush_model()
-2
View File
@@ -5,8 +5,6 @@ import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class PaymentToken(models.Model):
_name = 'payment.token'
+20 -131
View File
@@ -75,7 +75,6 @@ class PaymentTransaction(models.Model):
readonly=True,
index=True,
)
payment_id = fields.Many2one(string="Payment", comodel_name='account.payment', readonly=True)
source_transaction_id = fields.Many2one(
string="Source Transaction",
comodel_name='payment.transaction',
@@ -90,11 +89,6 @@ class PaymentTransaction(models.Model):
readonly=True,
)
refunds_count = fields.Integer(string="Refunds Count", compute='_compute_refunds_count')
invoice_ids = fields.Many2many(
string="Invoices", comodel_name='account.move', relation='account_invoice_transaction_rel',
column1='transaction_id', column2='invoice_id', readonly=True, copy=False,
domain=[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))])
invoices_count = fields.Integer(string="Invoices Count", compute='_compute_invoices_count')
# Fields used for user redirection & payment post-processing
is_post_processed = fields.Boolean(
@@ -135,21 +129,6 @@ class PaymentTransaction(models.Model):
#=== COMPUTE METHODS ===#
@api.depends('invoice_ids')
def _compute_invoices_count(self):
self.env.cr.execute(
'''
SELECT transaction_id, count(invoice_id)
FROM account_invoice_transaction_rel
WHERE transaction_id IN %s
GROUP BY transaction_id
''',
[tuple(self.ids)]
)
tx_data = dict(self.env.cr.fetchall()) # {id: count}
for tx in self:
tx.invoices_count = tx_data.get(tx.id, 0)
def _compute_refunds_count(self):
rg_data = self.env['payment.transaction']._read_group(
domain=[('source_transaction_id', 'in', self.ids), ('operation', '=', 'refund')],
@@ -256,33 +235,6 @@ class PaymentTransaction(models.Model):
#=== ACTION METHODS ===#
def action_view_invoices(self):
""" Return the action for the views of the invoices linked to the transaction.
Note: self.ensure_one()
:return: The action
:rtype: dict
"""
self.ensure_one()
action = {
'name': _("Invoices"),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'target': 'current',
}
invoice_ids = self.invoice_ids.ids
if len(invoice_ids) == 1:
invoice = invoice_ids[0]
action['res_id'] = invoice
action['view_mode'] = 'form'
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
else:
action['view_mode'] = 'tree,form'
action['domain'] = [('id', 'in', invoice_ids)]
return action
def action_view_refunds(self):
""" Return the action for the views of the refund transactions linked to the transaction.
@@ -423,26 +375,14 @@ class PaymentTransaction(models.Model):
def _compute_reference_prefix(self, provider, separator, **values):
""" Compute the reference prefix from the transaction values.
If the `values` parameter has an entry with 'invoice_ids' as key and a list of (4, id, O) or
(6, 0, ids) X2M command as value, the prefix is computed based on the invoice name(s).
Otherwise, an empty string is returned.
Note: This method should be called in sudo mode to give access to documents (INV, SO, ...).
:param str provider: The provider of the acquirer handling the transaction
:param str separator: The custom separator used to separate data references
:param dict values: The transaction values used to compute the reference prefix. It should
have the structure {'invoice_ids': [(X2M command), ...], ...}.
:return: The computed reference prefix if invoice ids are found, an empty string otherwise
:param dict values: The transaction values used to compute the reference prefix.
:return: an empty string
:rtype: str
"""
command_list = values.get('invoice_ids')
if command_list:
# Extract invoice id(s) from the X2M commands
invoice_ids = self._fields['invoice_ids'].convert_to_cache(command_list, self)
invoices = self.env['account.move'].browse(invoice_ids).exists()
if len(invoices) == len(invoice_ids): # All ids are valid
return separator.join(invoices.mapped('name'))
return ''
@api.model
@@ -689,57 +629,67 @@ class PaymentTransaction(models.Model):
""" Update the transactions' state to 'pending'.
:param str state_message: The reason for which the transaction is set in 'pending' state
:return: None
:return: updated transactions
:rtype: `payment.transaction` recordset
"""
allowed_states = ('draft',)
target_state = 'pending'
txs_to_process = self._update_state(allowed_states, target_state, state_message)
txs_to_process._log_received_message()
return txs_to_process
def _set_authorized(self, state_message=None):
""" Update the transactions' state to 'authorized'.
:param str state_message: The reason for which the transaction is set in 'authorized' state
:return: None
:return: updated transactions
:rtype: `payment.transaction` recordset
"""
allowed_states = ('draft', 'pending')
target_state = 'authorized'
txs_to_process = self._update_state(allowed_states, target_state, state_message)
txs_to_process._log_received_message()
return txs_to_process
def _set_done(self, state_message=None):
""" Update the transactions' state to 'done'.
:return: None
:param str state_message: The reason for which the transaction is set in 'done' state
:return: updated transactions
:rtype: `payment.transaction` recordset
"""
allowed_states = ('draft', 'pending', 'authorized', 'error')
target_state = 'done'
txs_to_process = self._update_state(allowed_states, target_state, state_message)
txs_to_process._log_received_message()
return txs_to_process
def _set_canceled(self, state_message=None):
""" Update the transactions' state to 'cancel'.
:param str state_message: The reason for which the transaction is set in 'cancel' state
:return: None
:return: updated transactions
:rtype: `payment.transaction` recordset
"""
allowed_states = ('draft', 'pending', 'authorized', 'done') # 'done' for Authorize refunds.
target_state = 'cancel'
txs_to_process = self._update_state(allowed_states, target_state, state_message)
# Cancel the existing payments
txs_to_process.mapped('payment_id').action_cancel()
txs_to_process._log_received_message()
return txs_to_process
def _set_error(self, state_message):
""" Update the transactions' state to 'error'.
:param str state_message: The reason for which the transaction is set in 'error' state
:return: None
:return: updated transactions
:rtype: `payment.transaction` recordset
"""
allowed_states = ('draft', 'pending', 'authorized', 'done') # 'done' for Stripe refunds.
target_state = 'error'
txs_to_process = self._update_state(allowed_states, target_state, state_message)
txs_to_process._log_received_message()
return txs_to_process
def _update_state(self, allowed_states, target_state, state_message):
""" Update the transactions' state to the target state if the current state allows it.
@@ -938,57 +888,7 @@ class PaymentTransaction(models.Model):
:return: None
"""
# Validate invoices automatically once the transaction is confirmed
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
# Create and post missing payments for transactions requiring reconciliation
for tx in self.filtered(lambda t: t.operation != 'validation' and not t.payment_id):
tx._create_payment()
def _create_payment(self, **extra_create_values):
"""Create an `account.payment` record for the current transaction.
If the transaction is linked to some invoices, their reconciliation is done automatically.
Note: self.ensure_one()
:param dict extra_create_values: Optional extra create values
:return: The created payment
:rtype: recordset of `account.payment`
"""
self.ensure_one()
payment_method_line = self.acquirer_id.journal_id.inbound_payment_method_line_ids\
.filtered(lambda l: l.code == self.provider)
payment_values = {
'amount': abs(self.amount), # A tx may have a negative amount, but a payment must >= 0
'payment_type': 'inbound' if self.amount > 0 else 'outbound',
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.commercial_partner_id.id,
'partner_type': 'customer',
'journal_id': self.acquirer_id.journal_id.id,
'company_id': self.acquirer_id.company_id.id,
'payment_method_line_id': payment_method_line.id,
'payment_token_id': self.token_id.id,
'payment_transaction_id': self.id,
'ref': self.reference,
**extra_create_values,
}
payment = self.env['account.payment'].create(payment_values)
payment.action_post()
# Track the payment to make a one2one.
self.payment_id = payment
if self.invoice_ids:
self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()
(payment.line_ids + self.invoice_ids.line_ids).filtered(
lambda line: line.account_id == payment.destination_account_id
and not line.reconciled
).reconcile()
return payment
return
#=== BUSINESS METHODS - LOGGING ===#
@@ -1014,7 +914,7 @@ class PaymentTransaction(models.Model):
tx._log_message_on_linked_documents(message)
def _log_message_on_linked_documents(self, message):
""" Log a message on the payment and the invoices linked to the transaction.
""" Log a message on the records linked to the transaction.
For a module to implement payments and link documents to a transaction, it must override
this method and call super, then log the message on documents linked to the transaction.
@@ -1025,12 +925,6 @@ class PaymentTransaction(models.Model):
:return: None
"""
self.ensure_one()
if self.source_transaction_id.payment_id:
self.source_transaction_id.payment_id.message_post(body=message)
for invoice in self.source_transaction_id.invoice_ids:
invoice.message_post(body=message)
for invoice in self.invoice_ids:
invoice.message_post(body=message)
#=== BUSINESS METHODS - GETTERS ===#
@@ -1099,11 +993,6 @@ class PaymentTransaction(models.Model):
"(%(acq_name)s).", ref=self.reference, amount=formatted_amount,
acq_name=self.acquirer_id.name
)
if self.payment_id:
message += "<br />" + _(
"The related payment is posted: %s",
self.payment_id._get_html_link(),
)
elif self.state == 'error':
message = _(
"The transaction with reference %(ref)s for %(amount)s encountered an error"
+1 -2
View File
@@ -1,10 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_payment_link_wizard,access_payment_link_wizard,payment.model_payment_link_wizard,base.group_user,0,0,0,0
payment_acquirer_onboarding_wizard,payment.acquirer.onboarding.wizard,model_payment_acquirer_onboarding_wizard,base.group_system,1,1,1,0
payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1
payment_icon_all,payment.icon.all,model_payment_icon,,1,0,0,0
payment_icon_system,payment.icon.system,model_payment_icon,base.group_system,1,1,1,1
payment_link_wizard,payment.link.wizard,model_payment_link_wizard,account.group_account_user,1,1,1,0
payment_refund_wizard,payment.refund.wizard,model_payment_refund_wizard,account.group_account_invoice,1,1,1,0
payment_token_all,payment.token.all,model_payment_token,,1,0,0,0
payment_token_portal,payment.token.portal,model_payment_token,base.group_portal,1,1,1,1
payment_token_system,payment.token.system,model_payment_token,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_payment_link_wizard access_payment_link_wizard payment.model_payment_link_wizard base.group_user 0 0 0 0
3 payment_acquirer_onboarding_wizard payment.acquirer.onboarding.wizard model_payment_acquirer_onboarding_wizard base.group_system 1 1 1 0
4 payment_acquirer_system payment.acquirer.system model_payment_acquirer base.group_system 1 1 1 1
5 payment_icon_all payment.icon.all model_payment_icon 1 0 0 0
6 payment_icon_system payment.icon.system model_payment_icon base.group_system 1 1 1 1
payment_link_wizard payment.link.wizard model_payment_link_wizard account.group_account_user 1 1 1 0
payment_refund_wizard payment.refund.wizard model_payment_refund_wizard account.group_account_invoice 1 1 1 0
7 payment_token_all payment.token.all model_payment_token 1 0 0 0
8 payment_token_portal payment.token.portal model_payment_token base.group_portal 1 1 1 1
9 payment_token_system payment.token.system model_payment_token base.group_system 1 1 1 1
+1 -17
View File
@@ -15,15 +15,7 @@
<field name="name">Access own transactions only</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
<field name="domain_force">['|', ('partner_id', '=', False), ('partner_id', '=', user.partner_id.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
</record>
<record id="payment_transaction_billing_rule" model="ir.rule">
<field name="name">Access every transaction</field>
<field name="model_id" ref="payment.model_payment_transaction"/>
<!-- Reset the domain defined by payment.transaction_user_rule -->
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
</record>
<record id="transaction_company_rule" model="ir.rule">
@@ -41,14 +33,6 @@
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
</record>
<record id="payment_token_billing_rule" model="ir.rule">
<field name="name">Access every token</field>
<field name="model_id" ref="payment.model_payment_token"/>
<!-- Reset the domain defined by payment.token_user_rule -->
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
</record>
<record id="payment_token_company_rule" model="ir.rule">
<field name="name">Access tokens in own companies only</field>
<field name="model_id" ref="payment.model_payment_token"/>
@@ -299,8 +299,6 @@ odoo.define('payment.payment_form_mixin', require => {
'currency_id': this.txContext.currencyId
? parseInt(this.txContext.currencyId) : null,
'partner_id': parseInt(this.txContext.partnerId),
'invoice_id': this.txContext.invoiceId
? parseInt(this.txContext.invoiceId) : null,
'flow': flow,
'tokenization_requested': this.txContext.tokenizationRequested,
'landing_route': this.txContext.landingRoute,
-1
View File
@@ -2,7 +2,6 @@
from . import common
from . import http_common
from . import test_account_payment
from . import test_flows
from . import test_multicompany_flows
from . import test_payment_acquirer
+25 -72
View File
@@ -6,26 +6,17 @@ from unittest.mock import patch
from lxml import objectify
from odoo.fields import Command
from odoo.tests.common import TransactionCase
from odoo.tools.misc import hmac as hmac_tool
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
_logger = logging.getLogger(__name__)
class PaymentCommon(AccountTestInvoicingCommon):
class PaymentCommon(TransactionCase):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
Method_get_payment_method_information = AccountPaymentMethod._get_payment_method_information
def _get_payment_method_information(self):
res = Method_get_payment_method_information(self)
res['none'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]}
return res
def setUpClass(cls):
super().setUpClass()
cls.currency_euro = cls._prepare_currency('EUR')
cls.currency_usd = cls._prepare_currency('USD')
@@ -81,12 +72,6 @@ class PaymentCommon(AccountTestInvoicingCommon):
'arch': arch,
})
with patch.object(AccountPaymentMethod, '_get_payment_method_information', _get_payment_method_information):
cls.env['account.payment.method'].create({
'name': 'Dummy method',
'code': 'none',
'payment_type': 'inbound'
})
cls.dummy_acquirer = cls.env['payment.acquirer'].create({
'name': "Dummy Acquirer",
'provider': 'none',
@@ -94,7 +79,6 @@ class PaymentCommon(AccountTestInvoicingCommon):
'is_published': True,
'allow_tokenization': True,
'redirect_form_view_id': redirect_form.id,
'journal_id': cls.company_data['default_journal_bank'].id,
})
cls.acquirer = cls.dummy_acquirer
@@ -104,27 +88,26 @@ class PaymentCommon(AccountTestInvoicingCommon):
cls.currency = cls.currency_euro
cls.partner = cls.default_partner
cls.reference = "Test Transaction"
cls.account = cls.company.account_journal_payment_credit_account_id
cls.invoice = cls.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'line_ids': [
(0, 0, {
'account_id': cls.account.id,
'currency_id': cls.currency_euro.id,
'debit': 100.0,
'credit': 0.0,
'amount_currency': 200.0,
}),
(0, 0, {
'account_id': cls.account.id,
'currency_id': cls.currency_euro.id,
'debit': 0.0,
'credit': 100.0,
'amount_currency': -200.0,
}),
],
})
account_payment_module = cls.env['ir.module.module']._get('account_payment')
cls.account_payment_installed = account_payment_module.state in ('installed', 'to upgrade')
def setUp(self):
def stop_patcher_without_fail():
""" Magic hack: we start the patcher even if it was started already so that we do not
call stop on a non started patcher. """
self.reconcile_after_done_patcher.start()
self.reconcile_after_done_patcher.stop()
super().setUp()
if self.account_payment_installed:
# disable account payment generation if account_payment is installed
# because the accounting setup of acquirers is not managed in this common
self.reconcile_after_done_patcher = patch(
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._reconcile_after_done',
)
self.reconcile_after_done_patcher.start()
self.addCleanup(stop_patcher_without_fail)
#=== Utils ===#
@@ -166,13 +149,8 @@ class PaymentCommon(AccountTestInvoicingCommon):
else:
acquirer = base_acquirer.copy({'company_id': company.id})
update_values['state'] = 'test'
acquirer.write(update_values)
if not acquirer.journal_id:
acquirer.journal_id = cls.env['account.journal'].search([
('company_id', '=', company.id),
('type', '=', 'bank')
], limit=1)
acquirer.state = 'test'
return acquirer
def _create_transaction(self, flow, sudo=True, **values):
@@ -201,31 +179,6 @@ class PaymentCommon(AccountTestInvoicingCommon):
('reference', '=', reference),
])
def _prepare_transaction_values(self, payment_option_id, flow):
""" Prepare the basic payment/transaction route values.
:param int payment_option_id: The payment option handling the transaction, as a
`payment.acquirer` id or a `payment.token` id
:param str flow: The payment flow
:return: The route values
:rtype: dict
"""
return {
'amount': self.amount,
'currency_id': self.currency.id,
'partner_id': self.partner.id,
'access_token': self._generate_test_access_token(
self.partner.id, self.amount, self.currency.id
),
'payment_option_id': payment_option_id,
'reference_prefix': 'test',
'tokenization_requested': True,
'landing_route': 'Test',
'is_validation': False,
'invoice_id': self.invoice.id,
'flow': flow,
}
def _generate_test_access_token(self, *values):
""" Generate an access token based on the provided values for testing purposes.
+25
View File
@@ -194,6 +194,31 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
# payment/transaction #
#######################
def _prepare_transaction_values(self, payment_option_id, flow):
""" Prepare the basic payment/transaction route values.
:param int payment_option_id: The payment option handling the transaction, as a
`payment.acquirer` id or a `payment.token` id
:param str flow: The payment flow
:return: The route values
:rtype: dict
"""
return {
'amount': self.amount,
'currency_id': self.currency.id,
'partner_id': self.partner.id,
'access_token': self._generate_test_access_token(
self.partner.id, self.amount, self.currency.id
),
'payment_option_id': payment_option_id,
'reference_prefix': 'test',
'tokenization_requested': True,
'landing_route': 'Test',
'is_validation': False,
'flow': flow,
}
def _portal_transaction(self, **route_kwargs):
"""/payment/transaction feedback
-37
View File
@@ -277,43 +277,6 @@ class TestFlows(PaymentHttpCommon):
response = self._portal_pay(**route_values)
self.assertEqual(response.status_code, 404)
def test_invoice_payment_flow(self):
"""Test the payment of an invoice through the payment/pay route"""
# Pay for this invoice (no impact even if amounts do not match)
route_values = self._prepare_pay_values()
route_values['invoice_id'] = self.invoice.id
route_values['company_id'] = self.company.id
tx_context = self._get_tx_checkout_context(**route_values)
self.assertEqual(tx_context['invoice_id'], self.invoice.id)
# payment/transaction
route_values = {
k: tx_context[k]
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'invoice_id',
]
}
route_values.update({
'flow': 'direct',
'payment_option_id': self.acquirer.id,
'tokenization_requested': False,
})
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
tx_sudo = self._get_tx(processing_values['reference'])
# Note: strangely, the check
# self.assertEqual(tx_sudo.invoice_ids, invoice)
# doesn't work, and cache invalidation doesn't work either.
self.invoice.invalidate_recordset(['transaction_ids'])
self.assertEqual(self.invoice.transaction_ids, tx_sudo)
def test_transaction_wrong_flow(self):
transaction_values = self._prepare_pay_values()
transaction_values.update({
@@ -11,11 +11,11 @@ from odoo.addons.payment.tests.http_common import PaymentHttpCommon
class TestMultiCompanyFlows(PaymentHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.company_a = cls.company_data['company']
cls.company_b = cls.company_data_2['company']
cls.company_a = cls.env.company # cls.company_data['company']
cls.company_b = cls.env.company.create({'name': "Payment Test Company"}) # cls.company_data_2['company']
cls.user_company_a = cls.internal_user
cls.user_company_b = cls.env['res.users'].create({
@@ -43,8 +43,8 @@ class TestPaymentAcquirer(PaymentCommon):
set to 0. """
self.acquirer.maximum_amount = 0.
compatible_acquirers = self.acquirer._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency.id
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertIn(self.acquirer, compatible_acquirers)
@@ -53,8 +53,8 @@ class TestPaymentAcquirer(PaymentCommon):
amount. """
self.acquirer.maximum_amount = self.amount + 10.0
compatible_acquirers = self.acquirer._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency.id
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertIn(self.acquirer, compatible_acquirers)
@@ -63,7 +63,7 @@ class TestPaymentAcquirer(PaymentCommon):
amount. """
self.acquirer.maximum_amount = self.amount - 10.0
compatible_acquirers = self.acquirer._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency.id
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertNotIn(self.acquirer, compatible_acquirers)
@@ -11,7 +11,6 @@ class TestPaymentTransaction(PaymentCommon):
def test_refunds_count(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False
tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment
for reference_index, operation in enumerate(
('online_redirect', 'online_direct', 'online_token', 'validation', 'refund')
):
@@ -32,7 +31,6 @@ class TestPaymentTransaction(PaymentCommon):
def test_refund_transaction_values(self):
self.acquirer.support_refund = 'partial'
tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment
# Test the default values of a full refund transaction
refund_tx = tx._create_refund_transaction()
@@ -82,11 +80,3 @@ class TestPaymentTransaction(PaymentCommon):
msg="The amount of the refund transaction should be the negative value of the amount "
"to refund."
)
def test_no_payment_for_validations(self):
tx = self._create_transaction(flow='dummy', operation='validation') # Overwrite the flow
tx._reconcile_after_done()
payment_count = self.env['account.payment'].search_count(
[('payment_transaction_id', '=', tx.id)]
)
self.assertEqual(payment_count, 0, msg="validation transactions should not create payments")
@@ -89,10 +89,7 @@
placeholder="Select countries. Leave empty to make available everywhere."
options="{'no_open': True, 'no_create': True}"/>
</group>
<group string="Payment Followup" name="payment_followup">
<field name="journal_id" context="{'default_type': 'bank'}"
attrs="{'required': [('state', '!=', 'disabled'), ('provider', 'not in', ['none', 'transfer'])]}"/>
</group>
<group string="Payment Followup" name="payment_followup" invisible="1"/>
</group>
</page>
<page string="Fees" name="fees" attrs="{'invisible': [('support_fees', '=', False)]}">
@@ -225,9 +222,4 @@
</field>
</record>
<menuitem action="action_payment_acquirer"
id="payment_acquirer_menu"
parent="account.root_payment_menu"
sequence="10"/>
</odoo>
@@ -43,9 +43,4 @@
</field>
</record>
<menuitem action="action_payment_icon"
id="payment_icon_menu"
parent="account.root_payment_menu"
groups="base.group_no_one"/>
</odoo>
+1 -4
View File
@@ -17,7 +17,6 @@
- 'transaction_route' - The route used to create a transaction when the user clicks Pay
- 'landing_route' - The route the user is redirected to after the transaction
- 'footer_template_id' - The template id for the submit button. Optional
- 'invoice_id' - The id of the account move being paid. Optional
-->
<form name="o_payment_checkout"
class="o_payment_form mt-3 clearfix"
@@ -28,8 +27,7 @@
t-att-data-access-token="access_token"
t-att-data-transaction-route="transaction_route"
t-att-data-landing-route="landing_route"
t-att-data-allow-token-selection="True"
t-att-data-invoice-id="invoice_id">
t-att-data-allow-token-selection="True">
<t t-set="acquirer_count" t-value="len(acquirers) if acquirers else 0"/>
<t t-set="token_count" t-value="len(tokens) if tokens else 0"/>
@@ -174,7 +172,6 @@
t-att-data-reference-prefix="reference_prefix"
t-att-data-partner-id="partner_id"
t-att-data-access-token="access_token"
t-att-data-invoice-id="invoice_id"
t-att-data-transaction-route="transaction_route"
t-att-data-assign-token-route="assign_token_route"
t-att-data-landing-route="landing_route"
@@ -71,9 +71,4 @@
</field>
</record>
<menuitem action="action_payment_token"
id="payment_token_menu"
parent="account.root_payment_menu"
groups="base.group_no_one"/>
</odoo>
@@ -14,11 +14,6 @@
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_invoices" type="object"
class="oe_stat_button" icon="fa-money"
attrs="{'invisible': [('invoices_count', '=', 0)]}">
<field name="invoices_count" widget="statinfo" string="Invoice(s)"/>
</button>
<button name="action_view_refunds"
type="object"
class="oe_stat_button"
@@ -30,7 +25,6 @@
<group>
<group name="transaction_details">
<field name="reference"/>
<field name="payment_id"/>
<field name="source_transaction_id"
attrs="{'invisible': [('source_transaction_id', '=', False)]}"/>
<field name="amount"/>
@@ -155,10 +149,4 @@
<field name="context">{'create': False}</field>
</record>
<menuitem action="action_payment_transaction"
id="payment_transaction_menu"
parent="account.root_payment_menu"
groups="base.group_no_one"
sequence="20"/>
</odoo>
-2
View File
@@ -1,6 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_register
from . import payment_link_wizard
from . import payment_onboarding_wizard
from . import payment_refund_wizard
+46 -43
View File
@@ -1,4 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug import urls
from odoo import _, api, fields, models
@@ -9,38 +10,31 @@ from odoo.addons.payment import utils as payment_utils
class PaymentLinkWizard(models.TransientModel):
_name = "payment.link.wizard"
_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.payment_reference,
'amount': record[amount_field],
'currency_id': record.currency_id.id,
'partner_id': record.partner_id.id,
'amount_max': record[amount_field],
})
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)
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')
link = fields.Char(string='Payment Link', compute='_compute_values')
description = fields.Char('Payment Ref')
access_token = fields.Char(compute='_compute_values')
company_id = fields.Many2one('res.company', compute='_compute_company')
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_acquirer_ids = fields.Many2many(
comodel_name='payment.acquirer',
string="Payment Acquirers Available",
@@ -64,21 +58,12 @@ class PaymentLinkWizard(models.TransientModel):
@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))
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', 'payment_acquirer_selection')
def _compute_values(self):
for payment_link in self:
payment_link.access_token = payment_utils.generate_access_token(
payment_link.partner_id.id, payment_link.amount, payment_link.currency_id.id
)
# must be called after token generation, obvsly - the link needs an up-to-date token
self._generate_link()
@api.depends('res_model', 'res_id')
def _compute_company(self):
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
@@ -135,16 +120,34 @@ class PaymentLinkWizard(models.TransientModel):
for link in self:
link.has_multiple_acquirers = len(link.available_acquirer_ids) > 1
def _generate_link(self):
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_acquirer_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
payment_link.link = f'{base_url}/payment/pay' \
f'?reference={urls.url_quote(payment_link.description)}' \
f'&amount={payment_link.amount}' \
f'&currency_id={payment_link.currency_id.id}' \
f'&partner_id={payment_link.partner_id.id}' \
f'&company_id={payment_link.company_id.id}' \
f'&invoice_id={payment_link.res_id}' \
f'{"&acquirer_id=" + str(payment_link.payment_acquirer_selection) if payment_link.payment_acquirer_selection != "all" else "" }' \
f'&access_token={payment_link.access_token}'
url_params = {
'reference': urls.url_quote(payment_link.description),
'amount': self.amount,
'access_token': self._get_access_token(),
**self._get_additional_link_values(),
}
if payment_link.payment_acquirer_selection != 'all':
url_params['acquirer_id'] = str(payment_link.payment_acquirer_selection)
payment_link.link = f'{base_url}/payment/pay?{urls.url_encode(url_params)}'
def _get_additional_link_values(self):
self.ensure_one()
return {
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'company_id': self.company_id.id,
}
@@ -17,10 +17,9 @@
<field name="has_multiple_acquirers" invisible="1"/>
<field name="description"/>
<field name="amount"/>
<field name="currency_id" invisible="1"/>
<field name="payment_acquirer_selection"
attrs="{'invisible':[('has_multiple_acquirers', '=', False)]}"/>
<field name="currency_id" invisible="1"/>
<field name="access_token" invisible="1"/>
</group>
</group>
<group>
@@ -36,14 +35,4 @@
</field>
</record>
<record id="action_invoice_order_generate_link" model="ir.actions.act_window">
<field name="name">Generate a Payment Link</field>
<field name="res_model">payment.link.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="payment_link_wizard_view_form"/>
<field name="target">new</field>
<field name="binding_model_id" ref="model_account_move"/>
<field name="binding_view_types">form</field>
</record>
</odoo>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="payment_acquirer_onboarding_wizard_form">
<record id="payment_acquirer_onboarding_wizard_form" model="ir.ui.view">
<field name="name">payment.acquirer.onboarding.wizard.form</field>
<field name="model">payment.acquirer.onboarding.wizard</field>
<field name="arch" type="xml">
@@ -21,6 +21,7 @@ class PaymentWizard(models.TransientModel):
paypal_seller_account = fields.Char("Merchant Account ID", default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_seller_account'))
paypal_pdt_token = fields.Char("PDT Identity Token", default=lambda self: self._get_default_payment_acquirer_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_acquirer_onboarding_value('manual_name'))
journal_name = fields.Char("Bank Name", default=lambda self: self._get_default_payment_acquirer_onboarding_value('journal_name'))
acc_number = fields.Char("Account Number", default=lambda self: self._get_default_payment_acquirer_onboarding_value('acc_number'))
@@ -133,5 +134,6 @@ class PaymentWizard(models.TransientModel):
def _start_stripe_onboarding(self):
""" Start Stripe Connect onboarding. """
menu_id = self.env.ref('payment.payment_acquirer_menu').id
menu = self.env.ref('account_payment.payment_acquirer_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)
+5 -1
View File
@@ -3,7 +3,11 @@
from . import controllers
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment import setup_provider, reset_payment_acquirer
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'adyen')
def uninstall_hook(cr, registry):
+1
View File
@@ -15,6 +15,7 @@
'data/payment_acquirer_data.xml', # Depends on views/payment_adyen_templates.xml
],
'application': True,
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'assets': {
'web.assets_frontend': [
@@ -7,10 +7,4 @@
<field name="allow_tokenization">True</field>
</record>
<record id="payment_method_adyen" model="account.payment.method">
<field name="name">Adyen</field>
<field name="code">adyen</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
-1
View File
@@ -1,6 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_method
from . import payment_acquirer
from . import payment_token
from . import payment_transaction
@@ -140,12 +140,6 @@ class PaymentAcquirer(models.Model):
"""
return f'ODOO_PARTNER_{partner_id}'
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'adyen':
return super()._get_default_payment_method_id()
return self.env.ref('payment_adyen.payment_method_adyen').id
def _neutralize(self):
super()._neutralize()
self._neutralize_fields('adyen', [
+2 -2
View File
@@ -6,8 +6,8 @@ from odoo.addons.payment.tests.common import PaymentCommon
class AdyenCommon(PaymentCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.adyen = cls._prepare_acquirer('adyen', update_values={
'adyen_merchant_account': 'dummy',
+9 -5
View File
@@ -6,11 +6,7 @@ from . import models
from odoo.exceptions import UserError
from odoo.tools import config
from odoo.addons.payment import reset_payment_acquirer
def uninstall_hook(cr, registry):
reset_payment_acquirer(cr, registry, 'alipay')
from odoo.addons.payment import setup_provider, reset_payment_acquirer
def pre_init_hook(cr):
@@ -18,3 +14,11 @@ def pre_init_hook(cr):
raise UserError(
"This module is deprecated and cannot be installed. "
"Consider installing the Payment Acquirer: AsiaPay module instead.")
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'alipay')
def uninstall_hook(cr, registry):
reset_payment_acquirer(cr, registry, 'alipay')
+1
View File
@@ -14,6 +14,7 @@
],
'application': False,
'pre_init_hook': 'pre_init_hook',
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
}
@@ -19,10 +19,4 @@
<field name="redirect_form_view_id" ref="redirect_form"/>
</record>
<record id="payment_method_alipay" model="account.payment.method">
<field name="name">Alipay</field>
<field name="code">alipay</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
-1
View File
@@ -1,5 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_method
from . import payment_acquirer
from . import payment_transaction
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountPaymentMethod(models.Model):
_inherit = 'account.payment.method'
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['alipay'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
return res
@@ -69,12 +69,6 @@ class PaymentAcquirer(models.Model):
else: # test environment
return 'https://openapi.alipaydev.com/gateway.do'
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'alipay':
return super()._get_default_payment_method_id()
return self.env.ref('payment_alipay.payment_method_alipay').id
def _neutralize(self):
super()._neutralize()
self._neutralize_fields('alipay', [
+2 -2
View File
@@ -6,8 +6,8 @@ from odoo.addons.payment.tests.common import PaymentCommon
class AlipayCommon(PaymentCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.currency_yuan = cls._prepare_currency('CNY')
cls.alipay = cls._prepare_acquirer('alipay', update_values={
+5 -1
View File
@@ -3,7 +3,11 @@
from . import controllers
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment import setup_provider, reset_payment_acquirer
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'aps')
def uninstall_hook(cr, registry):
+1
View File
@@ -14,6 +14,7 @@
'data/payment_acquirer_data.xml',
],
'application': True,
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
}
@@ -6,10 +6,4 @@
<field name="redirect_form_view_id" ref="redirect_form"/>
</record>
<record id="payment_method_aps" model="account.payment.method">
<field name="name">Amazon Payment Services</field>
<field name="code">aps</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
-1
View File
@@ -1,5 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_method
from . import payment_acquirer
from . import payment_transaction
@@ -1,13 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountPaymentMethod(models.Model):
_inherit = 'account.payment.method'
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['aps'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
return res
@@ -59,12 +59,6 @@ class PaymentAcquirer(models.Model):
signing_string = ''.join([key, sign_data, key])
return hashlib.sha256(signing_string.encode()).hexdigest()
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'aps':
return super()._get_default_payment_method_id()
return self.env.ref('payment_aps.payment_method_aps').id
def _neutralize(self):
super()._neutralize()
self._neutralize_fields('aps', [
+2 -2
View File
@@ -6,8 +6,8 @@ from odoo.addons.payment.tests.http_common import PaymentHttpCommon
class APSCommon(PaymentHttpCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.aps = cls._prepare_acquirer('aps', update_values={
'aps_merchant_identifier': '123456abc',
+5 -1
View File
@@ -3,7 +3,11 @@
from . import controllers
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment import setup_provider, reset_payment_acquirer
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'asiapay')
def uninstall_hook(cr, registry):
+1
View File
@@ -15,6 +15,7 @@
'data/payment_acquirer_data.xml',
],
'application': True,
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
}
@@ -6,10 +6,4 @@
<field name="redirect_form_view_id" ref="redirect_form"/>
</record>
<record id="payment_method_asiapay" model="account.payment.method">
<field name="name">AsiaPay</field>
<field name="code">asiapay</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
@@ -1,5 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_method
from . import payment_acquirer
from . import payment_transaction
@@ -1,13 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountPaymentMethod(models.Model):
_inherit = 'account.payment.method'
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['asiapay'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
return res
@@ -86,12 +86,6 @@ class PaymentAcquirer(models.Model):
shasign.update(signing_string.encode())
return shasign.hexdigest()
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'asiapay':
return super()._get_default_payment_method_id()
return self.env.ref('payment_asiapay.payment_method_asiapay').id
def _neutralize(self):
super()._neutralize()
self._neutralize_fields('asiapay', [
+2 -2
View File
@@ -6,8 +6,8 @@ from odoo.addons.payment.tests.common import PaymentCommon
class AsiaPayCommon(PaymentCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.asiapay = cls._prepare_acquirer('asiapay', update_values={
'asiapay_merchant_id': '123456789',
+5 -1
View File
@@ -3,7 +3,11 @@
from . import controllers
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment import setup_provider, reset_payment_acquirer
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'authorize')
def uninstall_hook(cr, registry):
+1
View File
@@ -14,6 +14,7 @@
'data/payment_acquirer_data.xml',
],
'application': True,
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'assets': {
'web.assets_frontend': [
@@ -7,10 +7,4 @@
<field name="allow_tokenization">True</field>
</record>
<record id="payment_method_authorize" model="account.payment.method">
<field name="name">Authorize.Net</field>
<field name="code">authorize</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
@@ -1,6 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_method
from . import payment_acquirer
from . import payment_token
from . import payment_transaction
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountPaymentMethod(models.Model):
_inherit = 'account.payment.method'
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['authorize'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
return res
@@ -146,12 +146,6 @@ class PaymentAcquirer(models.Model):
return self.authorize_currency_id
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'authorize':
return super()._get_default_payment_method_id()
return self.env.ref('payment_authorize.payment_method_authorize').id
def _neutralize(self):
super()._neutralize()
self._neutralize_fields('authorize', [
+2 -2
View File
@@ -6,8 +6,8 @@ from odoo.addons.payment.tests.common import PaymentCommon
class AuthorizeCommon(PaymentCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.authorize = cls._prepare_acquirer('authorize', update_values={
'authorize_login': 'dummy',
+5 -1
View File
@@ -3,7 +3,11 @@
from . import controllers
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment import setup_provider, reset_payment_acquirer
def post_init_hook(cr, registry):
setup_provider(cr, registry, 'buckaroo')
def uninstall_hook(cr, registry):
+1
View File
@@ -14,6 +14,7 @@
'data/payment_acquirer_data.xml',
],
'application': True,
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
}
@@ -6,10 +6,4 @@
<field name="redirect_form_view_id" ref="redirect_form"/>
</record>
<record id="payment_method_buckaroo" model="account.payment.method">
<field name="name">Buckaroo</field>
<field name="code">buckaroo</field>
<field name="payment_type">inbound</field>
</record>
</odoo>
@@ -1,5 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_payment_method
from . import payment_acquirer
from . import payment_transaction
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class AccountPaymentMethod(models.Model):
_inherit = 'account.payment.method'
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['buckaroo'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]}
return res
@@ -64,12 +64,6 @@ class PaymentAcquirer(models.Model):
# Calculate the SHA-1 hash over the signing string
return sha1(sign_string.encode('utf-8')).hexdigest()
def _get_default_payment_method_id(self):
self.ensure_one()
if self.provider != 'buckaroo':
return super()._get_default_payment_method_id()
return self.env.ref('payment_buckaroo.payment_method_buckaroo').id
def _neutralize(self):
super()._neutralize()
self._neutralize_fields('buckaroo', ['buckaroo_website_key', 'buckaroo_secret_key'])

Some files were not shown because too many files have changed in this diff Show More