19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -11,7 +11,7 @@ from odoo.addons.payment.controllers import portal as payment_portal
class PaymentPortal(payment_portal.PaymentPortal):
@route('/invoice/transaction/<int:invoice_id>', type='json', auth='public')
@route('/invoice/transaction/<int:invoice_id>', type='jsonrpc', auth='public')
def invoice_transaction(self, invoice_id, access_token, **kwargs):
""" Create a draft transaction and return its processing values.
@ -30,13 +30,43 @@ class PaymentPortal(payment_portal.PaymentPortal):
except AccessError:
raise ValidationError(_("The access token is invalid."))
kwargs['reference_prefix'] = None # Allow the reference to be computed based on the invoice
logged_in = not request.env.user._is_public()
partner = request.env.user.partner_id if logged_in else invoice_sudo.partner_id
kwargs['partner_id'] = partner.id
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
partner_sudo = request.env.user.partner_id if logged_in else invoice_sudo.partner_id
self._validate_transaction_kwargs(kwargs, additional_allowed_keys={'name_next_installment'})
return self._process_transaction(partner_sudo.id, invoice_sudo.currency_id.id, [invoice_id], False, **kwargs)
@route('/invoice/transaction/overdue', type='jsonrpc', auth='public')
def overdue_invoices_transaction(self, payment_reference, **kwargs):
""" Create a draft transaction for overdue invoices and return its processing values.
:param str payment_reference: The reference to the current payment
:param dict kwargs: Locally unused data passed to `_create_transaction`
:return: The mandatory values for the processing of the transaction
:rtype: dict
:raise: ValidationError if the user is not logged in, or all the overdue invoices don't share the same currency.
"""
logged_in = not request.env.user._is_public()
if not logged_in:
raise ValidationError(_("Please log in to pay your overdue invoices"))
partner = request.env.user.partner_id
overdue_invoices = request.env['account.move'].search(self._get_overdue_invoices_domain())
currencies = overdue_invoices.mapped('currency_id')
if not all(currency == currencies[0] for currency in currencies):
raise ValidationError(_("Impossible to pay all the overdue invoices if they don't share the same currency."))
self._validate_transaction_kwargs(kwargs)
return self._process_transaction(partner.id, currencies[0].id, overdue_invoices.ids, payment_reference, **kwargs)
def _process_transaction(self, partner_id, currency_id, invoice_ids, payment_reference, **kwargs):
kwargs.update({
'currency_id': currency_id,
'partner_id': partner_id,
'reference_prefix': payment_reference,
}) # Inject the create values taken from the invoice into the kwargs.
tx_sudo = self._create_transaction(
custom_create_values={'invoice_ids': [Command.set([invoice_id])]}, **kwargs,
custom_create_values={
'invoice_ids': [Command.set(invoice_ids)],
},
**kwargs,
)
return tx_sudo._get_processing_values()
@ -47,9 +77,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
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.
@ -73,48 +100,53 @@ class PaymentPortal(payment_portal.PaymentPortal):
raise ValidationError(_("The provided parameters are invalid."))
kwargs.update({
# To display on the payment form; will be later overwritten when creating the tx.
'reference': invoice_sudo.name,
# To fix the currency if incorrect and avoid mismatches when creating the tx.
'currency_id': invoice_sudo.currency_id.id,
# To fix the partner if incorrect and avoid mismatches when creating the tx.
'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.
def _get_extra_payment_form_values(self, invoice_id=None, access_token=None, **kwargs):
""" Override of `payment` to reroute the payment flow to the portal view of the invoice.
: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.
:param str invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param str access_token: The portal or payment access token, respectively if we are in a
portal or payment link flow.
:return: The extended rendering context values.
:rtype: dict
"""
rendering_context_values = super()._get_custom_rendering_context_values(
invoice_id=invoice_id, **kwargs
form_values = super()._get_extra_payment_form_values(
invoice_id=invoice_id, access_token=access_token, **kwargs
)
if invoice_id:
rendering_context_values['invoice_id'] = invoice_id
invoice_id = self._cast_as_int(invoice_id)
try: # Check document access against what could be a portal access token.
invoice_sudo = self._document_check_access('account.move', invoice_id, access_token)
except AccessError: # It is a payment access token computed on the payment context.
if not payment_utils.check_access_token(
access_token,
kwargs.get('partner_id'),
kwargs.get('amount'),
kwargs.get('currency_id'),
):
raise
invoice_sudo = request.env['account.move'].sudo().browse(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
form_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
)
# Reroute the next steps of the payment flow to the portal view of the invoice.
form_values.update({
'transaction_route': f'/invoice/transaction/{invoice_id}',
'landing_route': f'{invoice_sudo.access_url}'
f'?access_token={invoice_sudo._portal_ensure_token()}',
'access_token': invoice_sudo.access_token,
})
return form_values

View file

@ -1,68 +1,176 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, http
from odoo.exceptions import AccessError, MissingError, ValidationError
from odoo.http import request
from odoo.addons.account.controllers import portal
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.controllers.portal import PaymentPortal
from odoo.addons.portal.controllers.portal import _build_url_w_params
class PortalAccount(portal.PortalAccount):
class PortalAccount(portal.PortalAccount, PaymentPortal):
def _invoice_get_page_view_values(self, invoice, access_token, **kwargs):
values = super()._invoice_get_page_view_values(invoice, access_token, **kwargs)
def _invoice_get_page_view_values(self, invoice, access_token, payment=False, amount=None, **kwargs):
# EXTENDS account
values = super()._invoice_get_page_view_values(invoice, access_token, amount=amount, **kwargs)
if not invoice._has_to_be_paid():
# Do not compute payment-related stuff if given invoice doesn't have to be paid.
return {
**values,
'payment': payment, # We want to show the dialog even when everything has been paid (with a custom message)
}
epd = values.get('epd_discount_amount_currency', 0.0)
discounted_amount = invoice.amount_residual - epd
common_view_values = self._get_common_page_view_values(
invoices_data={
'partner': invoice.partner_id,
'company': invoice.company_id,
'total_amount': invoice.amount_total,
'currency': invoice.currency_id,
'amount_residual': discounted_amount,
'landing_route': invoice.get_portal_url(),
'transaction_route': f'/invoice/transaction/{invoice.id}',
},
access_token=access_token,
**kwargs)
amount_custom = float(amount or 0.0)
values |= {
**common_view_values,
'amount_custom': amount_custom,
'payment': payment,
'invoice_id': invoice.id,
'invoice_name': invoice.name,
'show_epd': epd,
}
return values
@http.route(['/my/invoices/overdue'], type='http', auth='public', methods=['GET'], website=True, sitemap=False)
def portal_my_overdue_invoices(self, access_token=None, **kw):
try:
request.env['account.move'].check_access('read')
except (AccessError, MissingError):
return request.redirect('/my')
overdue_invoices = request.env['account.move'].search(self._get_overdue_invoices_domain())
values = self._overdue_invoices_get_page_view_values(overdue_invoices, **kw)
return request.render("account_payment.portal_overdue_invoices_page", values) if 'payment' in values else request.redirect('/my/invoices')
def _overdue_invoices_get_page_view_values(self, overdue_invoices, **kwargs):
values = {'page_name': 'overdue_invoices'}
if len(overdue_invoices) == 0:
return values
first_invoice = overdue_invoices[0]
partner = first_invoice.partner_id
company = first_invoice.company_id
currency = first_invoice.currency_id
if any(invoice.partner_id != partner for invoice in overdue_invoices):
raise ValidationError(_("Overdue invoices should share the same partner."))
if any(invoice.company_id != company for invoice in overdue_invoices):
raise ValidationError(_("Overdue invoices should share the same company."))
if any(invoice.currency_id != currency for invoice in overdue_invoices):
raise ValidationError(_("Overdue invoices should share the same currency."))
total_amount = sum(overdue_invoices.mapped('amount_total'))
amount_residual = sum(overdue_invoices.mapped('amount_residual'))
batch_name = company.get_next_batch_payment_communication() if len(overdue_invoices) > 1 else first_invoice.name
values.update({
'payment': {
'date': fields.Date.today(),
'reference': batch_name,
'amount': total_amount,
'currency': currency,
},
'amount': total_amount,
})
common_view_values = self._get_common_page_view_values(
invoices_data={
'partner': partner,
'company': company,
'total_amount': total_amount,
'currency': currency,
'amount_residual': amount_residual,
'payment_reference': batch_name,
'landing_route': '/my/invoices/',
'transaction_route': '/invoice/transaction/overdue',
},
**kwargs)
values |= common_view_values
return values
def _get_common_page_view_values(self, invoices_data, access_token=None, **kwargs):
logged_in = not request.env.user._is_public()
# We set partner_id to the partner id of the current user if logged in, otherwise we set it
# to the invoice partner id. We do this to ensure that payment tokens are assigned to the
# correct partner and to avoid linking tokens to the public user.
partner_sudo = request.env.user.partner_id if logged_in else invoice.partner_id
invoice_company = invoice.company_id or request.env.company
partner_sudo = request.env.user.partner_id if logged_in else invoices_data['partner']
invoice_company = invoices_data['company'] or request.env.company
availability_report = {}
# Select all the payment methods and tokens that match the payment context.
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
invoice_company.id,
partner_sudo.id,
invoice.amount_total,
currency_id=invoice.currency_id.id
) # In sudo mode to read the fields of providers and partner (if not logged in)
tokens = request.env['payment.token'].search(
[('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
) # Tokens are cleared at the end if the user is not logged in
invoices_data['total_amount'],
currency_id=invoices_data['currency'].id,
report=availability_report,
**kwargs,
) # In sudo mode to read the fields of providers and partner (if logged out).
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
providers_sudo.ids,
partner_sudo.id,
currency_id=invoices_data['currency'].id,
report=availability_report,
) # In sudo mode to read the fields of providers.
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
providers_sudo.ids, partner_sudo.id
) # In sudo mode to read the partner's tokens (if logged out) and provider fields.
# Make sure that the partner's company matches the invoice's company.
if not PaymentPortal._can_partner_pay_in_company(partner_sudo, invoice_company):
providers_sudo = request.env['payment.provider'].sudo()
tokens = request.env['payment.token']
company_mismatch = not PaymentPortal._can_partner_pay_in_company(
partner_sudo, invoice_company
)
fees_by_provider = {
pro_sudo: pro_sudo._compute_fees(
invoice.amount_residual, invoice.currency_id, invoice.partner_id.country_id
) for pro_sudo in providers_sudo.filtered('fees_active')
portal_page_values = {
'company_mismatch': company_mismatch,
'expected_company': invoice_company,
}
values.update({
'providers': providers_sudo,
'tokens': tokens,
'fees_by_provider': fees_by_provider,
'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping(
providers_sudo, logged_in=logged_in
payment_form_values = {
'show_tokenize_input_mapping': PaymentPortal._compute_show_tokenize_input_mapping(
providers_sudo
),
'amount': invoice.amount_residual,
'currency': invoice.currency_id,
}
payment_context = {
'currency': invoices_data['currency'],
'partner_id': partner_sudo.id,
'providers_sudo': providers_sudo,
'payment_methods_sudo': payment_methods_sudo,
'tokens_sudo': tokens_sudo,
'availability_report': availability_report,
'transaction_route': invoices_data['transaction_route'],
'landing_route': invoices_data['landing_route'],
'access_token': access_token,
'transaction_route': f'/invoice/transaction/{invoice.id}/',
'landing_route': _build_url_w_params(invoice.access_url, {'access_token': access_token})
})
if not logged_in:
# Don't display payment tokens of the invoice partner if the user is not logged in, but
# inform that logging in will make them available.
values.update({
'existing_token': bool(tokens),
'tokens': request.env['payment.token'],
})
'payment_reference': invoices_data.get('payment_reference', False),
}
# Merge the dictionaries while allowing the redefinition of keys.
values = portal_page_values | payment_form_values | payment_context | self._get_extra_payment_form_values(**kwargs)
return values
@http.route()
def portal_my_invoice_detail(self, invoice_id, payment_token=None, amount=None, **kw):
# EXTENDS account
# If we have a custom payment amount, make sure it hasn't been tampered with
if amount and not payment_utils.check_access_token(payment_token, invoice_id, amount):
return request.redirect('/my')
return super().portal_my_invoice_detail(invoice_id, amount=amount, **kw)