[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:
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+10
-6
@@ -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,
|
||||
}
|
||||
+16
@@ -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,
|
||||
}
|
||||
+7
-1
@@ -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
|
||||
|
@@ -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);
|
||||
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
+10
-2
@@ -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>
|
||||
+2
-2
@@ -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', '<=', 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'¤cy_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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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', [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,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', [
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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', [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user